Skip to content

Create an EKS cluster with Terraform

First PublishedByAtif Alam

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.


Split files so reviews stay readable:

FilePurpose
versions.tfTerraform + provider version constraints
providers.tfAWS provider, region, default tags
variables.tf / terraform.tfvarsRegion, cluster name, CIDRs
vpc.tfVPC module
eks.tfEKS cluster, node groups, add-ons
outputs.tfcluster_name, vpc_id, etc.

versions.tf
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
providers.tf
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.


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 costsingle_nat_gateway = true is cheaper but a single AZ failure can break outbound traffic from private subnets in other AZs.

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 accessProsCons
Private onlyAPI not exposed on the public internetYou need VPN, Direct Connect, or a bastion to run kubectl from your laptop
Public + privateEasy kubectl from anywhere if authorizedLarger attack surface; must lock down with IAM + optional CIDR allowlists
Public disabled, private enabledStrong default for prodSame 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.


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-driverEBS CSI so PersistentVolumeClaim with gp3 (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.


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.tf
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):

Terminal window
aws eks update-kubeconfig --region <region> --name <cluster_name>
kubectl get nodes
kubectl get pods -A

Reaching a private API from your workstation

Section titled “Reaching a private API from your workstation”

Pick one primary pattern for your team:

  1. 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.
  2. AWS Direct Connect — Same idea for data-center–linked networks.
  3. Bastion / jump host — EC2 in a public subnet with SSM Session Manager (no SSH keys on the internet); run kubectl only 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.


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.


TopicNotes
UpgradesUpgrade control plane first (EKS console or Terraform cluster_version), then node groups (AMI release version). Test in a lower environment.
CostNAT gateways, node instance types, and idle ELBs dominate early bills.
DestroyRun terraform destroy only after deleting LoadBalancer-type Services (otherwise AWS may leave ELBs). Delete PVCs using EBS volumes or handle volume retention.

  • 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.