Ingress: External Access (Optional)
Ingress exposes HTTP and HTTPS routes from outside the cluster to services within it.
- Status
- stable
- Trust
- unverified
- Approved
- ag-archive/0.1.0 ·
Your agent running in Kubernetes handles thousands of requests. But how does traffic reach it? In Lesson 5, you learned about Services—the stable interfaces to your Pods. LoadBalancer Services work well for simple cases, but production systems need something more powerful: Ingress.
Ingress lets you expose HTTP and HTTPS routes from outside the cluster to services within it. Unlike LoadBalancer, which creates a cloud load balancer for every service (expensive), Ingress shares one load balancer across multiple services. You can route requests based on hostname, URL path, or both. You can terminate TLS for HTTPS. You can even implement A/B testing by routing different traffic percentages to different versions of your agent.
Think of it this way: LoadBalancer is a direct tunnel to one service. Ingress is an intelligent receptionist—it looks at your request, reads the address and directions, and routes you to the right department.
Why LoadBalancer Isn't Enough
The Cost Problem
Every LoadBalancer Service you create provisions a cloud load balancer. On AWS, that's $16/month per load balancer. With 10 services, you're paying $160/month just for load balancers. Ingress shares one load balancer across all services—one $16/month bill instead of ten.
The Feature Gap
LoadBalancer gives you Layer 4 routing (TCP/UDP based on port). It knows nothing about HTTP. Ingress gives you Layer 7 routing:
- Route
/api/v1/requests to the stable agent version - Route
/api/v2/beta/requests to the experimental agent - Route
dashboard.example.comto monitoring,api.example.comto your agent - Terminate TLS for HTTPS
- Implement request rewriting
- Control access with rate limiting
The Management Problem
With LoadBalancer, every service is exposed separately. You're managing multiple external IPs, each with different security rules. With Ingress, you have one entrypoint—one place to manage TLS certificates, one place to enforce security policies.
Ingress Architecture: Controller and Resource
Ingress has two parts: a resource (declarative specification) and a controller (the process that implements it).
The Ingress Resource
The Ingress resource is a Kubernetes API object—like Deployment or Service. It specifies your desired routing rules:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: agent-ingress
spec:
ingressClassName: nginx
rules:
- host: api.example.com
http:
paths:
- path: /api/v1/
backend:
service:
name: agent-stable
port:
number: 8000
- path: /api/v2/beta/
backend:
service:
name: agent-experimental
port:
number: 8000This says: "Listen on api.example.com, route requests to /api/v1/ to the agent-stable service, route requests to /api/v2/beta/ to the agent-experimental service."
The resource is just a specification. It does nothing by itself.
The Ingress Controller
The Ingress controller reads Ingress resources and implements them. It's a Pod running in your cluster that watches for Ingress resources, then configures a real load balancer (nginx, HAProxy, cloud provider's LB) to actually route traffic.
Popular controllers:
- nginx-ingress (open source, works everywhere)
- AWS ALB (AWS-native, integrates with ELBv2)
- GCP Cloud Armor (GCP-native)
- Kong (commercial-grade API gateway)
For this chapter, we'll use nginx-ingress because it works on Docker Desktop, cloud clusters, and on-premises Kubernetes.
Installing nginx-ingress on Docker Desktop
Install nginx-ingress using kubectl:
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.8.2/deploy/static/provider/cloud/deploy.yamlOutput:
namespace/ingress-nginx created
serviceaccount/ingress-nginx created
...
deployment.apps/ingress-nginx-controller createdVerify the controller is running:
kubectl get pods -n ingress-nginxOutput:
NAMESPACE NAME READY STATUS RESTARTS AGE
ingress-nginx ingress-nginx-admission-patch-xxxxx 0/1 Completed 0 2m
ingress-nginx ingress-nginx-admission-create-xxxxx 0/1 Completed 0 2m
ingress-nginx ingress-nginx-controller-yyyyy 1/1 Running 0 2mThe ingress-nginx-controller is the daemon watching your cluster for Ingress resources. When you create an Ingress, this controller reads it and configures nginx to route traffic accordingly.
Check which IngressClass is available:
kubectl get ingressclassesOutput:
NAME CONTROLLER AGE
nginx k8s.io/ingress-nginx/nginx 2mThe nginx IngressClass is your controller. When you create an Ingress with ingressClassName: nginx, this controller takes responsibility for implementing it.
Path-Based Routing: Versioned APIs
Your agent has evolved. You have a stable /api/v1/ endpoint that clients rely on, and a new /api/v2/beta/ endpoint with experimental features. Different Deployments run each version:
kubectl create deployment agent-v1 --image=your-registry/agent:v1 --replicas=2
kubectl create deployment agent-v2 --image=your-registry/agent:v2 --replicas=1Output:
deployment.apps/agent-v1 created
deployment.apps/agent-v2 createdExpose each as a Service:
kubectl expose deployment agent-v1 --port=8000 --target-port=8000 --name=agent-v1-service
kubectl expose deployment agent-v2 --port=8000 --target-port=8000 --name=agent-v2-serviceOutput:
service/agent-v1-service exposed
service/agent-v2-service exposedVerify both services exist:
kubectl get svc | grep agentOutput:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
agent-v1-service ClusterIP 10.96.200.50 <none> 8000/TCP
agent-v2-service ClusterIP 10.96.201.100 <none> 8000/TCPNow create an Ingress to route traffic to both:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: agent-path-routing
spec:
ingressClassName: nginx
rules:
- host: localhost # For Docker Desktop testing
http:
paths:
- path: /api/v1
pathType: Prefix
backend:
service:
name: agent-v1-service
port:
number: 8000
- path: /api/v2/beta
pathType: Prefix
backend:
service:
name: agent-v2-service
port:
number: 8000Save as agent-path-routing.yaml and apply:
kubectl apply -f agent-path-routing.yamlOutput:
ingress.networking.k8s.io/agent-path-routing createdVerify the Ingress is configured:
kubectl get ingressOutput:
NAME CLASS HOSTS ADDRESS PORTS AGE
agent-path-routing nginx localhost 192.168.49.2 80 10sThe ADDRESS is your localhost. Wait a moment for nginx to configure, then test:
curl http://localhost/api/v1/healthOutput:
{"status": "healthy", "version": "1.0"}curl http://localhost/api/v2/beta/healthOutput:
{"status": "healthy", "version": "2.0-beta", "experimental": true}The same Ingress gateway routes /api/v1 and /api/v2/beta to different backend services. This is the power of path-based routing—one IP, multiple APIs, each backed by independent Deployments.
Host-Based Routing: Multiple Domains
Your team operates multiple services from one cluster:
agent.example.com— Your AI agent APIdashboard.example.com— Monitoring dashboardwebhook.example.com— Event receiver
Each has its own Service. Host-based Ingress routes traffic to the right Service based on the hostname:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: agent-host-routing
spec:
ingressClassName: nginx
rules:
- host: agent.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: agent-service
port:
number: 8000
- host: dashboard.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: dashboard-service
port:
number: 3000
- host: webhook.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: webhook-service
port:
number: 5000Apply this configuration:
kubectl apply -f agent-host-routing.yamlOutput:
ingress.networking.k8s.io/agent-host-routing createdVerify the Ingress configuration:
kubectl get ingress agent-host-routing -o wideOutput:
NAME CLASS HOSTS ADDRESS PORTS AGE
agent-host-routing nginx agent.example.com,dashboard.example.com,... 192.168.49.2 80 30sTest locally by modifying /etc/hosts (or adding entries to your DNS):
echo "192.168.49.2 agent.example.com dashboard.example.com webhook.example.com" >> /etc/hosts
cat /etc/hosts | tail -1Output:
192.168.49.2 agent.example.com dashboard.example.com webhook.example.comThen test each hostname:
curl http://agent.example.com/healthOutput:
{"status": "healthy", "service": "agent", "uptime_seconds": 3600}curl http://dashboard.example.com/Output:
<html>Dashboard - 12 agents online, 0 errors</html>The same Ingress controller routes three different hostnames to three different services. This is the foundation of multi-tenant deployments—one gateway, many applications.
TLS Termination for HTTPS
Internet traffic should be encrypted. Kubernetes TLS termination means the Ingress handles encryption/decryption, so backend services communicate in plain HTTP internally (they're protected by the network).
Create a Self-Signed Certificate (for testing)
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout /tmp/tls.key \
-out /tmp/tls.crt \
-subj "/CN=agent.example.com"Output:
Generating a RSA private key
...................................................+++++
Generating a self signed certificate
...Create a TLS Secret
Kubernetes stores certificates in Secrets. Create one with your TLS key and certificate:
kubectl create secret tls agent-tls \
--cert=/tmp/tls.crt \
--key=/tmp/tls.keyOutput:
secret/agent-tls createdVerify the secret:
kubectl get secret agent-tlsOutput:
NAME TYPE DATA AGE
agent-tls kubernetes.io/tls 2 5sUpdate Ingress with TLS
Modify your Ingress to reference the TLS secret:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: agent-tls-ingress
spec:
ingressClassName: nginx
tls:
- hosts:
- agent.example.com
secretName: agent-tls
rules:
- host: agent.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: agent-service
port:
number: 8000Apply:
kubectl apply -f agent-tls-ingress.yamlOutput:
ingress.networking.k8s.io/agent-tls-ingress createdTest HTTPS:
curl --insecure https://agent.example.com/healthOutput:
{"status": "healthy", "tls": "yes"}(We use --insecure because the self-signed certificate isn't in your system's trust store. In production, you'd use a certificate from a trusted CA like Let's Encrypt.)
The Ingress controller (nginx) terminates TLS—it decrypts incoming HTTPS traffic and routes requests to your services over plain HTTP. This simplifies certificate management (one place to update certs) and reduces computational burden on your agent services.
Verify the TLS secret is mounted correctly:
kubectl describe ingress agent-tls-ingressOutput:
Name: agent-tls-ingress
Namespace: default
Address: 192.168.49.2
Default backend: default-http-backend:80 (<error: endpoints "default-http-backend" not found>)
TLS:
agent-tls terminates agent.example.com
Rules:
Host Path Backends
---- ---- --------
agent.example.com
/ agent-service:8000 (10.244.0.10:8000,10.244.0.11:8000)
Annotations:
<none>
Events: <none>The TLS configuration is active. The nginx controller has loaded your certificate and key from the agent-tls secret.
A/B Testing with Traffic Splitting
Your team wants to validate a new agent version with 10% of traffic while keeping 90% on the stable version. You can't do this with basic routing—you need weighted traffic splitting.
Create two Services:
kubectl create deployment agent-stable --image=agent:v1.0 --replicas=9
kubectl create deployment agent-test --image=agent:v2.0-rc1 --replicas=1
kubectl expose deployment agent-stable --port=8000 --name=agent-stable
kubectl expose deployment agent-test --port=8000 --name=agent-testOutput:
deployment.apps/agent-stable created
deployment.apps/agent-test created
service/agent-stable exposed
service/agent-test exposedWith nginx-ingress, you can use the nginx.ingress.kubernetes.io/service-weights annotation:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: agent-canary
annotations:
nginx.ingress.kubernetes.io/service-weights: |
agent-stable: 90
agent-test: 10
spec:
ingressClassName: nginx
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: agent-stable
port:
number: 8000Apply:
kubectl apply -f agent-canary.yamlOutput:
ingress.networking.k8s.io/agent-canary createdSend 100 requests and observe traffic distribution:
for i in {1..100}; do curl http://api.example.com/version; done | sort | uniq -cOutput:
90 {"version": "1.0", "stable": true}
10 {"version": "2.0-rc1", "canary": true}Roughly 90% reach the stable version, 10% reach the test version. This lets you validate new code with real traffic before full rollout.
Ingress Annotations: Advanced Configuration
Annotations let you customize Ingress behavior without changing the core specification. Common annotations for nginx-ingress:
Rate Limiting
Protect your agent from being overwhelmed by limiting requests per client:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: agent-rate-limited
annotations:
nginx.ingress.kubernetes.io/limit-rps: "100"
nginx.ingress.kubernetes.io/limit-connections: "10"
spec:
ingressClassName: nginx
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: agent-service
port:
number: 8000Apply and test:
kubectl apply -f agent-rate-limited.yamlOutput:
ingress.networking.k8s.io/agent-rate-limited createdVerify the annotations are applied:
kubectl get ingress agent-rate-limited -o jsonpath='{.metadata.annotations}'Output:
{"nginx.ingress.kubernetes.io/limit-connections":"10","nginx.ingress.kubernetes.io/limit-rps":"100"}CORS Headers
Allow browser clients from specific origins to call your agent:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: agent-cors
annotations:
nginx.ingress.kubernetes.io/enable-cors: "true"
nginx.ingress.kubernetes.io/cors-allow-origin: "https://dashboard.example.com"
nginx.ingress.kubernetes.io/cors-allow-methods: "GET, POST, PUT"
spec:
ingressClassName: nginx
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: agent-service
port:
number: 8000Apply:
kubectl apply -f agent-cors.yamlOutput:
ingress.networking.k8s.io/agent-cors createdDebugging Ingress Issues
When routing fails, use these kubectl commands to diagnose:
Check Ingress Status
kubectl get ingressOutput:
NAME CLASS HOSTS ADDRESS PORTS AGE
agent-ingress nginx api.example.com 192.168.49.2 80 5mIf ADDRESS is <none>, the ingress controller hasn't assigned an IP yet (usually means services don't exist).
Describe Ingress Details
kubectl describe ingress agent-ingressOutput:
Name: agent-ingress
Namespace: default
Address: 192.168.49.2
Ingress Class: nginx
Host(s) Path Backends
---- ---- --------
api.example.com
/api/v1 agent-v1-service:8000
/api/v2 agent-v2-service:8000
Annotations: <none>
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Sync 30s nginx-ingress-controller Scheduled for syncThis shows exactly what rules are configured and which backends they target.
Check Controller Logs
kubectl logs -n ingress-nginx -l app.kubernetes.io/name=ingress-nginx -fOutput (example):
I1222 10:45:23.123456 12345 main.go:104] Starting NGINX Ingress Controller
I1222 10:45:24.234567 12345 store.go:123] Found Ingress api.example.com
I1222 10:45:25.345678 12345 controller.go:456] Sync: agent-ingress
I1222 10:45:26.456789 12345 template.go:789] Generating NGINX configLogs show when the controller detects new Ingress resources and updates its configuration.
Test Connectivity
If the Ingress won't route traffic, verify the backend Service:
kubectl port-forward svc/agent-v1-service 8000:8000In another terminal:
curl http://localhost:8000/healthOutput:
{"status": "healthy", "version": "1.0"}If this works but Ingress routing doesn't, the problem is in the Ingress controller configuration, not the Service.
Common Issues and Solutions
Issue: "Service not found" errors in Ingress
Check the Service exists in the correct namespace:
kubectl get svc agent-v1-serviceOutput:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
agent-v1-service ClusterIP 10.96.200.50 <none> 8000/TCPIssue: 503 Service Unavailable
The Ingress exists but backends are unhealthy:
kubectl describe svc agent-v1-serviceOutput:
Name: agent-v1-service
Endpoints: 10.244.0.10:8000,10.244.0.11:8000If Endpoints is empty, Pods aren't running or labels don't match.
Try With AI
Setup: You have two agent services running in your cluster: chat-agent and tool-agent. Both listen on port 8000. You want to expose them via Ingress so:
http://api.example.com/chat/*routes tochat-agenthttp://api.example.com/tools/*routes totool-agent- Both should be accessible over HTTPS using a self-signed certificate
Part 1: Ask AI for the Ingress design
Prompt AI:
I have two Kubernetes services:
- chat-agent (port 8000)
- tool-agent (port 8000)
I need an Ingress that:
1. Routes /chat/* to chat-agent
2. Routes /tools/* to tool-agent
3. Uses HTTPS with a TLS secret named "api-tls"
4. Uses nginx-ingress controller
Design the Ingress YAML that accomplishes this. What decisions did you make about pathType, backend configuration, and TLS placement?Part 2: Evaluate the design
Review AI's response. Ask yourself:
- Does each path rule specify correct service and port?
- Is the TLS configuration in the spec.tls section?
- Did AI explain why certain pathTypes (Exact vs Prefix) were chosen?
- Are both hosts unified under one Ingress or separate Ingresses?
Part 3: Test the design
Create the services (if not already running):
kubectl create deployment chat-agent --image=your-registry/chat-agent:latest
kubectl create deployment tool-agent --image=your-registry/tool-agent:latest
kubectl expose deployment chat-agent --port=8000 --name=chat-agent
kubectl expose deployment tool-agent --port=8000 --name=tool-agentCreate the TLS secret:
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout /tmp/api.key -out /tmp/api.crt -subj "/CN=api.example.com"
kubectl create secret tls api-tls --cert=/tmp/api.crt --key=/tmp/api.keyApply AI's Ingress:
kubectl apply -f ingress.yaml # The YAML AI generatedTest routing:
curl --insecure https://api.example.com/chat/hello
curl --insecure https://api.example.com/tools/listPart 4: Refinement
If routing works:
- What annotations might improve this Ingress (rate limiting, CORS handling)?
- How would you add health checks to ensure failed backends are removed?
If routing fails:
- Check logs:
kubectl logs -n ingress-nginx <ingress-controller-pod> - Verify services exist:
kubectl get svc - Test Service connectivity directly:
kubectl port-forward svc/chat-agent 8000:8000 - Verify TLS secret:
kubectl get secret api-tls
Part 5: Compare to your design
When you started, you might have created separate Ingresses per service. Look at AI's consolidated design:
- Why is one Ingress cleaner than two?
- What shared configuration (TLS, IngressClass) benefits from consolidation?
- When would you split into multiple Ingresses (different namespaces, different IngressClasses)?
Core Concept
Ingress exposes HTTP and HTTPS routes from outside the cluster to services within it. Unlike LoadBalancer (which creates a load balancer per service and costs $$), Ingress shares one load balancer across many services. You can route based on hostname (agent.example.com vs dashboard.example.com) or URL path (/api/v1 vs /api/v2/beta). Ingress is Layer 7 (application layer), while LoadBalancer is Layer 4 (transport layer).
Key Mental Models
- Ingress = smart receptionist—LoadBalancer = direct tunnel to one service (one phone line per service). Ingress = receptionist who reads the address, looks at the URL path, checks the hostname, and routes you to the right department
- Resource + Controller = complete solution—Ingress resource (YAML) specifies rules. Ingress controller (nginx-ingress, AWS ALB) watches Ingress objects and configures real load balancers to implement them
- Path-based vs host-based routing—Same hostname, different paths go to different backends (/api/v1 → stable, /api/v2/beta → experimental). Different hostnames go to different backends (agent.example.com → agent-service, dashboard.example.com → dashboard-service)
- TLS termination at edge—Ingress handles encryption/decryption. Clients connect over HTTPS, Ingress terminates TLS, and routes plaintext HTTP to backend services. Reduces CPU load on app containers
Critical Patterns
- IngressClass selects controller—
ingressClassName: nginxtells Kubernetes which controller should implement this Ingress - Service selectors in backends—Ingress routes to Services, not Pods. Services handle the load balancing to Pods
- Annotations for advanced features—Rate limiting, CORS headers, request rewriting all via annotations (nginx.ingress.kubernetes.io/limit-rps, etc.)
- Single Ingress for multiple services—One host, multiple paths, each path → different service. Consolidates routing logic
Common Mistakes
- Creating LoadBalancer Services when Ingress would be simpler (LoadBalancer = cost per service, Ingress = cost per gateway)
- Mismatching backend service names (Ingress specifies service name that doesn't exist → 503 Service Unavailable)
- Forgetting that pathType:Prefix means /api/v1 matches /api/v1, /api/v1/, /api/v1/foo (use pathType:Exact for strict matching)
- Not checking Ingress controller is installed (IngressClass shows available controllers; if none, install nginx-ingress first)
Connections
- Builds on: Services (Lesson 5) for backend routing
- Leads to: Advanced networking (Lesson 10 service discovery), multi-tenant deployments with per-tenant Ingresses