Projet 3 : Cluster Kubernetes (EKS)
Contexte
Votre entreprise migre vers une architecture microservices. Vous devez créer un cluster Kubernetes sur AWS (EKS) qui sera la plateforme d'exécution pour toutes les applications.
Exigences
- Cluster EKS version 1.28+
- Node groups managés avec Auto Scaling
- OIDC provider pour les IAM Roles des pods
- AWS Load Balancer Controller
- Cluster Autoscaler
- Métriques avec CloudWatch Container Insights
Architecture cible
Structure du projet
projet-eks/
├── main.tf
├── variables.tf
├── outputs.tf
├── versions.tf
├── eks.tf
├── node-groups.tf
├── iam.tf
├── addons.tf
├── kubernetes.tf
└── terraform.tfvars
Implémentation
versions.tf
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
kubernetes = {
source = "hashicorp/kubernetes"
version = "~> 2.23"
}
helm = {
source = "hashicorp/helm"
version = "~> 2.11"
}
tls = {
source = "hashicorp/tls"
version = "~> 4.0"
}
}
}
variables.tf
variable "region" {
description = "AWS Region"
type = string
default = "eu-west-1"
}
variable "project_name" {
description = "Project name"
type = string
}
variable "environment" {
description = "Environment"
type = string
}
variable "vpc_id" {
description = "VPC ID"
type = string
}
variable "cluster_version" {
description = "EKS cluster version"
type = string
default = "1.28"
}
variable "node_instance_types" {
description = "Instance types for node groups"
type = list(string)
default = ["t3.large"]
}
variable "node_desired_size" {
description = "Desired number of nodes"
type = number
default = 2
}
variable "node_min_size" {
description = "Minimum number of nodes"
type = number
default = 1
}
variable "node_max_size" {
description = "Maximum number of nodes"
type = number
default = 5
}
variable "enable_spot_nodes" {
description = "Enable Spot instance node group"
type = bool
default = true
}
variable "cluster_endpoint_public_access" {
description = "Enable public access to cluster endpoint"
type = bool
default = true
}
variable "cluster_endpoint_private_access" {
description = "Enable private access to cluster endpoint"
type = bool
default = true
}
iam.tf
# EKS Cluster Role
resource "aws_iam_role" "eks_cluster" {
name = "${local.name_prefix}-eks-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
}
# Node Group Role
resource "aws_iam_role" "eks_nodes" {
name = "${local.name_prefix}-eks-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
}
# OIDC Provider pour IRSA (IAM Roles for Service Accounts)
data "tls_certificate" "eks" {
url = aws_eks_cluster.main.identity[0].oidc[0].issuer
}
resource "aws_iam_openid_connect_provider" "eks" {
client_id_list = ["sts.amazonaws.com"]
thumbprint_list = [data.tls_certificate.eks.certificates[0].sha1_fingerprint]
url = aws_eks_cluster.main.identity[0].oidc[0].issuer
tags = {
Name = "${local.name_prefix}-eks-oidc"
}
}
eks.tf
resource "aws_eks_cluster" "main" {
name = "${local.name_prefix}-cluster"
role_arn = aws_iam_role.eks_cluster.arn
version = var.cluster_version
vpc_config {
subnet_ids = data.aws_subnets.private.ids
endpoint_public_access = var.cluster_endpoint_public_access
endpoint_private_access = var.cluster_endpoint_private_access
security_group_ids = [aws_security_group.eks_cluster.id]
}
encryption_config {
resources = ["secrets"]
provider {
key_arn = aws_kms_key.eks.arn
}
}
enabled_cluster_log_types = [
"api",
"audit",
"authenticator",
"controllerManager",
"scheduler"
]
depends_on = [
aws_iam_role_policy_attachment.eks_cluster_policy,
aws_cloudwatch_log_group.eks
]
tags = {
Name = "${local.name_prefix}-cluster"
}
}
# KMS Key pour chiffrer les secrets
resource "aws_kms_key" "eks" {
description = "KMS key for EKS cluster ${local.name_prefix}"
deletion_window_in_days = 7
enable_key_rotation = true
tags = {
Name = "${local.name_prefix}-eks-kms"
}
}
resource "aws_kms_alias" "eks" {
name = "alias/${local.name_prefix}-eks"
target_key_id = aws_kms_key.eks.key_id
}
# CloudWatch Log Group pour les logs du cluster
resource "aws_cloudwatch_log_group" "eks" {
name = "/aws/eks/${local.name_prefix}-cluster/cluster"
retention_in_days = 30
}
# Security Group du cluster
resource "aws_security_group" "eks_cluster" {
name = "${local.name_prefix}-eks-cluster-sg"
description = "Security group for EKS cluster"
vpc_id = var.vpc_id
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${local.name_prefix}-eks-cluster-sg"
}
}
node-groups.tf
# Node Group On-Demand (General Purpose)
resource "aws_eks_node_group" "general" {
cluster_name = aws_eks_cluster.main.name
node_group_name = "${local.name_prefix}-general"
node_role_arn = aws_iam_role.eks_nodes.arn
subnet_ids = data.aws_subnets.private.ids
capacity_type = "ON_DEMAND"
instance_types = var.node_instance_types
disk_size = 50
scaling_config {
desired_size = var.node_desired_size
max_size = var.node_max_size
min_size = var.node_min_size
}
update_config {
max_unavailable_percentage = 25
}
labels = {
role = "general"
environment = var.environment
}
taint {
key = "dedicated"
value = "general"
effect = "NO_SCHEDULE"
}
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 = "${local.name_prefix}-general"
"k8s.io/cluster-autoscaler/${local.name_prefix}-cluster" = "owned"
"k8s.io/cluster-autoscaler/enabled" = "true"
}
lifecycle {
ignore_changes = [scaling_config[0].desired_size]
}
}
# Node Group Spot (Cost Optimized)
resource "aws_eks_node_group" "spot" {
count = var.enable_spot_nodes ? 1 : 0
cluster_name = aws_eks_cluster.main.name
node_group_name = "${local.name_prefix}-spot"
node_role_arn = aws_iam_role.eks_nodes.arn
subnet_ids = data.aws_subnets.private.ids
capacity_type = "SPOT"
instance_types = ["t3.large", "t3a.large", "m5.large", "m5a.large"]
disk_size = 50
scaling_config {
desired_size = 2
max_size = 10
min_size = 0
}
update_config {
max_unavailable_percentage = 50
}
labels = {
role = "spot"
environment = var.environment
"node.kubernetes.io/lifecycle" = "spot"
}
taint {
key = "spot"
value = "true"
effect = "NO_SCHEDULE"
}
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 = "${local.name_prefix}-spot"
"k8s.io/cluster-autoscaler/${local.name_prefix}-cluster" = "owned"
"k8s.io/cluster-autoscaler/enabled" = "true"
}
lifecycle {
ignore_changes = [scaling_config[0].desired_size]
}
}
addons.tf
# EKS Addons
resource "aws_eks_addon" "vpc_cni" {
cluster_name = aws_eks_cluster.main.name
addon_name = "vpc-cni"
resolve_conflicts_on_create = "OVERWRITE"
resolve_conflicts_on_update = "PRESERVE"
}
resource "aws_eks_addon" "coredns" {
cluster_name = aws_eks_cluster.main.name
addon_name = "coredns"
resolve_conflicts_on_create = "OVERWRITE"
resolve_conflicts_on_update = "PRESERVE"
depends_on = [aws_eks_node_group.general]
}
resource "aws_eks_addon" "kube_proxy" {
cluster_name = aws_eks_cluster.main.name
addon_name = "kube-proxy"
resolve_conflicts_on_create = "OVERWRITE"
resolve_conflicts_on_update = "PRESERVE"
}
resource "aws_eks_addon" "ebs_csi_driver" {
cluster_name = aws_eks_cluster.main.name
addon_name = "aws-ebs-csi-driver"
service_account_role_arn = aws_iam_role.ebs_csi.arn
resolve_conflicts_on_create = "OVERWRITE"
resolve_conflicts_on_update = "PRESERVE"
}
# IAM Role pour EBS CSI Driver
resource "aws_iam_role" "ebs_csi" {
name = "${local.name_prefix}-ebs-csi-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = {
Federated = aws_iam_openid_connect_provider.eks.arn
}
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"${replace(aws_iam_openid_connect_provider.eks.url, "https://", "")}:sub" = "system:serviceaccount:kube-system:ebs-csi-controller-sa"
"${replace(aws_iam_openid_connect_provider.eks.url, "https://", "")}:aud" = "sts.amazonaws.com"
}
}
}
]
})
}
resource "aws_iam_role_policy_attachment" "ebs_csi" {
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEBSCSIDriverPolicy"
role = aws_iam_role.ebs_csi.name
}
kubernetes.tf
# Configuration des providers Kubernetes et Helm
provider "kubernetes" {
host = aws_eks_cluster.main.endpoint
cluster_ca_certificate = base64decode(aws_eks_cluster.main.certificate_authority[0].data)
exec {
api_version = "client.authentication.k8s.io/v1beta1"
command = "aws"
args = ["eks", "get-token", "--cluster-name", aws_eks_cluster.main.name]
}
}
provider "helm" {
kubernetes {
host = aws_eks_cluster.main.endpoint
cluster_ca_certificate = base64decode(aws_eks_cluster.main.certificate_authority[0].data)
exec {
api_version = "client.authentication.k8s.io/v1beta1"
command = "aws"
args = ["eks", "get-token", "--cluster-name", aws_eks_cluster.main.name]
}
}
}
# AWS Load Balancer Controller
module "aws_load_balancer_controller" {
source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks"
role_name = "${local.name_prefix}-aws-lb-controller"
attach_load_balancer_controller_policy = true
oidc_providers = {
main = {
provider_arn = aws_iam_openid_connect_provider.eks.arn
namespace_service_accounts = ["kube-system:aws-load-balancer-controller"]
}
}
}
resource "helm_release" "aws_load_balancer_controller" {
name = "aws-load-balancer-controller"
repository = "https://aws.github.io/eks-charts"
chart = "aws-load-balancer-controller"
namespace = "kube-system"
version = "1.6.2"
set {
name = "clusterName"
value = aws_eks_cluster.main.name
}
set {
name = "serviceAccount.create"
value = "true"
}
set {
name = "serviceAccount.name"
value = "aws-load-balancer-controller"
}
set {
name = "serviceAccount.annotations.eks\\.amazonaws\\.com/role-arn"
value = module.aws_load_balancer_controller.iam_role_arn
}
depends_on = [aws_eks_node_group.general]
}
# Cluster Autoscaler
module "cluster_autoscaler" {
source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks"
role_name = "${local.name_prefix}-cluster-autoscaler"
attach_cluster_autoscaler_policy = true
cluster_autoscaler_cluster_names = [aws_eks_cluster.main.name]
oidc_providers = {
main = {
provider_arn = aws_iam_openid_connect_provider.eks.arn
namespace_service_accounts = ["kube-system:cluster-autoscaler"]
}
}
}
resource "helm_release" "cluster_autoscaler" {
name = "cluster-autoscaler"
repository = "https://kubernetes.github.io/autoscaler"
chart = "cluster-autoscaler"
namespace = "kube-system"
version = "9.29.0"
set {
name = "autoDiscovery.clusterName"
value = aws_eks_cluster.main.name
}
set {
name = "awsRegion"
value = var.region
}
set {
name = "rbac.serviceAccount.create"
value = "true"
}
set {
name = "rbac.serviceAccount.name"
value = "cluster-autoscaler"
}
set {
name = "rbac.serviceAccount.annotations.eks\\.amazonaws\\.com/role-arn"
value = module.cluster_autoscaler.iam_role_arn
}
depends_on = [aws_eks_node_group.general]
}
outputs.tf
output "cluster_name" {
description = "EKS cluster name"
value = aws_eks_cluster.main.name
}
output "cluster_endpoint" {
description = "EKS cluster endpoint"
value = aws_eks_cluster.main.endpoint
}
output "cluster_certificate_authority_data" {
description = "Base64 encoded certificate data"
value = aws_eks_cluster.main.certificate_authority[0].data
sensitive = true
}
output "cluster_oidc_issuer_url" {
description = "OIDC Identity issuer URL"
value = aws_eks_cluster.main.identity[0].oidc[0].issuer
}
output "cluster_oidc_provider_arn" {
description = "OIDC provider ARN"
value = aws_iam_openid_connect_provider.eks.arn
}
output "kubeconfig_command" {
description = "Command to update kubeconfig"
value = "aws eks update-kubeconfig --region ${var.region} --name ${aws_eks_cluster.main.name}"
}
Déploiement
# 1. Initialiser
terraform init
# 2. Déployer
terraform apply
# 3. Configurer kubectl
aws eks update-kubeconfig --region eu-west-1 --name $(terraform output -raw cluster_name)
# 4. Vérifier le cluster
kubectl get nodes
kubectl get pods -A
Tester le cluster
# Déployer une application test
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-test
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:alpine
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: nginx-test
annotations:
service.beta.kubernetes.io/aws-load-balancer-type: external
service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
spec:
type: LoadBalancer
selector:
app: nginx
ports:
- port: 80
targetPort: 80
Coûts estimés
| Ressource | Coût mensuel |
|---|---|
| EKS Control Plane | $72 |
| Node Group On-Demand (2x t3.large) | ~$120 |
| Node Group Spot (3x t3.large) | ~$50 |
| CloudWatch Logs | ~$10 |
| Total | ~$250/mois |