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.

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 planmontre 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.