Skip to content

Module 5.2: Ingress

Hands-On Lab Available
K8s Cluster intermediate 30 min
Launch Lab ↗

Opens in Killercoda in a new tab

Complexity: [MEDIUM] - Important for external access, multiple concepts

Time to Complete: 45-55 minutes

Prerequisites: Module 5.1 (Services), understanding of HTTP and DNS


After completing this module, you will be able to:

  • Create Ingress resources with host-based and path-based routing rules
  • Configure TLS termination and multiple backend Services in a single Ingress
  • Debug Ingress routing failures by checking controller logs, annotations, and backend Service health
  • Explain the relationship between Ingress resources, IngressClasses, and Ingress controllers

Ingress provides HTTP/HTTPS routing from outside the cluster to Services inside. Instead of exposing multiple LoadBalancer Services (expensive) or using NodePorts (ugly URLs), Ingress gives you host/path-based routing with a single entry point.

The CKAD exam tests:

  • Creating Ingress resources
  • Host-based and path-based routing
  • TLS termination
  • Understanding Ingress controllers

The Hotel Reception Analogy

Ingress is like a hotel reception desk. Guests (requests) arrive at one entrance (Ingress) and ask for different services: “restaurant” goes to the dining room Service, “spa” goes to the wellness Service, “room 203” goes to a specific guest Service. The receptionist (Ingress controller) routes everyone to the right place based on what they ask for.


The Ingress Controller is a pod that watches Ingress resources and configures routing. Common controllers:

  • Envoy Gateway (reference Gateway API implementation)
  • Traefik (supports both Ingress and Gateway API)
  • Kong (supports both Ingress and Gateway API)
  • Cilium (CNI with built-in Ingress and Gateway API support)
  • NGINX Gateway Fabric (successor to ingress-nginx)

Note: The popular ingress-nginx controller was retired on March 31, 2026 and no longer receives updates. For new deployments, use Gateway API (see CKA Module 3.5) with one of the controllers above.

Important: Ingress resources do nothing without a controller!

Terminal window
# Check if you have an Ingress controller
k get pods -n ingress-nginx
# or
k get pods -A | grep -i ingress

The Ingress is a Kubernetes resource defining routing rules:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-ingress
spec:
rules:
- host: myapp.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-service
port:
number: 80

Matches URL path prefix:

pathType: Prefix
path: /api
# Matches: /api, /api/, /api/users, /api/users/123

Pause and predict: You create an Ingress resource with routing rules, but visiting the URL returns nothing. kubectl get ingress shows no ADDRESS. What is most likely missing from the cluster?

Matches exact path only:

pathType: Exact
path: /api
# Matches: /api only
# Does NOT match: /api/, /api/users

Depends on IngressClass (controller-specific).


Different hosts to different services:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: host-routing
spec:
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api-service
port:
number: 80
- host: web.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web-service
port:
number: 80

Different paths to different services:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: path-routing
spec:
rules:
- host: example.com
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: api-service
port:
number: 80
- path: /web
pathType: Prefix
backend:
service:
name: web-service
port:
number: 80

Stop and think: An Ingress has two path rules: /api with pathType: Prefix and /api/v2 with pathType: Prefix. A request comes in for /api/v2/users. Which backend receives the traffic? Why?

Catch-all for unmatched requests:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: with-default
spec:
defaultBackend:
service:
name: default-service
port:
number: 80
rules:
- host: example.com
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: api-service
port:
number: 80

Terminal window
# Create TLS secret from cert and key
k create secret tls my-tls-secret \
--cert=path/to/tls.crt \
--key=path/to/tls.key
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: tls-ingress
spec:
tls:
- hosts:
- secure.example.com
secretName: my-tls-secret
rules:
- host: secure.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: secure-service
port:
number: 80

┌─────────────────────────────────────────────────────────────┐
│ Ingress Flow │
├─────────────────────────────────────────────────────────────┤
│ │
│ Internet │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ Ingress Controller │ │
│ │ (nginx, traefik, etc.) │ │
│ │ │ │
│ │ Reads Ingress rules │ │
│ │ Routes based on host/path │ │
│ └─────────────────────────────────────┘ │
│ │ │
│ │ api.example.com/users │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ Ingress Resource │ │
│ │ │ │
│ │ rules: │ │
│ │ - host: api.example.com │ │
│ │ paths: │ │
│ │ - /users → user-svc:80 │ │
│ │ - /orders → order-svc:80 │ │
│ │ - host: web.example.com │ │
│ │ paths: │ │
│ │ - / → frontend-svc:80 │ │
│ └─────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ user-svc │ │order-svc │ │frontend │ │
│ │ :80 │ │ :80 │ │svc :80 │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘

Specifies which controller handles the Ingress:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-ingress
spec:
ingressClassName: nginx # Which controller
rules:
- host: example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-service
port:
number: 80
Terminal window
# List available IngressClasses
k get ingressclass

Controller-specific behavior via annotations:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: annotated-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
rules:
- host: example.com
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: api-service
port:
number: 80

Common NGINX annotations:

  • nginx.ingress.kubernetes.io/rewrite-target: URL rewriting
  • nginx.ingress.kubernetes.io/ssl-redirect: Force HTTPS
  • nginx.ingress.kubernetes.io/proxy-body-size: Max request body

Terminal window
# Create Ingress imperatively (limited)
k create ingress my-ingress \
--rule="host.example.com/path=service:port"
# View Ingress
k get ingress
k describe ingress NAME
# Get Ingress address
k get ingress NAME -o jsonpath='{.status.loadBalancer.ingress[0].ip}'
# Check IngressClass
k get ingressclass
# View controller logs
k logs -n ingress-nginx -l app.kubernetes.io/name=ingress-nginx

  • Ingress is just a configuration, not a service. The actual routing is done by the Ingress controller pods.

  • Multiple Ingress resources can exist for the same host. Controllers typically merge them.

  • The kubernetes.io/ingress.class annotation is deprecated. Use spec.ingressClassName instead (Kubernetes 1.18+).

  • Ingress can’t route non-HTTP traffic. For TCP/UDP, use LoadBalancer Services or the newer Gateway API.


MistakeWhy It HurtsSolution
No Ingress controller installedIngress does nothingInstall nginx-ingress or similar
Wrong pathTypeRoutes don’t matchUse Prefix for most cases
Service name/port mismatch503 errorsVerify service exists and port matches
Missing host in rulesMatches all hostsAdd explicit host or use carefully
TLS secret in wrong namespaceTLS failsSecret must be in same namespace as Ingress

  1. A developer creates an Ingress resource with correct rules and applies it successfully. But when they visit the URL, nothing happens — no response at all. kubectl get ingress shows the resource exists but the ADDRESS column is empty. Other teams’ Ingress resources work fine. What should the developer check?

    Answer The Ingress resource exists as data in the API server, but an Ingress controller pod must be running to read those rules and configure actual routing. Since other Ingresses work, the controller is installed — the issue is likely that the developer's Ingress doesn't specify the correct `ingressClassName` (or uses a different class than the installed controller). Check `kubectl get ingressclass` to see available classes, then add `spec.ingressClassName: ` to the Ingress. Also verify the backend Service exists and has endpoints with `kubectl get endpoints `.
  2. Users report 503 errors when accessing app.example.com/api. The Ingress exists and the controller is working. kubectl describe ingress shows the backend is api-service:80. What are the most likely causes and how do you debug them?

    Answer A 503 from the Ingress controller typically means the backend Service has no healthy endpoints. Debug systematically: (1) Check `kubectl get endpoints api-service` — if empty, the Service selector doesn't match any pods or no pods are Ready. (2) Check `kubectl get pods -l ` to see if pods exist and are Running/Ready. (3) Verify the Service port (80) matches what the Ingress specifies. (4) Verify the Service's `targetPort` matches what the application actually listens on. (5) Check the Ingress controller logs: `kubectl logs -n ingress-nginx -l app.kubernetes.io/name=ingress-nginx` for specific error messages. The chain is: Ingress -> Service -> Endpoints -> Pods, and any break in that chain produces 503s.
  3. A team needs HTTPS for their application. They create a TLS Secret and reference it in their Ingress, but browsers show a certificate warning. The Ingress spec has tls: - hosts: [app.example.com] and secretName: app-tls. What are two common reasons for the certificate warning?

    Answer Two common causes: (1) The TLS Secret is in a different namespace than the Ingress resource. Kubernetes TLS Secrets must be in the same namespace as the Ingress that references them — cross-namespace Secret references are not allowed. Check with `kubectl get secret app-tls` in the Ingress's namespace. (2) The certificate's Common Name (CN) or Subject Alternative Names (SANs) don't match the hostname in the Ingress rule. If the cert was issued for `www.example.com` but the Ingress uses `app.example.com`, browsers will warn about the mismatch. Verify with `openssl x509 -in cert.pem -text | grep -A1 "Subject Alternative Name"`.
  4. You need to route shop.example.com to one Service and blog.example.com to another, both through a single Ingress. The blog should also be accessible at shop.example.com/blog. How would you structure the Ingress rules?

    Answer Use a combination of host-based and path-based routing. Define two host rules: one for `blog.example.com` routing `/` to the blog-service, and one for `shop.example.com` with two path entries — `/blog` routing to blog-service and `/` (catch-all) routing to shop-service. The more specific path `/blog` takes priority over the prefix `/`. Order the paths from most specific to least specific. Both host rules can be in a single Ingress resource. The key detail is `pathType: Prefix` — the `/blog` prefix rule will match `/blog`, `/blog/`, and `/blog/posts/123`, forwarding all to the blog service.

Task: Create Ingress with path-based routing.

Setup:

Terminal window
# Create two deployments
k create deployment web --image=nginx
k create deployment api --image=nginx
# Create services
k expose deployment web --port=80
k expose deployment api --port=80
# Wait for pods
k wait --for=condition=Ready pod -l app=web --timeout=60s
k wait --for=condition=Ready pod -l app=api --timeout=60s

Part 1: Simple Ingress

Terminal window
cat << 'EOF' | k apply -f -
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: simple-ingress
spec:
rules:
- http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web
port:
number: 80
EOF
k get ingress simple-ingress
k describe ingress simple-ingress

Part 2: Path-Based Routing

Terminal window
cat << 'EOF' | k apply -f -
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: path-ingress
spec:
rules:
- http:
paths:
- path: /web
pathType: Prefix
backend:
service:
name: web
port:
number: 80
- path: /api
pathType: Prefix
backend:
service:
name: api
port:
number: 80
EOF
k describe ingress path-ingress

Part 3: Host-Based Routing

Terminal window
cat << 'EOF' | k apply -f -
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: host-ingress
spec:
rules:
- host: web.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web
port:
number: 80
- host: api.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api
port:
number: 80
EOF
k describe ingress host-ingress

Cleanup:

Terminal window
k delete ingress simple-ingress path-ingress host-ingress
k delete deployment web api
k delete svc web api

Drill 1: Simple Ingress (Target: 2 minutes)

Section titled “Drill 1: Simple Ingress (Target: 2 minutes)”
Terminal window
k create deployment drill1 --image=nginx
k expose deployment drill1 --port=80
cat << 'EOF' | k apply -f -
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: drill1
spec:
rules:
- http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: drill1
port:
number: 80
EOF
k get ingress drill1
k delete ingress drill1 deploy drill1 svc drill1

Drill 2: Host-Based Routing (Target: 3 minutes)

Section titled “Drill 2: Host-Based Routing (Target: 3 minutes)”
Terminal window
k create deployment app1 --image=nginx
k create deployment app2 --image=nginx
k expose deployment app1 --port=80
k expose deployment app2 --port=80
cat << 'EOF' | k apply -f -
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: drill2
spec:
rules:
- host: app1.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: app1
port:
number: 80
- host: app2.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: app2
port:
number: 80
EOF
k describe ingress drill2
k delete ingress drill2 deploy app1 app2 svc app1 app2

Drill 3: Path-Based Routing (Target: 3 minutes)

Section titled “Drill 3: Path-Based Routing (Target: 3 minutes)”
Terminal window
k create deployment frontend --image=nginx
k create deployment backend --image=nginx
k expose deployment frontend --port=80
k expose deployment backend --port=80
cat << 'EOF' | k apply -f -
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: drill3
spec:
rules:
- host: myapp.local
http:
paths:
- path: /frontend
pathType: Prefix
backend:
service:
name: frontend
port:
number: 80
- path: /backend
pathType: Prefix
backend:
service:
name: backend
port:
number: 80
EOF
k get ingress drill3
k delete ingress drill3 deploy frontend backend svc frontend backend

Drill 4: Ingress with Default Backend (Target: 3 minutes)

Section titled “Drill 4: Ingress with Default Backend (Target: 3 minutes)”
Terminal window
k create deployment default-app --image=nginx
k create deployment api-app --image=nginx
k expose deployment default-app --port=80
k expose deployment api-app --port=80
cat << 'EOF' | k apply -f -
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: drill4
spec:
defaultBackend:
service:
name: default-app
port:
number: 80
rules:
- http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: api-app
port:
number: 80
EOF
k describe ingress drill4
k delete ingress drill4 deploy default-app api-app svc default-app api-app

Drill 5: Create Ingress Imperatively (Target: 2 minutes)

Section titled “Drill 5: Create Ingress Imperatively (Target: 2 minutes)”
Terminal window
k create deployment drill5 --image=nginx
k expose deployment drill5 --port=80
# Create ingress imperatively
k create ingress drill5 --rule="drill5.local/=drill5:80"
k get ingress drill5
k describe ingress drill5
k delete ingress drill5 deploy drill5 svc drill5

Drill 6: Ingress with TLS (Target: 4 minutes)

Section titled “Drill 6: Ingress with TLS (Target: 4 minutes)”
Terminal window
# Create self-signed cert (for demo)
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout /tmp/tls.key -out /tmp/tls.crt \
-subj "/CN=secure.local" 2>/dev/null
# Create TLS secret
k create secret tls drill6-tls --cert=/tmp/tls.crt --key=/tmp/tls.key
k create deployment drill6 --image=nginx
k expose deployment drill6 --port=80
cat << 'EOF' | k apply -f -
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: drill6
spec:
tls:
- hosts:
- secure.local
secretName: drill6-tls
rules:
- host: secure.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: drill6
port:
number: 80
EOF
k describe ingress drill6
k delete ingress drill6 secret drill6-tls deploy drill6 svc drill6
rm /tmp/tls.key /tmp/tls.crt

Module 5.3: NetworkPolicies - Control pod-to-pod communication.