Amazon EKS — Terraform EKS Module

3 minute read

For production EKS clusters, Terraform gives you full control, state management, and repeatability. The official terraform-aws-modules/eks/aws module simplifies the setup significantly.


Why Terraform for EKS?

Benefit Description
Reproducible Same config creates identical clusters every time
State Management Track what exists via S3 + DynamoDB remote state
Modular Separate VPC, EKS, add-ons into reusable modules
Plan Before Apply terraform plan shows exactly what will change
Multi-Environment Same modules for dev, staging, and prod
Version Control Infrastructure changes tracked in Git

Project Structure

terraform/
├── main.tf              # Provider and module calls
├── variables.tf         # Input variables
├── outputs.tf           # Output values
├── versions.tf          # Provider version constraints
├── terraform.tfvars     # Variable values
└── backend.tf           # S3 remote state config

Provider Configuration

# versions.tf
terraform {
  required_version = ">= 1.5.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
    kubernetes = {
      source  = "hashicorp/kubernetes"
      version = "~> 2.25"
    }
    helm = {
      source  = "hashicorp/helm"
      version = "~> 2.12"
    }
  }
}

provider "aws" {
  region = var.region
}

# Configure Kubernetes provider to use the EKS cluster
provider "kubernetes" {
  host                   = module.eks.cluster_endpoint
  cluster_ca_certificate = base64decode(module.eks.cluster_certificate_authority_data)

  exec {
    api_version = "client.authentication.k8s.io/v1beta1"
    command     = "aws"
    args        = ["eks", "get-token", "--cluster-name", module.eks.cluster_name]
  }
}

VPC Module

# main.tf — VPC
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.0"

  name = "${var.cluster_name}-vpc"
  cidr = "10.0.0.0/16"

  azs             = ["${var.region}a", "${var.region}b", "${var.region}c"]
  private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
  public_subnets  = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]

  enable_nat_gateway   = true
  single_nat_gateway   = false  # One per AZ for production HA
  enable_dns_hostnames = true
  enable_dns_support   = true

  # Tags required for EKS subnet discovery
  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
    Terraform   = "true"
  }
}

EKS Module

# main.tf — EKS
module "eks" {
  source  = "terraform-aws-modules/eks/aws"
  version = "~> 20.0"

  cluster_name    = var.cluster_name
  cluster_version = "1.29"

  cluster_endpoint_public_access  = true
  cluster_endpoint_private_access = true

  # EKS Add-ons
  cluster_addons = {
    coredns = {
      most_recent = true
    }
    kube-proxy = {
      most_recent = true
    }
    vpc-cni = {
      most_recent    = true
      before_compute = true  # Ensure VPC CNI is ready before nodes
      configuration_values = jsonencode({
        env = {
          ENABLE_PREFIX_DELEGATION = "true"
        }
      })
    }
    aws-ebs-csi-driver = {
      most_recent              = true
      service_account_role_arn = module.ebs_csi_irsa.iam_role_arn
    }
  }

  vpc_id     = module.vpc.vpc_id
  subnet_ids = module.vpc.private_subnets

  # Cluster access
  enable_cluster_creator_admin_permissions = true

  # Managed Node Groups
  eks_managed_node_groups = {
    general = {
      ami_type       = "AL2023_x86_64_STANDARD"
      instance_types = ["m5.large"]

      min_size     = 2
      max_size     = 6
      desired_size = 3

      labels = {
        role = "general"
      }

      tags = {
        Environment = var.environment
      }
    }

    spot = {
      ami_type       = "AL2023_x86_64_STANDARD"
      instance_types = ["m5.large", "m5a.large", "m4.large"]
      capacity_type  = "SPOT"

      min_size     = 0
      max_size     = 10
      desired_size = 2

      labels = {
        role = "spot-worker"
      }
    }
  }

  tags = {
    Environment = var.environment
    Terraform   = "true"
  }
}

Variables

# variables.tf
variable "region" {
  description = "AWS region"
  type        = string
  default     = "us-east-1"
}

variable "cluster_name" {
  description = "EKS cluster name"
  type        = string
  default     = "my-eks-cluster"
}

variable "environment" {
  description = "Environment name"
  type        = string
  default     = "production"
}

Outputs

# outputs.tf
output "cluster_name" {
  description = "EKS cluster name"
  value       = module.eks.cluster_name
}

output "cluster_endpoint" {
  description = "EKS cluster API endpoint"
  value       = module.eks.cluster_endpoint
}

output "cluster_version" {
  description = "EKS cluster Kubernetes version"
  value       = module.eks.cluster_version
}

output "configure_kubectl" {
  description = "Command to configure kubectl"
  value       = "aws eks update-kubeconfig --region ${var.region} --name ${module.eks.cluster_name}"
}

Remote State Backend

# backend.tf
terraform {
  backend "s3" {
    bucket         = "my-terraform-state-bucket"
    key            = "eks/terraform.tfstate"
    region         = "us-east-1"
    dynamodb_table = "terraform-lock"
    encrypt        = true
  }
}

Deploy

# Initialize
terraform init

# Review the plan
terraform plan

# Apply
terraform apply

# Configure kubectl
aws eks update-kubeconfig --region us-east-1 --name my-eks-cluster

# Verify
kubectl get nodes

When to Choose Terraform Over eksctl

Scenario Use
Learning, quick experiments eksctl
Production clusters Terraform
Multi-environment setups Terraform
Need to manage VPC separately Terraform
CI/CD pipeline for infrastructure Terraform
Team collaboration on infra Terraform
One-off cluster, no IaC requirement eksctl

What’s Next?

For completeness, let’s also look at creating a cluster via the AWS Console.

Next: AWS Console Setup →

Updated: