Amazon EKS Ingress Controllers: AWS Load Balancer Controller, ALB Routing, and TLS
On Amazon EKS, Ingress lets you route HTTP and HTTPS traffic to multiple services through a single AWS Application Load Balancer. This page explains what Ingress is, how it differs from a Kubernetes Service, how the AWS Load Balancer Controller makes it work on EKS, and when to use it.
Simple explanation
If you are new to Kubernetes, here is the short version.
A Service is the standard Kubernetes way to expose a set of pods to network traffic. When you create a Service of type LoadBalancer, EKS provisions an AWS load balancer and gives you an external address. One Service, one load balancer.
An Ingress is a higher-level routing layer. It is a Kubernetes resource that defines rules like: requests for api.example.com/v1 go to the API service, and requests for dashboard.example.com go to the dashboard service. One Ingress resource can route traffic to many Services through a single load balancer.
An ingress controller is the software that makes those rules work. By itself, an Ingress resource is just YAML. It does not do anything. The ingress controller watches for Ingress resources in the cluster and configures a load balancer to enforce the routing rules you defined.
An Ingress is like a sign at the front entrance of a building: “Reception: floor 1, Engineering: floor 3, Finance: floor 5.” The sign defines the rules. The ingress controller is the security desk that actually reads the sign and directs each visitor to the right floor. Without the security desk, the sign is just a piece of paper.
Ingress vs Service: what is the difference
A Kubernetes Service of type LoadBalancer creates one AWS load balancer per Service. If you have five services to expose externally, an API, a dashboard, a webhook handler, a docs site, and a metrics endpoint, you end up with five separate load balancers. Each one costs money, needs its own DNS record, and has its own health check configuration.
An Ingress sits in front of multiple Services and routes requests to the right one based on URL path or hostname:
api.example.comroutes to the API Servicedashboard.example.comroutes to the Dashboard Serviceexample.com/webhooksroutes to the Webhook Service
All of this goes through a single AWS Application Load Balancer. One load balancer, one set of TLS certificates, one DNS entry point.
When a LoadBalancer Service is enough:
- You only have one service to expose externally
- You need TCP or UDP routing, not HTTP
- You want the simplest possible setup with no extra components installed
When Ingress is the better choice:
- You have multiple HTTP/HTTPS services to expose
- You want path-based or host-based routing
- You want TLS termination in one place
- You want to reduce the number of load balancers and the costs that come with them
What an ingress controller does on EKS
On Amazon EKS, the main AWS-native option for running an ingress controller is the AWS Load Balancer Controller.
The controller is a pod that runs inside your cluster. It watches for Ingress and Service resources using the Kubernetes API. When you create an Ingress resource with the right annotations, the controller reads it, calls the AWS API, and creates or updates an Application Load Balancer to match your routing rules.
This is different from a self-hosted ingress controller like NGINX, which runs a proxy pod inside the cluster. With AWS Load Balancer Controller, the load balancer itself lives in AWS infrastructure, not inside your cluster. That means native AWS integrations (WAF, ACM, Shield, CloudWatch), better throughput, and no in-cluster proxy to scale or maintain.
AWS Load Balancer Controller is not included on new EKS clusters. If you apply an Ingress resource without installing it first, nothing happens, no error, no ALB, just silence. Install it via Helm before expecting any Ingress to work.
How it works on AWS
Here is the request flow when a user hits a service exposed via Ingress on EKS:
User browser
↓
Route 53 → resolves the domain to the ALB DNS name
↓
Application Load Balancer → listener rules match hostname or path
↓
Target group → routes directly to pod IPs
↓
Pod (your application)Route 53 typically points your domain to the ALB’s DNS name using an Alias record. The Application Load Balancer handles TLS termination, evaluates listener rules, and distributes traffic. The EKS networking model (VPC CNI) gives every pod a real VPC IP address, which is why the ALB can route directly to pods rather than going through node ports.
Path-based routing sends traffic to different Services based on the URL path. A request to /api/ goes to the API service and a request to /admin/ goes to the admin service, both through the same ALB and the same hostname.
Host-based routing sends traffic to different Services based on the hostname. api.example.com goes to one Service and dashboard.example.com goes to another. Each hostname maps to a separate listener rule on the ALB.
You can combine both: route by hostname first, then by path within that hostname.
What you need before this works
Before an Ingress resource on EKS will create a working ALB, you need all of the following in place:
- A working EKS cluster. If you do not have one, start with creating your first EKS cluster.
- AWS Load Balancer Controller installed. Not present by default. Install it via Helm.
- IRSA configured for the controller. The controller needs an IAM role with permission to create and manage ALBs, target groups, and security groups. That role is linked to the controller’s Kubernetes service account via IAM Roles for Service Accounts.
- Subnets tagged correctly. The controller uses subnet tags to find where to place the ALB. Public subnets need
kubernetes.io/role/elb: 1for internet-facing ALBs. Private subnets needkubernetes.io/role/internal-elb: 1for internal ALBs. - Security groups that allow ALB traffic. The ALB needs to reach your pods on the target port.
- An ACM certificate if using HTTPS. You do not store certificates in Kubernetes. The ALB uses a certificate from AWS Certificate Manager, which handles renewal automatically.
Without IRSA configured correctly, the controller starts up, watches for Ingress resources, and then fails to call the AWS API. No ALB is created. No visible error appears on the Ingress resource itself. You only see it if you check the controller logs. Set up IRSA before you try to create any Ingress.
Basic Ingress example
Here is the simplest useful Ingress: one backend service, HTTPS enabled.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-app-ingress
namespace: production
annotations:
kubernetes.io/ingress.class: alb
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:eu-west-1:123456789012:certificate/abc123
alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS": 443}]'
alb.ingress.kubernetes.io/ssl-redirect: '443'
spec:
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-app-service
port:
number: 80When you apply this, the AWS Load Balancer Controller creates an internet-facing ALB that listens on ports 80 and 443, redirects HTTP to HTTPS automatically, terminates TLS using the ACM certificate, and routes all traffic to my-app-service.
Here is an example with multiple backends using path-based and host-based routing:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: main-ingress
namespace: production
annotations:
kubernetes.io/ingress.class: alb
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:eu-west-1:123456789012:certificate/abc123
alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS": 443}]'
alb.ingress.kubernetes.io/ssl-redirect: '443'
spec:
rules:
- host: api.example.com
http:
paths:
- path: /v1
pathType: Prefix
backend:
service:
name: api-service
port:
number: 80
- path: /webhooks
pathType: Prefix
backend:
service:
name: webhook-service
port:
number: 80
- host: dashboard.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: dashboard-service
port:
number: 80Important AWS Load Balancer Controller annotations
The controller uses annotations on the Ingress resource to configure the ALB. These are the ones you will use most often:
| Annotation | Values | What it does |
|---|---|---|
alb.ingress.kubernetes.io/scheme | internet-facing, internal | Controls whether the ALB is publicly accessible or only within your VPC. Use internal for services that should only be reached from inside the cluster or VPC. |
alb.ingress.kubernetes.io/target-type | ip, instance | Use ip on EKS. Routes traffic directly to pod IPs using VPC CNI, bypassing node ports and kube-proxy. The instance mode adds an unnecessary extra hop on EKS. |
alb.ingress.kubernetes.io/certificate-arn | ACM certificate ARN | The ARN of your ACM certificate for HTTPS. Without this, the ALB will not serve HTTPS traffic. |
alb.ingress.kubernetes.io/listen-ports | JSON array of port configs | Defines which ports the ALB listens on. Specify both HTTP 80 and HTTPS 443 to support both protocols. |
alb.ingress.kubernetes.io/ssl-redirect | 443 | Automatically redirects HTTP requests to HTTPS. Requires listen-ports to include HTTP 80 and HTTPS 443. |
alb.ingress.kubernetes.io/group.name | Any string, e.g. shared-alb | Groups multiple Ingress resources across namespaces onto a single ALB. Without this, each Ingress creates its own ALB. Shared groups are the most cost-effective pattern when you have many services. |
If you have more than one Ingress resource in your cluster, set the same group.name on all of them. The controller will merge their routing rules onto a single ALB automatically. Adding this later means a brief period of downtime as the controller creates a new shared ALB and removes the old individual ones.
Newer versions of the AWS Load Balancer Controller support spec.ingressClassName: alb as an alternative to the kubernetes.io/ingress.class: alb annotation. Both work. ingressClassName is the Kubernetes-standard approach going forward, so prefer it on newer clusters.
TLS termination with ACM
With AWS Load Balancer Controller, TLS is terminated at the ALB, not inside your cluster. Here is how the setup works:
- Request or import a certificate in AWS Certificate Manager for your domain.
- Add the certificate ARN to your Ingress as the
certificate-arnannotation. - The controller configures the ALB with an HTTPS listener using that certificate.
- The ALB handles the TLS handshake with the client.
- The ALB forwards the request to your pod as plain HTTP over the VPC network.
Your pods do not need to handle certificates at all. ACM manages certificate renewal automatically, with no secrets to rotate and no Kubernetes resources to update when a certificate renews.
After TLS is terminated at the ALB, the connection to your pod is unencrypted HTTP. This traffic stays inside your VPC, and AWS encrypts VPC traffic at the infrastructure level, so it does not cross the public internet unencrypted. For most applications this is fine. If your compliance requirements demand end-to-end TLS down to the pod, you need to configure TLS on your containers separately and update the ALB target group to use HTTPS.
AWS Load Balancer Controller vs NGINX Ingress on EKS
Both work on EKS. The choice depends on what your team needs.
AWS Load Balancer Controller provisions native AWS ALBs and NLBs directly from Kubernetes resources. The load balancer lives in AWS infrastructure, not inside your cluster. It integrates with ACM for TLS, WAF for request filtering, Shield for DDoS protection, and CloudWatch for metrics. All configuration is done through annotations on the Ingress resource.
NGINX Ingress Controller runs an NGINX proxy pod inside your cluster and exposes it via a LoadBalancer Service (which creates an NLB). It is more flexible for routing: it supports matching by headers, query strings, and request body in ways that ALB listener rules cannot. TLS is managed via Kubernetes Secrets, often using cert-manager for automated issuance and renewal.
| AWS Load Balancer Controller | NGINX Ingress | |
|---|---|---|
| Load balancer location | AWS-managed ALB outside the cluster | NGINX proxy pod inside the cluster |
| TLS certificates | ACM, auto-renewed | Kubernetes Secrets, often via cert-manager |
| AWS integrations | WAF, Shield, S3 access logs, CloudWatch | Limited, only what the NLB in front provides |
| Routing flexibility | Host and path-based | Host, path, headers, query strings, and more |
| Cloud portability | AWS only | Works on any Kubernetes cluster |
| Operational overhead | No proxy pod to maintain | You manage the NGINX pod, its resources, and scaling |
For most new EKS projects, AWS Load Balancer Controller is the right default. It avoids running and scaling a proxy pod inside the cluster, and it integrates cleanly with the rest of the AWS stack. Choose NGINX Ingress when you need routing rules that ALB annotations cannot express, or when you need consistent ingress behaviour across clouds.
When to use Ingress on EKS
Use Ingress when:
- You have more than one HTTP/HTTPS service to expose and want to avoid one load balancer per service
- You need to route requests based on hostname or URL path
- You want TLS termination in one place, managed through ACM
- You want to group services from different teams or namespaces behind a single ALB using
group.name - You are building a microservices architecture where a single external entry point fans out to multiple backends
Do not use Ingress when:
- You need to expose a single TCP or UDP service; use a LoadBalancer Service or an NLB directly instead
- You only have one HTTP service and want the simplest possible setup
- You need WebSocket or gRPC connections; these work with ALB but require additional annotations and care
Common mistakes
- Forgetting the ingress class. Without
kubernetes.io/ingress.class: alb(orspec.ingressClassName: alb), the AWS Load Balancer Controller ignores your Ingress resource entirely. No ALB is created, and no error appears inkubectl get ingress. You have to check the controller logs to see what happened. - Using
target-type: instanceon EKS. Theinstancemode routes traffic to node ports via kube-proxy, adding an unnecessary extra hop. On EKS with VPC CNI, every pod has a real VPC IP. Usetarget-type: ipto route directly to pods. - Missing or misconfigured IRSA. The controller needs an IAM role with permissions to create and manage ALBs, target groups, and security groups. Without IRSA configured correctly, the controller fails silently. Check the controller logs in
kube-systemfor permission denied errors. - Untagged subnets. The controller uses subnet tags to find where to place the ALB. Missing tags mean no ALB. Public subnets need
kubernetes.io/role/elb: 1; private subnets needkubernetes.io/role/internal-elb: 1. - Expecting Ingress to work before the controller is installed. Confirm the controller is running before applying any Ingress resources:
kubectl get pods -n kube-system | grep aws-load-balancer. - Wrong backend service port. The port in the Ingress backend spec must match the port the Service exposes, not the container port. If the Service exposes port 80 but the Ingress references port 8080, traffic will not reach your pods.
Troubleshooting checks
If your Ingress is not working, run through these in order.
Check whether the ALB was created:
kubectl get ingress -n <namespace>The ADDRESS column should show the ALB DNS name. If it is empty, the controller did not provision the ALB.
Describe the Ingress for controller events:
kubectl describe ingress <name> -n <namespace>The Events section at the bottom shows what the controller attempted and where it stopped.
Check the controller is running:
kubectl get pods -n kube-system | grep aws-load-balancerCheck controller logs for errors:
kubectl logs -n kube-system -l app.kubernetes.io/name=aws-load-balancer-controllerIf the logs show AccessDenied or similar AWS API errors, IRSA is not set up correctly. The controller pod is running but cannot call the AWS API. Check that the IAM role ARN on the service account annotation matches the role that has the Load Balancer Controller IAM policy attached.
Common specific issues:
- No ADDRESS on the Ingress: Controller is not running, IRSA is missing, or subnets are not tagged correctly.
- ALB created but targets are unhealthy: Confirm
target-type: ipis set, the Service selector matches running pods, and the Service port matches what the container listens on. - HTTPS not working: Verify the ACM certificate ARN is correct and the certificate status is
Issued. The domain on the certificate must match the host rule in the Ingress. - HTTP not redirecting to HTTPS: Both
listen-ports(including HTTP 80 and HTTPS 443) andssl-redirect: '443'must be set together. - 404 from the ALB: The ALB listener rules were created but no backend rule matched the request. Check that the path and host in the Ingress spec match what the request is sending.
- Internal service not reachable from outside: The
schemeannotation may be set tointernalwhen you needinternet-facing, or the ALB security group does not allow inbound traffic on port 443.
For cluster-level logging context, see logging in Kubernetes. For security controls around ingress, see securing EKS clusters.
Cost and architecture considerations
The main cost benefit of Ingress is consolidation: one ALB in front of many services instead of one load balancer per service.
A LoadBalancer Service on EKS typically provisions one Network Load Balancer. If you have ten services exposed via separate LoadBalancer Services, you pay for ten load balancers. Put those same ten services behind a single ALB via Ingress with a shared group.name, and you pay for one.
ALB pricing has two parts: a fixed hourly rate per load balancer, plus a usage-based charge (Load Balancer Capacity Units) that scales with connections, data processed, and rules evaluated. For most applications with moderate traffic, the usage cost is low and consolidating behind fewer ALBs saves money clearly. Check the current AWS pricing page for your region, since rates vary. At very high traffic volumes, model the usage cost directly against the fixed cost of running more load balancers.
Practical patterns worth knowing:
- One ALB per environment (dev, staging, prod) using
group.nameis a common and cost-effective pattern. Teams define their own Ingress resources and the controller merges them into the shared ALB automatically. - Separate internet-facing and internal ALBs is a clean split: one group for public traffic, one for internal service-to-service communication.
- WAF on public ALBs adds cost but provides meaningful protection if your services handle user-generated content or are exposed to untrusted traffic.
Summary
- An Ingress defines HTTP routing rules; an ingress controller enforces them. You need both. The Ingress resource alone does nothing.
- On EKS, AWS Load Balancer Controller is the AWS-native ingress controller. It creates real ALBs from your Ingress resources and must be installed separately via Helm.
- NGINX Ingress is a valid alternative for teams needing advanced routing rules or cross-cloud consistency, but AWS Load Balancer Controller is the right default for EKS.
- Before Ingress will work: install the controller, configure IRSA, tag your subnets, and have an ACM certificate ready if using HTTPS.
- Use
alb.ingress.kubernetes.io/group.nameto share one ALB across multiple Ingress resources. This is the most cost-effective pattern. - TLS is terminated at the ALB via ACM. Your pods receive plain HTTP. No certificates need to be stored in Kubernetes Secrets.
Frequently asked questions
What is the difference between an Ingress and an ingress controller?
An Ingress is a Kubernetes resource, a YAML file that defines HTTP routing rules (which hostname or path routes to which Service). An ingress controller is the software that reads those rules and configures a load balancer to enforce them. Without a running ingress controller, an Ingress resource does nothing.
Do I need AWS Load Balancer Controller to use Ingress on EKS?
To get ALB-backed Ingress on EKS, yes. AWS Load Balancer Controller is the AWS-native option that provisions real ALBs from your Ingress resources. Alternatively, you can install NGINX Ingress Controller, which runs a proxy pod inside the cluster. Both work on EKS; AWS Load Balancer Controller is the default choice for most teams building on AWS.
When should I use Ingress instead of a LoadBalancer Service?
Use Ingress when you have multiple HTTP/HTTPS services to expose and want to route them through a single load balancer using path-based or host-based rules. A LoadBalancer Service is simpler when you only have one service to expose. The more services you have, the more Ingress saves on cost and operational overhead.
Can I use NGINX Ingress Controller on EKS instead of AWS Load Balancer Controller?
Yes. NGINX Ingress Controller runs inside the cluster as a pod and exposes itself via a LoadBalancer Service, which creates an NLB. It is a good choice if you need NGINX-specific features, consistent behaviour across cloud providers, or fine-grained proxy configuration. AWS Load Balancer Controller is the better default if you want native ALB provisioning and AWS-native features like WAF integration and ACM certificates.
Does Ingress terminate TLS, or do my pods handle certificates?
With AWS Load Balancer Controller, TLS is terminated at the ALB using an ACM certificate. Traffic from the ALB to your pods travels over the VPC network as plain HTTP, so your pods do not handle certificates. If you use NGINX Ingress, TLS termination happens at the NGINX pod using a certificate stored in a Kubernetes Secret.