Nginx Ingress Controller on AWS EKS
Expose Multiple Services with a Single Load Balancer
Learn how to install and configure Nginx Ingress Controller to route traffic to multiple services using hostnames.
What You’ll Learn
- Install Nginx Ingress Controller using Helm
- Configure Ingress resources with modern
ingressClassNamesyntax - Route traffic to multiple services using host-based routing
- Get AWS Network Load Balancer (NLB) IP address
- Configure local hosts file for testing (Mac & PC)
- Troubleshoot common Ingress routing issues
Prerequisites
- AWS EKS cluster running (see Exercise 1)
- kubectl configured and connected to your cluster
- Helm 3 installed (https://helm.sh/docs/intro/install/)
- Todo app and MongoDB deployed (see Exercise 2 or 3)
Verify prerequisites:
kubectl get nodes
helm version
kubectl get podsUnderstanding Ingress
What is Ingress?
Problem: Each Kubernetes Service with type: LoadBalancer creates a separate AWS Load Balancer:
Service 1 β Load Balancer 1 ($20/month)
Service 2 β Load Balancer 2 ($20/month)
Service 3 β Load Balancer 3 ($20/month)
Total: $60/month + expensive!Solution: Ingress uses a single Load Balancer to route to multiple services:
βββ Service 1
Load Balancer β Ingress Controller ββΌββ Service 2
βββ Service 3
Total: $20/month (just one!)How Ingress Works
Browser β DNS β Load Balancer β Ingress Controller β Service β PodsExample:
todo-app.mydomain.tld β NLB β Nginx Ingress β todo-service β Todo pods
mongo-express.mydomain.tld β NLB β Nginx Ingress β mongo-express-service β MongoDB podsRouting is based on:
- HTTP Host header (
todo-app.mydomain.tld) - URL paths (
/api,/admin, etc.)
Step 1: Install Nginx Ingress Controller
1.1 Add Helm Repository
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update1.2 Create Helm Values File
Create a file named ingress-nginx-values.yaml:
# ingress-nginx-values.yaml
controller:
# The kind of deployment to use
kind: DaemonSet
# Configuration for the controller's service
service:
# Key annotations for AWS auto-provisioning
annotations:
# Specifies the type of AWS load balancer to create. "nlb" is for Network Load Balancer.
service.beta.kubernetes.io/aws-load-balancer-type: "nlb"
# Optional: Sets the NLB to be internet-facing. Use "internal" for a private NLB.
service.beta.kubernetes.io/aws-load-balancer-scheme: "internet-facing"
# Set externalTrafficPolicy to Local to preserve the source IP of the client
externalTrafficPolicy: LocalWhy these settings?
kind: DaemonSet- Runs one Ingress controller pod on each nodenlb- Network Load Balancer (better performance than Classic LB)internet-facing- Makes the NLB publicly accessibleexternalTrafficPolicy: Local- Preserves client source IP addresses
1.3 Install with Helm
helm install ingress-nginx ingress-nginx/ingress-nginx \
--namespace ingress-nginx \
--create-namespace \
--values ingress-nginx-values.yamlExpected output:
NAME: ingress-nginx
NAMESPACE: ingress-nginx
STATUS: deployed1.4 Verify Installation
Check the pods:
kubectl get pods -n ingress-nginxExpected:
NAME READY STATUS RESTARTS AGE
ingress-nginx-controller-xxxxx 1/1 Running 0 30sCheck the service:
kubectl get svc -n ingress-nginxExpected:
NAME TYPE EXTERNAL-IP
ingress-nginx-controller LoadBalancer k8s-ingressn-ingressn-xxxx.elb.eu-west-1.amazonaws.comWait 2-3 minutes for AWS to create the NLB. EXTERNAL-IP will show <pending> initially.
Check IngressClass was created:
kubectl get ingressclassExpected:
NAME CONTROLLER PARAMETERS AGE
nginx k8s.io/ingress-nginx <none> 2mStep 2: Create Ingress Resource
2.1 Create Ingress Manifest
Create a file named ingress.yaml:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-ingress
spec:
ingressClassName: nginx
rules:
- host: todo-app.mydomain.tld
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: todo-service
port:
number: 80
- host: mongo-express.mydomain.tld
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: mongo-express-service
port:
number: 8081
- http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: todo-service
port:
number: 80Key points:
ingressClassName: nginx- MODERN syntax (replaces deprecated annotation)rules- Host-based routing rules with specific hostnames- Catch-all rule (rule without
hostfield) - Acts as fallback for unmatched hostnames path: /withpathType: Prefix- Match all paths under this host
Why no spec.defaultBackend?
The spec.defaultBackend field is not supported by nginx ingress controller - it ignores this field and uses its own internal 404 handler. Instead, use a catch-all rule (a rule with no host field) which matches any hostname not explicitly defined.
IMPORTANT: Do NOT use the deprecated annotation:
# β DEPRECATED - Don't use this!
metadata:
annotations:
kubernetes.io/ingress.class: "nginx"β Use this instead:
spec:
ingressClassName: nginx2.2 Apply the Ingress
kubectl apply -f ingress.yaml2.3 Verify Ingress
kubectl get ingressExpected:
NAME CLASS HOSTS ADDRESS PORTS AGE
my-ingress nginx todo-app.mydomain.tld,mongo-express.mydomain.tld k8s-ingressn-xxxx.elb.eu-west-1... 80 10sImportant: Check that CLASS shows nginx (not <none>)!
Get detailed information:
kubectl describe ingress my-ingressLook for:
Ingress Class: nginx
Default backend: <default>
Rules:
Host Path Backends
---- ---- --------
todo-app.mydomain.tld / todo-service:80 (192.168.x.x:80,...)
mongo-express.mydomain.tld / mongo-express-service:8081 (192.168.x.x:8081)
* / todo-service:80 (192.168.x.x:80,...)Note: The * (wildcard) rule is the catch-all that handles any hostname not explicitly matched.
Step 3: Get the Load Balancer IP Address
3.1 Get the NLB Hostname
kubectl get ingress my-ingress -o jsonpath='{.status.loadBalancer.ingress[0].hostname}'Example output:
k8s-ingressn-ingressn-7978c4cb7c-ed4ba9b49be47e70.elb.eu-west-1.amazonaws.com3.2 Resolve Hostname to IP Address
Option 1 - Using nslookup:
nslookup $(kubectl get ingress my-ingress -o jsonpath='{.status.loadBalancer.ingress[0].hostname}')Option 2 - Using dig:
dig +short $(kubectl get ingress my-ingress -o jsonpath='{.status.loadBalancer.ingress[0].hostname}')Example output:
63.35.45.207Copy this IP address - you’ll need it for the hosts file!
3.3 Test Direct Access with Host Header
Before editing your hosts file, verify the Ingress is working:
# Replace with your actual NLB hostname
curl -H "Host: todo-app.mydomain.tld" http://k8s-ingressn-ingressn-xxxx.elb.eu-west-1.amazonaws.com/Expected: You should see HTML from the todo app!
<!DOCTYPE html>
<html lang="en">
<head>
<title>Todo App</title>
...Step 4: Configure Local Hosts File
To test with domain names locally, add entries to your hosts file.
4.1 Edit Hosts File on Mac/Linux
Open the hosts file with sudo:
sudo nano /etc/hostsOr if you prefer vim:
sudo vim /etc/hostsAdd these lines at the end (replace with your actual IP):
63.35.45.207 todo-app.mydomain.tld
63.35.45.207 mongo-express.mydomain.tldSave and exit:
- Nano: Press
Ctrl+O,Enter, thenCtrl+X - Vim: Press
Esc, type:wq, pressEnter
4.2 Edit Hosts File on Windows
Method 1 - Notepad (Administrator):
- Press
Windows + R - Type:
notepad - Right-click Notepad β Run as administrator
- File β Open β Browse to:
C:\Windows\System32\drivers\etc\hosts - Change file filter to All Files (.) to see the hosts file
- Add these lines at the end (replace with your actual IP):
63.35.45.207 todo-app.mydomain.tld 63.35.45.207 mongo-express.mydomain.tld - File β Save
Method 2 - PowerShell (Administrator):
# Run PowerShell as Administrator
Add-Content -Path C:\Windows\System32\drivers\etc\hosts -Value "`n63.35.45.207 todo-app.mydomain.tld"
Add-Content -Path C:\Windows\System32\drivers\etc\hosts -Value "63.35.45.207 mongo-express.mydomain.tld"4.3 Verify Hosts File
Mac/Linux:
cat /etc/hosts | grep mydomainWindows (PowerShell):
Get-Content C:\Windows\System32\drivers\etc\hosts | Select-String mydomainExpected output:
63.35.45.207 todo-app.mydomain.tld
63.35.45.207 mongo-express.mydomain.tld4.4 Flush DNS Cache (Optional)
Mac:
sudo dscacheutil -flushcache
sudo killall -HUP mDNSResponderWindows (PowerShell as Admin):
ipconfig /flushdnsLinux:
sudo systemd-resolve --flush-cachesStep 5: Test the Ingress Routing
5.1 Test Todo App
Open in your browser:
http://todo-app.mydomain.tldExpected: You should see the Todo application!
5.2 Test Mongo Express
Open in your browser:
http://mongo-express.mydomain.tldExpected: You should see a Basic Auth login prompt.
Login credentials (from your deployment):
- Username:
admin - Password:
pass
5.3 Test from Command Line
Test Todo App:
curl http://todo-app.mydomain.tldTest Mongo Express:
curl http://mongo-express.mydomain.tldExpected: HTTP 401 Unauthorized (because it requires Basic Auth)
5.4 Verify DNS Resolution
ping todo-app.mydomain.tldExpected:
PING todo-app.mydomain.tld (63.35.45.207): 56 data bytesTroubleshooting
Issue 1: Ingress CLASS Shows <none>
Symptom:
kubectl get ingress
NAME CLASS HOSTS
my-ingress <none> todo-app.mydomain.tld,...Cause: Using deprecated annotation instead of ingressClassName.
Solution: Update your Ingress manifest:
spec:
ingressClassName: nginx # Add this line!
rules:
...Remove any deprecated annotations:
# Delete this if present:
metadata:
annotations:
kubernetes.io/ingress.class: "nginx" # Remove!Apply the fix:
kubectl apply -f ingress.yamlIssue 2: 404 Not Found
Symptom: Browser shows “404 Not Found” or “default backend - 404”
Check 1 - Verify services exist:
kubectl get svcCheck 2 - Verify service endpoints:
kubectl get endpoints todo-service
kubectl get endpoints mongo-express-serviceExpected: Should show pod IPs, not empty.
Check 3 - Check Ingress controller logs:
kubectl logs -n ingress-nginx -l app.kubernetes.io/name=ingress-nginxCheck 4 - Verify Host header:
# Test with explicit Host header
curl -v -H "Host: todo-app.mydomain.tld" http://YOUR-NLB-ADDRESS/Solution: Ensure service names in Ingress match actual service names:
# Compare these:
kubectl get ingress my-ingress -o yaml | grep "name:"
kubectl get svcIssue 3: Can’t Resolve Domain Names
Symptom: ping todo-app.mydomain.tld fails with “cannot resolve hostname”
Check hosts file was saved:
Mac/Linux:
cat /etc/hosts | grep mydomainWindows:
Get-Content C:\Windows\System32\drivers\etc\hosts | Select-String mydomainSolution: Re-edit hosts file and ensure you saved it.
Issue 4: Connection Refused or Timeout
Symptom: Browser can’t connect or times out
Check 1 - NLB is ready:
kubectl get svc -n ingress-nginxEnsure EXTERNAL-IP is not <pending>.
Check 2 - Ingress controller is running:
kubectl get pods -n ingress-nginxCheck 3 - Test NLB directly:
# Get NLB hostname
kubectl get ingress my-ingress -o jsonpath='{.status.loadBalancer.ingress[0].hostname}'
# Test it
curl -H "Host: todo-app.mydomain.tld" http://NLB-HOSTNAME/Solution: Wait 2-3 minutes for AWS NLB provisioning if it’s newly created.
Issue 5: Wrong Service Port
Symptom: 502 Bad Gateway or 503 Service Unavailable
Check service ports match:
kubectl get svcCompare with Ingress:
kubectl get ingress my-ingress -o yamlExample mismatch:
# Ingress says:
backend:
service:
port:
number: 80 # β Wrong!
# But service exposes:
apiVersion: v1
kind: Service
spec:
ports:
- port: 8081 # Actual portSolution: Update Ingress to use the correct port.
Issue 6: Default Backend Not Working
Symptom: Accessing the NLB directly (or with an unmatched hostname) returns 404 from nginx, even though you defined spec.defaultBackend.
Example:
curl http://NLB-HOSTNAME/
# Returns: 404 Not Found (from nginx)Cause: The nginx ingress controller does not support the spec.defaultBackend field. It ignores this field and uses its own internal 404 handler.
Solution: Use a catch-all rule instead:
spec:
ingressClassName: nginx
# β This doesn't work with nginx ingress:
# defaultBackend:
# service:
# name: todo-service
rules:
- host: todo-app.mydomain.tld
# ... specific rules
# β
Use this instead - catch-all rule (no host field):
- http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: todo-service
port:
number: 80The catch-all rule (rule without a host field) matches any hostname, effectively acting as a default backend.
Cleanup
Remove Ingress Resource
kubectl delete ingress my-ingressUninstall Nginx Ingress Controller
helm uninstall ingress-nginx -n ingress-nginxThis will:
- Delete the Ingress controller pods
- Delete the AWS NLB (~2-3 minutes)
- Remove the namespace
Remove Hosts File Entries
Mac/Linux:
sudo nano /etc/hosts
# Delete the lines you addedWindows: Open Notepad as Administrator and remove the lines from:
C:\Windows\System32\drivers\etc\hostsUnderstanding the Costs
AWS Network Load Balancer (NLB)
Fixed costs:
- $0.027/hour (~$20/month) per NLB
- $0.008/GB data processed
Cost comparison:
Without Ingress:
- 3 services = 3 Load Balancers = $60/month
With Ingress:
- 1 Ingress NLB = $20/month
- Saves $40/month! π°Plus: Easier to manage SSL/TLS certificates for all services in one place.
Production Considerations
Use Real Domain Names
Replace mydomain.tld with your actual domain:
rules:
- host: todo.example.com # Your real domainConfigure DNS:
Add CNAME or A records in your DNS provider:
todo.example.com CNAME k8s-ingressn-xxxx.elb.eu-west-1.amazonaws.com
mongo-express.example.com CNAME k8s-ingressn-xxxx.elb.eu-west-1.amazonaws.comOr use A records with the NLB IP address:
todo.example.com A 63.35.45.207
mongo-express.example.com A 63.35.45.207Add TLS/SSL
Install cert-manager for automatic SSL certificates:
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.0/cert-manager.yamlUpdate Ingress to use HTTPS:
spec:
ingressClassName: nginx
tls:
- hosts:
- todo-app.example.com
secretName: todo-tls
rules:
- host: todo-app.example.com
...Use Ingress Annotations
Control Nginx behavior with annotations:
metadata:
annotations:
# Redirect HTTP to HTTPS
nginx.ingress.kubernetes.io/ssl-redirect: "true"
# Increase body size limit (for file uploads)
nginx.ingress.kubernetes.io/proxy-body-size: "50m"
# Add CORS headers
nginx.ingress.kubernetes.io/enable-cors: "true"Summary
What you accomplished:
- β Installed Nginx Ingress Controller using Helm
- β Created IngressClass resource
- β
Configured Ingress with modern
ingressClassNamesyntax - β Set up host-based routing to multiple services
- β Obtained NLB IP address from AWS
- β Configured local hosts file (Mac & Windows)
- β Tested routing with custom domain names
- β Learned to troubleshoot common Ingress issues
Key concepts learned:
- Ingress vs Service LoadBalancer - Cost and management benefits
- IngressClass - Modern way to specify Ingress controllers
- Host-based routing - Multiple domains β one Load Balancer
- Path-based routing - Different paths β different services
- AWS NLB integration - Automatic provisioning via Kubernetes
- Local testing - Hosts file for development
Architecture you built:
Browser (todo-app.mydomain.tld)
β
Hosts file (63.35.45.207)
β
AWS Network Load Balancer
β
Nginx Ingress Controller Pod
β
βββββββββββ¬βββββββββββββββββββββββββββββ
β Route 1 β todo-app.mydomain.tld β β todo-service β Todo pods
βββββββββββΌβββββββββββββββββββββββββββββ€
β Route 2 β mongo-express.mydomain.tld β β mongo-express-service β Mongo Express pod
βββββββββββΌβββββββββββββββββββββββββββββ€
β Route 3 β * (catch-all) β β todo-service β Todo pods (default)
βββββββββββ΄βββββββββββββββββββββββββββββTotal cost: ~$20/month for unlimited services (vs $20/service without Ingress)
Next Steps
Ready for more?
- Add SSL/TLS - Secure your applications with HTTPS
- Configure cert-manager - Automatic Let’s Encrypt certificates
- Advanced routing - Path-based routing, rewrites, redirects
- Rate limiting - Protect against abuse
- Authentication - Add basic auth or OAuth2 proxy
- Monitoring - Prometheus metrics from Ingress controller
Congratulations! π
You now know how to:
- Use Ingress to save money on AWS
- Route traffic to multiple services efficiently
- Configure domain names for local testing
- Troubleshoot Ingress routing issues
- Deploy production-ready Ingress configurations
This is a crucial skill for production Kubernetes clusters!
Happy Routing! π