Kubernetes Ingress: How to Expose Your Services to the World
Complete guide to Kubernetes Ingress. Ingress controller comparison, TLS configuration, advanced routing. Practical examples and best practices.
You've deployed your application on Kubernetes. Pods are running, services work. But how do you access it from outside? With a LoadBalancer Service you get a public IP, but you pay for a load balancer for each service. With NodePort you expose a port on every node, but it's inconvenient and inflexible.
Ingress is the solution: a single entry point that handles routing, TLS, and load balancing for all your HTTP/HTTPS services.
What is Ingress
Ingress is a Kubernetes resource that defines rules for incoming HTTP/HTTPS traffic. It says things like: "requests for api.example.com go to the api service", "requests for example.com/blog go to the blog service".
But Ingress alone doesn't do anything. It's just a definition. To make it work you need an Ingress Controller — a component that reads Ingress resources and actually implements them.
It's like the difference between a configuration file and the program that uses it. Ingress is the configuration, Ingress Controller is the program.
Ingress Controllers: The Options
There are many ingress controllers. The most used:
NGINX Ingress Controller — The most common. Uses NGINX under the hood, has almost all the features you need, good documentation. It's what I recommend by default if you don't have specific requirements.
Traefik — Popular, good integration with Docker and Kubernetes, native dynamic configuration. Default on K3s.
HAProxy Ingress — Excellent performance, enterprise features. Solid choice for critical workloads.
Contour — Based on Envoy, good for environments already using Envoy/Istio.
Kong — Has integrated API Gateway features. If you need rate limiting, authentication, etc., Kong has them built-in.
For most cases, NGINX or Traefik work fine. The choice often depends on what you already know or use.
Installing NGINX Ingress Controller
# Via Helm (recommended)
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update
helm install ingress-nginx ingress-nginx/ingress-nginx
After installation, you'll have a LoadBalancer Service that's the entry point. On cloud providers, you get a public IP. On-premise, you might need to configure MetalLB or similar.
Verify it works:
kubectl get pods -n ingress-nginx
kubectl get svc -n ingress-nginx
The First Ingress Rule
Suppose we have a deployment and a service:
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
replicas: 2
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
spec:
containers:
- name: my-app
image: nginx:latest
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: my-app-svc
spec:
selector:
app: my-app
ports:
- port: 80
targetPort: 80
Now let's create an Ingress:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-app-ingress
spec:
ingressClassName: nginx
rules:
- host: my-app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-app-svc
port:
number: 80
Apply and verify:
kubectl apply -f ingress.yaml
kubectl get ingress
Now requests to my-app.example.com (assuming DNS points to the ingress controller IP) reach your service.
Path-Based Routing
A single Ingress can handle multiple routes:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: multi-path-ingress
spec:
ingressClassName: nginx
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
- path: /
pathType: Prefix
backend:
service:
name: default-service
port:
number: 80
Order matters. More specific paths should come first. /api is matched before /.
pathType
Prefix: Matches path as prefix./apimatches/api,/api/users,/api/v1/users.Exact: Matches exactly./apimatches only/api, not/api/users.ImplementationSpecific: Behavior depends on ingress controller.
Host-Based Routing
You can serve different hosts with the same Ingress:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: multi-host-ingress
spec:
ingressClassName: nginx
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
Each host can have independent path rules.
TLS/HTTPS
In production, you want HTTPS. Two ways to handle it.
Manual Certificate
Create a Secret with the certificate:
kubectl create secret tls my-tls-secret \
--cert=path/to/cert.pem \
--key=path/to/key.pem
Reference it in the Ingress:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: tls-ingress
spec:
ingressClassName: nginx
tls:
- hosts:
- my-app.example.com
secretName: my-tls-secret
rules:
- host: my-app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-app-svc
port:
number: 80
cert-manager (Recommended)
cert-manager automates everything: requests certificates from Let's Encrypt, renews them, manages them.
# Install cert-manager
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.14.0/cert-manager.yaml
Create a ClusterIssuer for Let's Encrypt:
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: your-email@example.com
privateKeySecretRef:
name: letsencrypt-prod
solvers:
- http01:
ingress:
class: nginx
Now Ingress can request certificates automatically:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: auto-tls-ingress
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
ingressClassName: nginx
tls:
- hosts:
- my-app.example.com
secretName: my-app-tls # cert-manager creates this
rules:
- host: my-app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-app-svc
port:
number: 80
cert-manager sees the annotation, requests the certificate, saves it in the Secret, and renews it before expiration. Zero maintenance.
Useful Annotations (NGINX)
Ingress controllers support annotations for advanced configurations. These are specific to NGINX Ingress Controller:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: annotated-ingress
annotations:
# Redirect HTTP to HTTPS
nginx.ingress.kubernetes.io/ssl-redirect: "true"
# Rate limiting
nginx.ingress.kubernetes.io/limit-rps: "10"
# Proxy timeout
nginx.ingress.kubernetes.io/proxy-read-timeout: "300"
nginx.ingress.kubernetes.io/proxy-send-timeout: "300"
# Rewrite path
nginx.ingress.kubernetes.io/rewrite-target: /$2
# Client max body size (upload)
nginx.ingress.kubernetes.io/proxy-body-size: "50m"
# CORS
nginx.ingress.kubernetes.io/enable-cors: "true"
# Basic auth
nginx.ingress.kubernetes.io/auth-type: basic
nginx.ingress.kubernetes.io/auth-secret: basic-auth
spec:
# ...
Each ingress controller has its own annotations. Check your controller's documentation.
Rewrite Target
A common pattern: you want /api to go to the service, but the service expects requests at /.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: rewrite-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
ingressClassName: nginx
rules:
- host: example.com
http:
paths:
- path: /api(/|$)(.*)
pathType: ImplementationSpecific
backend:
service:
name: api-service
port:
number: 80
Request to example.com/api/users → service receives /users.
Ingress Class
From Kubernetes 1.18+, IngressClass is the recommended way to specify which controller handles an Ingress.
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
name: nginx
annotations:
ingressclass.kubernetes.io/is-default-class: "true"
spec:
controller: k8s.io/ingress-nginx
If you mark an IngressClass as default, Ingresses without ingressClassName use it.
Default Backend
What happens if a request doesn't match any rule? You can define a default backend:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: default-backend-ingress
spec:
ingressClassName: nginx
defaultBackend:
service:
name: default-service
port:
number: 80
rules:
# normal rules...
Useful for serving a custom 404 page or redirect to a landing page.
Troubleshooting
Ingress doesn't work:
- Verify controller is running:
kubectl get pods -n ingress-nginx - Check Ingress events:
kubectl describe ingress my-ingress - Verify backend service exists and works
- Check controller logs:
kubectl logs -n ingress-nginx deployment/ingress-nginx-controller
TLS certificate doesn't work:
- Verify Secret exists:
kubectl get secret my-tls-secret - Check that host names match the certificate
- With cert-manager, check Certificate status:
kubectl get certificates
404 even though path should match:
- Verify pathType (Prefix vs Exact)
- Check path order (more specific first)
- Verify service has endpoints:
kubectl get endpoints my-service
Best Practices
Always use TLS in production. With cert-manager it's practically free. There's no excuse not to have HTTPS.
One Ingress per application. Makes it easier to manage independent lifecycle and permissions.
Monitor the ingress controller. It's a single point of failure. Configure alerting on CPU, memory, and error rate.
Rate limiting. Protect your services from abuse. Better to set it up from the start than after an incident.
Replicate the controller. By default many controllers run with one replica. In production, at least 2-3.
Backup configurations. Ingresses are Kubernetes resources, so GitOps. But make sure TLS Secrets are managed correctly (don't commit private keys!).
Conclusion
Ingress is the standard way to expose HTTP services on Kubernetes. Once you understand the concept (Ingress = rules, Ingress Controller = implementation), the rest is configuration.
Start simple: NGINX Ingress Controller, cert-manager for TLS, basic rules. Add complexity (rewrite, rate limiting, etc.) when you need it.
It's not rocket science, but there are enough details to waste time if you don't know them. I hope this guide saves you a few hours of debugging.
Well-made infrastructure is invisible. When Ingress works, nobody thinks about it. And that's how it should be.