Frontend Labs
⋯

25. Traffic in: Gateway API, TLS, and cert-manager

25.1 Ingress → Gateway API

The Ingress API is frozen. Its common controller, ingress-nginx, was retired by Kubernetes SIG Network in March 2026 (no further releases or security fixes). Annotations-for-everything also made Ingress unportable. Gateway API is the successor: typed, role-oriented, and supported by Envoy Gateway, Istio, Cilium, NGINX Gateway Fabric, Traefik, Kong, and the cloud LBs (GKE, AWS LB Controller, Azure).

GatewayClass   (infra provider: "use Envoy Gateway")             owned by: platform team
   └─ Gateway  (listeners: :443 for *.orbit.example, TLS cert)    owned by: platform team
        └─ HTTPRoute (orbit.example/api/orders -> svc orders:80)  owned by: app team, in its namespace

⚠️ Caveat (verify): confirm the ingress-nginx retirement status and dates on the Kubernetes blog (kubernetes.io/blog) before citing them. The direction (move to Gateway API) is well established; the exact dates are what to check.

25.2 Install Envoy Gateway and cert-manager

helm upgrade --install eg oci://docker.io/envoyproxy/gateway-helm \
  -n envoy-gateway-system --create-namespace --wait
helm repo add jetstack https://charts.jetstack.io
helm upgrade --install cert-manager jetstack/cert-manager -n cert-manager --create-namespace \
  --set crds.enabled=true --set config.enableGatewayAPI=true --wait
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata: { name: eg }
spec: { controllerName: gateway.envoyproxy.io/gatewayclass-controller }
---
# Lab: a self-signed CA. Production: ACME/Let's Encrypt (below) or your corporate CA.
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata: { name: selfsigned }
spec: { selfSigned: {} }
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: public
  namespace: envoy-gateway-system
  annotations: { cert-manager.io/cluster-issuer: selfsigned }   # cert-manager creates the Secret
spec:
  gatewayClassName: eg
  listeners:
    - name: https
      protocol: HTTPS
      port: 443
      hostname: "orbit.localtest.me"          # *.localtest.me resolves to 127.0.0.1
      tls: { mode: Terminate, certificateRefs: [{ name: orbit-tls }] }
      allowedRoutes: { namespaces: { from: Selector, selector: { matchLabels: { gateway-access: "true" } } } }
    - name: http
      protocol: HTTP
      port: 80
      allowedRoutes: { namespaces: { from: Same } }
---
# HTTP -> HTTPS redirect
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata: { name: https-redirect, namespace: envoy-gateway-system }
spec:
  parentRefs: [{ name: public, sectionName: http }]
  rules: [{ filters: [{ type: RequestRedirect, requestRedirect: { scheme: https, statusCode: 301 } }] }]

Production issuer:

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata: { name: letsencrypt }
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: platform@acme.example
    privateKeySecretRef: { name: letsencrypt-account }
    solvers:
      - http01: { gatewayHTTPRoute: { parentRefs: [{ name: public, namespace: envoy-gateway-system, kind: Gateway }] } }

⚠️ Caveat (verify): pin --version for both charts. The config.enableGatewayAPI value and the Gateway-annotation certificate flow depend on the cert-manager version. Check its Gateway API docs page; on older releases it was a feature-gate flag. On kind, the Gateway gets an address only while cloud-provider-kind is running.

25.3 Routes owned by the app team

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata: { name: orbit-api, namespace: orbit }     # namespace labeled gateway-access=true
spec:
  parentRefs: [{ name: public, namespace: envoy-gateway-system, sectionName: https }]
  hostnames: ["orbit.localtest.me"]
  rules:
    - matches: [{ path: { type: PathPrefix, value: /api/products } }]
      filters:
        - type: URLRewrite
          urlRewrite: { path: { type: ReplacePrefixMatch, replacePrefixMatch: /products } }
      backendRefs: [{ name: catalog, port: 80 }]
    - matches: [{ path: { type: PathPrefix, value: /api/orders }, method: POST }]
      filters:
        - type: URLRewrite
          urlRewrite: { path: { type: ReplacePrefixMatch, replacePrefixMatch: /orders } }
      timeouts: { request: 5s }
      backendRefs: [{ name: orders, port: 80 }]
kubectl label namespace orbit gateway-access=true
kubectl get gateway -n envoy-gateway-system public        # PROGRAMMED=True, ADDRESS from cloud-provider-kind
GW=$(kubectl get gateway -n envoy-gateway-system public -o jsonpath='{.status.addresses[0].value}')
curl -sk --resolve orbit.localtest.me:443:$GW https://orbit.localtest.me/api/products | jq

Traffic splitting (the foundation for canaries, §37) is just weights:

backendRefs:
  - { name: orders,        port: 80, weight: 90 }
  - { name: orders-canary, port: 80, weight: 10 }

25.4 Edge resilience policies (Envoy Gateway)

apiVersion: gateway.envoyproxy.io/v1alpha1
kind: BackendTrafficPolicy
metadata: { name: orders-resilience, namespace: orbit }
spec:
  targetRefs: [{ group: gateway.networking.k8s.io, kind: HTTPRoute, name: orbit-api }]
  retry:
    numRetries: 2
    retryOn: { triggers: [connect-failure, retriable-status-codes], httpStatusCodes: [503] }
    perRetry: { backOff: { baseInterval: 100ms, maxInterval: 1s } }
  circuitBreaker: { maxPendingRequests: 1024, maxParallelRequests: 1024 }
  rateLimit:
    type: Local
    local: { rules: [{ limit: { requests: 200, unit: Second } }] }

Retrying POST /orders at the edge is safe only because the API requires an Idempotency-Key. That's the payoff of designing for it in §11.

⚠️ Caveat (verify): Envoy Gateway's BackendTrafficPolicy is v1alpha1, and its field names (retry, circuitBreaker, rateLimit) have moved between releases. Validate against the installed CRDs: kubectl apply --dry-run=server -f policy.yaml. Local rate limiting may need extra configuration depending on the version.


Generated from real-life-k8s-guide.md · 8 Oct 2026 16:36