Ingress Lab¶
Duration: 25 minutes
Objectives¶
- Install NGINX Ingress Controller in kind
- Create services and deployments
- Configure path-based routing
- Configure host-based routing
- Test Ingress routing
- Implement TLS termination
Prerequisites¶
- kind cluster running (from Part 1)
- kubectl configured
- Basic understanding of Services and Deployments
Lab Tasks¶
Task 1: Install NGINX Ingress Controller¶
Install the NGINX Ingress Controller for kind:
# Apply the NGINX Ingress Controller manifest
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml
# Wait for the controller to be ready
kubectl wait --namespace ingress-nginx \
--for=condition=ready pod \
--selector=app.kubernetes.io/component=controller \
--timeout=90s
# Verify installation
kubectl get pods -n ingress-nginx
kubectl get svc -n ingress-nginx
Expected Output: You should see the ingress-nginx-controller pod running and a LoadBalancer service.
Solution
Task 2: Deploy Backend Applications¶
Create two simple applications that we'll route to using Ingress:
# Create a namespace for this lab
kubectl create namespace ingress-lab
# Set default namespace
kubectl config set-context --current --namespace=ingress-lab
Create backend-apps.yaml:
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend
namespace: ingress-lab
spec:
replicas: 2
selector:
matchLabels:
app: frontend
template:
metadata:
labels:
app: frontend
spec:
containers:
- name: nginx
image: nginx:1.25-alpine
ports:
- containerPort: 80
command: ["/bin/sh", "-c"]
args:
- |
echo '<h1>Frontend Application</h1><p>Path: /frontend</p>' > /usr/share/nginx/html/index.html
nginx -g 'daemon off;'
---
apiVersion: v1
kind: Service
metadata:
name: frontend-service
namespace: ingress-lab
spec:
selector:
app: frontend
ports:
- port: 80
targetPort: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend
namespace: ingress-lab
spec:
replicas: 2
selector:
matchLabels:
app: backend
template:
metadata:
labels:
app: backend
spec:
containers:
- name: nginx
image: nginx:1.25-alpine
ports:
- containerPort: 80
command: ["/bin/sh", "-c"]
args:
- |
echo '<h1>Backend API</h1><p>Path: /api</p>' > /usr/share/nginx/html/index.html
nginx -g 'daemon off;'
---
apiVersion: v1
kind: Service
metadata:
name: backend-service
namespace: ingress-lab
spec:
selector:
app: backend
ports:
- port: 80
targetPort: 80
Apply the manifests:
Solution
$ kubectl get pods -n ingress-lab
NAME READY STATUS RESTARTS AGE
frontend-xxxxxxxxxx-xxxxx 1/1 Running 0 1m
frontend-xxxxxxxxxx-xxxxx 1/1 Running 0 1m
backend-xxxxxxxxxx-xxxxx 1/1 Running 0 1m
backend-xxxxxxxxxx-xxxxx 1/1 Running 0 1m
$ kubectl get svc -n ingress-lab
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
frontend-service ClusterIP 10.96.xxx.xxx <none> 80/TCP 1m
backend-service ClusterIP 10.96.xxx.xxx <none> 80/TCP 1m
Task 3: Create Path-Based Ingress¶
Create an Ingress that routes traffic based on URL paths.
Create path-ingress.yaml:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: path-ingress
namespace: ingress-lab
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: nginx
rules:
- http:
paths:
- path: /frontend
pathType: Prefix
backend:
service:
name: frontend-service
port:
number: 80
- path: /api
pathType: Prefix
backend:
service:
name: backend-service
port:
number: 80
Apply and test:
# Apply the Ingress
kubectl apply -f path-ingress.yaml
# Check Ingress status
kubectl get ingress
kubectl describe ingress path-ingress
# Test from outside the cluster
curl http://localhost/frontend
curl http://localhost/api
Expected Output:
curl http://localhost/frontendshould show "Frontend Application"curl http://localhost/apishould show "Backend API"
Solution
$ kubectl apply -f path-ingress.yaml
ingress.networking.k8s.io/path-ingress created
$ kubectl get ingress -n ingress-lab
NAME CLASS HOSTS ADDRESS PORTS AGE
path-ingress nginx * localhost 80 30s
$ curl http://localhost/frontend
<h1>Frontend Application</h1><p>Path: /frontend</p>
$ curl http://localhost/api
<h1>Backend API</h1><p>Path: /api</p>
Task 4: Create Host-Based Ingress¶
Now create an Ingress that routes based on hostname.
First, create additional services:
Create host-based-apps.yaml:
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app
namespace: ingress-lab
spec:
replicas: 1
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: nginx
image: nginx:1.25-alpine
ports:
- containerPort: 80
command: ["/bin/sh", "-c"]
args:
- |
echo '<h1>Web Application</h1><p>Host: web.local</p>' > /usr/share/nginx/html/index.html
nginx -g 'daemon off;'
---
apiVersion: v1
kind: Service
metadata:
name: web-service
namespace: ingress-lab
spec:
selector:
app: web
ports:
- port: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-app
namespace: ingress-lab
spec:
replicas: 1
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: nginx
image: nginx:1.25-alpine
ports:
- containerPort: 80
command: ["/bin/sh", "-c"]
args:
- |
echo '<h1>API Application</h1><p>Host: api.local</p>' > /usr/share/nginx/html/index.html
nginx -g 'daemon off;'
---
apiVersion: v1
kind: Service
metadata:
name: api-backend-service
namespace: ingress-lab
spec:
selector:
app: api
ports:
- port: 80
Create host-ingress.yaml:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: host-ingress
namespace: ingress-lab
spec:
ingressClassName: nginx
rules:
- host: web.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web-service
port:
number: 80
- host: api.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api-backend-service
port:
number: 80
Apply and test:
# Apply the apps and Ingress
kubectl apply -f host-based-apps.yaml
kubectl apply -f host-ingress.yaml
# Test with Host header
curl -H "Host: web.local" http://localhost
curl -H "Host: api.local" http://localhost
Expected Output:
web.localrequest shows "Web Application"api.localrequest shows "API Application"
Solution
Task 5: Combine Path and Host Routing¶
Create an Ingress that uses both path and host-based routing.
Create combined-ingress.yaml:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: combined-ingress
namespace: ingress-lab
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
ingressClassName: nginx
rules:
- host: app.local
http:
paths:
- path: /web(/|$)(.*)
pathType: Prefix
backend:
service:
name: web-service
port:
number: 80
- path: /api(/|$)(.*)
pathType: Prefix
backend:
service:
name: api-backend-service
port:
number: 80
Test:
kubectl apply -f combined-ingress.yaml
curl -H "Host: app.local" http://localhost/web
curl -H "Host: app.local" http://localhost/api
Solution
$ kubectl apply -f combined-ingress.yaml
$ curl -H "Host: app.local" http://localhost/web
<h1>Web Application</h1><p>Host: web.local</p>
$ curl -H "Host: app.local" http://localhost/api
<h1>API Application</h1><p>Host: api.local</p>
The rewrite-target: /$2 annotation removes the /web and /api prefix before forwarding to backends.
Task 6: Add TLS/HTTPS Support¶
Create a self-signed certificate and configure HTTPS.
# Generate self-signed certificate
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout tls.key -out tls.crt \
-subj "/CN=secure.local/O=workshop"
# Create TLS secret
kubectl create secret tls secure-tls \
--cert=tls.crt --key=tls.key \
-n ingress-lab
# Verify secret
kubectl get secret secure-tls -n ingress-lab
Create tls-ingress.yaml:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: tls-ingress
namespace: ingress-lab
annotations:
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
spec:
ingressClassName: nginx
tls:
- hosts:
- secure.local
secretName: secure-tls
rules:
- host: secure.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web-service
port:
number: 80
Apply and test:
kubectl apply -f tls-ingress.yaml
# Test HTTPS (with -k to ignore self-signed cert warning)
curl -k -H "Host: secure.local" https://localhost
# Test HTTP redirect to HTTPS
curl -v -H "Host: secure.local" http://localhost 2>&1 | grep -i location
Expected Output:
- HTTPS request succeeds
- HTTP request redirects to HTTPS (301)
Solution
$ openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout tls.key -out tls.crt \
-subj "/CN=secure.local/O=workshop"
Generating a RSA private key
...
writing new private key to 'tls.key'
$ kubectl create secret tls secure-tls --cert=tls.crt --key=tls.key -n ingress-lab
secret/secure-tls created
$ kubectl apply -f tls-ingress.yaml
ingress.networking.k8s.io/tls-ingress created
$ curl -k -H "Host: secure.local" https://localhost
<h1>Web Application</h1><p>Host: web.local</p>
$ curl -v -H "Host: secure.local" http://localhost 2>&1 | grep -i location
< location: https://secure.local/
The HTTP request is automatically redirected to HTTPS (301) due to the force-ssl-redirect annotation.
Task 7: Add Custom Annotations¶
Enhance your Ingress with custom annotations for rate limiting and CORS.
Create annotated-ingress.yaml:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: annotated-ingress
namespace: ingress-lab
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
nginx.ingress.kubernetes.io/rate-limit: "10"
nginx.ingress.kubernetes.io/limit-connections: "5"
nginx.ingress.kubernetes.io/enable-cors: "true"
nginx.ingress.kubernetes.io/cors-allow-origin: "*"
nginx.ingress.kubernetes.io/cors-allow-methods: "GET, POST, OPTIONS"
nginx.ingress.kubernetes.io/proxy-read-timeout: "60"
nginx.ingress.kubernetes.io/proxy-send-timeout: "60"
spec:
ingressClassName: nginx
rules:
- http:
paths:
- path: /protected
pathType: Prefix
backend:
service:
name: backend-service
port:
number: 80
Test:
kubectl apply -f annotated-ingress.yaml
# Test rate limiting
for i in {1..15}; do
curl -s -o /dev/null -w "%{http_code}\n" http://localhost/protected
sleep 0.1
done
Expected Output: First 10 requests return 200, subsequent requests may return 429 (Too Many Requests) or 503.
Solution
$ kubectl apply -f annotated-ingress.yaml
$ for i in {1..15}; do
curl -s -o /dev/null -w "%{http_code}\n" http://localhost/protected
sleep 0.1
done
200
200
200
200
200
200
200
200
200
200
503
503
503
503
503
After 10 requests (the rate limit), NGINX returns 503 Service Temporarily Unavailable.
Verification¶
Check all your Ingress resources:
# List all Ingresses
kubectl get ingress -n ingress-lab
# Describe to see rules and backends
kubectl describe ingress -n ingress-lab
# Check Ingress Controller logs
kubectl logs -n ingress-nginx -l app.kubernetes.io/component=controller --tail=50
Cleanup¶
# Delete the namespace (removes all resources)
kubectl delete namespace ingress-lab
# Optional: Uninstall NGINX Ingress Controller
kubectl delete -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml
Bonus Challenges¶
Challenge 1: Default Backend¶
Create a custom 404 page as the default backend for unmatched routes.
Hint: Use defaultBackend in the Ingress spec.
Solution
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: default-backend
namespace: ingress-lab
spec:
replicas: 1
selector:
matchLabels:
app: default-backend
template:
metadata:
labels:
app: default-backend
spec:
containers:
- name: nginx
image: nginx:1.25-alpine
ports:
- containerPort: 80
command: ["/bin/sh", "-c"]
args:
- |
cat > /usr/share/nginx/html/index.html <<EOF
<!DOCTYPE html>
<html>
<head><title>404 Not Found</title></head>
<body>
<h1>404 - Page Not Found</h1>
<p>The requested resource was not found on this server.</p>
</body>
</html>
EOF
nginx -g 'daemon off;'
---
apiVersion: v1
kind: Service
metadata:
name: default-backend-service
namespace: ingress-lab
spec:
selector:
app: default-backend
ports:
- port: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-with-default
namespace: ingress-lab
spec:
ingressClassName: nginx
defaultBackend:
service:
name: default-backend-service
port:
number: 80
rules:
- host: app.local
http:
paths:
- path: /exists
pathType: Prefix
backend:
service:
name: web-service
port:
number: 80
Challenge 2: Path Type Comparison¶
Create three Ingresses with different pathType values (Prefix, Exact, ImplementationSpecific) and test the differences.
Solution
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: pathtype-comparison
namespace: ingress-lab
spec:
ingressClassName: nginx
rules:
- host: pathtype.local
http:
paths:
# Prefix: Matches /prefix, /prefix/, /prefix/anything
- path: /prefix
pathType: Prefix
backend:
service:
name: web-service
port:
number: 80
# Exact: Only matches /exact (no trailing slash)
- path: /exact
pathType: Exact
backend:
service:
name: api-backend-service
port:
number: 80
$ kubectl apply -f pathtype-comparison.yaml
# Prefix tests
$ curl -H "Host: pathtype.local" http://localhost/prefix
<h1>Web Application</h1>
$ curl -H "Host: pathtype.local" http://localhost/prefix/
<h1>Web Application</h1>
$ curl -H "Host: pathtype.local" http://localhost/prefix/sub/path
<h1>Web Application</h1>
# Exact tests
$ curl -H "Host: pathtype.local" http://localhost/exact
<h1>API Application</h1>
$ curl -H "Host: pathtype.local" http://localhost/exact/
<html><head><title>404 Not Found</title></head>...
$ curl -H "Host: pathtype.local" http://localhost/exact/anything
<html><head><title>404 Not Found</title></head>...
Summary:
- Prefix: Matches the path and any sub-paths
- Exact: Only matches the exact path, no trailing slash or sub-paths
Challenge 3: Basic Authentication¶
Implement HTTP Basic Auth on one of your Ingress paths.
Hint:
Use annotation: nginx.ingress.kubernetes.io/auth-type: basic
Solution
# Install htpasswd (if not available)
# On Fedora: dnf install httpd-tools
# On Ubuntu: apt-get install apache2-utils
# Create auth file with user 'admin' and password 'secret'
$ htpasswd -c auth admin
New password: secret
Re-type new password: secret
Adding password for user admin
# Create secret
$ kubectl create secret generic basic-auth --from-file=auth -n ingress-lab
secret/basic-auth created
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: auth-ingress
namespace: ingress-lab
annotations:
nginx.ingress.kubernetes.io/auth-type: basic
nginx.ingress.kubernetes.io/auth-secret: basic-auth
nginx.ingress.kubernetes.io/auth-realm: "Authentication Required - Workshop"
spec:
ingressClassName: nginx
rules:
- host: auth.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web-service
port:
number: 80
$ kubectl apply -f auth-ingress.yaml
# Without credentials - returns 401
$ curl -H "Host: auth.local" http://localhost
<html>
<head><title>401 Authorization Required</title></head>
...
</html>
# With correct credentials - works
$ curl -u admin:secret -H "Host: auth.local" http://localhost
<h1>Web Application</h1><p>Host: web.local</p>
# With incorrect credentials - returns 401
$ curl -u admin:wrongpass -H "Host: auth.local" http://localhost
<html>
<head><title>401 Authorization Required</title></head>
...
</html>
Challenge 4: Canary Deployments¶
Use Ingress annotations to implement a canary deployment that routes 10% of traffic to a new version.
Hint: Look up nginx.ingress.kubernetes.io/canary annotations.
Solution
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app-v2
namespace: ingress-lab
spec:
replicas: 1
selector:
matchLabels:
app: web
version: v2
template:
metadata:
labels:
app: web
version: v2
spec:
containers:
- name: nginx
image: nginx:1.25-alpine
ports:
- containerPort: 80
command: ["/bin/sh", "-c"]
args:
- |
echo '<h1>Web Application V2</h1><p>New version with new features!</p>' > /usr/share/nginx/html/index.html
nginx -g 'daemon off;'
---
apiVersion: v1
kind: Service
metadata:
name: web-service-v2
namespace: ingress-lab
spec:
selector:
app: web
version: v2
ports:
- port: 80
---
# Main Ingress (90% traffic)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: web-main
namespace: ingress-lab
spec:
ingressClassName: nginx
rules:
- host: canary.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web-service
port:
number: 80
---
# Canary Ingress (10% traffic)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: web-canary
namespace: ingress-lab
annotations:
nginx.ingress.kubernetes.io/canary: "true"
nginx.ingress.kubernetes.io/canary-weight: "10"
spec:
ingressClassName: nginx
rules:
- host: canary.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web-service-v2
port:
number: 80
$ kubectl apply -f web-v2.yaml
$ kubectl apply -f canary-ingress.yaml
# Make 20 requests and count responses
$ for i in {1..20}; do
curl -s -H "Host: canary.local" http://localhost | grep -o "V2" || echo "V1"
done
# Expected output: ~18 "V1" and ~2 "V2" (approximately 10% to v2)
You can also use header-based canary:
Troubleshooting Tips¶
Ingress not working:
- Check Ingress Controller is running:
kubectl get pods -n ingress-nginx - Verify Ingress:
kubectl describe ingress <name> - Check service endpoints exist:
kubectl get endpoints - Review controller logs:
kubectl logs -n ingress-nginx <pod-name>
404 errors:
- Verify service name and port match in Ingress
- Check path and pathType configuration
- Ensure ingressClassName is set correctly
Host-based routing not working:
- Use
-H "Host: hostname"with curl - Check DNS or /etc/hosts configuration
- Verify host field in Ingress rules
TLS issues:
- Verify secret exists:
kubectl get secret <secret-name> - Check secret has tls.crt and tls.key
- Ensure hostname in TLS matches rules
Summary¶
You've learned to:
- Install and configure NGINX Ingress Controller
- Create path-based routing (single domain, multiple paths)
- Implement host-based routing (multiple domains)
- Combine path and host routing
- Configure TLS/HTTPS termination
- Use annotations for advanced features
- Debug Ingress issues
In production, you would typically:
- Use real TLS certificates (Let's Encrypt, cert-manager)
- Configure proper DNS records
- Set resource limits on Ingress Controller
- Monitor Ingress metrics
- Implement rate limiting and security policies
Key takeaways¶
- Ingress controllers act as the single entry point for external traffic into the cluster
- Path-based routing serves multiple services from the same domain via URL paths
- Host-based routing routes traffic based on the HTTP Host header to different backend services
- TLS termination offloads HTTPS handling to the Ingress controller, keeping backend services simple
- Annotations customize Ingress controller behavior without modifying the controller itself
- Ingress resources require a running controller — the resource alone does nothing without one
Next section¶
Once you've reviewed the content and completed the lab, proceed to the next section.