Kubernetes basics · Lesson 12 of 12 · about 30 minutes

Getting traffic in: the Gateway API

One front door for every app in the cluster. A Gateway receives outside traffic, and routes decide which Service each request goes to, by domain name and path.

By the end you'll know

  • Why a cluster usually has one front door instead of one per app
  • The three pieces of the Gateway API, and who owns each one
  • How to send requests to different apps by domain name and path
  • What Ingress is, so you recognize it in older material

The problem: many apps, one way in

In Lesson 8 you gave pods a stable address with a Service, and saw that a LoadBalancer Service gets a real address on the internet. That works for one app. But a cluster often runs many: a shop, a blog, an API. A load balancer for each one means a separate bill and a separate address for each, and every app has to handle its own certificates.

Instead, clusters usually have one front door. All outside traffic arrives there, and a set of rules decides where each request goes: shop.example.com to the shop, /api to the API, and so on. The apps themselves keep plain ClusterIP Services, reachable only inside the cluster.

The Kubernetes way to describe that front door is the Gateway API.

Three pieces

The Gateway API splits the job into three objects:

  • GatewayClass: which software runs the front door. Kubernetes doesn't include one; you install a gateway controller, such as Envoy Gateway, and a GatewayClass points to it. You usually set this up once per cluster.
  • Gateway: the front door itself. It says which ports to listen on, for which protocols (HTTP or HTTPS), and which certificate to use. The controller reads it and starts a proxy (a program that receives requests and passes them on) to do the actual listening.
  • HTTPRoute: the routing rules. "Requests for this domain name and this path go to this Service." Each app can have its own.
Think of an office building again. The GatewayClass is the security company the building hired. The Gateway is the front entrance and its reception desk: one way in, staffed by that company. The HTTPRoutes are the directory in the lobby: "Shop, 3rd floor. Blog, 5th floor." Each team can update its own line in the directory without touching the entrance.

That last point is the reason for the split. On a shared cluster, a platform team owns the Gateway, and app teams write their own HTTPRoutes, in their own namespaces, without needing access to the front door.

How a request finds its appA request for www.example.com/shop arrives at the Gateway called front-door, which listens for HTTP on port 80 and is run by Envoy Gateway through the GatewayClass eg. The HTTPRoute my-site has two rules: paths starting with /shop go to the Service shop, and paths starting with /blog go to the Service blog. A request that matches no rule gets 404 Not Found.A requestwww.example.comGET /shopGateway: front-doorlistens for HTTP on port 80HTTPRoute: my-site/shop → Service shop/blog → Service blogno rule matches: 404 Not FoundGatewayClass eg: run by Envoy GatewayService: shop"hello from the shop"Service: blog"hello from the blog"One front door. The Gateway listens, and the HTTPRoute decides where each request goes.
The Gateway receives every request. The HTTPRoute's rules match on the domain name and the path, and pass the request to a plain ClusterIP Service.

What about Ingress?

Before the Gateway API, Kubernetes used an object called Ingress for this job. You'll still see it in older tutorials, Helm charts and certification exams, so it helps to recognize it.

Ingress still works, but it's frozen: it gets fixes, not new features. The most popular Ingress controller, ingress-nginx, was retired in March 2026, and Kubernetes recommends the Gateway API for new work. This track only teaches the Gateway API.

IngressGateway API
IngressClassGatewayClass
One Ingress object holds everythingSplit into a Gateway (the front door) and HTTPRoutes (the rules)
Extra features go in annotations that only one controller understandsExtra features are standard fields

Try it: install a gateway controller

Start a cluster:

BASH
kind create cluster --name learn

Kubernetes doesn't come with a gateway controller, so install one. This applies Envoy Gateway's install file straight from its official release page:

BASH
kubectl apply --server-side -f https://github.com/envoyproxy/gateway/releases/download/v1.9.2/install.yaml

The file is large. Besides the controller, it adds the Gateway API's object types (GatewayClass, Gateway, HTTPRoute and more) to your cluster, because Kubernetes doesn't include those by default either. --server-side lets the cluster handle a file that size. Wait for the controller to be ready:

BASH
kubectl wait --timeout=5m -n envoy-gateway-system deployment/envoy-gateway --for=condition=Available

When it prints condition met, you're ready.

Try it: two apps behind one front door

First, two small apps. http-echo is a tiny web server that replies to every request with one line of text, which makes it easy to see which app answered:

BASH
kubectl create deployment shop --image=hashicorp/http-echo:1.0.0
kubectl set env deployment/shop ECHO_TEXT="hello from the shop"
kubectl expose deployment shop --port=80 --target-port=5678

kubectl create deployment blog --image=hashicorp/http-echo:1.0.0
kubectl set env deployment/blog ECHO_TEXT="hello from the blog"
kubectl expose deployment blog --port=80 --target-port=5678

kubectl set env sets the text each app replies with, and kubectl expose creates a ClusterIP Service in front of it, as in Lesson 8. The app listens on port 5678, and the Service offers it on port 80.

Now the front door. Save this as gateway.yaml:

YAML
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
  name: eg
spec:
  controllerName: gateway.envoyproxy.io/gatewayclass-controller   # Envoy Gateway runs this class
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: front-door
spec:
  gatewayClassName: eg          # which GatewayClass runs this Gateway
  listeners:
    - name: http
      protocol: HTTP
      port: 80                  # listen for plain HTTP on port 80
BASH
kubectl apply -f gateway.yaml
kubectl get pods -n envoy-gateway-system

Envoy Gateway saw your Gateway and started a proxy for it: a new pod whose name starts with envoy-default-front-door-. Run the second command again until it shows 2/2 and Running. It can take a minute the first time, while the image downloads.

Then the rules. Save this as routes.yaml:

YAML
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: my-site
spec:
  parentRefs:
    - name: front-door          # attach these rules to the front-door Gateway
  hostnames:
    - "www.example.com"         # only for requests to this domain name
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /shop        # paths starting with /shop...
      backendRefs:
        - name: shop            # ...go to the Service shop, port 80
          port: 80
    - matches:
        - path:
            type: PathPrefix
            value: /blog
      backendRefs:
        - name: blog
          port: 80
BASH
kubectl apply -f routes.yaml
kubectl get httproute

On a cloud, the Gateway would get a public address here. On kind there's no cloud to create one, so kubectl get gateway shows no address, just like the LoadBalancer Service in Lesson 8. Use port forwarding to reach the proxy instead. The first command finds the Service Envoy Gateway created for your front door, and the second forwards your laptop's port 8080 to it:

BASH
SVC=$(kubectl get service -n envoy-gateway-system -l gateway.envoyproxy.io/owning-gateway-name=front-door -o name)
kubectl port-forward -n envoy-gateway-system $SVC 8080:80

Leave that running, and in a second terminal, send some requests. -H "Host: ..." tells the proxy which domain name you're asking for, since you're really talking to localhost:

BASH
curl -H "Host: www.example.com" http://localhost:8080/shop
curl -H "Host: www.example.com" http://localhost:8080/blog
curl -H "Host: www.example.com" http://localhost:8080/

hello from the shop, then hello from the blog: one address, two apps, chosen by path. The last request gets a 404, because no rule matches /. The front door only lets requests through when a rule says where they go.

HTTPS: where certificates fit

So far everything is plain HTTP. On a real site, the Gateway is also where HTTPS happens. You add a listener with protocol: HTTPS on port 443, and point it at a Secret that holds the site's certificate. The proxy decrypts the request and passes it on to the Service inside the cluster.

That keeps certificates in one place, instead of every app handling its own. A tool called cert-manager can create those certificates and renew them before they expire, so nobody has to remember to.

Who may attach routes

On a shared cluster you wouldn't want just anyone attaching rules to the front door. A listener's allowedRoutes decides which namespaces may attach HTTPRoutes to it:

YAML
      allowedRoutes:
        namespaces:
          from: Same      # only routes in the Gateway's own namespace

Same is the default. All allows every namespace, and Selector allows only namespaces with a certain label. It's the same idea as Lesson 10: give exactly the access that's needed, and nothing more.

In my real cluster

Lesson 8 described the path a request to api.civicgrid.org takes: Cloudflare, then a Cloudflare Tunnel, then Envoy Gateway. Here's the configuration behind the gateway part. It's the same three pieces you just built, plus HTTPS.

The Gateway, civicgrid-gateway, has one HTTPS listener on port 443, only for api.civicgrid.org, with a certificate managed by cert-manager. Its allowedRoutes is Same, so only routes in the civicgrid namespace can attach to it. One HTTPRoute sends everything for api.civicgrid.org to the API's ClusterIP Service on port 8000.

The one difference: on a cloud, Envoy Gateway normally gives the proxy a LoadBalancer Service with a public address. I changed mine to ClusterIP, so the front door has no public address at all. The only way in is the tunnel: the Cloudflare connector runs as a pod in the cluster and reaches the proxy's Service by its cluster DNS name, the same way any app reaches a Service.

This is the route, close to word for word from my GitOps repository (the Git repository Argo CD deploys from):

YAML
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: civicgrid-api
  namespace: civicgrid
spec:
  parentRefs:
    - name: civicgrid-gateway
  hostnames:
    - "api.civicgrid.org"
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /                       # every path
      backendRefs:
        - name: civicgrid-civicgrid-api    # the API's ClusterIP Service
          port: 8000

Break it on purpose

Ask for the wrong domain name. Run:

BASH
curl -H "Host: shop.example.com" http://localhost:8080/shop

404. The path is right, but the route only covers www.example.com, so the proxy has nowhere to send it. When a gateway returns 404 for a page you know exists, compare the domain name in the request with the route's hostnames first.

Point a rule at a Service that doesn't exist. In routes.yaml, change name: shop to name: shpo, apply, and request the shop again:

BASH
kubectl apply -f routes.yaml
curl -H "Host: www.example.com" http://localhost:8080/shop

This time it's a 500: the rule matched, but its destination doesn't exist. The route's status tells you why:

BASH
kubectl describe httproute my-site

Under Conditions, look for ResolvedRefs with status False and the reason BackendNotFound. A 404 means no rule matched; a 500 means a rule matched but couldn't reach its Service. Change the name back to shop, apply, and it works again.

Clean up

Press Ctrl+C in the terminal running the port forward, then:

BASH
kind delete cluster --name learn

That's the last lesson in Kubernetes basics. You've gone from what Kubernetes solves to running apps, connecting them, and letting the outside world in.

Check yourself

  1. Why do clusters usually have one front door instead of a LoadBalancer Service for every app?

  2. Which piece of the Gateway API holds the rule that sends /shop requests to the Service shop?

  3. You create a Gateway on a brand-new cluster, and nothing happens. What's the most likely reason?

  4. A request through the gateway returns 404, but the app is running. What should you check first?

  5. You find kind: Ingress in an old Helm chart. What is it?

  6. A Gateway listener has allowedRoutes set to from: Same. What does that mean?

Key terms

  • Gateway API: the Kubernetes way to describe how outside traffic gets into a cluster and where it goes.
  • Gateway controller: the software that does the work, such as Envoy Gateway. It reads Gateways and runs a proxy for each one.
  • GatewayClass: says which gateway controller runs a Gateway.
  • Gateway: the front door. Its listeners say which ports, protocols and certificates to use.
  • HTTPRoute: routing rules that send requests to Services, matched by domain name and path.
  • Proxy: a program that receives requests and passes them on. Envoy is the proxy behind Envoy Gateway.
  • Ingress: the older, single-object way to do the same job. Still works, but frozen.
  • cert-manager: a tool that creates certificates and renews them automatically.

Go deeper