Chapter 27 of 47 · Services and discovery

Ingress and Gateway API: one front door

Exposing every Service as a LoadBalancer means one cloud load balancer, one IP and one TLS certificate per app. An Ingress puts a single smart front door in front of all of them: it reads the host name and path of each request and forwards it to the right Service. The Gateway API is its newer, more powerful successor.

Two parts: rules and a controller

Ingress rules and the controller that runs themYou write an Ingress object with rules. An ingress controller, a reverse proxy running as Pods, reads those rules and routes traffic from its own LoadBalancer Service to your Services. Users LoadBalancerjust one Ingress controllerPods running a proxy Ingress (rules)the YAML you write reads posts-srv comments-srv client-srv

Ingress = the rules

A plain Kubernetes object: "/api/posts goes to posts-srv". On its own it does nothing at all.

Ingress controller = the doorman

A reverse proxy (nginx, Traefik, HAProxy, Envoy…) running as Pods. It watches Ingress objects and configures itself. You install one per cluster.

Classic mistake: applying an Ingress in a cluster with no controller. kubectl get ingress shows it, the ADDRESS column stays empty, and nothing answers.

Installing a controller

$ helm repo add traefik https://traefik.github.io/charts
$ helm install traefik traefik/traefik -n traefik --create-namespace
$ kubectl get svc -n traefik
NAME      TYPE           EXTERNAL-IP    PORT(S)
traefik   LoadBalancer   34.120.8.10    80:31080/TCP,443:31443/TCP

Traefik ships built into k3s and supports both Ingress and Gateway API. Note that the controller itself is exposed by one LoadBalancer Service: that is the single front door.

Path-based routing

One host, different paths → different Services. Click the parts:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: blog
spec:
  ingressClassName: traefik
  rules:
    - host: blog.example.com
      http:
        paths:
          - path: /api/posts
            pathType: Prefix
            backend:
              service:
                name: posts-srv
                port:
                  number: 4000
          - path: /api/comments
            pathType: Prefix
            backend:
              service: { name: comments-srv, port: { number: 4001 } }
          - path: /
            pathType: Prefix
            backend:
              service: { name: client-srv, port: { number: 3000 } }

pathType

Prefix: matches the path and anything below it, compared segment by segment (/api/posts matches /api/posts/42, not /api/postsXYZ). Exact: only that exact path. When several match, Exact wins, then the longest Prefix.

Your Node services receive the full path: posts sees /api/posts/42, so its routes must include the prefix. (Rewriting paths is controller-specific; see annotations below.)

Host-based routing

Different host names on the same front door, each with its own rules. Point all the DNS names at the controller's one IP:

  rules:
    - host: blog.example.com
      http: { paths: [ ... blog paths ... ] }
    - host: admin.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend: { service: { name: admin-srv, port: { number: 8080 } } }
# DNS (at your domain provider)
blog.example.com    A  34.120.8.10
admin.example.com   A  34.120.8.10      ← same IP, the Host header decides

Try the router

These are the rules from both examples combined, plus an Exact rule for /healthz. Type a URL or use the buttons:

TLS termination

"Terminating" TLS means the front door decrypts HTTPS, so your services only ever speak plain HTTP inside the cluster. One place holds the certificates; no app needs to handle them.

  1. Browser ══ HTTPS (encrypted) ══▶ blog.example.com:443

    The browser connects to the controller's public IP on port 443.

  2. Ingress controller:
      host blog.example.com → certificate from Secret blog-tls
      decrypt ✓

    The controller picks the certificate for that host from a kubernetes.io/tls Secret (chapter 19) and decrypts.

  3. controller ── HTTP (plain) ──▶ posts-srv:4000 ──▶ posts Pod

    Inside the cluster the request is plain HTTP. (For encryption inside too, use a service mesh or re-encrypt; most apps do not.)

spec:
  tls:
    - hosts: [blog.example.com, admin.example.com]
      secretName: blog-tls          # type kubernetes.io/tls, holds tls.crt + tls.key
  rules: ...
Free, automatic certificates: install cert-manager once. Add one annotation to the Ingress and it gets a Let's Encrypt certificate, stores it in the Secret, and renews it before it expires:
metadata:
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt

Annotations: the messy part

The Ingress API only covers hosts, paths and TLS. Anything else (rewrites, redirects, timeouts, rate limits, CORS) is done with controller-specific annotations:

# ingress-nginx
nginx.ingress.kubernetes.io/rewrite-target: /$2
nginx.ingress.kubernetes.io/proxy-body-size: 10m
# Traefik uses its own Middleware objects instead

So an Ingress that works on one controller can silently behave differently on another. This is the main reason the Gateway API exists.

Gateway API

The Gateway API (stable since 2023, gateway.networking.k8s.io/v1) splits the front door into roles and puts common features in the standard itself:

ObjectWho owns itSays
GatewayClassInfrastructure provider"Gateways of this kind are implemented by Traefik / Envoy / the cloud."
GatewayCluster operator"Listen on port 443 for *.example.com, with this certificate."
HTTPRouteApp developer"Send /api/posts to posts-srv." Lives next to your app.

Gateway (operator)

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: web
spec:
  gatewayClassName: traefik
  listeners:
    - name: https
      protocol: HTTPS
      port: 443
      hostname: "*.example.com"
      tls:
        certificateRefs:
          - name: blog-tls

HTTPRoute (you)

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: blog
spec:
  parentRefs:
    - name: web
  hostnames: [blog.example.com]
  rules:
    - matches:
        - path: { type: PathPrefix, value: /api/posts }
      backendRefs:
        - name: posts-srv
          port: 4000
    - matches:
        - path: { type: PathPrefix, value: / }
      backendRefs:
        - name: client-srv
          port: 3000
IngressGateway API
Host and path routing, TLS✓✓
Header matching, traffic splitting (90/10 canary), redirects, rewritesAnnotations, different per controllerBuilt into the standard
TCP, UDP, gRPCNo (HTTP only)✓ (TCPRoute, GRPCRoute…)
Roles (operator vs developer)One object for everythingGateway vs Route
StatusStable, frozen: no new featuresStable, where new work happens

Advice: learn Ingress, because it is everywhere. For a new cluster, prefer the Gateway API.

Try it locally

$ minikube addons enable ingress
$ kubectl apply -f blog-ingress.yaml
$ kubectl get ingress
NAME   CLASS   HOSTS              ADDRESS        PORTS
blog   nginx   blog.example.com   192.168.49.2   80
# make the name point to your cluster, on your laptop only:
$ echo "$(minikube ip) blog.example.com" | sudo tee -a /etc/hosts
$ curl http://blog.example.com/api/posts

Recap

Ingress and Gateway API: common questions

What is the difference between Ingress and an Ingress controller?

An Ingress is a set of routing rules. An Ingress controller is a reverse proxy running in the cluster that reads those rules and routes traffic. Without a controller, an Ingress does nothing.

Should I use Ingress or the Gateway API?

Ingress is everywhere and worth knowing, but it is frozen. For new clusters the Gateway API is recommended: it standardises features that Ingress needs annotations for.