Terraform et Kubernetes : Provisionner et Gérer un Cluster K8s

Provisionnez un cluster Kubernetes avec Terraform. Déploiement EKS sur AWS, provider Kubernetes, déploiement de ressources K8s, charts Helm et gestion RBAC. Guide complet.

Kubernetes est devenu le standard de facto pour l'orchestration de conteneurs, et Terraform est l'outil de référence pour l'Infrastructure as Code. Mais que se passe-t-il quand ces deux technologies se rencontrent ? Vous obtenez une combinaison extrêmement puissante qui vous permet de provisionner un cluster Kubernetes complet et d'y déployer vos applications, le tout depuis un seul outil et un code versionné.

Dans cet article, nous allons explorer comment utiliser Terraform pour provisionner et gérer un cluster Kubernetes. Nous couvrirons le provisioning d'un cluster EKS sur AWS, la configuration du provider Kubernetes, le déploiement de ressources K8s, l'utilisation du provider Helm, et la gestion des RBAC. Vous repartirez avec un exemple complet et fonctionnel de bout en bout.

Diagramme - Terraform et Kubernetes : Provisionner et Gérer un Cluster K8s

Pourquoi Utiliser Terraform avec Kubernetes ?

Avant de plonger dans le code, comprenons pourquoi cette combinaison est si populaire.

Les avantages de l'approche Terraform pour K8s

  • Infrastructure unifiée : gérer le cluster ET les ressources cloud associées (VPC, IAM, load balancers) dans un même workflow
  • State management : Terraform maintient un état de vos ressources K8s, ce qui facilite les mises à jour et le suivi des changements
  • Approche déclarative cohérente : le même langage HCL pour tout, de l'infrastructure bas niveau aux déploiements applicatifs
  • Planification des changements : terraform plan montre exactement ce qui va changer avant de l'appliquer
  • Intégration CI/CD : un pipeline unique pour toute l'infrastructure

Terraform vs kubectl vs Helm : quand utiliser quoi ?

Il est important de comprendre que Terraform n'est pas toujours le meilleur choix pour gérer les ressources Kubernetes :

  • Terraform : idéal pour le provisioning du cluster, les ressources d'infrastructure (namespaces, RBAC, quotas) et les déploiements stables
  • Helm : parfait pour les applications packagées avec des charts communautaires, intégrable via le provider Terraform Helm
  • kubectl / ArgoCD / Flux : préférable pour les déploiements applicatifs fréquents avec du GitOps

La bonne pratique est souvent d'utiliser Terraform pour le provisioning du cluster et l'infrastructure de base, puis un outil GitOps pour les déploiements applicatifs. Cependant, pour les infrastructures plus simples ou les cas où l'on souhaite tout centraliser, Terraform peut gérer l'ensemble.

Les Providers Terraform pour Kubernetes

Terraform dispose de plusieurs providers pour interagir avec Kubernetes :

Le provider kubernetes

Le provider officiel hashicorp/kubernetes permet de gérer les ressources Kubernetes natives :

terraform {
  required_providers {
    kubernetes = {
      source  = "hashicorp/kubernetes"
      version = "~> 2.27"
    }
  }
}

provider "kubernetes" {
  host                   = data.aws_eks_cluster.cluster.endpoint
  cluster_ca_certificate = base64decode(data.aws_eks_cluster.cluster.certificate_authority[0].data)
  token                  = data.aws_eks_cluster_auth.cluster.token
}

Le provider helm

Le provider hashicorp/helm permet de déployer des charts Helm via Terraform :

terraform {
  required_providers {
    helm = {
      source  = "hashicorp/helm"
      version = "~> 2.12"
    }
  }
}

provider "helm" {
  kubernetes {
    host                   = data.aws_eks_cluster.cluster.endpoint
    cluster_ca_certificate = base64decode(data.aws_eks_cluster.cluster.certificate_authority[0].data)
    token                  = data.aws_eks_cluster_auth.cluster.token
  }
}

Le provider kubectl

Le provider tiers gavinbunney/kubectl est utile pour appliquer des manifestes YAML bruts :

terraform {
  required_providers {
    kubectl = {
      source  = "gavinbunney/kubectl"
      version = "~> 1.14"
    }
  }
}

Provisionner un Cluster EKS avec Terraform

Passons à la pratique avec un exemple complet de provisioning d'un cluster Amazon EKS. Nous allons créer toute l'infrastructure nécessaire : VPC, subnets, security groups, rôles IAM et le cluster EKS lui-même.

Structure du projet

eks-project/
├── modules/
│   ├── vpc/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── outputs.tf
│   └── eks/
│       ├── main.tf
│       ├── variables.tf
│       └── outputs.tf
├── environments/
│   └── prod/
│       ├── main.tf
│       ├── variables.tf
│       ├── outputs.tf
│       ├── providers.tf
│       ├── backend.tf
│       ├── kubernetes.tf
│       └── terraform.tfvars
└── README.md

Configuration du VPC

Un cluster EKS nécessite un VPC correctement configuré avec des subnets publics et privés :

# modules/vpc/main.tf
data "aws_availability_zones" "available" {
  state = "available"
}

resource "aws_vpc" "eks" {
  cidr_block           = var.vpc_cidr
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = {
    Name                                        = "${var.cluster_name}-vpc"
    "kubernetes.io/cluster/${var.cluster_name}" = "shared"
  }
}

resource "aws_subnet" "public" {
  count = length(var.public_subnet_cidrs)

  vpc_id                  = aws_vpc.eks.id
  cidr_block              = var.public_subnet_cidrs[count.index]
  availability_zone       = data.aws_availability_zones.available.names[count.index]
  map_public_ip_on_launch = true

  tags = {
    Name                                        = "${var.cluster_name}-public-${count.index + 1}"
    "kubernetes.io/cluster/${var.cluster_name}" = "shared"
    "kubernetes.io/role/elb"                    = "1"
  }
}

resource "aws_subnet" "private" {
  count = length(var.private_subnet_cidrs)

  vpc_id            = aws_vpc.eks.id
  cidr_block        = var.private_subnet_cidrs[count.index]
  availability_zone = data.aws_availability_zones.available.names[count.index]

  tags = {
    Name                                        = "${var.cluster_name}-private-${count.index + 1}"
    "kubernetes.io/cluster/${var.cluster_name}" = "shared"
    "kubernetes.io/role/internal-elb"           = "1"
  }
}

resource "aws_internet_gateway" "eks" {
  vpc_id = aws_vpc.eks.id

  tags = {
    Name = "${var.cluster_name}-igw"
  }
}

resource "aws_eip" "nat" {
  count  = length(var.public_subnet_cidrs)
  domain = "vpc"

  tags = {
    Name = "${var.cluster_name}-nat-eip-${count.index + 1}"
  }
}

resource "aws_nat_gateway" "eks" {
  count = length(var.public_subnet_cidrs)

  allocation_id = aws_eip.nat[count.index].id
  subnet_id     = aws_subnet.public[count.index].id

  tags = {
    Name = "${var.cluster_name}-nat-${count.index + 1}"
  }

  depends_on = [aws_internet_gateway.eks]
}

resource "aws_route_table" "public" {
  vpc_id = aws_vpc.eks.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.eks.id
  }

  tags = {
    Name = "${var.cluster_name}-public-rt"
  }
}

resource "aws_route_table" "private" {
  count  = length(var.private_subnet_cidrs)
  vpc_id = aws_vpc.eks.id

  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.eks[count.index].id
  }

  tags = {
    Name = "${var.cluster_name}-private-rt-${count.index + 1}"
  }
}

resource "aws_route_table_association" "public" {
  count          = length(var.public_subnet_cidrs)
  subnet_id      = aws_subnet.public[count.index].id
  route_table_id = aws_route_table.public.id
}

resource "aws_route_table_association" "private" {
  count          = length(var.private_subnet_cidrs)
  subnet_id      = aws_subnet.private[count.index].id
  route_table_id = aws_route_table.private[count.index].id
}

Rôles IAM pour EKS

# modules/eks/iam.tf
# Rôle IAM pour le plan de contrôle EKS
resource "aws_iam_role" "eks_cluster" {
  name = "${var.cluster_name}-cluster-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "eks.amazonaws.com"
        }
      }
    ]
  })
}

resource "aws_iam_role_policy_attachment" "eks_cluster_policy" {
  policy_arn = "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy"
  role       = aws_iam_role.eks_cluster.name
}

resource "aws_iam_role_policy_attachment" "eks_vpc_resource_controller" {
  policy_arn = "arn:aws:iam::aws:policy/AmazonEKSVPCResourceController"
  role       = aws_iam_role.eks_cluster.name
}

# Rôle IAM pour les Node Groups
resource "aws_iam_role" "eks_nodes" {
  name = "${var.cluster_name}-node-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "ec2.amazonaws.com"
        }
      }
    ]
  })
}

resource "aws_iam_role_policy_attachment" "eks_worker_node_policy" {
  policy_arn = "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy"
  role       = aws_iam_role.eks_nodes.name
}

resource "aws_iam_role_policy_attachment" "eks_cni_policy" {
  policy_arn = "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy"
  role       = aws_iam_role.eks_nodes.name
}

resource "aws_iam_role_policy_attachment" "eks_container_registry" {
  policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly"
  role       = aws_iam_role.eks_nodes.name
}

Le cluster EKS et les Node Groups

# modules/eks/main.tf
resource "aws_eks_cluster" "main" {
  name     = var.cluster_name
  version  = var.kubernetes_version
  role_arn = aws_iam_role.eks_cluster.arn

  vpc_config {
    subnet_ids              = concat(var.public_subnet_ids, var.private_subnet_ids)
    endpoint_private_access = true
    endpoint_public_access  = var.enable_public_access
    security_group_ids      = [aws_security_group.eks_cluster.id]
  }

  encryption_config {
    provider {
      key_arn = aws_kms_key.eks.arn
    }
    resources = ["secrets"]
  }

  enabled_cluster_log_types = [
    "api",
    "audit",
    "authenticator",
    "controllerManager",
    "scheduler"
  ]

  depends_on = [
    aws_iam_role_policy_attachment.eks_cluster_policy,
    aws_iam_role_policy_attachment.eks_vpc_resource_controller,
  ]

  tags = {
    Name        = var.cluster_name
    Environment = var.environment
  }
}

resource "aws_kms_key" "eks" {
  description             = "Clé KMS pour le chiffrement des secrets EKS"
  deletion_window_in_days = 7
  enable_key_rotation     = true
}

resource "aws_security_group" "eks_cluster" {
  name_prefix = "${var.cluster_name}-cluster-"
  vpc_id      = var.vpc_id

  ingress {
    from_port = 443
    to_port   = 443
    protocol  = "tcp"
    cidr_blocks = [var.vpc_cidr]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "${var.cluster_name}-cluster-sg"
  }
}

resource "aws_eks_node_group" "main" {
  cluster_name    = aws_eks_cluster.main.name
  node_group_name = "${var.cluster_name}-main-nodes"
  node_role_arn   = aws_iam_role.eks_nodes.arn
  subnet_ids      = var.private_subnet_ids

  instance_types = var.node_instance_types
  capacity_type  = var.capacity_type

  scaling_config {
    desired_size = var.node_desired_size
    min_size     = var.node_min_size
    max_size     = var.node_max_size
  }

  update_config {
    max_unavailable = 1
  }

  labels = {
    role        = "general"
    environment = var.environment
  }

  depends_on = [
    aws_iam_role_policy_attachment.eks_worker_node_policy,
    aws_iam_role_policy_attachment.eks_cni_policy,
    aws_iam_role_policy_attachment.eks_container_registry,
  ]

  tags = {
    Name        = "${var.cluster_name}-main-nodes"
    Environment = var.environment
  }
}

Variables et outputs du module EKS

# modules/eks/variables.tf
variable "cluster_name" {
  description = "Nom du cluster EKS"
  type        = string
}

variable "kubernetes_version" {
  description = "Version de Kubernetes"
  type        = string
  default     = "1.29"
}

variable "vpc_id" {
  description = "ID du VPC"
  type        = string
}

variable "vpc_cidr" {
  description = "CIDR block du VPC"
  type        = string
}

variable "public_subnet_ids" {
  description = "IDs des subnets publics"
  type        = list(string)
}

variable "private_subnet_ids" {
  description = "IDs des subnets privés"
  type        = list(string)
}

variable "node_instance_types" {
  description = "Types d'instances pour les nodes"
  type        = list(string)
  default     = ["t3.medium"]
}

variable "capacity_type" {
  description = "Type de capacité : ON_DEMAND ou SPOT"
  type        = string
  default     = "ON_DEMAND"
}

variable "node_desired_size" {
  description = "Nombre souhaité de nodes"
  type        = number
  default     = 2
}

variable "node_min_size" {
  description = "Nombre minimum de nodes"
  type        = number
  default     = 1
}

variable "node_max_size" {
  description = "Nombre maximum de nodes"
  type        = number
  default     = 5
}

variable "enable_public_access" {
  description = "Activer l'accès public à l'API server"
  type        = bool
  default     = true
}

variable "environment" {
  description = "Nom de l'environnement"
  type        = string
}

# modules/eks/outputs.tf
output "cluster_endpoint" {
  description = "Endpoint de l'API server EKS"
  value       = aws_eks_cluster.main.endpoint
}

output "cluster_certificate_authority" {
  description = "Certificat CA du cluster"
  value       = aws_eks_cluster.main.certificate_authority[0].data
}

output "cluster_name" {
  description = "Nom du cluster EKS"
  value       = aws_eks_cluster.main.name
}

output "cluster_security_group_id" {
  description = "Security group du cluster"
  value       = aws_security_group.eks_cluster.id
}

output "node_group_role_arn" {
  description = "ARN du rôle IAM des nodes"
  value       = aws_iam_role.eks_nodes.arn
}

Configurer les Providers Kubernetes et Helm

Une fois le cluster EKS provisionné, nous devons configurer les providers Kubernetes et Helm pour y déployer des ressources :

# environments/prod/providers.tf
terraform {
  required_version = ">= 1.7.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.30"
    }
    kubernetes = {
      source  = "hashicorp/kubernetes"
      version = "~> 2.27"
    }
    helm = {
      source  = "hashicorp/helm"
      version = "~> 2.12"
    }
  }
}

provider "aws" {
  region = var.region

  default_tags {
    tags = {
      Project     = var.project_name
      Environment = var.environment
      ManagedBy   = "terraform"
    }
  }
}

# Récupérer les informations de connexion au cluster
data "aws_eks_cluster" "cluster" {
  name = module.eks.cluster_name
}

data "aws_eks_cluster_auth" "cluster" {
  name = module.eks.cluster_name
}

provider "kubernetes" {
  host                   = data.aws_eks_cluster.cluster.endpoint
  cluster_ca_certificate = base64decode(data.aws_eks_cluster.cluster.certificate_authority[0].data)
  token                  = data.aws_eks_cluster_auth.cluster.token
}

provider "helm" {
  kubernetes {
    host                   = data.aws_eks_cluster.cluster.endpoint
    cluster_ca_certificate = base64decode(data.aws_eks_cluster.cluster.certificate_authority[0].data)
    token                  = data.aws_eks_cluster_auth.cluster.token
  }
}

Déployer des Ressources Kubernetes avec Terraform

Maintenant que nos providers sont configurés, déployons des ressources Kubernetes directement depuis Terraform.

Namespaces

# kubernetes.tf - Namespaces
resource "kubernetes_namespace" "application" {
  metadata {
    name = "application"

    labels = {
      environment = var.environment
      managed-by  = "terraform"
    }

    annotations = {
      "description" = "Namespace pour les applications métier"
    }
  }
}

resource "kubernetes_namespace" "monitoring" {
  metadata {
    name = "monitoring"

    labels = {
      environment = var.environment
      managed-by  = "terraform"
    }
  }
}

resource "kubernetes_namespace" "ingress" {
  metadata {
    name = "ingress-nginx"

    labels = {
      environment = var.environment
      managed-by  = "terraform"
    }
  }
}

Resource Quotas et Limit Ranges

# Quotas de ressources par namespace
resource "kubernetes_resource_quota" "application" {
  metadata {
    name      = "application-quota"
    namespace = kubernetes_namespace.application.metadata[0].name
  }

  spec {
    hard = {
      "requests.cpu"    = "4"
      "requests.memory" = "8Gi"
      "limits.cpu"      = "8"
      "limits.memory"   = "16Gi"
      "pods"            = "20"
      "services"        = "10"
    }
  }
}

resource "kubernetes_limit_range" "application" {
  metadata {
    name      = "application-limits"
    namespace = kubernetes_namespace.application.metadata[0].name
  }

  spec {
    limit {
      type = "Container"
      default = {
        cpu    = "500m"
        memory = "512Mi"
      }
      default_request = {
        cpu    = "100m"
        memory = "128Mi"
      }
      max = {
        cpu    = "2"
        memory = "4Gi"
      }
    }
  }
}

ConfigMaps et Secrets

# ConfigMap
resource "kubernetes_config_map" "app_config" {
  metadata {
    name      = "app-config"
    namespace = kubernetes_namespace.application.metadata[0].name
  }

  data = {
    "DATABASE_HOST"     = module.rds.endpoint
    "DATABASE_PORT"     = "5432"
    "DATABASE_NAME"     = "myapp"
    "REDIS_HOST"        = module.elasticache.endpoint
    "LOG_LEVEL"         = var.environment == "prod" ? "warn" : "debug"
    "FEATURE_FLAG_V2"   = "true"
  }
}

# Secret (utiliser un gestionnaire de secrets en production)
resource "kubernetes_secret" "app_secrets" {
  metadata {
    name      = "app-secrets"
    namespace = kubernetes_namespace.application.metadata[0].name
  }

  type = "Opaque"

  data = {
    "DATABASE_PASSWORD" = data.aws_secretsmanager_secret_version.db_password.secret_string
    "API_KEY"           = data.aws_secretsmanager_secret_version.api_key.secret_string
  }
}

Deployment et Service

# Deployment
resource "kubernetes_deployment" "web_app" {
  metadata {
    name      = "web-app"
    namespace = kubernetes_namespace.application.metadata[0].name

    labels = {
      app     = "web-app"
      version = var.app_version
    }
  }

  spec {
    replicas = var.environment == "prod" ? 3 : 1

    selector {
      match_labels = {
        app = "web-app"
      }
    }

    strategy {
      type = "RollingUpdate"
      rolling_update {
        max_surge       = "25%"
        max_unavailable = "25%"
      }
    }

    template {
      metadata {
        labels = {
          app     = "web-app"
          version = var.app_version
        }

        annotations = {
          "prometheus.io/scrape" = "true"
          "prometheus.io/port"   = "8080"
        }
      }

      spec {
        service_account_name = kubernetes_service_account.web_app.metadata[0].name

        container {
          name  = "web-app"
          image = "${var.ecr_repository_url}:${var.app_version}"

          port {
            container_port = 8080
            protocol       = "TCP"
          }

          env_from {
            config_map_ref {
              name = kubernetes_config_map.app_config.metadata[0].name
            }
          }

          env_from {
            secret_ref {
              name = kubernetes_secret.app_secrets.metadata[0].name
            }
          }

          resources {
            requests = {
              cpu    = "200m"
              memory = "256Mi"
            }
            limits = {
              cpu    = "1"
              memory = "1Gi"
            }
          }

          liveness_probe {
            http_get {
              path = "/health"
              port = 8080
            }
            initial_delay_seconds = 30
            period_seconds        = 10
            timeout_seconds       = 5
            failure_threshold     = 3
          }

          readiness_probe {
            http_get {
              path = "/ready"
              port = 8080
            }
            initial_delay_seconds = 10
            period_seconds        = 5
            timeout_seconds       = 3
            failure_threshold     = 3
          }
        }
      }
    }
  }

  lifecycle {
    ignore_changes = [
      spec[0].template[0].metadata[0].annotations["kubectl.kubernetes.io/restartedAt"],
    ]
  }
}

# Service
resource "kubernetes_service" "web_app" {
  metadata {
    name      = "web-app"
    namespace = kubernetes_namespace.application.metadata[0].name

    annotations = {
      "service.beta.kubernetes.io/aws-load-balancer-type" = "nlb"
    }
  }

  spec {
    selector = {
      app = "web-app"
    }

    port {
      name        = "http"
      port        = 80
      target_port = 8080
      protocol    = "TCP"
    }

    type = "ClusterIP"
  }
}

Déployer des Charts Helm avec Terraform

Le provider Helm permet de déployer des applications packagées directement depuis Terraform. C'est particulièrement utile pour les composants d'infrastructure comme l'ingress controller, le monitoring ou le cert-manager.

NGINX Ingress Controller

resource "helm_release" "nginx_ingress" {
  name       = "ingress-nginx"
  repository = "https://kubernetes.github.io/ingress-nginx"
  chart      = "ingress-nginx"
  version    = "4.9.0"
  namespace  = kubernetes_namespace.ingress.metadata[0].name

  set {
    name  = "controller.replicaCount"
    value = var.environment == "prod" ? "3" : "1"
  }

  set {
    name  = "controller.service.type"
    value = "LoadBalancer"
  }

  set {
    name  = "controller.service.annotations.service\\.beta\\.kubernetes\\.io/aws-load-balancer-type"
    value = "nlb"
  }

  set {
    name  = "controller.service.annotations.service\\.beta\\.kubernetes\\.io/aws-load-balancer-cross-zone-load-balancing-enabled"
    value = "true"
  }

  set {
    name  = "controller.metrics.enabled"
    value = "true"
  }

  set {
    name  = "controller.resources.requests.cpu"
    value = "100m"
  }

  set {
    name  = "controller.resources.requests.memory"
    value = "128Mi"
  }

  depends_on = [
    module.eks
  ]
}

Prometheus et Grafana pour le monitoring

resource "helm_release" "prometheus_stack" {
  name       = "kube-prometheus-stack"
  repository = "https://prometheus-community.github.io/helm-charts"
  chart      = "kube-prometheus-stack"
  version    = "55.5.0"
  namespace  = kubernetes_namespace.monitoring.metadata[0].name

  values = [
    yamlencode({
      grafana = {
        enabled = true
        adminPassword = data.aws_secretsmanager_secret_version.grafana_password.secret_string
        ingress = {
          enabled = true
          ingressClassName = "nginx"
          hosts = ["grafana.${var.domain_name}"]
        }
        persistence = {
          enabled = true
          size    = "10Gi"
        }
      }
      prometheus = {
        prometheusSpec = {
          retention = "30d"
          storageSpec = {
            volumeClaimTemplate = {
              spec = {
                accessModes = ["ReadWriteOnce"]
                resources = {
                  requests = {
                    storage = "50Gi"
                  }
                }
              }
            }
          }
        }
      }
      alertmanager = {
        enabled = true
      }
    })
  ]

  depends_on = [
    helm_release.nginx_ingress
  ]
}

Cert-Manager pour les certificats TLS

resource "helm_release" "cert_manager" {
  name       = "cert-manager"
  repository = "https://charts.jetstack.io"
  chart      = "cert-manager"
  version    = "1.14.0"
  namespace  = "cert-manager"
  create_namespace = true

  set {
    name  = "installCRDs"
    value = "true"
  }

  set {
    name  = "global.leaderElection.namespace"
    value = "cert-manager"
  }
}

Gestion des RBAC Kubernetes avec Terraform

La gestion fine des accès au cluster est essentielle en environnement de production. Terraform permet de gérer les RBAC de manière déclarative et versionnée.

Service Account pour l'application

resource "kubernetes_service_account" "web_app" {
  metadata {
    name      = "web-app-sa"
    namespace = kubernetes_namespace.application.metadata[0].name

    annotations = {
      # IAM Roles for Service Accounts (IRSA)
      "eks.amazonaws.com/role-arn" = aws_iam_role.web_app_irsa.arn
    }
  }
}

# Rôle IAM pour IRSA
resource "aws_iam_role" "web_app_irsa" {
  name = "${var.cluster_name}-web-app-irsa"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Principal = {
          Federated = module.eks.oidc_provider_arn
        }
        Action = "sts:AssumeRoleWithWebIdentity"
        Condition = {
          StringEquals = {
            "${module.eks.oidc_provider}:sub" = "system:serviceaccount:application:web-app-sa"
            "${module.eks.oidc_provider}:aud" = "sts.amazonaws.com"
          }
        }
      }
    ]
  })
}

# Politique pour accéder à S3
resource "aws_iam_role_policy_attachment" "web_app_s3" {
  role       = aws_iam_role.web_app_irsa.name
  policy_arn = aws_iam_policy.web_app_s3_access.arn
}

ClusterRole et ClusterRoleBinding

# Rôle en lecture seule pour les développeurs
resource "kubernetes_cluster_role" "developer_readonly" {
  metadata {
    name = "developer-readonly"
  }

  rule {
    api_groups = [""]
    resources  = ["pods", "services", "configmaps", "events", "namespaces"]
    verbs      = ["get", "list", "watch"]
  }

  rule {
    api_groups = ["apps"]
    resources  = ["deployments", "replicasets", "statefulsets"]
    verbs      = ["get", "list", "watch"]
  }

  rule {
    api_groups = [""]
    resources  = ["pods/log"]
    verbs      = ["get", "list"]
  }
}

# Binding pour un groupe de développeurs
resource "kubernetes_cluster_role_binding" "developer_readonly" {
  metadata {
    name = "developer-readonly-binding"
  }

  role_ref {
    api_group = "rbac.authorization.k8s.io"
    kind      = "ClusterRole"
    name      = kubernetes_cluster_role.developer_readonly.metadata[0].name
  }

  subject {
    kind      = "Group"
    name      = "developers"
    api_group = "rbac.authorization.k8s.io"
  }
}

# Rôle avec plus de permissions pour les DevOps
resource "kubernetes_cluster_role" "devops_admin" {
  metadata {
    name = "devops-admin"
  }

  rule {
    api_groups = ["*"]
    resources  = ["*"]
    verbs      = ["*"]
  }
}

# Rôle spécifique à un namespace
resource "kubernetes_role" "app_deployer" {
  metadata {
    name      = "app-deployer"
    namespace = kubernetes_namespace.application.metadata[0].name
  }

  rule {
    api_groups = ["apps"]
    resources  = ["deployments"]
    verbs      = ["get", "list", "watch", "create", "update", "patch"]
  }

  rule {
    api_groups = [""]
    resources  = ["services", "configmaps"]
    verbs      = ["get", "list", "watch", "create", "update", "patch"]
  }
}

resource "kubernetes_role_binding" "app_deployer" {
  metadata {
    name      = "app-deployer-binding"
    namespace = kubernetes_namespace.application.metadata[0].name
  }

  role_ref {
    api_group = "rbac.authorization.k8s.io"
    kind      = "Role"
    name      = kubernetes_role.app_deployer.metadata[0].name
  }

  subject {
    kind      = "Group"
    name      = "app-deployers"
    api_group = "rbac.authorization.k8s.io"
  }
}

Intégration avec d'Autres Ressources Cloud

L'un des grands avantages de Terraform est sa capacité à lier les ressources Kubernetes aux ressources cloud environnantes.

Base de données RDS accessible depuis le cluster

# Base de données PostgreSQL dans les subnets privés du VPC
resource "aws_db_subnet_group" "main" {
  name       = "${var.cluster_name}-db-subnet"
  subnet_ids = module.vpc.private_subnet_ids
}

resource "aws_security_group" "rds" {
  name_prefix = "${var.cluster_name}-rds-"
  vpc_id      = module.vpc.vpc_id

  ingress {
    from_port       = 5432
    to_port         = 5432
    protocol        = "tcp"
    security_groups = [module.eks.cluster_security_group_id]
  }
}

resource "aws_db_instance" "main" {
  identifier     = "${var.cluster_name}-postgres"
  engine         = "postgres"
  engine_version = "15.4"
  instance_class = "db.t3.medium"

  allocated_storage     = 100
  max_allocated_storage = 500
  storage_encrypted     = true

  db_name  = "myapp"
  username = "admin"
  password = data.aws_secretsmanager_secret_version.db_password.secret_string

  db_subnet_group_name   = aws_db_subnet_group.main.name
  vpc_security_group_ids = [aws_security_group.rds.id]

  backup_retention_period = 7
  multi_az               = var.environment == "prod"
  deletion_protection    = var.environment == "prod"

  skip_final_snapshot = var.environment != "prod"
}

# Injecter l'endpoint RDS dans un ConfigMap K8s
resource "kubernetes_config_map" "database_config" {
  metadata {
    name      = "database-config"
    namespace = kubernetes_namespace.application.metadata[0].name
  }

  data = {
    "DB_HOST"     = aws_db_instance.main.endpoint
    "DB_PORT"     = "5432"
    "DB_NAME"     = aws_db_instance.main.db_name
    "DB_USERNAME" = aws_db_instance.main.username
  }
}

Bucket S3 avec accès IRSA

resource "aws_s3_bucket" "app_assets" {
  bucket = "${var.cluster_name}-app-assets"
}

resource "aws_iam_policy" "web_app_s3_access" {
  name        = "${var.cluster_name}-web-app-s3-access"
  description = "Accès S3 pour l'application web"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "s3:GetObject",
          "s3:PutObject",
          "s3:ListBucket",
          "s3:DeleteObject"
        ]
        Resource = [
          aws_s3_bucket.app_assets.arn,
          "${aws_s3_bucket.app_assets.arn}/*"
        ]
      }
    ]
  })
}

Exemple Complet : Le Main File

Voici comment tout s'assemble dans le fichier principal :

# environments/prod/main.tf
module "vpc" {
  source = "../../modules/vpc"

  cluster_name         = var.cluster_name
  vpc_cidr             = "10.0.0.0/16"
  public_subnet_cidrs  = ["10.0.1.0/24", "10.0.2.0/24"]
  private_subnet_cidrs = ["10.0.10.0/24", "10.0.20.0/24"]
}

module "eks" {
  source = "../../modules/eks"

  cluster_name        = var.cluster_name
  kubernetes_version  = "1.29"
  vpc_id              = module.vpc.vpc_id
  vpc_cidr            = "10.0.0.0/16"
  public_subnet_ids   = module.vpc.public_subnet_ids
  private_subnet_ids  = module.vpc.private_subnet_ids
  node_instance_types = ["t3.large"]
  node_desired_size   = 3
  node_min_size       = 2
  node_max_size       = 10
  environment         = var.environment
}

# Puis les ressources Kubernetes, Helm, RDS, S3...
# comme détaillé dans les sections précédentes

Bonnes Pratiques et Pièges à Éviter

1. Dépendances entre providers

Un piège classique est la dépendance circulaire : le provider Kubernetes a besoin du cluster EKS, mais celui-ci n'existe pas encore lors du premier terraform apply. La solution est d'utiliser les data sources avec des depends_on explicites ou de séparer en deux phases.

2. Gestion du state

Séparez le state du cluster EKS et celui des ressources Kubernetes déployées dessus. Cela permet de recréer les déploiements sans toucher au cluster et vice versa.

3. Mises à jour du cluster

Les mises à jour de version Kubernetes doivent être planifiées soigneusement. Incrémentez d'une version mineure à la fois et testez en environnement de staging d'abord.

4. ignore_changes stratégique

Certains champs sont modifiés dynamiquement par Kubernetes (comme le nombre de replicas géré par un HPA). Utilisez ignore_changes pour éviter les conflits :

resource "kubernetes_deployment" "web_app" {
  # ...
  lifecycle {
    ignore_changes = [
      spec[0].replicas,  # Géré par HPA
    ]
  }
}

Conclusion

L'intégration de Terraform avec Kubernetes offre une approche puissante et cohérente pour gérer votre infrastructure cloud-native de bout en bout. En utilisant Terraform pour provisionner le cluster EKS, configurer les RBAC, déployer les composants d'infrastructure via Helm et lier les ressources cloud aux workloads Kubernetes, vous obtenez une infrastructure entièrement déclarative, versionnée et reproductible.

Les points clés à retenir sont les suivants. Utilisez Terraform pour le provisioning du cluster et l'infrastructure de base. Exploitez le provider Helm pour les composants standards comme l'ingress controller et le monitoring. Adoptez IRSA pour un accès sécurisé aux ressources AWS depuis les pods. Séparez vos states pour une gestion plus fine. Considérez un outil GitOps comme ArgoCD pour les déploiements applicatifs fréquents.

Dans le prochain article de cette série, nous explorerons comment importer des ressources existantes dans votre code Terraform, une compétence essentielle pour migrer progressivement vers l'Infrastructure as Code.

Vous vous êtes abonné avec succès à CodeClan
Parfait ! Ensuite, complétez le paiement pour obtenir un accès complet à tout le contenu premium.
Erreur ! Impossible de s'inscrire. Lien invalide.
Bienvenue ! Vous vous êtes connecté avec succès.
Erreur ! Impossible de se connecter. Veuillez réessayer.
Succès ! Votre compte est entièrement activé, vous avez maintenant accès à tout le contenu.
Erreur ! Le paiement Stripe a échoué.
Succès ! Vos informations de facturation sont mises à jour.
Erreur ! La mise à jour des informations de facturation a échoué.