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 ingressClassName syntax
  • 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 pods

Understanding 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 β†’ Pods

Example:

todo-app.mydomain.tld β†’ NLB β†’ Nginx Ingress β†’ todo-service β†’ Todo pods
mongo-express.mydomain.tld β†’ NLB β†’ Nginx Ingress β†’ mongo-express-service β†’ MongoDB pods

Routing 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 update

1.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: Local

Why these settings?

  • kind: DaemonSet - Runs one Ingress controller pod on each node
  • nlb - Network Load Balancer (better performance than Classic LB)
  • internet-facing - Makes the NLB publicly accessible
  • externalTrafficPolicy: 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.yaml

Expected output:

NAME: ingress-nginx
NAMESPACE: ingress-nginx
STATUS: deployed

1.4 Verify Installation

Check the pods:

kubectl get pods -n ingress-nginx

Expected:

NAME                             READY   STATUS    RESTARTS   AGE
ingress-nginx-controller-xxxxx   1/1     Running   0          30s

Check the service:

kubectl get svc -n ingress-nginx

Expected:

NAME                                 TYPE           EXTERNAL-IP
ingress-nginx-controller             LoadBalancer   k8s-ingressn-ingressn-xxxx.elb.eu-west-1.amazonaws.com

Wait 2-3 minutes for AWS to create the NLB. EXTERNAL-IP will show <pending> initially.

Check IngressClass was created:

kubectl get ingressclass

Expected:

NAME    CONTROLLER             PARAMETERS   AGE
nginx   k8s.io/ingress-nginx   <none>       2m

Step 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: 80

Key points:

  • ingressClassName: nginx - MODERN syntax (replaces deprecated annotation)
  • rules - Host-based routing rules with specific hostnames
  • Catch-all rule (rule without host field) - Acts as fallback for unmatched hostnames
  • path: / with pathType: 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: nginx

2.2 Apply the Ingress

kubectl apply -f ingress.yaml

2.3 Verify Ingress

kubectl get ingress

Expected:

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      10s

Important: Check that CLASS shows nginx (not <none>)!

Get detailed information:

kubectl describe ingress my-ingress

Look 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.com

3.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.207

Copy 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/hosts

Or if you prefer vim:

sudo vim /etc/hosts

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

Save and exit:

  • Nano: Press Ctrl+O, Enter, then Ctrl+X
  • Vim: Press Esc, type :wq, press Enter

4.2 Edit Hosts File on Windows

Method 1 - Notepad (Administrator):

  1. Press Windows + R
  2. Type: notepad
  3. Right-click Notepad β†’ Run as administrator
  4. File β†’ Open β†’ Browse to:
    C:\Windows\System32\drivers\etc\hosts
  5. Change file filter to All Files (.) to see the hosts file
  6. 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
  7. 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 mydomain

Windows (PowerShell):

Get-Content C:\Windows\System32\drivers\etc\hosts | Select-String mydomain

Expected output:

63.35.45.207    todo-app.mydomain.tld
63.35.45.207    mongo-express.mydomain.tld

4.4 Flush DNS Cache (Optional)

Mac:

sudo dscacheutil -flushcache
sudo killall -HUP mDNSResponder

Windows (PowerShell as Admin):

ipconfig /flushdns

Linux:

sudo systemd-resolve --flush-caches

Step 5: Test the Ingress Routing

5.1 Test Todo App

Open in your browser:

http://todo-app.mydomain.tld

Expected: You should see the Todo application!

5.2 Test Mongo Express

Open in your browser:

http://mongo-express.mydomain.tld

Expected: 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.tld

Test Mongo Express:

curl http://mongo-express.mydomain.tld

Expected: HTTP 401 Unauthorized (because it requires Basic Auth)

5.4 Verify DNS Resolution

ping todo-app.mydomain.tld

Expected:

PING todo-app.mydomain.tld (63.35.45.207): 56 data bytes

Troubleshooting

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.yaml

Issue 2: 404 Not Found

Symptom: Browser shows “404 Not Found” or “default backend - 404”

Check 1 - Verify services exist:

kubectl get svc

Check 2 - Verify service endpoints:

kubectl get endpoints todo-service
kubectl get endpoints mongo-express-service

Expected: Should show pod IPs, not empty.

Check 3 - Check Ingress controller logs:

kubectl logs -n ingress-nginx -l app.kubernetes.io/name=ingress-nginx

Check 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 svc

Issue 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 mydomain

Windows:

Get-Content C:\Windows\System32\drivers\etc\hosts | Select-String mydomain

Solution: 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-nginx

Ensure EXTERNAL-IP is not <pending>.

Check 2 - Ingress controller is running:

kubectl get pods -n ingress-nginx

Check 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 svc

Compare with Ingress:

kubectl get ingress my-ingress -o yaml

Example mismatch:

# Ingress says:
backend:
  service:
    port:
      number: 80  # ❌ Wrong!

# But service exposes:
apiVersion: v1
kind: Service
spec:
  ports:
  - port: 8081  # Actual port

Solution: 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: 80

The 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-ingress

Uninstall Nginx Ingress Controller

helm uninstall ingress-nginx -n ingress-nginx

This 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 added

Windows: Open Notepad as Administrator and remove the lines from:

C:\Windows\System32\drivers\etc\hosts

Understanding 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 domain

Configure 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.com

Or use A records with the NLB IP address:

todo.example.com         A      63.35.45.207
mongo-express.example.com A      63.35.45.207

Add 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.yaml

Update 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 ingressClassName syntax
  • βœ… 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:

  1. Ingress vs Service LoadBalancer - Cost and management benefits
  2. IngressClass - Modern way to specify Ingress controllers
  3. Host-based routing - Multiple domains β†’ one Load Balancer
  4. Path-based routing - Different paths β†’ different services
  5. AWS NLB integration - Automatic provisioning via Kubernetes
  6. 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?

  1. Add SSL/TLS - Secure your applications with HTTPS
  2. Configure cert-manager - Automatic Let’s Encrypt certificates
  3. Advanced routing - Path-based routing, rewrites, redirects
  4. Rate limiting - Protect against abuse
  5. Authentication - Add basic auth or OAuth2 proxy
  6. 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! πŸš€