# Gateway API: GatewayClass, Gateway and HTTPRoute with a canary split

> How Gateway API splits ownership between the platform team and app teams: GatewayClass, a shared Gateway, a ReferenceGrant for a cross-namespace certificate, and an HTTPRoute that sends 10% of traffic to v2.

Interactive graph: https://k8wiz.com/graphs/gateway-api-httproute-canary

Gateway API replaces one Ingress object with three, one for each team. The infrastructure provider supplies the GatewayClass, the platform team runs a shared Gateway, and app teams attach HTTPRoutes to it from their own namespaces.

Two different mechanisms control what may cross a namespace. A Gateway's `allowedRoutes` decides which namespaces may attach routes to it. A ReferenceGrant, written in the namespace being pointed at, allows a reference into that namespace, here a Gateway reading a certificate Secret from `certs`. This example uses Envoy Gateway, but the objects are the same for Istio, Cilium, NGINX Gateway Fabric and the other implementations.

## Resources

### Client Browser

outside the cluster

Calls `https://shop.example.com/api/orders`.

### Load balancer Cloud load balancer

outside the cluster

Provisioned by the cloud for the `type: LoadBalancer` Service that the implementation creates for the Gateway. Its address shows up in the Gateway's `status.addresses`.

### GatewayClass eg

comes with an add-on, gateway.networking.k8s.io/v1

Cluster-scoped. Says which controller implements Gateways of this class, through `controllerName`. The controller sets `Accepted: True` in its status when it takes the class.

### Deployment envoy-gateway

comes with an add-on, apps/v1, namespace envoy-gateway-system

The Gateway API implementation. It watches GatewayClasses with its `controllerName`, then Gateways and Routes, and runs the Envoy proxies that carry the traffic.

### Gateway public

you write it, gateway.networking.k8s.io/v1, namespace infra

Owned by the platform team. It opens an HTTPS listener on port 443 for `*.example.com`, takes its certificate from the `certs` namespace, and lets routes attach only from namespaces labeled `gateway-access: public`.

### Service envoy-infra-public-…

created by a controller, v1, namespace envoy-gateway-system

Created by Envoy Gateway for this Gateway, with a Deployment of Envoy Pods behind it. It is `type: LoadBalancer`, so the cloud gives it a public address. Other implementations create similar objects under other names.

### ReferenceGrant allow-infra-gateways

you write it, gateway.networking.k8s.io/v1beta1, namespace certs

Lives in `certs`, the namespace being referenced. It allows Gateways in `infra` to read Secrets here. Without it, the listener reports `ResolvedRefs: False` with the reason `RefNotPermitted`.

### Secret wildcard-example-com

you write it, v1, namespace certs

The `*.example.com` certificate, kept in its own namespace so that only a few people can read the private key. cert-manager can issue it with a DNS-01 challenge.

### HTTPRoute orders

you write it, gateway.networking.k8s.io/v1, namespace shop

Owned by the app team in `shop`. It attaches to the `public` Gateway, matches `shop.example.com` with paths under `/api`, and splits requests 90/10 between two Services by weight.

### Namespace shop

you write it, v1

Carries the label `gateway-access: public`, which is what the Gateway's `allowedRoutes` selector checks. Remove the label and every route in the namespace gets detached.

### Service orders-v1

you write it, v1, namespace shop

Selects the Pods of the current version with `app: orders, version: v1`. Gets 90% of the requests.

### Service orders-v2

you write it, v1, namespace shop

Selects the canary Pods with `app: orders, version: v2`. Gets 10% of the requests until you change the weights.

### Pod ×4 orders-v1-…

created by a controller, v1, namespace shop

Pods of the stable version, from a Deployment named `orders-v1`.

### Pod ×1 orders-v2-…

created by a controller, v1, namespace shop

Pods of the canary version, from a Deployment named `orders-v2`. Weights split requests, not Pods, so one Pod can take 10% of the traffic.

## How the resources connect

- Client Browser → Load balancer Cloud load balancer: HTTPS :443 (traffic)
- Load balancer Cloud load balancer → Service envoy-infra-public-…: forwards (traffic)
- GatewayClass eg → Deployment envoy-gateway: controllerName (references)
- Gateway public → GatewayClass eg: gatewayClassName (references)
- Deployment envoy-gateway → Service envoy-infra-public-…: creates (creates)
- Gateway public → Secret wildcard-example-com: certificateRefs (references)
- ReferenceGrant allow-infra-gateways → Gateway public: allows from infra (references)
- ReferenceGrant allow-infra-gateways → Secret wildcard-example-com: allows Secrets (references)
- Gateway public → Namespace shop: allowedRoutes selector (selects)
- HTTPRoute orders → Gateway public: parentRefs (references)
- HTTPRoute orders → Service orders-v1: weight 90 (references)
- HTTPRoute orders → Service orders-v2: weight 10 (references)
- Service orders-v1 → Pod ×4 orders-v1-…: version=v1 (selects)
- Service orders-v2 → Pod ×1 orders-v2-…: version=v2 (selects)
- Service envoy-infra-public-… → Pod ×4 orders-v1-…: 90% of requests (traffic)
- Service envoy-infra-public-… → Pod ×1 orders-v2-…: 10% of requests (traffic)

## How it works

1. **The implementation claims the GatewayClass.** Installing Envoy Gateway gives you its controller and a GatewayClass whose `controllerName` it recognizes. The controller marks the class `Accepted`. Gateways that name another class are left to other controllers.
2. **The platform team creates the Gateway.** The `public` Gateway in `infra` asks for an HTTPS listener on 443. The controller creates an Envoy Deployment and a `LoadBalancer` Service for it, and the cloud gives that Service an address.
3. **A ReferenceGrant lets the Gateway read the certificate.** The certificate lives in `certs`, not `infra`. References across namespaces are denied by default, so the owner of `certs` writes a ReferenceGrant there that allows Gateways from `infra` to read Secrets.
4. **The app team attaches an HTTPRoute.** In `shop`, the HTTPRoute names the Gateway and its `https` listener in `parentRefs`. The Gateway accepts it because the `shop` namespace carries the `gateway-access: public` label. The route's `status.parents` shows `Accepted: True` for that Gateway.
5. **Weights split the traffic.** For each request to `/api`, Envoy picks a backend by weight. 90 out of 100 go to `orders-v1` and 10 to `orders-v2`. Envoy sends them straight to Pod IPs, using each Service's selector to find the endpoints.
6. **Promote the canary.** When v2 looks healthy, change the weights to 50/50, then 0/100, and finally remove `orders-v1` from `backendRefs`. Each change is one small edit to the HTTPRoute, and the app team makes it without touching the Gateway.

## Common mistakes

- An HTTPRoute whose `parentRefs` points at a Gateway that doesn't allow its namespace isn't rejected when you apply it. It just never gets traffic. Check `kubectl get httproute orders -n shop -o yaml` and read `status.parents[].conditions`. `Accepted: False` with the reason `NotAllowedByListeners` means the namespace selector didn't match.
- A ReferenceGrant goes in the namespace being referenced, not in the one making the reference. Writing it in `infra` does nothing.
- Backends in another namespace need a ReferenceGrant too, this time in the Service's namespace with `from: HTTPRoute`.
- `hostnames` on the route must overlap the listener's `hostname`. A route for `shop.example.org` on a `*.example.com` listener is not accepted.
- A weight of 0 sends no traffic but keeps the backend in the route, which is handy for preparing a canary before you send it anything.

## The manifests you write

```yaml
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: public
  namespace: infra
spec:
  gatewayClassName: eg
  listeners:
    - name: https
      protocol: HTTPS
      port: 443
      hostname: "*.example.com"
      tls:
        mode: Terminate
        certificateRefs:
          - kind: Secret
            name: wildcard-example-com
            namespace: certs
      allowedRoutes:
        namespaces:
          from: Selector
          selector:
            matchLabels:
              gateway-access: public
---
apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata:
  name: allow-infra-gateways
  namespace: certs
spec:
  from:
    - group: gateway.networking.k8s.io
      kind: Gateway
      namespace: infra
  to:
    - group: ""
      kind: Secret
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: orders
  namespace: shop
spec:
  parentRefs:
    - name: public
      namespace: infra
      sectionName: https
  hostnames: ["shop.example.com"]
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /api
      backendRefs:
        - name: orders-v1
          port: 8080
          weight: 90
        - name: orders-v2
          port: 8080
          weight: 10
---
apiVersion: v1
kind: Namespace
metadata:
  name: shop
  labels:
    gateway-access: public
---
apiVersion: v1
kind: Service
metadata:
  name: orders-v1
  namespace: shop
spec:
  selector:
    app: orders
    version: v1
  ports:
    - port: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: orders-v2
  namespace: shop
spec:
  selector:
    app: orders
    version: v2
  ports:
    - port: 8080
```
