Lab 15 – Gateway API

Lab 15 – Gateway API

Cíl: nainstalovat Gateway controller a nasměrovat dvě aplikace podle jména hosta přes jeden port – a pak rozdělit provoz mezi dvě verze. Kapitola: Gateway API

Objekty Gateway API jsou jen data. Nejdřív musí existovat dvě věci: CRD (nejsou součástí Kubernetes) a controller, který je čte.

Instalace CRD

# 1 – standardní kanál: GatewayClass, Gateway, HTTPRoute
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.3.0/standard-install.yaml
kubectl get crd | grep gateway.networking
kubectl api-resources --api-group=gateway.networking.k8s.io

Právě jste rozšířili API server o nové druhy objektů – přesně ten mechanismus CRD, který probíráme třetí den, tady potkaný v praxi. Na stránce vydání zkontrolujte, jestli není novější verze.

# 2 – explain na nich funguje okamžitě
kubectl explain gateway.spec.listeners
kubectl explain httproute.spec.rules.backendRefs

Instalace controlleru

# 3 – NGINX Gateway Fabric. Datová rovina tu nemá cloudový LoadBalancer, takže
#     si vyžádáme NodePort a připíchneme port 30080 k listeneru na portu 80.
helm install ngf oci://ghcr.io/nginx/charts/nginx-gateway-fabric \
  --create-namespace -n nginx-gateway \
  --set nginx.service.type=NodePort \
  --set nginx.service.nodePorts[0].port=30080 \
  --set nginx.service.nodePorts[0].listenerPort=80
kubectl get pods -n nginx-gateway -w        # Ctrl-C až bude Running

Pokud helm chybí:

curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
chmod +x get_helm.sh && sudo ./get_helm.sh && helm version

nodePorts je seznam dvojic {port, listenerPort}: „vystav node port 30080 pro listener Gateway na portu 80“. Dvojice, jejíž listenerPort neodpovídá žádnému listeneru, se tiše ignoruje – když tedy Gateway později přepnete na port 8080, tenhle záznam přestane platit a Kubernetes přidělí náhodný port.

# 4 – co tím vzniklo?
kubectl get gatewayclass
kubectl get all -n nginx-gateway

Dvě věci k povšimnutí, protože v tomhle labu způsobují nejvíc zmatku:

  • GatewayClass se jménem nginx je to, na co bude ukazovat vaše Gateway.
  • Jediná Service tady je ngf-nginx-gateway-fabric a je ClusterIP – a je to tak správně. Tohle je řídicí rovina: pod s controllerem, který přes gRPC mluví s NGINX agenty. Není to proxy a váš provoz nikdy neobsluhuje.

Zatím neexistuje žádný pod s proxy ani žádná NodePort Service.

Varování

Datová rovina neexistuje, dokud neexistuje Gateway, a nevzniká v tomhle namespace. NGINX Gateway Fabric 2.x vytváří jeden NGINX Deployment a jednu Service na každou Gateway, ve vlastním namespace té Gateway. Po kroku 6 je tedy najdete v default, ne v nginx-gateway.

Když teď budete v nginx-gateway hledat NodePort Service, najdete jen ClusterIP řídicí roviny a usoudíte, že --set nginx.service.type nic neudělal. Udělal – jen se díváte na špatné místo ve špatnou chvíli.

Dvě aplikace

# 5 –
cd ~
kubectl create deploy shop --image nginx:1.27
kubectl create deploy wiki --image httpd:2.4
kubectl expose deploy shop --port 80
kubectl expose deploy wiki --port 80
kubectl get svc shop wiki

Obě jsou ClusterIP. Zvenčí na ně nikdo nedosáhne a ani po tomhle labu nebudou dostupné jinak než přes Gateway.

Gateway

# 6 –
cat > gateway.yaml <<'YAML'
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: web-gw
spec:
  gatewayClassName: nginx
  listeners:
    - name: http
      protocol: HTTP
      port: 80
      allowedRoutes:
        namespaces:
          from: All
YAML
kubectl apply -f gateway.yaml
kubectl get gateway web-gw
kubectl describe gateway web-gw | tail -20

Sledujte PROGRAMMED. True znamená, že controller Gateway přijal a postavil pro ni datovou rovinu; False znamená přečíst si podmínky v describe. Ingress nic takového neměl – prostě tiše nedělal nic.

# 7 – TEĎ datová rovina existuje – v namespace té Gateway
kubectl get pods,svc -n default -l gateway.networking.k8s.io/gateway-name=web-gw
kubectl get svc -A | grep -i web-gw

V default se objevil NGINX Deployment a Service pojmenovaná podle Gateway, a ta Service je NodePort s portem 30080. Porovnejte s nginx-gateway, kde je pořád jen ClusterIP řídicí roviny:

# 8 – obě poloviny vedle sebe
kubectl get svc -n nginx-gateway           # ClusterIP – řídicí rovina
kubectl get svc -n default | grep web-gw   # NodePort – datová rovina, váš provoz
# 9 – port si přečtěte z clusteru, nespoléhejte na připíchnutí
PORT=$(kubectl get svc -n default -l gateway.networking.k8s.io/gateway-name=web-gw \
  -o jsonpath='{.items[0].spec.ports[?(@.port==80)].nodePort}')
echo "node port brány: $PORT"

Vždycky si ho přečtěte zpátky. Pokud se připíchnutí neuplatnilo – jiný port listeneru, starší chart, překlep v --set$PORT vám i tak dá skutečnou hodnotu a všechny curl níže budou fungovat.

Tip

Pokud je Service přesto ClusterIP, dá se to spravit bez reinstalace:

SVC=$(kubectl get svc -n default -l gateway.networking.k8s.io/gateway-name=web-gw -o name)
kubectl patch $SVC -n default -p '{"spec":{"type":"NodePort"}}'
kubectl patch $SVC -n default --type=json \
  -p '[{"op":"replace","path":"/spec/ports/0/nodePort","value":30080}]'

Je to ruční zásah do objektu, který vlastní controller, takže to berte jako náhradní řešení: správná oprava je helm upgrade se správnými hodnotami, protože controller vám ten patch může přepsat.

Routy

# 10 – jedna routa na aplikaci, vlastní ji aplikační tým
cat > routes.yaml <<'YAML'
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: shop
spec:
  parentRefs:
    - name: web-gw
  hostnames: ["shop.k8s.lab"]
  rules:
    - matches:
        - path: { type: PathPrefix, value: / }
      backendRefs:
        - name: shop
          port: 80
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: wiki
spec:
  parentRefs:
    - name: web-gw
  hostnames: ["wiki.k8s.lab"]
  rules:
    - matches:
        - path: { type: PathPrefix, value: / }
      backendRefs:
        - name: wiki
          port: 80
YAML
kubectl apply -f routes.yaml
kubectl get httproute
kubectl describe httproute shop | tail -15

Každá routa si svou Gateway pojmenuje v parentRefs; Gateway nevyjmenovává nikoho. Přidat aplikaci znamená přidat routu, nikdy editovat sdílenou konfiguraci.

# 11 – test: stejný port, jiná hlavička Host
curl -s -H 'Host: shop.k8s.lab' http://worker01:$PORT | grep -o '<title>.*</title>'
curl -s -H 'Host: wiki.k8s.lab' http://worker01:$PORT | head -3
curl -s -o /dev/null -w '%{http_code}\n' -H 'Host: nothing.k8s.lab' http://worker01:$PORT

Jeden port, dvě aplikace, směrování podle jména. To 404 vrací sama proxy – požadavek dostala a neměla pro něj routu.

Rozdělení provozu

Tohle Ingress bez anotací výrobce vyjádřit neuměl.

# 12 – druhá verze e-shopu
kubectl create deploy shop-v2 --image httpd:2.4
kubectl expose deploy shop-v2 --port 80

kubectl patch httproute shop --type merge -p '{"spec":{"rules":[{"matches":[{"path":{"type":"PathPrefix","value":"/"}}],"backendRefs":[{"name":"shop","port":80,"weight":90},{"name":"shop-v2","port":80,"weight":10}]}]}}'

for i in $(seq 1 20); do
  curl -s -H 'Host: shop.k8s.lab' http://worker01:$PORT | grep -o -E 'nginx|It works'
done | sort | uniq -c

Zhruba 18 ku 2. Změňte váhy a spusťte znovu – to je canary release v jednom poli jednoho objektu.

Objevovací část

Infodiscovery

Úkol A. Nasměrujte http://api.k8s.lab/wiki na Service wiki, zatímco http://api.k8s.lab/ vrátí 404.

kubectl patch httproute wiki --type merge -p '{"spec":{"hostnames":["wiki.k8s.lab","api.k8s.lab"],"rules":[{"matches":[{"path":{"type":"PathPrefix","value":"/wiki"}}],"backendRefs":[{"name":"wiki","port":80}]}]}}'
curl -s -H 'Host: api.k8s.lab' http://worker01:$PORT/wiki | head -3
curl -s -o /dev/null -w '%{http_code}\n' -H 'Host: api.k8s.lab' http://worker01:$PORT/

Backend dostane cestu /wiki a httpd na ni vrátí 404 – vyřeší to filtr URLRewrite, který je tady typované pole, ne anotace.

Úkol B. Pošlete požadavky s hlavičkou x-version: beta na shop-v2 a všechny ostatní na shop. Shodu podle hlavičky Ingress nikdy neuměl.

kubectl patch httproute shop --type merge -p '{"spec":{"rules":[{"matches":[{"headers":[{"name":"x-version","value":"beta"}]}],"backendRefs":[{"name":"shop-v2","port":80}]},{"matches":[{"path":{"type":"PathPrefix","value":"/"}}],"backendRefs":[{"name":"shop","port":80}]}]}}'
curl -s -H 'Host: shop.k8s.lab' -H 'x-version: beta' http://worker01:$PORT | grep -o 'It works'
curl -s -H 'Host: shop.k8s.lab' http://worker01:$PORT | grep -o '<title>.*</title>'

Vyhrává konkrétnější pravidlo; při shodě rozhoduje pořadí v seznamu.

Úkol C. Zařiďte, aby jména fungovala ze support01 bez hlavičky Host:.

echo "$(getent hosts worker01 | awk '{print $1}') shop.k8s.lab wiki.k8s.lab api.k8s.lab" \
  | sudo tee -a /etc/hosts
curl -s http://shop.k8s.lab:$PORT | grep -o '<title>.*</title>'

V produkci je to DNS ukazující na load balancer před uzly.

Úkol D. Routa nefunguje. Zjistěte jen pomocí kubectl, jestli ji Gateway přijala, na které Services ukazuje a ve kterém namespace běží její datová rovina.

kubectl describe httproute shop | tail -20        # Parents / podmínky: Accepted, ResolvedRefs
kubectl get httproute shop -o jsonpath='{range .spec.rules[*].backendRefs[*]}{.name}{":"}{.port}{" "}{end}{"\n"}'
kubectl get gateway web-gw -o yaml | grep -A10 conditions
kubectl get pods,svc -A -l gateway.networking.k8s.io/gateway-name=web-gw
kubectl logs -n nginx-gateway -l app.kubernetes.io/name=nginx-gateway-fabric --tail=20

ResolvedRefs: False znamená, že backendová Service neexistuje nebo má jiný port – stav vám to řekne, což je praktická výhoda oproti Ingressu. Poslední dva příkazy oddělují obě poloviny: datová rovina v namespace Gateway, logy controlleru v nginx-gateway.

Úklid

kubectl delete -f routes.yaml -f gateway.yaml
kubectl delete deploy shop wiki shop-v2
kubectl delete svc shop wiki shop-v2
sudo sed -i '/k8s.lab/d' /etc/hosts

# smazáním Gateway zmizela i její datová rovina – ověřte
kubectl get pods,svc -n default | grep web-gw || echo "datová rovina je pryč"

Smazáním Gateway zmizel i NGINX Deployment a Service: controller je vlastní, přesně jako ReplicaSet vlastní své pody.

Controller i CRD nechte nainstalované – lab 23 je používá.