Ingress Fundamentals
Understand why Ingress exists, its limitations, and why Gateway API replaces it
- Status
- stable
- Trust
- unverified
- Approved
- ag-archive/0.1.0 ·
Your Task API is deployed to Kubernetes. Run this command:
kubectl get services -n task-apiOutput:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
task-api ClusterIP 10.96.45.123 <none> 8000/TCP 2hNow try accessing it from your browser at http://10.96.45.123:8000. What happens?
Nothing. The page never loads.
That ClusterIP address exists only inside the Kubernetes cluster. Your browser, running outside the cluster on your laptop, cannot reach it. The IP 10.96.45.123 is meaningless to your operating system's network stack.
This lesson answers a fundamental question: How do external users reach services running inside Kubernetes?
The External Access Problem
Kubernetes networking creates a private network inside the cluster. Every Pod gets an IP address, and Services provide stable endpoints for groups of Pods. But this private network is invisible to the outside world.
Think of it like a corporate office building:
- Inside the building, employees can call each other using 4-digit extension numbers (3401, 2156)
- Those extension numbers don't work if you dial them from outside the building
- External callers need a public phone number that routes to the internal extension
Kubernetes Services are the internal extension numbers. External users need a public entry point.
kubectl port-forward: The Development Workaround
You've probably used this already:
kubectl port-forward service/task-api 8000:8000 -n task-apiOutput:
Forwarding from 127.0.0.1:8000 -> 8000
Forwarding from [::1]:8000 -> 8000Now http://localhost:8000 works in your browser. But ask yourself: What happens when you close that terminal?
The forwarding stops. Your service becomes unreachable again.
kubectl port-forward tunnels traffic from your local machine through the Kubernetes API server to the Pod. It's useful for debugging, but it's not a solution for production:
- Only works while the command runs
- One user at a time (your machine)
- No TLS termination
- No load balancing
- No rate limiting
You need something that provides permanent external access.
Service Types: A Progression
Kubernetes offers multiple ways to expose Services externally. Each builds on the previous, solving additional problems while adding complexity.
ClusterIP: Internal Only
ClusterIP is the default Service type. When you create a Service without specifying a type, you get ClusterIP:
apiVersion: v1
kind: Service
metadata:
name: task-api
namespace: task-api
spec:
selector:
app: task-api
ports:
- port: 8000
targetPort: 8000
# type: ClusterIP # This is the defaultOutput:
service/task-api createdClusterIP provides:
- A stable virtual IP inside the cluster
- DNS name (
task-api.task-api.svc.cluster.local) - Load balancing across matching Pods
ClusterIP does NOT provide:
- Any external access
- Reachability from outside the cluster
ClusterIP is perfect for internal services that only other Pods need to reach. Your database, cache, and message queue typically use ClusterIP because external users should never access them directly.
NodePort: Expose on Every Node
NodePort opens a specific port on every node in your cluster:
apiVersion: v1
kind: Service
metadata:
name: task-api-nodeport
namespace: task-api
spec:
type: NodePort
selector:
app: task-api
ports:
- port: 8000
targetPort: 8000
nodePort: 30080 # Must be 30000-32767Output:
service/task-api-nodeport createdCheck the service:
kubectl get service task-api-nodeport -n task-apiOutput:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
task-api-nodeport NodePort 10.96.78.234 <none> 8000:30080/TCP 10sNow you can access your service at http://<any-node-ip>:30080. If you're using Docker Desktop, try:
curl http://localhost:30080/healthOutput:
{"status": "healthy"}NodePort solves external access, but creates new problems:
| Problem | Why It Matters |
|---|---|
| Port range limited to 30000-32767 | Users must type :30080 in URLs |
| Which node IP do users use? | If that node goes down, users get errors |
| No TLS termination | HTTP only, or you handle TLS in your app |
| One port per service | Limited to ~2,700 services cluster-wide |
NodePort works for development and testing. For production, you need something that handles node failures and provides standard ports (80/443).
LoadBalancer: Cloud Provider Integration
LoadBalancer requests an external load balancer from your cloud provider:
apiVersion: v1
kind: Service
metadata:
name: task-api-lb
namespace: task-api
spec:
type: LoadBalancer
selector:
app: task-api
ports:
- port: 80 # External port
targetPort: 8000Output:
service/task-api-lb createdWait a moment, then check:
kubectl get service task-api-lb -n task-apiOutput (cloud environment):
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
task-api-lb LoadBalancer 10.96.89.123 203.0.113.45 80:31234/TCP 2mNow http://203.0.113.45 reaches your service on port 80. The cloud provider created a load balancer, gave it a public IP, and configured it to forward traffic to your nodes.
Question: If LoadBalancer solves external access, why do we need Ingress?
Answer: Cost and routing.
Consider this scenario: You have 10 microservices. With LoadBalancer Services:
- 10 load balancers (one per service)
- 10 public IP addresses
- 10 monthly charges from your cloud provider
At $15-25 per load balancer per month, that's $150-250/month just for external access. And you still can't do:
- Path-based routing (
/apito service A,/webto service B) - Host-based routing (
api.example.comvsweb.example.com) - TLS termination at one place
- Rate limiting
You need a single entry point that routes to multiple services.
Ingress: Layer 7 Routing
Ingress provides HTTP/HTTPS routing rules that direct traffic to Services based on hostnames and paths.
┌─────────────────┐
│ Service A │
│ (ClusterIP) │
└────────▲────────┘
│
┌────────────────────────┼────────────────────────┐
│ │ /api/* │
Internet │ ┌───────────────────┴───────────────────┐ │
│ │ │ Ingress │ │
│ │ │ - Host: example.com │ │
└───────────┼───►│ - /api/* → service-a │ │
│ │ - /web/* → service-b │ │
│ │ - TLS termination │ │
│ └───────────────────┬───────────────────┘ │
│ │ /web/* │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Service B │ │
│ │ (ClusterIP) │ │
└───────────────┴─────────────────┴───────────────┘
Kubernetes ClusterHere's an Ingress resource:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: example-ingress
spec:
ingressClassName: nginx # Which controller handles this
rules:
- host: example.com
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: api-service
port:
number: 8000
- path: /web
pathType: Prefix
backend:
service:
name: web-service
port:
number: 80
tls:
- hosts:
- example.com
secretName: example-tlsOutput:
ingress.networking.k8s.io/example-ingress createdIngress gives you:
- Single entry point: One load balancer for multiple services
- Path-based routing:
/apigoes to API service,/webgoes to web service - Host-based routing:
api.example.comvsadmin.example.com - TLS termination: HTTPS handled at the edge, services receive HTTP
This looks like the solution. But there's a problem.
The Annotation Chaos Problem
Ingress is a Kubernetes-native resource, but it only defines basic routing. Real production needs require:
- Rate limiting
- Request timeouts
- Header manipulation
- Authentication
- Circuit breaking
These features don't exist in the Ingress specification. So every Ingress controller added them through annotations.
NGINX Ingress Controller
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-ingress
annotations:
nginx.ingress.kubernetes.io/rate-limit: "100"
nginx.ingress.kubernetes.io/rate-limit-window: "1m"
nginx.ingress.kubernetes.io/proxy-read-timeout: "60"
nginx.ingress.kubernetes.io/proxy-send-timeout: "60"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/use-regex: "true"
spec:
ingressClassName: nginx
# ... rulesTraefik Ingress Controller
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-ingress
annotations:
traefik.ingress.kubernetes.io/rate-limit: "average=100,burst=200"
traefik.ingress.kubernetes.io/request-timeout: "60s"
traefik.ingress.kubernetes.io/redirect-entry-point: "https"
traefik.ingress.kubernetes.io/router.middlewares: "default-ratelimit@kubernetescrd"
spec:
ingressClassName: traefik
# ... rulesKong Ingress Controller
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-ingress
annotations:
konghq.com/plugins: "rate-limiting"
konghq.com/read-timeout: "60000"
konghq.com/write-timeout: "60000"
konghq.com/strip-path: "true"
konghq.com/protocols: "https"
spec:
ingressClassName: kong
# ... rulesQuestion: What happens when you switch from NGINX to Traefik?
Answer: You rewrite every annotation. The rate limiting syntax, timeout format, TLS configuration, and middleware references are all different. This is vendor lock-in through annotations.
Even worse: annotations are unstructured strings. There's no schema validation. Typo in an annotation name? Kubernetes accepts it silently and the feature simply doesn't work.
Ingress Limitations
The annotation problem is one of several fundamental limitations in the Ingress API.
1. No Role Separation
In production, different teams have different responsibilities:
| Role | Responsibility |
|---|---|
| Platform Team | Installs and maintains the ingress controller |
| Security Team | Configures TLS policies and authentication |
| Application Team | Defines routes for their services |
With Ingress, everyone edits the same resource. The application developer who adds a route can accidentally modify the TLS configuration. There's no RBAC granularity within the Ingress resource.
2. Annotation Chaos (Vendor Lock-In)
Every feature beyond basic routing requires vendor-specific annotations. Your infrastructure becomes tightly coupled to one ingress controller. Switching controllers means rewriting every Ingress resource.
3. Limited Expressiveness
The Ingress spec cannot express common requirements:
- Header-based routing: Route based on
X-API-Versionheader - Traffic splitting: Send 10% of traffic to canary version
- Request mirroring: Copy traffic to shadow environment for testing
- Query parameter matching: Route based on
?version=2
These require controller-specific CRDs or annotations, losing the benefit of a standard API.
4. HTTP/HTTPS Only
Ingress only handles HTTP traffic. If you need:
- gRPC routing
- TCP passthrough
- UDP services (DNS, games)
- WebSocket with specific handling
You're back to vendor-specific solutions.
5. No Status Reporting
When an Ingress resource fails to configure correctly, how do you know? The Ingress status field is minimal. Controllers report status inconsistently, making debugging difficult.
Enter Gateway API
The Kubernetes community recognized these limitations and created Gateway API—a successor to Ingress designed from the ground up for:
| Gateway API Feature | Ingress Limitation Solved |
|---|---|
| Resource separation (GatewayClass, Gateway, HTTPRoute) | Role separation with RBAC |
| Typed policies (BackendTrafficPolicy, SecurityPolicy) | No more annotation chaos |
| Header matching, traffic splitting, mirroring built-in | Full expressiveness |
| TCP, UDP, gRPC, TLS routes | Not HTTP-only |
| Rich status reporting | Clear debugging |
The Gateway API Model
Gateway API splits responsibilities into distinct resources:
┌─────────────────────────────────────────────────────────────────┐
│ Cluster Admin / Platform Team │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ GatewayClass │ │
│ │ - Defines which controller handles Gateways │ │
│ │ - One per controller type (Envoy Gateway, Traefik, etc.) │ │
│ └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Platform Team / Infrastructure │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Gateway │ │
│ │ - Defines listeners (ports, protocols, TLS) │ │
│ │ - Binds to GatewayClass │ │
│ │ - Controls which namespaces can attach routes │ │
│ └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Application Team / Developers │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ HTTPRoute / GRPCRoute / TCPRoute │ │
│ │ - Defines routing rules (paths, headers, weights) │ │
│ │ - Attaches to Gateway │ │
│ │ - Points to backend Services │ │
│ └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘This separation enables:
- Platform teams manage infrastructure (GatewayClass, Gateway)
- Application teams manage routing (HTTPRoute, GRPCRoute)
- RBAC prevents app developers from modifying infrastructure
- Portability because routes use standard API, not annotations
Portable Configurations
With Gateway API, switching from Envoy Gateway to Traefik Gateway requires changing ONE field:
# Before: Envoy Gateway
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: my-gateway
spec:
gatewayClassName: eg # Envoy Gateway
# ... listeners unchanged
# After: Traefik Gateway
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: my-gateway
spec:
gatewayClassName: traefik # Traefik Gateway
# ... listeners unchanged (identical)Your HTTPRoute resources remain identical. No annotation rewrites. No vendor lock-in.
Exercises
Work through these exercises to solidify your understanding of Service types and Ingress limitations.
Exercise 1: Explore Service Types
If you have a Kubernetes cluster running, list all Services:
kubectl get services --all-namespacesOutput:
NAMESPACE NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
default kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 7d
kube-system kube-dns ClusterIP 10.96.0.10 <none> 53/UDP,53/TCP 7dFor each Service, identify:
- Why is it ClusterIP instead of NodePort or LoadBalancer?
- Who needs to reach this Service (internal Pods or external users)?
Exercise 2: Create and Compare Service Types
Create a simple deployment to experiment with:
kubectl create deployment nginx-test --image=nginxOutput:
deployment.apps/nginx-test createdNow expose it with different Service types:
# ClusterIP (default)
kubectl expose deployment nginx-test --port=80 --name=nginx-clusterip
# NodePort
kubectl expose deployment nginx-test --port=80 --type=NodePort --name=nginx-nodeport
# LoadBalancer
kubectl expose deployment nginx-test --port=80 --type=LoadBalancer --name=nginx-lbOutput:
service/nginx-clusterip exposed
service/nginx-nodeport exposed
service/nginx-lb exposedCompare them:
kubectl get services nginx-clusterip nginx-nodeport nginx-lbOutput:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx-clusterip ClusterIP 10.96.23.45 <none> 80/TCP 30s
nginx-nodeport NodePort 10.96.34.56 <none> 80:31234/TCP 25s
nginx-lb LoadBalancer 10.96.45.67 <pending> 80:32345/TCP 20sNotice: LoadBalancer shows <pending> on Docker Desktop because there's no cloud provider to create a load balancer. On GKE, EKS, or AKS, it would receive an external IP.
Clean up:
kubectl delete deployment nginx-test
kubectl delete service nginx-clusterip nginx-nodeport nginx-lbExercise 3: Examine Ingress Controller Annotations
Visit the documentation for any two Ingress controllers:
Find the annotation for:
- Rate limiting
- Request timeout
- TLS redirect
Compare the syntax. How different are they?
Exercise 4: Map Scenarios to Service Types
For each scenario, which Service type would you choose?
| Scenario | Your Choice | Why? |
|---|---|---|
| Database only accessed by app Pods | ||
| Development testing from your laptop | ||
| Single service needs internet access (cloud) | ||
| 20 services need internet access (cloud) | ||
| gRPC service with multiple routes |
Answers:
- ClusterIP - Internal only, no external access needed
- NodePort or port-forward - Quick development access
- LoadBalancer - Simple, one service
- Ingress or Gateway API - One entry point, multiple routes
- Gateway API - gRPC routing not supported by Ingress
Reflect on Your Skill
You built a traffic-engineer skill in Lesson 0. Based on what you learned about Ingress limitations, consider:
What Decision Logic Should Your Skill Include?
Your skill should help you choose:
| Question | If Yes | If No |
|---|---|---|
| Internal-only service? | ClusterIP | Continue |
| Development/testing only? | NodePort | Continue |
| Single service, cloud environment? | LoadBalancer | Continue |
| Multiple services, complex routing? | Gateway API | LoadBalancer |
| Need header matching, traffic splitting? | Gateway API | Ingress might work |
Does your skill encode this decision tree?
Which Ingress Limitations Affect Your AI Agent Deployment?
Consider your Task API deployment:
- Rate limiting: AI agents can be expensive. You need per-user limits.
- Traffic splitting: You'll want canary deployments for new model versions.
- Header-based routing: API versioning through headers.
- Observability: Need rich status reporting for debugging.
All of these require Gateway API's expressiveness. Ingress annotations would create vendor lock-in.
What Should You Add to Your Skill?
If your skill currently generates Ingress resources, you now understand why Gateway API is better. The remaining lessons will teach Gateway API patterns that your skill should capture instead.
Core Concept
Kubernetes creates a private network invisible to the outside world; understanding the progression from ClusterIP to NodePort to LoadBalancer to Ingress reveals why Gateway API was created to replace Ingress's annotation chaos and lack of role separation.
Key Mental Models
- External Access Problem: ClusterIP addresses exist only inside the cluster; external browsers cannot reach internal IPs
- Service Type Progression: ClusterIP (internal) -> NodePort (ports 30000-32767) -> LoadBalancer (cloud IPs) -> Ingress (L7 routing)
- Annotation Chaos: Every Ingress controller uses different annotation syntax for rate limiting, timeouts, and TLS
- Gateway API Solution: Three-tier resources (GatewayClass, Gateway, HTTPRoute) enabling role separation and portability
Critical Patterns
- Use ClusterIP for internal services only (databases, caches)
- Use NodePort for development/testing quick access
- Use LoadBalancer for single-service cloud deployments
- Use Gateway API for multiple services with complex routing needs
AI Collaboration Keys
- Ask AI to map your scenario to the appropriate Service type
- Use AI to compare annotation syntax between different Ingress controllers
- Generate the decision tree for choosing between Ingress and Gateway API
Common Mistakes
- Using LoadBalancer per service (expensive: 10 services = 10 monthly charges)
- Assuming port-forward is a production solution (stops when terminal closes)
- Rewriting all annotations when switching Ingress controllers (vendor lock-in)
Connections
- Builds on: Kubernetes Service concepts from Chapter 80
- Leads to: Lesson 2 (Traefik Ingress Controller) for simpler routing before Gateway API complexity