Infrastructure as Code : Déployer Kubernetes (EKS) sur AWS avec Terraform
Infrastructure as Code (IaC) n’est plus une option en 2025 : c’est un standard. Fini les configurations manuelles, les “clics” dans la console AWS, les environnements qui dérivent entre dev/staging/prod.
Terraform + Kubernetes = le combo gagnant pour industrialiser vos déploiements cloud.
Dans cet article, je vous partage un guide complet et reproductible pour déployer un cluster Amazon EKS production-ready avec Terraform, en suivant les best practices AWS 2025 :
- ✅ VPC multi-AZ sécurisé
- ✅ EKS avec node groups managed + spot instances
- ✅ Pod Identity (nouvelle méthode IAM pour les workloads)
- ✅ EKS Access Entries (authentification IAM vers le cluster)
- ✅ Add-ons essentiels (ALB Controller, External DNS, Cert Manager)
- ✅ Observabilité native AWS (CloudWatch + ADOT)
- ✅ FinOps (tags, cost allocation, estimation coûts)
Pourquoi cet article ? Parce que trop de clusters EKS partent en production avec des configurations bancales : pas de multi-AZ, node groups mal dimensionnés, pas d’observabilité, facture AWS qui explose. Ce guide vous donne les fondations solides pour éviter ces erreurs.
1. Pourquoi Terraform pour Kubernetes sur AWS ?
Infrastructure as Code : les 3 piliers
- Reproductibilité : Même infra dev/staging/prod, zéro divergence
- Versioning : Infrastructure dans Git, rollback possible
- Collaboration : Équipe partage le même code, revues de code infra
Terraform vs alternatives
| Outil | Avantages | Inconvénients |
|---|---|---|
| Terraform | Multi-cloud, modules réutilisables, state management | Courbe apprentissage, state drift possible |
| CloudFormation | Natif AWS, intégration parfaite | Limité à AWS, syntaxe YAML verbeux |
| Pulumi | Langages de programmation (Python, Go) | Moins mature, communauté plus petite |
| CDK | Natif AWS, TypeScript/Python | Complexité supplémentaire, abstraction |
Mon choix : Terraform pour :
- Communauté énorme (modules community testés)
- Multi-cloud (AWS aujourd’hui, Azure demain si besoin)
- Syntaxe HCL claire et lisible
2. Architecture cible : Vue d’ensemble
Voici l’architecture EKS que nous allons déployer :
┌─────────────────────────────────────────────────────────────┐
│ AWS Account │
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ VPC (10.0.0.0/16) │ │
│ │ │ │
│ │ ┌─────────────────┐ ┌─────────────────┐ │ │
│ │ │ AZ eu-west-1a │ │ AZ eu-west-1b │ │ │
│ │ │ │ │ │ │ │
│ │ │ Public Subnet │ │ Public Subnet │ │ │
│ │ │ 10.0.1.0/24 │ │ 10.0.2.0/24 │ │ │
│ │ │ ┌───────────┐ │ │ ┌───────────┐ │ │ │
│ │ │ │ NAT GW │ │ │ │ NAT GW │ │ │ │
│ │ │ └───────────┘ │ │ └───────────┘ │ │ │
│ │ │ │ │ │ │ │
│ │ │ Private Subnet │ │ Private Subnet │ │ │
│ │ │ 10.0.11.0/24 │ │ 10.0.12.0/24 │ │ │
│ │ │ ┌───────────┐ │ │ ┌───────────┐ │ │ │
│ │ │ │ EKS Nodes │ │ │ │ EKS Nodes │ │ │ │
│ │ │ └───────────┘ │ │ └───────────┘ │ │ │
│ │ └─────────────────┘ └─────────────────┘ │ │
│ │ │ │
│ │ ┌──────────────────────────────────────────────────┐ │ │
│ │ │ EKS Control Plane (Managed AWS) │ │ │
│ │ └──────────────────────────────────────────────────┘ │ │
│ │ │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Observability: CloudWatch + ADOT + Container Insights│ │
│ └───────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
Composants clés :
- VPC multi-AZ : Haute disponibilité (2 AZ minimum)
- Subnets publics : NAT Gateways, Load Balancers
- Subnets privés : EKS worker nodes (pas d’IP publique)
- EKS Control Plane : Managé par AWS (HA native)
- Node Groups : Managed + Spot instances (FinOps)
- CloudWatch + ADOT : Observabilité native AWS
3. Prérequis
Outils à installer
# Terraform
brew install terraform
terraform -version # v1.6+
# AWS CLI
brew install awscli
aws --version # v2.13+
# kubectl
brew install kubectl
kubectl version --client
# (Optionnel) tfenv pour gérer versions Terraform
brew install tfenv
Configuration AWS
# Configurer credentials AWS
aws configure
# AWS Access Key ID: [votre key]
# AWS Secret Access Key: [votre secret]
# Default region: eu-west-1
# Default output format: json
# Vérifier accès
aws sts get-caller-identity
4. Setup initial : Backend Terraform
Problème : Par défaut, Terraform stocke son state localement. Impossible de collaborer, risque de perte.
Solution : Remote backend S3 + DynamoDB pour lock.
Créer le backend (one-time setup)
# Créer bucket S3 pour state
aws s3 mb s3://periscop-terraform-state-prod --region eu-west-1
# Activer versioning (rollback possible)
aws s3api put-bucket-versioning \
--bucket periscop-terraform-state-prod \
--versioning-configuration Status=Enabled
# Activer encryption
aws s3api put-bucket-encryption \
--bucket periscop-terraform-state-prod \
--server-side-encryption-configuration '{
"Rules": [{
"ApplyServerSideEncryptionByDefault": {
"SSEAlgorithm": "AES256"
}
}]
}'
Configuration Terraform backend
# backend.tf
terraform {
backend "s3" {
bucket = "periscop-terraform-state-prod"
key = "eks-cluster/terraform.tfstate"
region = "eu-west-1"
encrypt = true
}
required_version = ">= 1.6"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
kubernetes = {
source = "hashicorp/kubernetes"
version = "~> 2.23"
}
helm = {
source = "hashicorp/helm"
version = "~> 2.11"
}
}
}
provider "aws" {
region = var.aws_region
default_tags {
tags = {
Environment = var.environment
Project = "EKS-Cluster"
ManagedBy = "Terraform"
Owner = "DevOps"
}
}
}
5. VPC multi-AZ : Fondations réseau
Architecture VPC
Design :
- CIDR : 10.0.0.0/16 (65 536 IPs)
- 2 AZ : eu-west-1a, eu-west-1b (haute dispo)
- Subnets publics : 10.0.1.0/24, 10.0.2.0/24 (256 IPs chacun)
- Subnets privés : 10.0.11.0/24, 10.0.12.0/24 (workers EKS)
- NAT Gateways : 1 par AZ (haute dispo)
Code Terraform VPC
# vpc.tf
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.0"
name = "${var.cluster_name}-vpc"
cidr = "10.0.0.0/16"
azs = ["eu-west-1a", "eu-west-1b"]
private_subnets = ["10.0.11.0/24", "10.0.12.0/24"]
public_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
enable_nat_gateway = true
single_nat_gateway = false # 1 NAT par AZ (HA)
enable_dns_hostnames = true
enable_dns_support = true
# Tags requis pour EKS
public_subnet_tags = {
"kubernetes.io/role/elb" = "1"
"kubernetes.io/cluster/${var.cluster_name}" = "shared"
}
private_subnet_tags = {
"kubernetes.io/role/internal-elb" = "1"
"kubernetes.io/cluster/${var.cluster_name}" = "shared"
}
tags = {
Environment = var.environment
}
}
Pourquoi ces tags ?
kubernetes.io/role/elb: EKS sait créer Load Balancers publics icikubernetes.io/role/internal-elb: EKS sait créer Load Balancers privés ici
6. EKS Cluster : Control Plane + Node Groups
Control Plane EKS
# eks.tf
module "eks" {
source = "terraform-aws-modules/eks/aws"
version = "~> 19.0"
cluster_name = var.cluster_name
cluster_version = "1.28"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnets
# Control plane logs
cluster_enabled_log_types = [
"api",
"audit",
"authenticator",
"controllerManager",
"scheduler"
]
# Encryption secrets avec KMS
cluster_encryption_config = {
provider_key_arn = aws_kms_key.eks.arn
resources = ["secrets"]
}
# Public endpoint (pour kubectl depuis internet)
# Private endpoint (pour nodes dans VPC)
cluster_endpoint_public_access = true
cluster_endpoint_private_access = true
tags = {
Environment = var.environment
}
}
# KMS key pour encryption secrets
resource "aws_kms_key" "eks" {
description = "EKS Secret Encryption Key"
deletion_window_in_days = 7
enable_key_rotation = true
}
resource "aws_kms_alias" "eks" {
name = "alias/eks-${var.cluster_name}"
target_key_id = aws_kms_key.eks.key_id
}
Node Groups : Managed + Spot
# eks-node-groups.tf
module "eks" {
# ... (suite du module eks)
# Node group managed (on-demand, stable)
eks_managed_node_groups = {
general = {
name = "general-nodes"
instance_types = ["t3.medium"]
capacity_type = "ON_DEMAND"
min_size = 2
max_size = 6
desired_size = 2
# Labels Kubernetes
labels = {
role = "general"
}
# Taints (optionnel, ici aucun)
taints = []
tags = {
NodeGroup = "general"
}
}
# Node group spot (workloads non-critiques)
spot = {
name = "spot-nodes"
instance_types = ["t3.medium", "t3a.medium"]
capacity_type = "SPOT"
min_size = 1
max_size = 10
desired_size = 2
labels = {
role = "spot"
}
# Taint spot : workloads doivent tolérer
taints = [{
key = "spot"
value = "true"
effect = "NoSchedule"
}]
tags = {
NodeGroup = "spot"
}
}
}
}
Stratégie node groups :
- General (on-demand) : Workloads critiques (bases de données, apps principales)
- Spot : Workloads tolérants interruptions (jobs batch, CI/CD workers)
- FinOps : Spot instances = -70% coûts vs on-demand
7. IAM & Kubernetes Authentication
7.1 Pod Identity : Workloads → AWS Services
Problème : Comment donner permissions IAM aux pods (accès S3, DynamoDB, etc.) ?
Solution moderne (2023+) : EKS Pod Identity
Avantages vs IRSA (legacy) :
- ✅ Plus simple (pas besoin d’OIDC provider)
- ✅ Plus performant (credentials refresh automatique)
- ✅ Meilleur pour multi-cluster
Activer Pod Identity sur le cluster
# eks-pod-identity.tf
resource "aws_eks_addon" "pod_identity" {
cluster_name = module.eks.cluster_name
addon_name = "eks-pod-identity-agent"
addon_version = "<Check last availlable version>"
}
Exemple : Pod avec accès S3
# Créer IAM role pour l'application
resource "aws_iam_role" "app_s3_access" {
name = "eks-app-s3-access"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = {
Service = "pods.eks.amazonaws.com"
}
Action = "sts:AssumeRole"
}]
})
}
# Policy S3 read-only
resource "aws_iam_role_policy_attachment" "app_s3_read" {
role = aws_iam_role.app_s3_access.name
policy_arn = "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
}
# Association Pod Identity
resource "aws_eks_pod_identity_association" "app_s3" {
cluster_name = module.eks.cluster_name
namespace = "default"
service_account = "my-app-sa"
role_arn = aws_iam_role.app_s3_access.arn
}
Déploiement Kubernetes avec Pod Identity
# Créer ServiceAccount
apiVersion: v1
kind: ServiceAccount
metadata:
name: my-app-sa
namespace: default
---
# Pod utilisant ServiceAccount
apiVersion: v1
kind: Pod
metadata:
name: my-app
namespace: default
spec:
serviceAccountName: my-app-sa
containers:
- name: app
image: my-app:1.0
env:
- name: AWS_REGION
value: "eu-west-1"
# Credentials IAM automatiquement injectées !
Résultat : Le pod peut lister buckets S3 avec aws s3 ls automatiquement.
7.2 EKS Access Entries : IAM Users/Roles → Cluster Access
Problème : Comment donner accès kubectl à un rôle IAM (ex: DevOps team) ?
Solution moderne (2023+) : EKS Access Entries (remplace aws-auth ConfigMap)
Mapper un rôle IAM admin au cluster
# eks-access.tf
resource "aws_eks_access_entry" "admin" {
cluster_name = module.eks.cluster_name
principal_arn = "arn:aws:iam::123456789012:role/DevOpsAdminRole"
type = "STANDARD"
}
resource "aws_eks_access_policy_association" "admin_policy" {
cluster_name = module.eks.cluster_name
principal_arn = "arn:aws:iam::123456789012:role/DevOpsAdminRole"
policy_arn = "arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy"
access_scope {
type = "cluster"
}
}
Policies disponibles :
AmazonEKSClusterAdminPolicy: Full admin (équivalentsystem:masters)AmazonEKSAdminPolicy: Admin namespaceAmazonEKSEditPolicy: Edit resourcesAmazonEKSViewPolicy: Read-only
Tester l’accès
# Assumer le rôle IAM DevOpsAdminRole
aws sts assume-role --role-arn arn:aws:iam::123456789012:role/DevOpsAdminRole --role-session-name test
# Configurer kubeconfig
aws eks update-kubeconfig --name my-eks-cluster --region eu-west-1
# Tester accès
kubectl get nodes
8. Add-ons EKS : Load Balancer, DNS, Certificates
8.1 AWS Load Balancer Controller
Rôle : Créer ALB/NLB automatiquement depuis manifests Kubernetes
# eks-addons.tf
# IAM role pour Load Balancer Controller
resource "aws_iam_role" "lb_controller" {
name = "eks-lb-controller"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = {
Service = "pods.eks.amazonaws.com"
}
Action = "sts:AssumeRole"
}]
})
}
resource "aws_iam_role_policy_attachment" "lb_controller" {
role = aws_iam_role.lb_controller.name
policy_arn = "arn:aws:iam::aws:policy/ElasticLoadBalancingFullAccess"
}
resource "aws_eks_pod_identity_association" "lb_controller" {
cluster_name = module.eks.cluster_name
namespace = "kube-system"
service_account = "aws-load-balancer-controller"
role_arn = aws_iam_role.lb_controller.arn
}
# Déployer via Helm
resource "helm_release" "lb_controller" {
name = "aws-load-balancer-controller"
repository = "https://aws.github.io/eks-charts"
chart = "aws-load-balancer-controller"
namespace = "kube-system"
set {
name = "clusterName"
value = module.eks.cluster_name
}
set {
name = "serviceAccount.create"
value = "true"
}
set {
name = "serviceAccount.name"
value = "aws-load-balancer-controller"
}
}
8.2 External DNS
Rôle : Créer automatiquement DNS Route53 depuis Ingress
# IAM role pour External DNS
resource "aws_iam_role" "external_dns" {
name = "eks-external-dns"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = {
Service = "pods.eks.amazonaws.com"
}
Action = "sts:AssumeRole"
}]
})
}
resource "aws_iam_role_policy_attachment" "external_dns" {
role = aws_iam_role.external_dns.name
policy_arn = "arn:aws:iam::aws:policy/AmazonRoute53FullAccess"
}
resource "aws_eks_pod_identity_association" "external_dns" {
cluster_name = module.eks.cluster_name
namespace = "kube-system"
service_account = "external-dns"
role_arn = aws_iam_role.external_dns.arn
}
resource "helm_release" "external_dns" {
name = "external-dns"
repository = "https://kubernetes-sigs.github.io/external-dns/"
chart = "external-dns"
namespace = "kube-system"
set {
name = "serviceAccount.create"
value = "true"
}
set {
name = "serviceAccount.name"
value = "external-dns"
}
set {
name = "policy"
value = "sync"
}
}
8.3 Cert Manager (TLS automatique)
resource "helm_release" "cert_manager" {
name = "cert-manager"
repository = "https://charts.jetstack.io"
chart = "cert-manager"
namespace = "cert-manager"
create_namespace = true
set {
name = "installCRDs"
value = "true"
}
}
9. Observabilité : CloudWatch + ADOT
9.1 Pourquoi CloudWatch + ADOT ?
ADOT (AWS Distro for OpenTelemetry) = distribution OpenTelemetry optimisée AWS
Avantages :
- ✅ Intégration native AWS (CloudWatch, X-Ray)
- ✅ Pas d’infra à gérer (vs Prometheus self-hosted)
- ✅ Coûts prévisibles (vs explosion métriques Prometheus)
- ✅ Support AWS officiel
9.2 Activer CloudWatch Container Insights
# cloudwatch.tf
resource "aws_eks_addon" "adot" {
cluster_name = module.eks.cluster_name
addon_name = "adot"
addon_version = "<Check last availlable version>"
}
# IAM role pour ADOT Collector
resource "aws_iam_role" "adot_collector" {
name = "eks-adot-collector"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = {
Service = "pods.eks.amazonaws.com"
}
Action = "sts:AssumeRole"
}]
})
}
resource "aws_iam_role_policy_attachment" "adot_cloudwatch" {
role = aws_iam_role.adot_collector.name
policy_arn = "arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy"
}
resource "aws_eks_pod_identity_association" "adot" {
cluster_name = module.eks.cluster_name
namespace = "opentelemetry-operator-system"
service_account = "adot-collector"
role_arn = aws_iam_role.adot_collector.arn
}
9.3 Déployer ADOT Collector
# adot-collector.yaml
apiVersion: opentelemetry.io/v1alpha1
kind: OpenTelemetryCollector
metadata:
name: adot-collector
namespace: opentelemetry-operator-system
spec:
mode: daemonset
serviceAccount: adot-collector
config: |
receivers:
prometheus:
config:
scrape_configs:
- job_name: 'kubernetes-pods'
kubernetes_sd_configs:
- role: pod
processors:
batch:
timeout: 60s
exporters:
awsemf:
namespace: EKS/ContainerInsights
region: eu-west-1
log_group_name: /aws/eks/cluster-logs
dimension_rollup_option: NoDimensionRollup
service:
pipelines:
metrics:
receivers: [prometheus]
processors: [batch]
exporters: [awsemf]
9.4 CloudWatch Dashboards
Métriques disponibles :
- CPU/Memory par pod, node, namespace
- Network in/out
- Disk I/O
- Nombre de pods running/pending/failed
Dashboards automatiques : CloudWatch Container Insights génère dashboards EKS clés-en-main.
10. Teaser GitOps : Le futur de vos déploiements
Problème actuel : Infrastructure déployée avec Terraform, mais applications K8s ? kubectl apply manuel ?
Solution : GitOps avec ArgoCD ou Flux.
Principe :
- Git = source de vérité pour apps Kubernetes
- ArgoCD sync automatiquement Git → Cluster
- Rollback =
git revert - Audit complet (qui a déployé quoi, quand)
Exemple workflow :
Dev commit manifest YAML → Git
↓
ArgoCD détecte changement
↓
ArgoCD applique sur cluster EKS
↓
App déployée automatiquement
Dans un prochain article, je vous montrerai comment déployer ArgoCD sur ce cluster EKS et mettre en place un pipeline GitOps complet pour vos applications. Stay tuned ! 🚀
11. FinOps : Maîtriser les coûts EKS
11.1 Estimation coûts mensuelle
Cluster EKS (control plane) : ~€70/mois
Node groups :
- 2x t3.medium on-demand (general) : ~€60/mois
- 2x t3.medium spot (70% discount) : ~€18/mois
NAT Gateways (2 AZ) : ~€70/mois
Load Balancers (ALB) : ~€20/mois
CloudWatch Logs : ~€10/mois (5 GB)
Total estimé : ~€250-300/mois
11.2 Tags pour cost allocation
# Tous les tags sont propagés automatiquement via default_tags
provider "aws" {
default_tags {
tags = {
Environment = var.environment
Project = "EKS-Cluster"
ManagedBy = "Terraform"
Owner = "DevOps"
CostCenter = "Engineering"
}
}
}
AWS Cost Explorer peut ensuite grouper coûts par tag (Environment, Project, etc.).
11.3 Optimisations FinOps
- Spot instances : Node group spot = -70% coûts
- Cluster Autoscaler : Scale down nodes inutilisés
- Single NAT Gateway : 1 NAT au lieu de 2 si budget serré (trade-off HA)
- Reserved Instances : Si usage prévisible (engagement 1-3 ans)
- Savings Plans : Compute Savings Plans AWS (-40% à -60%)
12. Déploiement : Les commandes
Structure projet
eks-terraform/
├── backend.tf # Backend S3 + DynamoDB
├── provider.tf # Provider AWS
├── variables.tf # Variables
├── vpc.tf # Module VPC
├── eks.tf # Module EKS
├── eks-node-groups.tf # Node groups
├── eks-pod-identity.tf # Pod Identity
├── eks-access.tf # EKS Access Entries
├── eks-addons.tf # Add-ons (LB, DNS, Cert)
├── cloudwatch.tf # ADOT + CloudWatch
└── outputs.tf # Outputs
Variables (terraform.tfvars)
# terraform.tfvars
aws_region = "eu-west-1"
environment = "production"
cluster_name = "periscop-eks-prod"
Déployer
# 1. Initialiser Terraform
terraform init
# 2. Valider configuration
terraform validate
# 3. Plan (dry-run)
terraform plan
# 4. Appliquer (créer infra)
terraform apply
# Taper "yes" pour confirmer
# 5. Configurer kubectl
aws eks update-kubeconfig --name periscop-eks-prod --region eu-west-1
# 6. Vérifier nodes
kubectl get nodes
# 7. Déployer ADOT Collector
kubectl apply -f adot-collector.yaml
Détruire (cleanup)
# Attention : supprime TOUT
terraform destroy
13. Conclusion : Industrialiser vos déploiements Kubernetes
Vous avez maintenant un cluster EKS production-ready déployé avec Terraform :
- Infrastructure : VPC multi-AZ, NAT Gateways, subnets publics/privés
- Kubernetes : EKS 1.28, node groups managed + spot
- Sécurité : Encryption KMS, Pod Identity, EKS Access Entries
- Add-ons : ALB Controller, External DNS, Cert Manager
- Observabilité : CloudWatch + ADOT Container Insights
- FinOps : Tags, spot instances, estimation coûts
Ce que ce cluster vous apporte :
- Reproductibilité : Recréer l’infra en 15 min
- Collaboration : Équipe partage le même code Terraform
- Évolutivité : Ajout d’environnements (staging, dev) en quelques lignes
- Conformité : Logs, encryption, RBAC, audits
Prochaines étapes :
- Déployer vos applications : Créer Deployments, Services, Ingress
- GitOps : Installer ArgoCD (prochain article !)
- Monitoring avancé : Alertes CloudWatch, dashboards custom
- Sécurité : Network Policies, Pod Security Standards, OPA
- Scaling : Cluster Autoscaler, Karpenter (next-gen autoscaling)
Besoin d’aide pour déployer votre infra Kubernetes sur AWS avec Terraform ? PeriScop vous accompagne : audit architecture, déploiement cluster EKS production, formation équipe DevOps/SRE.