How to Set Up Kubernetes Ingress Controller

How to Set Up Kubernetes Ingress Controller

The first time I tried exposing a handful of services in a cluster, I created a LoadBalancer Service for each one and watched my cloud bill creep up with every new microservice. That’s the problem Ingress solves — a single entry point that intelligently routes HTTP and HTTPS traffic to many services based on hostnames and paths. In this article I’ll explain what Ingress actually is, why you need a controller (Kubernetes doesn’t ship with one by default), and walk through a complete setup using NGINX Ingress Controller, including TLS.

Ingress vs. Ingress Controller: The Distinction That Confuses Everyone

An Ingress resource is just a Kubernetes object — a set of routing rules you define in YAML. It does nothing on its own. An Ingress controller is the actual piece of software (usually a reverse proxy like NGINX, Traefik, or HAProxy) that watches for Ingress resources and configures itself to implement those rules. Without a controller running in your cluster, creating an Ingress resource has zero effect — it just sits there.

Prerequisites

Step 1: Install the NGINX Ingress Controller

The easiest way is via Helm:

helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update
helm install ingress-nginx ingress-nginx/ingress-nginx \
  --namespace ingress-nginx \
  --create-namespace

Verify it deployed correctly:

kubectl get pods -n ingress-nginx

Expected output:

NAME                                        READY   STATUS    RESTARTS   AGE
ingress-nginx-controller-7f9d9c6c4d-2plq9   1/1     Running   0          90s

On a cloud provider, this Helm chart provisions a LoadBalancer Service that gives you a public IP or hostname:

kubectl get svc -n ingress-nginx
NAME                                 TYPE           EXTERNAL-IP      PORT(S)
ingress-nginx-controller             LoadBalancer   34.111.22.187    80:31820/TCP,443:31821/TCP

That external IP is now your single entry point for everything.

Step 2: Deploy a Sample Application

Let’s give ourselves something to route to:

# app-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello-app
spec:
  replicas: 2
  selector:
    matchLabels:
      app: hello-app
  template:
    metadata:
      labels:
        app: hello-app
    spec:
      containers:
        - name: hello-app
          image: gcr.io/google-samples/hello-app:1.0
          ports:
            - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: hello-app-service
spec:
  selector:
    app: hello-app
  ports:
    - port: 80
      targetPort: 8080
kubectl apply -f app-deployment.yaml

Step 3: Create an Ingress Resource

Now the actual routing rule:

# hello-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: hello-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  ingressClassName: nginx
  rules:
    - host: hello.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: hello-app-service
                port:
                  number: 80
kubectl apply -f hello-ingress.yaml
kubectl get ingress
NAME             CLASS   HOSTS               ADDRESS          PORTS   AGE
hello-ingress    nginx   hello.example.com   34.111.22.187    80      45s

Point hello.example.com at that IP in your DNS provider (an A record), and once it propagates:

curl http://hello.example.com

Step 4: Route Multiple Services by Path or Host

This is where Ingress really pays off. You can route different paths of the same host to different services:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: multi-path-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
  ingressClassName: nginx
  rules:
    - host: myapp.example.com
      http:
        paths:
          - path: /api(/|$)(.*)
            pathType: ImplementationSpecific
            backend:
              service:
                name: api-service
                port:
                  number: 80
          - path: /web(/|$)(.*)
            pathType: ImplementationSpecific
            backend:
              service:
                name: web-service
                port:
                  number: 80

Or route entirely different hostnames to different backends within a single Ingress:

spec:
  ingressClassName: nginx
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: api-service
                port:
                  number: 80
    - host: admin.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: admin-service
                port:
                  number: 80

Step 5: Add TLS with cert-manager

Manually managing TLS certificates gets tedious fast, so I always install cert-manager to automate Let’s Encrypt certificates:

helm repo add jetstack https://charts.jetstack.io
helm repo update
helm install cert-manager jetstack/cert-manager \
  --namespace cert-manager \
  --create-namespace \
  --set installCRDs=true

Create a ClusterIssuer:

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: you@example.com
    privateKeySecretRef:
      name: letsencrypt-prod-key
    solvers:
      - http01:
          ingress:
            class: nginx

Then reference it in your Ingress, adding a tls block:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: hello-ingress
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - hello.example.com
      secretName: hello-tls
  rules:
    - host: hello.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: hello-app-service
                port:
                  number: 80

Apply it and cert-manager will automatically request, validate, and store a certificate as a Secret named hello-tls, renewing it before expiry.

Step 6: Debugging Ingress Issues

If traffic isn’t reaching your service, work through this checklist:

kubectl describe ingress hello-ingress
kubectl logs -n ingress-nginx deploy/ingress-nginx-controller
kubectl get endpoints hello-app-service

Empty endpoints usually means your Service selector doesn’t match any Pod labels. A 404 from NGINX itself (not your app) usually means the host header didn’t match any Ingress rule — check DNS and make sure you’re hitting the right hostname.

Security and Performance Best Practices

Common Mistakes

Choosing Between Ingress Controllers

NGINX Ingress Controller is what I reach for by default because it’s mature, widely documented, and handles the vast majority of routing needs out of the box. But it’s worth knowing the landscape:

If you’re starting a new project today, I’d at least evaluate the Gateway API alongside classic Ingress, since it addresses several long-standing Ingress limitations (like multi-protocol support and clearer multi-team ownership models) more cleanly.

Rate Limiting and Abuse Protection

Beyond basic routing, Ingress is a natural place to protect backends from abusive traffic before it ever reaches your Pods:

metadata:
  annotations:
    nginx.ingress.kubernetes.io/limit-rps: "10"
    nginx.ingress.kubernetes.io/limit-burst-multiplier: "5"

This limits each client IP to roughly 10 requests per second with a burst allowance, rejecting excess requests with a 503 before they ever hit your application containers — meaningfully cheaper than scaling Pods to absorb abusive traffic.

Canary Routing at the Ingress Layer

NGINX Ingress also supports weighted canary releases without needing a separate tool like Argo Rollouts, useful for lightweight gradual rollouts:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: hello-canary
  annotations:
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-weight: "10"
spec:
  ingressClassName: nginx
  rules:
    - host: hello.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: hello-app-v2-service
                port:
                  number: 80

This routes roughly 10% of traffic to the new version’s Service while the primary Ingress continues serving the rest, letting you validate a change against a small slice of real traffic before committing further.

Path Matching Pitfalls Worth Knowing

pathType behaves differently than many people expect on first use:

Getting pathType wrong is a very common source of “my Ingress rule matches everything” or “my Ingress rule matches nothing” bugs, so I always test with a couple of representative real request paths before trusting a rule in production.

Monitoring Ingress Traffic

The NGINX Ingress Controller exposes Prometheus metrics by default, which I’d wire up alongside whatever monitoring stack you’re already running (see the dedicated Prometheus guide):

kubectl get svc -n ingress-nginx ingress-nginx-controller-metrics

Key metrics worth alerting on: nginx_ingress_controller_requests broken down by status code (watch for a sustained rise in 5xx), and nginx_ingress_controller_request_duration_seconds for latency regressions — both give you visibility into problems at the edge before they’re reported by users.

Summary

Setting up Ingress in Kubernetes comes down to three pieces: install a controller (NGINX is the most common and battle-tested), define routing rules via Ingress resources, and layer TLS on top with cert-manager for real production use. Once this is running, you get a single, cost-efficient entry point that can route dozens of services by host or path, with centralized TLS termination and rate limiting.

References

Exit mobile version