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¶
- Navigate to: Services → Unbound DNS → Overrides → Host Overrides
- Click + Add
- Configure:
- Host: app name (e.g.,
home) - Domain:
internal - IP Address:
10.89.97.220 - 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¶
- Create Service (ClusterIP)
- Create Ingress pointing to Service
- Configure DNS (OPNsense or /etc/hosts)
- 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¶
-
Create Ingress manifest
-
Apply Ingress (LoadBalancer still works)
-
Configure DNS Add
myapp.internal → 10.89.97.220in OPNsense -
Test Ingress route
-
Backup LoadBalancer service
-
Convert to ClusterIP
-
Verify
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";
Related Documentation¶
Last Updated: 2025-11-20 Status: Active Controller Version: ingress-nginx 4.x LoadBalancer IP: 10.89.97.220