Create an EKS cluster with Terraform
This guide uses the community-maintained terraform-aws-modules/vpc and terraform-aws-modules/eks modules. Pin exact versions in real repos and read each module’s changelog before upgrading—APIs and defaults change between major versions.
Assumptions: Non-production account or dedicated sandbox; you can create VPCs, IAM roles, and EKS clusters. Adjust CIDRs, regions, and instance sizes for your org.
Recommended project layout
Section titled “Recommended project layout”Split files so reviews stay readable:
| File | Purpose |
|---|---|
versions.tf | Terraform + provider version constraints |
providers.tf | AWS provider, region, default tags |
variables.tf / terraform.tfvars | Region, cluster name, CIDRs |
vpc.tf | VPC module |
eks.tf | EKS cluster, node groups, add-ons |
outputs.tf | cluster_name, vpc_id, etc. |
Provider and versions
Section titled “Provider and versions”terraform { required_version = ">= 1.5.0"
required_providers { aws = { source = "hashicorp/aws" version = "~> 5.0" } }}provider "aws" { region = var.region
default_tags { tags = { Project = var.project_name ManagedBy = "terraform" } }}Use a regional provider for EKS; avoid mixing deprecated aws_eks_cluster patterns from old examples.
VPC: multi-AZ private subnets for nodes
Section titled “VPC: multi-AZ private subnets for nodes”EKS worker nodes should sit in private subnets with outbound internet via NAT (pull images, call AWS APIs). Public subnets are often used for internet-facing load balancers; tag them so Kubernetes can place ELBs correctly.
Illustrative vpc.tf (simplify CIDRs for your design):
data "aws_availability_zones" "available" { state = "available"}
module "vpc" { source = "terraform-aws-modules/vpc/aws" version = "~> 5.0" # Pin a specific version in production
name = "${var.cluster_name}-vpc" cidr = var.vpc_cidr
azs = slice(data.aws_availability_zones.available.names, 0, 3) private_subnets = var.private_subnet_cidrs public_subnets = var.public_subnet_cidrs
enable_nat_gateway = true single_nat_gateway = false # one NAT per AZ for HA; true saves cost in dev enable_dns_hostnames = true enable_dns_support = true
public_subnet_tags = { "kubernetes.io/role/elb" = "1" }
private_subnet_tags = { "kubernetes.io/role/internal-elb" = "1" }
tags = { "kubernetes.io/cluster/${var.cluster_name}" = "shared" }}kubernetes.io/cluster/<name> = shared— Marks subnets usable by the cluster (multiple clusters can share if you align tagging strategy with your platform team).- NAT cost —
single_nat_gateway = trueis cheaper but a single AZ failure can break outbound traffic from private subnets in other AZs.
EKS cluster: private API endpoint
Section titled “EKS cluster: private API endpoint”A private-only API server endpoint means the Kubernetes API is reachable only from inside the VPC (and paths you extend, e.g. VPN). That is the default posture for many production clusters.
Tradeoffs:
| Endpoint access | Pros | Cons |
|---|---|---|
| Private only | API not exposed on the public internet | You need VPN, Direct Connect, or a bastion to run kubectl from your laptop |
| Public + private | Easy kubectl from anywhere if authorized | Larger attack surface; must lock down with IAM + optional CIDR allowlists |
| Public disabled, private enabled | Strong default for prod | Same as private-only |
Illustrative eks.tf (structure only—align version and arguments with the registry doc for the version you pin):
module "eks" { source = "terraform-aws-modules/eks/aws" version = "~> 20.0" # Pin a specific version in production
cluster_name = var.cluster_name cluster_version = var.kubernetes_version
cluster_endpoint_public_access = false cluster_endpoint_private_access = true
vpc_id = module.vpc.vpc_id subnet_ids = module.vpc.private_subnets control_plane_subnet_ids = module.vpc.private_subnets
# Customer-managed KMS for Kubernetes secrets (see module README for your pinned version) create_kms_key = true
eks_managed_node_groups = { main = { name = "main" instance_types = ["m5.large"] min_size = 2 max_size = 6 desired_size = 2
labels = { role = "general" } } }
tags = var.common_tags}Managed node groups give AWS-managed AMIs and simplified rolling upgrades for workers. Fargate is an alternative (no nodes to patch); this guide focuses on node groups for a typical platform baseline.
Cluster add-ons
Section titled “Cluster add-ons”Install core add-ons through the EKS API so versions stay compatible with the control plane. The EKS module can manage cluster_addons, for example:
- vpc-cni — Pod networking in the VPC.
- coredns — In-cluster DNS.
- kube-proxy — Service proxying on nodes.
- aws-ebs-csi-driver — EBS CSI so
PersistentVolumeClaimwithgp3(etc.) works.
Example shape (attribute names differ by module version—copy from the module’s cluster_addons example):
cluster_addons = { vpc-cni = { most_recent = true } coredns = { most_recent = true } kube-proxy = { most_recent = true } aws-ebs-csi-driver = { most_recent = true } }The EBS CSI driver needs an IAM role (often created via the module’s IRSA integration). If add-ons fail to schedule, check node group IAM policies and subnet routing first.
IRSA (IAM Roles for Service Accounts)
Section titled “IRSA (IAM Roles for Service Accounts)”Pods should not use broad node-instance credentials. IRSA maps a Kubernetes service account to an IAM role via OIDC. Use it so only specific workloads can call S3, SQS, etc.
High level: EKS exposes an OIDC issuer URL; you create an IAM OIDC provider for the cluster, then attach roles trustable by selected service accounts. See AWS: IAM roles for service accounts. The Terraform EKS module can create the OIDC provider and output values you need for IRSA role trust policies.
Outputs and kubectl
Section titled “Outputs and kubectl”output "cluster_name" { value = module.eks.cluster_name}
output "cluster_endpoint" { value = module.eks.cluster_endpoint sensitive = true}
output "configure_kubectl" { value = "aws eks update-kubeconfig --region ${var.region} --name ${module.eks.cluster_name}"}From a host that can reach the private API (see next section):
aws eks update-kubeconfig --region <region> --name <cluster_name>kubectl get nodeskubectl get pods -AReaching a private API from your workstation
Section titled “Reaching a private API from your workstation”Pick one primary pattern for your team:
- Site-to-Site VPN or Client VPN — Your laptop or office network has routes into the VPC; the API server’s private IP/DNS resolves inside the VPC. Aligns with VPC connectivity topics.
- AWS Direct Connect — Same idea for data-center–linked networks.
- Bastion / jump host — EC2 in a public subnet with SSM Session Manager (no SSH keys on the internet); run
kubectlonly on the bastion, or use SSH port forwarding to the API if your security model allows it.
Temporary dev exception: Enabling cluster_endpoint_public_access = true with a strict CIDR allowlist (and later turning it off) is sometimes used for bootstrapping—document the risk and revert.
Security groups (short)
Section titled “Security groups (short)”The EKS module creates security groups for the control plane and nodes. Tighten egress/ingress only if you understand the EKS control plane networking requirements. Custom rules are common for restricted node-to-API paths.
Operations
Section titled “Operations”| Topic | Notes |
|---|---|
| Upgrades | Upgrade control plane first (EKS console or Terraform cluster_version), then node groups (AMI release version). Test in a lower environment. |
| Cost | NAT gateways, node instance types, and idle ELBs dominate early bills. |
| Destroy | Run terraform destroy only after deleting LoadBalancer-type Services (otherwise AWS may leave ELBs). Delete PVCs using EBS volumes or handle volume retention. |
Next steps
Section titled “Next steps”- Harden IAM and add IRSA per application.
- Adopt a GitOps or CI/CD flow for manifests (CI/CD, Kubernetes).
- Return to EKS overview for context and links.