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. /api matches /api, /api/users, /api/v1/users.
  • Exact: Matches exactly. /api matches 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:

  1. Verify controller is running: kubectl get pods -n ingress-nginx
  2. Check Ingress events: kubectl describe ingress my-ingress
  3. Verify backend service exists and works
  4. Check controller logs: kubectl logs -n ingress-nginx deployment/ingress-nginx-controller

TLS certificate doesn't work:

  1. Verify Secret exists: kubectl get secret my-tls-secret
  2. Check that host names match the certificate
  3. With cert-manager, check Certificate status: kubectl get certificates

404 even though path should match:

  1. Verify pathType (Prefix vs Exact)
  2. Check path order (more specific first)
  3. 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.