The AWS Load Balancer Controller
Introduction¶
On EKS, the AWS Load Balancer Controller (the successor to the "ALB Ingress Controller") watches Kubernetes objects and provisions AWS load balancers:
- Ingress → an Application Load Balancer (L7, path/host routing, WAF, OIDC auth).
- Service
type: LoadBalancerwith the right annotations → a Network Load Balancer (L4, static IPs, high throughput, preserves source IP).
Install prerequisites¶
- IAM: the controller needs an IAM policy (the published one) attached to a role, via IRSA (IAM Roles for Service Accounts) or Pod Identity.
- Subnet tags: the controller discovers subnets by tag —
kubernetes.io/role/elb = 1on public subnets (internet-facing LBs),kubernetes.io/role/internal-elb = 1on private subnets. Missing tags = "no subnets found" and nothing provisions. - Helm install:
helm install aws-load-balancer-controller eks/aws-load-balancer-controller \ -n kube-system --set clusterName=my-cluster \ --set serviceAccount.create=false --set serviceAccount.name=aws-load-balancer-controller
Ingress → ALB¶
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app
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:...:certificate/xxxx
alb.ingress.kubernetes.io/healthcheck-path: /healthz
alb.ingress.kubernetes.io/group.name: shared-alb # share one ALB across Ingresses
alb.ingress.kubernetes.io/wafv2-acl-arn: arn:aws:wafv2:...
spec:
ingressClassName: alb
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend: { service: { name: web, port: { number: 80 } } }
target-type: ip vs instance¶
ip— the ALB targets pod IPs directly (requires the VPC CNI so pods have VPC IPs, which EKS does). Traffic skips the node's kube-proxy hop — lower latency, works with Fargate, and the target group tracks pods as they come and go. Preferred.instance— the ALB targets node IPs on the Service's NodePort; kube-proxy then forwards to a pod (possibly on another node). Extra hop, source IP obscured unlessexternalTrafficPolicy: Local.
IngressGroup — sharing an ALB¶
alb.ingress.kubernetes.io/group.name lets multiple Ingress objects (even across
namespaces) share one ALB. Each contributes rules; group.order sets rule
priority. Saves money (one ALB, not N) and lets teams manage their own routes on
a shared entry point.
Service → NLB¶
apiVersion: v1
kind: Service
metadata:
name: grpc-svc
annotations:
service.beta.kubernetes.io/aws-load-balancer-type: external
service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
service.beta.kubernetes.io/aws-load-balancer-healthcheck-protocol: HTTP
service.beta.kubernetes.io/aws-load-balancer-healthcheck-path: /grpc.health.v1.Health/Check
spec:
type: LoadBalancer
loadBalancerClass: service.k8s.aws/nlb
ports: [{ port: 443, targetPort: 8443 }]
selector: { app: grpc-svc }
Verification and troubleshooting¶
kubectl -n kube-system logs deploy/aws-load-balancer-controller
kubectl describe ingress app # events + the ALB DNS name once ready
kubectl get targetgroupbindings -A # the CRD the controller uses
aws elbv2 describe-target-health --target-group-arn ...
- Ingress stuck, no ALB, address empty — controller not running / crashing
(IAM policy missing →
AccessDeniedin logs), subnets not tagged, oringressClassName: albmissing. Logs name the exact problem. could not find any subnets for cluster— thekubernetes.io/role/elb(orinternal-elb) subnet tags, pluskubernetes.io/cluster/<name> = shared|owned.- ALB created, targets unhealthy — health check path returns non-2xx, wrong
port, the pod's security group doesn't allow the ALB's SG (the controller
manages a shared "backend" SG — ensure
enableBackendSecurityGroupor your node/pod SG rules allow it), ortarget-type: instance+ wrongexternalTrafficPolicy. - 502 from the ALB — backend closed the connection / idle timeout mismatch
(ALB default 60s), or HTTP/2 h2c to a backend that doesn't speak it. Set
alb.ingress.kubernetes.io/backend-protocol/-version. - Two Ingresses each made their own ALB — no shared
group.name, or mismatchedgroupsettings so they didn't merge. - Deleting the Ingress didn't delete the ALB — a finalizer/permission issue,
or the ALB is shared (still referenced by another Ingress in the group). Check
targetgroupbindingsand the controller logs. - Cross-zone traffic imbalance — NLB cross-zone load balancing is off by
default (and costs); enable with
service.beta.kubernetes.io/aws-load-balancer-attributes: load_balancing.cross_zone.enabled=true.
Related tools and reading¶
- On-site: HTTP Header Analyzer, SSL Certificate Checker.
- Related posts: ECS Fargate task networking, ALB vs NLB.
Stuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.More in Docker & Kubernetes
Kustomize Basics
Kustomize customizes plain YAML with overlays instead of templating. Here's bases vs overlays, the patch types, generators, and the gotchas around name suffixes.
September 13, 2026EKS vs Self-Managed Kubernetes
EKS runs the control plane for you; you still own nodes, networking, and add-ons. Here's the division of responsibility, the node options, and when self-managed is worth it.
September 11, 2026Pod Security Admission
PSP is gone. Pod Security Admission enforces the Pod Security Standards via namespace labels — three levels, three modes. Here's how to roll it out without breaking things.
August 30, 2026