Aller au contenu principal

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

RessourceCoû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

← Projet 2 | Projet 4: Architecture Serverless →