Why I Let Kubernetes Control My AWS ALB (While Terraform Handled the Rest)

I had just finished building my entire AWS stack in Terraform. VPC, subnets, EKS cluster, IAM roles — everything was clean, versioned, and reproducible. So when it came time to expose my application to the internet, my first instinct was to keep going with Terraform. I created the ALB, the target group, the listener, all in code. It felt safe.
Then reality hit. Kubernetes killed a pod and scheduled a new one. The old IP vanished. A fresh IP appeared. But my ALB target group was still pointing at the dead address. Traffic dropped. I found myself logging into the AWS console, manually deregistering stale targets and registering new ones, over and over again. I had become the human glue between a live system and a static snapshot.
I needed a different path. Not a different tool, but a different contract. I needed the load balancer to breathe with the cluster, not against it.
So I deleted the Terraform ALB resources and wrote an Ingress instead.
The Ingress YAML
Here is the file I applied:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: clouddevops-ingress
namespace: ivolve
annotations:
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}]'
spec:
ingressClassName: alb
rules:
- http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: clouddevops-service
port:
number: 80
It looks simple, but every line is doing real work.
name and namespace place the Ingress object inside the ivolve namespace. This means it can only discover services that live there.
ingressClassName: alb is the selector. It tells Kubernetes: "Do not hand this to the default NGINX controller. Hand it to the AWS Load Balancer Controller." Without this, the Ingress would sit in the cluster forever, ignored.
The annotations are the instructions the controller reads to build the actual AWS infrastructure:
scheme: internet-facingtells the controller to create a public ALB with a public DNS name and IP addresses. If I had usedinternal, the ALB would only be reachable from inside the VPC.target-type: ipis the critical piece. It tells the ALB to send traffic directly to the pod IP addresses, not to the EC2 worker node ports. Because my nodes live in private subnets with no public IPs, this is the only way the traffic path actually works. The ALB lives in the public subnet and routes straight into the pod's private network interface.listen-ports: '[{"HTTP": 80}]'configures the ALB listener. In production, this would become[{"HTTP": 80}, {"HTTPS": 443}]with a certificate attached, but for the first pass, port 80 is enough.
path: / and pathType: Prefix mean: catch every request that starts with / and send it to the backend.
backend.service.name: clouddevops-service is the handoff point. The Ingress does not talk to pods directly. It talks to the Kubernetes Service. That Service maintains an EndpointSlice — a live list of all healthy pod IPs. The controller reads that list and registers them with the ALB target group.
The Boundary
This experience taught me where the line lives. Terraform owns Day-0: the VPC, the subnets, the cluster, the IAM roles. These are the foundations. They change slowly, if ever. The Ingress and the controller own Day-2: the routing, the targets, the lifecycle. These change constantly.
I wanted to Terraform the ALB because it felt safe and static. But a load balancer in Kubernetes is not really infrastructure in the traditional sense. It is an extension of the application. Its targets are ephemeral. Its rules change with every deployment. Giving that contract to Terraform meant fighting the platform instead of using it.
I surrendered the ALB to the controller. My architecture finally matched the system it was running on.

