Application Routing with K8s Ingress & AWS Load Balancer Controller
Let a Kubernetes manifest provision a real AWS load balancer, and make the application reachable from the internet.
- Time
- 23 min
- Level
- Intermediate
- Objectives
- 4 objectives
- Cost
- Billable
Where this fits in the platform
Already built
This lab adds
- An ALB provisioned by the cluster from an Ingress object
Before you start
Cost — Billable
An Ingress backed by the AWS Load Balancer Controller creates a real ALB at ~$0.0225/hour (~$16/month) plus LCU charges — and it is created by Kubernetes, so `terraform destroy` will not remove it.
The scenario#
Each service was exposed with type: LoadBalancer, so there are six load balancers, six DNS names and six monthly charges for one application.
One entry point, routed by path, is the fix — and the mechanism that creates it is worth understanding, because when it silently does nothing there is no error anywhere.
Hands-on environment
Run it on AWS
This lab builds real cloud infrastructure, so it needs your own AWS account. Follow the cost and cleanup notes above — the resources are yours, and so is the bill.
Anything you tick here is your own record. EgyKode cannot see inside that terminal, so the success criteria stay self-assessed even when the environment checks your work for you.
The controller, and its AWS identity
Step 1 of 3
What you are building#
Internet
| one DNS name, one certificate
v
Application Load Balancer <- created BY the controller, from your manifest
| /api -> target group 1
| / -> target group 2
v
Pod IPs directly (target-type: ip)The Ingress object does nothing on its own. It is configuration waiting for
a controller. Without one, kubectl apply succeeds, kubectl get ingress
shows the object, ADDRESS stays empty forever and nothing anywhere reports an
error. That silence is the single most confusing failure in Kubernetes
networking, and knowing to check for the controller first saves hours.
Build it#
What you are proving: You can give a cluster controller its own AWS identity so it may create load balancers
This step settles no success criterion on its own.
helm repo add eks https://aws.github.io/eks-charts
helm upgrade --install aws-load-balancer-controller eks/aws-load-balancer-controller \
-n kube-system \
--set clusterName=platform \
--set serviceAccount.create=false \
--set serviceAccount.name=aws-load-balancer-controllerThe controller creates ALBs, target groups and listener rules in your AWS account, so it needs IAM permissions — through IRSA, not the node role:
kubectl annotate serviceaccount aws-load-balancer-controller -n kube-system \
eks.amazonaws.com/role-arn=arn:aws:iam::111122223333:role/alb-controllerWhat you are proving: You can explain what target-type ip changes, and why subnet tags decide whether anything happens at all
Marking this settles success criterion 3.
aws ec2 create-tags --resources subnet-0abc subnet-0def \
--tags Key=kubernetes.io/role/elb,Value=1 # public: internet-facing
aws ec2 create-tags --resources subnet-0ghi subnet-0jkl \
--tags Key=kubernetes.io/role/internal-elb,Value=1 # private: internalThis is how the controller discovers where to place the load balancer. Missing
tags produce unable to discover at least one subnet in the controller log and
nothing at all on the Ingress — which is why the controller log is the first
place to look, not the last.
What you are proving: You can provision a real ALB from a manifest, route two paths to two Services, and prove that deleting the Ingress removes it
Marking this settles success criteria 1 and 2 and 4.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: platform
namespace: platform
annotations:
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
alb.ingress.kubernetes.io/listen-ports: '[{"HTTP":80},{"HTTPS":443}]'
alb.ingress.kubernetes.io/ssl-redirect: "443"
alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:us-east-1:111122223333:certificate/abc
alb.ingress.kubernetes.io/healthcheck-path: /healthz
alb.ingress.kubernetes.io/group.name: platform # share ONE ALB
spec:
ingressClassName: alb
rules:
- host: app.example.com
http:
paths:
- path: /api
pathType: Prefix
backend:
service: { name: api, port: { number: 80 } }
- path: /
pathType: Prefix
backend:
service: { name: web, port: { number: 80 } }Three annotations that change behaviour rather than decorate it:
target-type: ipregisters Pod IPs in the target group. The alternative,instance, registers nodes and relies on a NodePort, adding a kube-proxy hop and losing the client IP. With the VPC CNI giving Pods real VPC addresses,ipis the better default — and it is required for Fargate.group.namelets several Ingress objects share one ALB. Without it every Ingress gets its own, and you have recreated the problem this lab exists to solve.ssl-redirectis enforced at the load balancer, so no request reaches your application over plain HTTP.
Rules are evaluated in order and the first match wins. Putting / before
/api swallows everything, and the symptom is the wrong service answering
rather than an error.
Verify it worked#
Destructive — This removes real resources. Check which environment you are in first.
# The ALB was created, and Kubernetes knows its address
kubectl get ingress -n platform
aws elbv2 describe-load-balancers \
--query 'LoadBalancers[?contains(LoadBalancerName,`platform`)].[DNSName,Scheme,State.Code]' --output table
# Targets are Pod IPs, and they are healthy
aws elbv2 describe-target-health --target-group-arn <arn> \
--query 'TargetHealthDescriptions[].[Target.Id,TargetHealth.State]' --output table
kubectl get pods -n platform -o wide # the same addresses
# Both paths route where you intended
curl -s -o /dev/null -w '%{http_code} ' https://app.example.com/api/health
curl -s -o /dev/null -w '%{http_code}\n' https://app.example.com/
# HTTP is redirected, not served
curl -sI http://app.example.com/ | head -2 # 301 to https
# Deleting the Ingress removes the ALB
kubectl delete ingress platform -n platform
aws elbv2 describe-load-balancers --query 'LoadBalancers[].LoadBalancerName'That last check is not ceremony. An ALB that outlives its Ingress keeps billing and its ENIs block the VPC from being destroyed later.
ADDRESS stays empty
Read the controller: kubectl logs -n kube-system deploy/aws-load-balancer-controller.
Almost always missing subnet tags, missing IRSA permissions, or a
ingressClassName no controller is watching.
unable to discover at least one subnet
The kubernetes.io/role/elb tags. Both the tag and a subnet in at least two
AZs are required.
Targets are unhealthy but the Pods are Ready
The ALB health check path differs from the readiness probe path, or the security group does not allow the ALB to reach the Pod port. These are separate health checks and they can disagree.
502 from the ALB
There are healthy targets and the application refused the connection — usually
the container listening on 127.0.0.1 rather than 0.0.0.0, or the wrong
targetPort.
404 from the ALB
No rule matched. Check the Host header and rule order.
terraform destroy hangs on the VPC
The ALB still exists because the Ingress was never deleted. Its ENIs hold the subnets.
Clean up#
Destructive — This removes real resources. Check which environment you are in first.
kubectl delete ingress --all -n platform
aws elbv2 describe-load-balancers --query 'LoadBalancers[].[LoadBalancerName,State.Code]' --output tableCost of this lab: Billable. An ALB is about $0.0225/hour (~$17/month) plus capacity units, and it keeps billing until the Ingress is deleted.
Success criteria
0 of 4
The concept behind it
Next up
Lab 37 of 59 on the project path
Previous: Amazon EKS Cluster & Managed Node Group Provisioning