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