Hands-on Projects — Build It End to End›04 · EKS with Terraform and an ALB

Lesson 04 of 6 · Platform builds

EKS with Terraform and an ALB

Stand up an EKS cluster with Terraform (VPC and EKS community modules), push an image to ECR, install the AWS Load Balancer Controller with IAM for service accounts, expose an app through an Application Load Balancer, and tear it all down cleanly.

Advanced
Key wordsEKSTerraformterraform-aws-modulesVPC moduleEKS modulemanaged node groupECRAWS Load Balancer ControllerIRSAALB Ingressterraform destroy
Terraform vpc + eks modules ECR hello image Users https via ALB AWS account · 3-AZ VPC EKS control plane managed by AWS Managed node group 2 × t3.medium (private subnets) AWS LB Controller IAM via IRSA / Pod Identity Deployment hello + Service target-type: ip Application Load Balancer (public subnets) created from the Ingress
Terraform builds the VPC and EKS; the AWS Load Balancer Controller turns an Ingress into an ALB.

The brief

"Build a small EKS environment with Terraform, deploy an app from ECR, expose it with an ALB, and clean up without leaving anything behind."

Instead of building the shop yourself (projects 1 and 3), you rent a managed shopping centre (EKS): the landlord runs the building management (control plane). You write down, in one order form (Terraform), how many shop units you want and where. A receptionist robot (the load balancer controller) puts up the front door and signs (ALB) whenever you ask for them.

Resources needed

Resource Detail
AWS account + admin-ish IAM for the lab VPC, EKS, IAM, ECR, EC2, ELB
Terraform ≥ 1.5, AWS CLI v2, kubectl, helm, docker Local tools
Remote state (optional for the lab) S3 backend (see Amazon EKS in Production with Terraform, lesson 02)
Cost EKS control plane (hourly fee), 2 × t3.medium nodes, NAT gateway, ALB: a few dollars per day; destroy after

Step 1: Terraform

# main.tf
terraform {
  required_providers { aws = { source = "hashicorp/aws", version = "~> 5.0" } }
}
provider "aws" { region = "eu-west-1" }

data "aws_availability_zones" "available" {}

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.0"
  name = "hello-vpc"
  cidr = "10.20.0.0/16"
  azs             = slice(data.aws_availability_zones.available.names, 0, 3)
  private_subnets = ["10.20.1.0/24", "10.20.2.0/24", "10.20.3.0/24"]
  public_subnets  = ["10.20.101.0/24", "10.20.102.0/24", "10.20.103.0/24"]
  enable_nat_gateway = true
  single_nat_gateway = true                      # lab: one NAT to save cost
  public_subnet_tags  = { "kubernetes.io/role/elb" = 1 }
  private_subnet_tags = { "kubernetes.io/role/internal-elb" = 1 }
}

module "eks" {
  source  = "terraform-aws-modules/eks/aws"
  version = "~> 20.0"                            # v21 renamed several inputs; pin and read the upgrade guide
  cluster_name    = "hello-eks"
  cluster_version = "1.31"
  cluster_endpoint_public_access           = true
  enable_cluster_creator_admin_permissions = true
  vpc_id     = module.vpc.vpc_id
  subnet_ids = module.vpc.private_subnets
  eks_managed_node_groups = {
    default = { instance_types = ["t3.medium"], min_size = 2, max_size = 3, desired_size = 2 }
  }
}

module "lb_controller_irsa" {
  source  = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks"
  version = "~> 5.0"
  role_name                              = "hello-eks-aws-lb-controller"
  attach_load_balancer_controller_policy = true
  oidc_providers = {
    main = {
      provider_arn               = module.eks.oidc_provider_arn
      namespace_service_accounts = ["kube-system:aws-load-balancer-controller"]
    }
  }
}

resource "aws_ecr_repository" "hello" { name = "hello-k8s" }

output "lb_controller_role_arn" { value = module.lb_controller_irsa.iam_role_arn }
output "ecr_url"                { value = aws_ecr_repository.hello.repository_url }
$ terraform init && terraform plan -out tfplan && terraform apply tfplan
$ aws eks update-kubeconfig --name hello-eks --region eu-west-1
$ kubectl get nodes

(Module inputs change between major versions; check each module's documentation for the version you pin.)

Step 2: push the image to ECR

$ ECR=$(terraform output -raw ecr_url)
$ aws ecr get-login-password --region eu-west-1 | docker login --username AWS --password-stdin ${ECR%/*}
$ docker build -t $ECR:v1 . && docker push $ECR:v1         # the hello-k8s app from project 1

Step 3: AWS Load Balancer Controller

$ kubectl -n kube-system create serviceaccount aws-load-balancer-controller
$ kubectl -n kube-system annotate serviceaccount aws-load-balancer-controller \
    eks.amazonaws.com/role-arn=$(terraform output -raw lb_controller_role_arn)
$ helm repo add eks https://aws.github.io/eks-charts
$ helm install aws-load-balancer-controller eks/aws-load-balancer-controller -n kube-system \
    --set clusterName=hello-eks --set serviceAccount.create=false --set serviceAccount.name=aws-load-balancer-controller
$ kubectl -n kube-system rollout status deploy/aws-load-balancer-controller

Step 4: deploy and expose

Use project 1's Deployment with image: <ECR>:v1 (no imagePullSecret needed on EKS), a ClusterIP Service, and:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: hello
  namespace: demo
  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: hello, port: { number: 80 } } }

Step 5: check the app

$ kubectl -n demo get pods,svc,ingress
$ ALB=$(kubectl -n demo get ingress hello -o jsonpath='{.status.loadBalancer.ingress[0].hostname}')
$ curl -s http://$ALB | head -5            # can take a few minutes for DNS and target health
$ kubectl -n kube-system logs deploy/aws-load-balancer-controller | tail

Add HTTPS with an ACM certificate annotation (alb.ingress.kubernetes.io/certificate-arn) and the deploy-time 502 fixes from Incident Handling, lesson 19.

Step 6: tear down (in this order)

$ kubectl -n demo delete ingress hello                  # controller deletes the ALB
$ sleep 60 && aws elbv2 describe-load-balancers --query 'LoadBalancers[].LoadBalancerName'
$ terraform destroy

Interview talking points

  • Modules and pinning; remote state and locking; plan review.
  • Subnet tags that let the controller find public/private subnets.
  • IRSA/Pod Identity for controllers; node role vs pod roles.
  • ECR on EKS "just works" (node role + credential provider) vs project 1.
  • Cost and cleanup: NAT gateway, ALB lifecycle, destroy order.

Command summary

terraform init ; terraform plan -out tfplan ; terraform apply tfplan
aws eks update-kubeconfig --name hello-eks --region eu-west-1 ; kubectl get nodes
aws ecr get-login-password | docker login --username AWS --password-stdin <acct>.dkr.ecr.<region>.amazonaws.com
docker build -t $ECR:v1 . ; docker push $ECR:v1
kubectl -n kube-system create sa aws-load-balancer-controller ; kubectl annotate sa … eks.amazonaws.com/role-arn=…
helm install aws-load-balancer-controller eks/aws-load-balancer-controller -n kube-system --set clusterName=hello-eks …
kubectl apply -f deployment.yaml -f service.yaml -f ingress.yaml ; kubectl -n demo get ingress
curl http://<alb-dns-name>
kubectl -n demo delete ingress hello ; terraform destroy

Recap

  • Terraform VPC + EKS modules (pinned), an ECR repo and an IRSA role for the controller.
  • AWS Load Balancer Controller turns an Ingress into an ALB (target-type: ip).
  • EKS nodes pull from ECR without secrets.
  • Delete the Ingress first, then terraform destroy.

This site is a public version of my personal engineering knowledge hub. It intentionally excludes confidential company information and internal operational details.