Skip to content

Complete EKS Cluster with Terraform & ALB

EKS design document

Creating a production-ready Amazon EKS cluster from scratch requires orchestrating several interconnected AWS resources — VPC subnets with proper load balancer discovery tags, IAM roles, the Kubernetes control plane, managed node groups, CNI prefix delegation, and the AWS Load Balancer Controller for exposing applications via Application Load Balancers (ALBs).

This guide walks through the end-to-end architecture and Terraform code to spin up a complete EKS cluster with ALB integration.


📺 Video Walkthrough


Architecture Overview

graph TD
    User["Internet Traffic"] --> ALB["AWS Application Load Balancer"]
    subgraph VPC ["VPC (10.0.0.0/16)"]
        subgraph PublicSubnets ["Public Subnets (eu-west-1a, eu-west-1b)"]
            ALB
            NAT["NAT Gateway"]
        end
        subgraph PrivateSubnets ["Private Subnets (eu-west-1a, eu-west-1b)"]
            Nodes["EKS Worker Nodes (Bottlerocket / AL2023)"]
            Pods["Pods (IP Target Mode)"]
        end
    end
    ALB -- "Direct to Pod IP (port 80)" --> Pods
    Nodes --> NAT

Components Provisioned

  • VPC: 2 public subnets and 2 private subnets across multiple AZs with a dedicated NAT Gateway.
  • EKS Control Plane: Kubernetes 1.31+ with API-only access authentication (authentication_mode = "API").
  • Managed Node Group: Spot / On-Demand instances running optimized Bottlerocket or AL2023 AMIs.
  • VPC-CNI Add-on: Configured with prefix delegation for high pod density per node.
  • AWS Load Balancer Controller: Deployed via Helm to provision ALBs and NLBs automatically with target-type: ip.

📁 Source Code: aws-eks-terraform / EKS-Cluster-ALB


Step-by-Step Implementation

Step 1. VPC with Load Balancer Subnet Tags

The AWS Load Balancer Controller relies on subnet tags to discover where public and internal load balancers should be placed.

vpc.tf

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.13.0"

  name = "eks-demo-vpc"
  cidr = "10.0.0.0/16"

  azs             = ["eu-west-1a", "eu-west-1b"]
  private_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
  public_subnets  = ["10.0.101.0/24", "10.0.102.0/24"]

  enable_nat_gateway   = true
  single_nat_gateway   = true
  enable_dns_hostnames = true
  enable_dns_support   = true

  public_subnet_tags = {
    "kubernetes.io/role/elb" = "1"
    "subnet_type"            = "public"
  }

  private_subnet_tags = {
    "kubernetes.io/role/internal-elb" = "1"
    "subnet_type"                     = "private"
  }
}


Step 2. Provision the EKS Cluster

eks.tf

module "eks" {
  source  = "terraform-aws-modules/eks/aws"
  version = "20.24.0"

  cluster_name    = "eks-demo"
  cluster_version = "1.31"

  cluster_endpoint_public_access           = true
  enable_cluster_creator_admin_permissions = true
  authentication_mode                      = "API"

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

  eks_managed_node_groups = {
    primary = {
      ami_type       = "BOTTLEROCKET_x86_64"
      instance_types = ["m7i.large", "m6i.large", "m5.large"]
      capacity_type  = "SPOT"

      min_size     = 1
      max_size     = 3
      desired_size = 2
    }
  }

  tags = {
    Environment = "dev"
    Terraform   = "true"
  }
}


Step 3. VPC-CNI Add-on with Prefix Delegation

Enabling ENABLE_PREFIX_DELEGATION significantly increases the number of available IP addresses per worker node, preventing IP exhaustion when running high-density container workloads.

cni.tf

resource "aws_eks_addon" "vpc_cni" {
  cluster_name                = module.eks.cluster_name
  addon_name                  = "vpc-cni"
  addon_version               = "v1.19.0-eksbuild.1"
  resolve_conflicts_on_create = "OVERWRITE"
  resolve_conflicts_on_update = "OVERWRITE"

  configuration_values = jsonencode({
    env = {
      ENABLE_PREFIX_DELEGATION          = "true"
      WARM_PREFIX_TARGET                = "1"
      ENABLE_POD_ENI                    = "true"
      POD_SECURITY_GROUP_ENFORCING_MODE = "standard"
    }
  })
}


Step 4. AWS Load Balancer Controller Deployment

Install the AWS Load Balancer Controller via Helm and configure its IAM role using EKS Pod Identity or IRSA.

alb-controller.tf

module "load_balancer_controller_pod_identity" {
  source  = "terraform-aws-modules/eks-pod-identity/aws"
  version = "~> 1.6"

  name = "aws-lbc-${module.eks.cluster_name}"

  attach_aws_lb_controller_policy = true

  associations = {
    eks_lbc = {
      cluster_name    = module.eks.cluster_name
      namespace       = "kube-system"
      service_account = "aws-load-balancer-controller"
    }
  }
}

resource "helm_release" "aws_load_balancer_controller" {
  name       = "aws-load-balancer-controller"
  namespace  = "kube-system"
  repository = "https://aws.github.io/eks-charts"
  chart      = "aws-load-balancer-controller"
  version    = "1.10.0"

  set {
    name  = "clusterName"
    value = module.eks.cluster_name
  }
  set {
    name  = "serviceAccount.create"
    value = "true"
  }
  set {
    name  = "serviceAccount.name"
    value = "aws-load-balancer-controller"
  }
  set {
    name  = "defaultTargetType"
    value = "ip"
  }

  depends_on = [
    module.eks,
    module.load_balancer_controller_pod_identity
  ]
}


Step 5. Expose an Application via Ingress (ALB)

Create a sample deployment, service, and Ingress object. The AWS Load Balancer Controller detects the Ingress and automatically provisions an internet-facing Application Load Balancer.

sample-app.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: echoserver
  namespace: default
spec:
  replicas: 2
  selector:
    matchLabels:
      app: echoserver
  template:
    metadata:
      labels:
        app: echoserver
    spec:
      containers:
        - name: echoserver
          image: k8s.gcr.io/e2e-test-images/echoserver:2.5
          ports:
            - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: echoserver-svc
  namespace: default
spec:
  type: ClusterIP
  ports:
    - port: 80
      targetPort: 8080
  selector:
    app: echoserver
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: echoserver-ingress
  namespace: default
  annotations:
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/target-type: ip
spec:
  ingressClassName: alb
  rules:
    - http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: echoserver-svc
                port:
                  number: 80


Verification & Testing

# 1. Update kubeconfig
aws eks --region eu-west-1 update-kubeconfig --name eks-demo

# 2. Deploy sample app and ingress
kubectl apply -f sample-app.yaml

# 3. Check Ingress status and obtain public ALB DNS name
kubectl get ingress echoserver-ingress

# 4. Test HTTP response
curl -i http://<ALB-DNS-NAME>