Amazon EKS — Terraform EKS Cluster Module
This module creates the EKS control plane, managed node groups, and the OIDC provider for IRSA. It encapsulates the core EKS infrastructure.
EKS Module — variables.tf
# modules/eks/variables.tf
variable "cluster_name" {
description = "EKS cluster name"
type = string
}
variable "cluster_version" {
description = "Kubernetes version"
type = string
default = "1.31"
}
variable "environment" {
description = "Environment name"
type = string
}
variable "vpc_id" {
description = "VPC ID"
type = string
}
variable "subnet_ids" {
description = "Private subnet IDs for EKS"
type = list(string)
}
variable "node_instance_types" {
description = "EC2 instance types for managed node group"
type = list(string)
default = ["m6i.large", "m5.large"]
}
variable "node_desired_size" {
type = number
default = 2
}
variable "node_min_size" {
type = number
default = 1
}
variable "node_max_size" {
type = number
default = 5
}
EKS Module — main.tf
# modules/eks/main.tf
# ── Cluster IAM Role ──
resource "aws_iam_role" "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" "cluster_policy" {
policy_arn = "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy"
role = aws_iam_role.cluster.name
}
# ── EKS Cluster ──
resource "aws_eks_cluster" "main" {
name = var.cluster_name
version = var.cluster_version
role_arn = aws_iam_role.cluster.arn
vpc_config {
subnet_ids = var.subnet_ids
endpoint_private_access = true
endpoint_public_access = true
public_access_cidrs = ["0.0.0.0/0"] # Restrict in production
}
enabled_cluster_log_types = [
"api",
"audit",
"authenticator",
"controllerManager",
"scheduler"
]
access_config {
authentication_mode = "API_AND_CONFIG_MAP"
bootstrap_cluster_creator_admin_permissions = true
}
depends_on = [
aws_iam_role_policy_attachment.cluster_policy,
]
}
# ── OIDC Provider for IRSA ──
data "tls_certificate" "cluster" {
url = aws_eks_cluster.main.identity[0].oidc[0].issuer
}
resource "aws_iam_openid_connect_provider" "cluster" {
client_id_list = ["sts.amazonaws.com"]
thumbprint_list = [data.tls_certificate.cluster.certificates[0].sha1_fingerprint]
url = aws_eks_cluster.main.identity[0].oidc[0].issuer
}
# ── Node Group IAM Role ──
resource "aws_iam_role" "node" {
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" "node_worker" {
policy_arn = "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy"
role = aws_iam_role.node.name
}
resource "aws_iam_role_policy_attachment" "node_cni" {
policy_arn = "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy"
role = aws_iam_role.node.name
}
resource "aws_iam_role_policy_attachment" "node_ecr" {
policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly"
role = aws_iam_role.node.name
}
resource "aws_iam_role_policy_attachment" "node_ssm" {
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
role = aws_iam_role.node.name
}
# ── Managed Node Group ──
resource "aws_eks_node_group" "main" {
cluster_name = aws_eks_cluster.main.name
node_group_name = "${var.cluster_name}-main"
node_role_arn = aws_iam_role.node.arn
subnet_ids = var.subnet_ids
instance_types = var.node_instance_types
capacity_type = "ON_DEMAND"
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 = {
Environment = var.environment
NodeGroup = "main"
}
depends_on = [
aws_iam_role_policy_attachment.node_worker,
aws_iam_role_policy_attachment.node_cni,
aws_iam_role_policy_attachment.node_ecr,
]
lifecycle {
ignore_changes = [scaling_config[0].desired_size]
}
}
EKS Module — outputs.tf
# modules/eks/outputs.tf
output "cluster_name" {
value = aws_eks_cluster.main.name
}
output "cluster_endpoint" {
value = aws_eks_cluster.main.endpoint
}
output "cluster_ca_certificate" {
value = aws_eks_cluster.main.certificate_authority[0].data
}
output "cluster_version" {
value = aws_eks_cluster.main.version
}
output "cluster_security_group_id" {
value = aws_eks_cluster.main.vpc_config[0].cluster_security_group_id
}
output "oidc_provider" {
description = "OIDC provider (without https://)"
value = replace(aws_eks_cluster.main.identity[0].oidc[0].issuer, "https://", "")
}
output "oidc_provider_arn" {
value = aws_iam_openid_connect_provider.cluster.arn
}
output "oidc_issuer_url" {
value = aws_eks_cluster.main.identity[0].oidc[0].issuer
}
output "node_role_arn" {
value = aws_iam_role.node.arn
}
Key Design Decisions
| Decision | Reasoning |
|---|---|
ignore_changes on desired_size |
Cluster Autoscaler / Karpenter manages scaling |
| SSM policy on node role | SSH-free node access via SSM Session Manager |
API_AND_CONFIG_MAP auth |
Supports both access entries and aws-auth (migration path) |
| All control plane logs enabled | Essential for troubleshooting |
| OIDC provider in same module | Tightly coupled with cluster identity |