Skip to content

NGINX Ingress Controller

HTTP ingress controller for hostname-based routing to Kubernetes services.


Overview

NGINX Ingress Controller enables multiple web applications to share a single IP address (10.89.97.220) using hostname-based routing.

Benefits: - Conserve MetalLB IP addresses - Professional URL structure (home.internal vs 10.89.97.213) - Centralized HTTP routing - Easy SSL/TLS management - Standard ingress patterns


Architecture

Client Request (http://home.internal)
OPNsense DNS (resolves to 10.89.97.220)
NGINX Ingress Controller (10.89.97.220)
    ↓ (checks Host header)
    ├─ home.internal → home-portal Service (ClusterIP)
    ├─ money.internal → money-tracker Service (ClusterIP)
    └─ travel.internal → trip-planner Service (ClusterIP)

Installation

Prerequisites

  • MetalLB installed and configured
  • Available IP in MetalLB pool
  • Helm installed

Install via Helm

# Add NGINX Ingress repository
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update

# Create values file
cat > /root/tower-fleet/manifests/core/nginx-ingress-values.yaml <<'EOF'
controller:
  service:
    type: LoadBalancer
    loadBalancerIP: 10.89.97.220

  resources:
    requests:
      cpu: 100m
      memory: 200Mi
    limits:
      cpu: 500m
      memory: 512Mi

  metrics:
    enabled: true
    serviceMonitor:
      enabled: true
      additionalLabels:
        release: kube-prometheus-stack

  config:
    ssl-redirect: "false"
EOF

# Install
helm install ingress-nginx ingress-nginx/ingress-nginx \
  --namespace ingress-nginx \
  --create-namespace \
  --values /root/tower-fleet/manifests/core/nginx-ingress-values.yaml

# Verify installation
kubectl get pods -n ingress-nginx
kubectl get svc -n ingress-nginx

Expected output:

NAME                       TYPE           EXTERNAL-IP     PORT(S)
ingress-nginx-controller   LoadBalancer   10.89.97.220    80:XXXXX/TCP,443:XXXXX/TCP


DNS Configuration

OPNsense Unbound DNS

  1. Navigate to: Services → Unbound DNS → Overrides → Host Overrides
  2. Click + Add
  3. Configure:
  4. Host: app name (e.g., home)
  5. Domain: internal
  6. IP Address: 10.89.97.220
  7. Click Save and Apply Changes

Repeat for each app.

Current configuration: - home.internal → 10.89.97.220 - money.internal → 10.89.97.220 - travel.internal → 10.89.97.220

Alternative: Local /etc/hosts

For testing without DNS configuration:

# Add to /etc/hosts on your machine
10.89.97.220 home.internal
10.89.97.220 money.internal
10.89.97.220 travel.internal

Creating an Ingress Resource

Standard Pattern

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: myapp
  namespace: myapp
  annotations:
    nginx.ingress.kubernetes.io/ssl-redirect: "false"
    nginx.ingress.kubernetes.io/proxy-body-size: "10m"
spec:
  ingressClassName: nginx
  rules:
  - host: myapp.internal
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: myapp
            port:
              number: 80

Service Requirements

Service must be type ClusterIP (not LoadBalancer):

apiVersion: v1
kind: Service
metadata:
  name: myapp
  namespace: myapp
spec:
  type: ClusterIP  # Required for Ingress
  selector:
    app: myapp
  ports:
  - port: 80
    targetPort: 3000

Deployment Flow

  1. Create Service (ClusterIP)
  2. Create Ingress pointing to Service
  3. Configure DNS (OPNsense or /etc/hosts)
  4. Test access via hostname

Deployed Applications

App Hostname Backend Service Namespace Status
Home Portal home.internal home-portal:80 home-portal ✅ Active
Money Tracker money.internal money-tracker:80 money-tracker ✅ Active
Trip Planner travel.internal trip-planner:80 trip-planner ✅ Active

Common Annotations

annotations:
  # Basic
  nginx.ingress.kubernetes.io/ssl-redirect: "false"

  # File uploads
  nginx.ingress.kubernetes.io/proxy-body-size: "100m"

  # Timeouts (long-running requests)
  nginx.ingress.kubernetes.io/proxy-read-timeout: "600"
  nginx.ingress.kubernetes.io/proxy-send-timeout: "600"
  nginx.ingress.kubernetes.io/proxy-connect-timeout: "60"

  # CORS
  nginx.ingress.kubernetes.io/enable-cors: "true"
  nginx.ingress.kubernetes.io/cors-allow-origin: "*"

  # WebSocket support
  nginx.ingress.kubernetes.io/proxy-http-version: "1.1"
  nginx.ingress.kubernetes.io/configuration-snippet: |
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";

Troubleshooting

Issue: 404 Not Found

Symptoms: Curl returns 404 "default backend"

Cause: Host header not matching any Ingress rule

Solution:

# Check Ingress configuration
kubectl get ingress -A

# Verify hostname matches
curl -v http://home.internal
# Look for "Host: home.internal" in request headers

# Test with explicit Host header
curl -H "Host: home.internal" http://10.89.97.220

Issue: DNS not resolving

Symptoms: curl: (6) Could not resolve host: home.internal

Solution:

# Check OPNsense DNS override configured
# Services → Unbound DNS → Overrides

# Or add to /etc/hosts:
echo "10.89.97.220 home.internal" | sudo tee -a /etc/hosts

# Test resolution
nslookup home.internal
ping home.internal

Issue: Service unreachable

Symptoms: Ingress exists but connection times out

Debugging:

# Check service is ClusterIP (not LoadBalancer)
kubectl get svc -n myapp

# Check pod is running
kubectl get pods -n myapp

# Test service directly from cluster
kubectl run -it --rm debug --image=curlimages/curl --restart=Never -- \
  curl http://myapp.myapp.svc.cluster.local

# Check Ingress backend
kubectl describe ingress myapp -n myapp

Issue: Wrong Service Type

Symptoms: App has both LoadBalancer IP and Ingress

Fix:

# Change Service to ClusterIP
kubectl patch svc myapp -n myapp -p '{"spec":{"type":"ClusterIP"}}'

# Verify
kubectl get svc -n myapp
# Should show type: ClusterIP, no EXTERNAL-IP


Monitoring

Metrics

Available in Prometheus: - nginx_ingress_controller_requests_total - Total HTTP requests - nginx_ingress_controller_request_duration_seconds - Request latency - nginx_ingress_controller_response_size_bytes - Response size

Logs

# View controller logs
kubectl logs -n ingress-nginx -l app.kubernetes.io/name=ingress-nginx -f

# View access logs
kubectl logs -n ingress-nginx -l app.kubernetes.io/component=controller -f

# Check specific errors
kubectl logs -n ingress-nginx -l app.kubernetes.io/component=controller | grep -i error

Grafana Dashboard

Recommended Dashboard: Import Grafana dashboard ID 9614 (NGINX Ingress Controller)

Shows: - Request rate - Success/error rates - Response times - Backend health


Migration from LoadBalancer

If you have an existing app using LoadBalancer:

Step-by-step Migration

  1. Create Ingress manifest

    apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
      name: myapp
      namespace: myapp
    spec:
      ingressClassName: nginx
      rules:
      - host: myapp.internal
        http:
          paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: myapp
                port:
                  number: 80
    

  2. Apply Ingress (LoadBalancer still works)

    kubectl apply -f ingress.yaml
    

  3. Configure DNS Add myapp.internal → 10.89.97.220 in OPNsense

  4. Test Ingress route

    curl http://myapp.internal
    # Should return app response
    

  5. Backup LoadBalancer service

    kubectl get svc myapp -n myapp -o yaml > myapp-svc-backup.yaml
    

  6. Convert to ClusterIP

    kubectl patch svc myapp -n myapp -p '{"spec":{"type":"ClusterIP"}}'
    

  7. Verify

    # Old IP should not work
    curl http://OLD_IP
    # Should timeout or fail
    
    # New hostname should work
    curl http://myapp.internal
    # Should return app response
    

Benefits: - Frees up MetalLB IP - Professional hostname - Easier SSL management - Centralized routing


SSL/TLS Configuration

Coming soon: Integration with cert-manager for automatic HTTPS certificates.

Current status: HTTP only (all apps use ssl-redirect: false)

Future implementation: 1. Install cert-manager 2. Create ClusterIssuer (self-signed or Let's Encrypt) 3. Update Ingress with TLS section 4. Automatic certificate generation


Advanced Configuration

Path-based Routing

Route multiple services under one hostname:

spec:
  rules:
  - host: apps.internal
    http:
      paths:
      - path: /portal
        pathType: Prefix
        backend:
          service:
            name: home-portal
            port:
              number: 80
      - path: /money
        pathType: Prefix
        backend:
          service:
            name: money-tracker
            port:
              number: 80

Access: - http://apps.internal/portal → home-portal - http://apps.internal/money → money-tracker

Rate Limiting

annotations:
  nginx.ingress.kubernetes.io/limit-rps: "10"
  nginx.ingress.kubernetes.io/limit-connections: "5"

Custom Headers

annotations:
  nginx.ingress.kubernetes.io/configuration-snippet: |
    more_set_headers "X-Frame-Options: DENY";
    more_set_headers "X-Content-Type-Options: nosniff";


Last Updated: 2025-11-20 Status: Active Controller Version: ingress-nginx 4.x LoadBalancer IP: 10.89.97.220