Lab 23 – Aplikace od začátku do konce

Lab 23 – Aplikace od začátku do konce

Infodiscovery volitelný / zbude-li čas

Cíl: postavit, provozovat, rozbít a opravit kompletní aplikaci se vším ze tří dnů.

Tenhle lab říká co, ne jak. Každý příkaz je v tahácích, v kubectl explain nebo pod --help.

Tip

Každá etapa končí boxem s řešením. Nejdřív ji zkuste sami; box otevřete, když se zaseknete nebo si chcete své řešení porovnat s funkčním. Každý box je samostatný, takže se můžete připojit na začátku libovolné etapy, i když jste předchozí neudělali po svém. Pokud jste ztracení úplně, skočte dolů na Dohnat to jedním vložením.

Postavte to

  1. Vytvořte namespace shop a nastavte ho jako výchozí.
  2. Vytvořte ConfigMapu web-content s index.html obsahujícím Shop v1.
  3. Vytvořte Deployment shop-web, image nginx:1.27, 3 repliky, a k tomu:
    • CPU request 50m, paměťový request 64Mi, paměťový limit 128Mi
    • ConfigMapu připojenou na /usr/share/nginx/html
    • readiness probe na /
  4. Vystavte to ClusterIP Service.
  5. Publikujte to přes Gateway pomocí HTTPRoute pro shop.k8s.lab. (Nezapomeňte, že datová rovina Service žije v namespace té Gateway.)
  6. Ověřte ze support01 – curl -H 'Host: shop.k8s.lab' http://worker01:<nodeport-brány> vrátí Shop v1.
# 1 – namespace a nastavení jako výchozí pro tento kontext
cd ~
kubectl create namespace shop
kubectl config set-context --current --namespace=shop

# 2 – obsah ze souboru, ať se klíč jmenuje index.html
echo '<h1>Shop v1</h1>' > index.html
kubectl create configmap web-content --from-file=index.html

# 3 – vygenerujte kostru Deploymentu a upravte ji
kubectl create deploy shop-web --image nginx:1.27 --replicas 3 \
  --dry-run=client -o yaml > shop-web.yaml

V shop-web.yaml nahraďte blok spec.template.spec tak, aby kontejner měl zdroje, probe a připojení – celý soubor má vypadat takhle:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: shop-web
  labels:
    app: shop-web
spec:
  replicas: 3
  selector:
    matchLabels:
      app: shop-web
  template:
    metadata:
      labels:
        app: shop-web
    spec:
      containers:
        - name: nginx
          image: nginx:1.27
          ports:
            - containerPort: 80
          resources:
            requests:
              cpu: "50m"
              memory: "64Mi"
            limits:
              memory: "128Mi"
          readinessProbe:
            httpGet:
              path: /
              port: 80
            initialDelaySeconds: 3
            periodSeconds: 5
          volumeMounts:
            - name: content
              mountPath: /usr/share/nginx/html
      volumes:
        - name: content
          configMap:
            name: web-content
kubectl apply -f shop-web.yaml
kubectl get pods -l app=shop-web        # počkejte na 3/3 READY

# 4 – ClusterIP service
kubectl expose deploy shop-web --port 80 --name shop-svc
kubectl get endpointslices -l kubernetes.io/service-name=shop-svc   # 3 adresy

# 5 – Gateway v TOMTO namespace a k tomu routa
cat > gateway.yaml <<'YAML'
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: shop-gw
spec:
  gatewayClassName: nginx
  listeners:
    - name: http
      protocol: HTTP
      port: 80
      allowedRoutes:
        namespaces:
          from: All
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: shop
spec:
  parentRefs:
    - name: shop-gw
  hostnames: ["shop.k8s.lab"]
  rules:
    - matches:
        - path: { type: PathPrefix, value: / }
      backendRefs:
        - name: shop-svc
          port: 80
YAML
kubectl apply -f gateway.yaml
kubectl get gateway shop-gw             # PROGRAMMED se má změnit na True

# 6 – datová rovina je v TOMTO namespace, pojmenovaná podle Gateway
kubectl get svc -n shop -l gateway.networking.k8s.io/gateway-name=shop-gw
PORT=$(kubectl get svc -n shop -l gateway.networking.k8s.io/gateway-name=shop-gw \
  -o jsonpath='{.items[0].spec.ports[?(@.port==80)].nodePort}')
echo "gateway node port: $PORT"
curl -s -H 'Host: shop.k8s.lab' http://worker01:$PORT

Očekávaný výsledek: <h1>Shop v1</h1>.

Když to nefunguje, v tomhle pořadí: kubectl get pods (jsou Ready? padající readiness probe je drží mimo Service), kubectl get endpointslices -l kubernetes.io/service-name=shop-svc (prázdné = neshoda labelů), kubectl describe httproute shop (hledejte ResolvedRefs).

Provozujte to

  1. Naškálujte na 5 replik a ověřte rozprostření po workerech.
  2. Změňte obsah na Shop v2 a udělejte změnu viditelnou bez ručního mazání podů.
  3. Povyšte image na nginx:1.28 bez výpadku. Doložte to smyčkou požadavků běžící během rolloutu.
  4. Vraťte se na předchozí revizi a ukažte historii.
# 7 – naškálujte a zkontrolujte umístění
kubectl scale deploy shop-web --replicas=5
kubectl get pods -l app=shop-web -o wide | sort -k7
# 8 – upravte ConfigMapu; PŘIPOJENÝ SOUBOR se obnoví sám
echo '<h1>Shop v2</h1>' > index.html
kubectl create configmap web-content --from-file=index.html \
  --dry-run=client -o yaml | kubectl apply -f -

sleep 70                                 # interval synchronizace kubeletu, až ~1 minuta
curl -s -H 'Host: shop.k8s.lab' http://worker01:$PORT     # Shop v2

O tohle v úkolu jde: ConfigMapa je připojená jako volume, takže se soubor obnoví sám a restart není potřeba. Kdybyste obsah vložili jako proměnnou prostředí, nezměnilo by se nic a museli byste použít kubectl rollout restart deploy/shop-web – což je tady taky přijatelná odpověď, jen hrubší nástroj.

# 9 – v DRUHÉM terminálu nejdřív spusťte smyčku
while true; do
  curl -s --max-time 1 -H 'Host: shop.k8s.lab' http://worker01:$PORT \
    | grep -o 'Shop v[0-9]' || echo FAIL
  sleep 0.2
done
# zpět v prvním terminálu
kubectl set image deploy/shop-web nginx=nginx:1.28
kubectl rollout status deploy/shop-web
kubectl get rs -l app=shop-web           # starý na 0, nový na 5

Smyčka nesmí vypsat FAIL. Ukončete ji Ctrl-C.

# 10 – návrat zpět
kubectl rollout undo deploy/shop-web
kubectl rollout status deploy/shop-web
kubectl rollout history deploy/shop-web
kubectl get pods -l app=shop-web -o jsonpath='{.items[0].spec.containers[0].image}{"\n"}'

Přidejte stav

  1. Dejte aplikaci PersistentVolumeClaim připojený na /data ve všech podech. Musí být sdílený všemi replikami.
  2. Zapište soubor do /data z jednoho podu a přečtěte ho z jiného.

Pět replik na třech uzlech znamená, že volume musí být ReadWriteMany. Dvě cesty – použijte tu, která odpovídá tomu, co jste postavili v labu 18.

Se StorageClass přes CSI (nejjednodušší, pokud jste driver nainstalovali):

cat > shop-pvc.yaml <<'YAML'
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: shop-data
spec:
  accessModes: ["ReadWriteMany"]
  resources:
    requests:
      storage: 300Mi
  storageClassName: nfs-csi
YAML
kubectl apply -f shop-pvc.yaml
kubectl get pvc shop-data                # Bound

Se statickým NFS PV (bez driveru): vezměte si místo toho pv-storage1/2 – jsou ReadWriteMany a nemají třídu.

kubectl get pv                           # najděte Available
cat > shop-pvc.yaml <<'YAML'
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: shop-data
spec:
  accessModes: ["ReadWriteMany"]
  resources:
    requests:
      storage: 300Mi
  storageClassName: ""
YAML
kubectl apply -f shop-pvc.yaml
kubectl get pvc shop-data

Pokud zůstane Pending, důvod řekne kubectl describe pvc shop-data – obvykle zbylý claimRef na PV: kubectl patch pv <pv> -p '{"spec":{"claimRef":null}}'.

Teď ho připojte do všech podů:

kubectl patch deploy shop-web --type merge -p '{
  "spec":{"template":{"spec":{
    "containers":[{"name":"nginx","volumeMounts":[
      {"name":"content","mountPath":"/usr/share/nginx/html"},
      {"name":"data","mountPath":"/data"}]}],
    "volumes":[
      {"name":"content","configMap":{"name":"web-content"}},
      {"name":"data","persistentVolumeClaim":{"claimName":"shop-data"}}]}}}}'
kubectl rollout status deploy/shop-web

Strategic merge patch nahrazuje celé seznamy, a proto se opakují oba volumes i oba mounty – vynechání content by tiše odpojilo váš web.

# 12 – zapište z jednoho podu, přečtěte z druhého
PODS=($(kubectl get pods -l app=shop-web -o name))
kubectl exec ${PODS[0]} -- sh -c 'echo "written by $(hostname)" > /data/shared.txt'
kubectl exec ${PODS[1]} -- cat /data/shared.txt
kubectl get pods -l app=shop-web -o wide | head -3    # ověřte, že jde o různé uzly

Rozbijte to a opravte

  1. Nastavte image na nginx:nosuchtag. Odpovězte jen z výstupu kubectl: běží web dál, a proč? Který ReplicaSet je zaseknutý a co říká jeho událost?
  2. Uveďte to do pořádku.
  3. Změňte selektor Service tak, aby neodpovídal ničemu. Doložte poruchu přes endpointy a opravte ji.
  4. Zakordonujte uzel s většinou podů, vyprázdněte ho a ukažte, že aplikace byla celou dobu dostupná.
# 13 – rozbijte image
kubectl set image deploy/shop-web nginx=nginx:nosuchtag
kubectl rollout status deploy/shop-web --timeout=30s     # nikdy nedoběhne
kubectl get pods -l app=shop-web                         # 5 běžících + nové selhávají
kubectl get rs -l app=shop-web
curl -s -H 'Host: shop.k8s.lab' http://worker01:$PORT    # pořád Shop v2

Odpovědi: web běží, protože maxUnavailable (výchozí 25 %) zakazuje odstranit zdravé pody, dokud nejsou náhrady Ready – a ty Ready nikdy nebudou. Zaseknutý je nový ReplicaSet, ten s nenulovým DESIRED a nulovým READY:

kubectl describe rs -l app=shop-web | grep -A5 Events
kubectl describe pod -l app=shop-web | grep -A5 'Failed'

Failed to pull image ... manifest unknown.

# 14 – uveďte to do pořádku
kubectl rollout undo deploy/shop-web
kubectl rollout status deploy/shop-web
kubectl get pods -l app=shop-web
# 15 – rozbijte selektor
kubectl patch svc shop-svc -p '{"spec":{"selector":{"app":"typo"}}}'
kubectl get endpointslices -l kubernetes.io/service-name=shop-svc    # žádné adresy
curl -s --max-time 3 -H 'Host: shop.k8s.lab' http://worker01:$PORT   # 502/timeout
kubectl describe svc shop-svc | grep -i selector

# oprava
kubectl patch svc shop-svc -p '{"spec":{"selector":{"app":"shop-web"}}}'
kubectl get endpointslices -l kubernetes.io/service-name=shop-svc    # adresy jsou zpět
curl -s -H 'Host: shop.k8s.lab' http://worker01:$PORT
# 16 – vyprázdněte nejvytíženější uzel, smyčka běží v druhém terminálu
kubectl get pods -l app=shop-web -o wide | sort -k7        # vyberte nejvytíženější
NODE=worker01                                              # upravte podle toho, co jste viděli
kubectl cordon $NODE
kubectl drain $NODE --ignore-daemonsets --delete-emptydir-data
kubectl get pods -l app=shop-web -o wide                   # přeplánováno jinam
kubectl uncordon $NODE
kubectl rollout restart deploy/shop-web                    # rozprostření

Smyčka nemá vypsat FAIL – pět replik na třech uzlech znamená, že vyprázdnění jednoho je nikdy neodstraní všechny. Pokud selhala, nejspíš jste vyprázdnili uzel s podem datové roviny Gateway: ten má jednu repliku, takže se přesunul samotný vstupní bod. Stojí to za povšimnutí – vysoce dostupná aplikace potřebuje i vysoce dostupnou vstupní cestu.

Zabalte to

  1. Udělejte z celku Helm chart s hodnotami pro počet replik, text a hostname.
  2. Nainstalujte ho dvakrát, pod dvěma jmény release a na dvou hostnames.
cd ~
helm create shopchart
cd shopchart
rm -f templates/hpa.yaml templates/ingress.yaml templates/serviceaccount.yaml
rm -rf templates/tests

values.yaml:

replicaCount: 3

image:
  repository: nginx
  tag: "1.27"
  pullPolicy: IfNotPresent

service:
  port: 80

content:
  message: "Shop from a chart"

host: shop.k8s.lab

serviceAccount:
  create: false

templates/configmap.yaml:

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ include "shopchart.fullname" . }}-content
data:
  index.html: |
    <h1>{{ .Values.content.message }}</h1>

templates/httproute.yaml:

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: {{ include "shopchart.fullname" . }}
spec:
  parentRefs:
    - name: shop-gw
  hostnames: ["{{ .Values.host }}"]
  rules:
    - matches:
        - path: { type: PathPrefix, value: / }
      backendRefs:
        - name: {{ include "shopchart.fullname" . }}
          port: {{ .Values.service.port }}

V templates/deployment.yaml připojte ConfigMapu na /usr/share/nginx/html přesně jako ručně, jako jméno ConfigMapy použijte {{ include "shopchart.fullname" . }}-content.

helm lint .
helm template test . | less              # přečtěte si to před instalací
# 18 – dvě release, dvě hostnames, jeden chart
cd ~
helm install shop1 ./shopchart -n shop --set host=shop1.k8s.lab
helm install shop2 ./shopchart -n shop \
  --set host=shop2.k8s.lab \
  --set content.message="Second shop" \
  --set replicaCount=1

helm list -n shop
kubectl get deploy,svc,httproute -n shop
curl -s -H 'Host: shop1.k8s.lab' http://worker01:$PORT
curl -s -H 'Host: shop2.k8s.lab' http://worker01:$PORT

Obě release existují vedle sebe, protože každý objekt je pojmenovaný podle release – právě to vám include "shopchart.fullname" dává.

Úklid

  1. Odstraňte obě release, namespace, PVC a uvolněte PV.
helm uninstall shop1 shop2 -n shop
kubectl delete -f ~/gateway.yaml -n shop --ignore-not-found
kubectl delete namespace shop            # vezme s sebou všechno ostatní

# PV je cluster-scoped a namespace přežije
kubectl get pv
kubectl patch pv <pv-name> -p '{"spec":{"claimRef":null}}'   # pokud je Released
kubectl config set-context --current --namespace=default
rm -rf ~/shopchart ~/shop-web.yaml ~/gateway.yaml ~/shop-pvc.yaml ~/index.html

Dohnat to jedním vložením

Ztratili jste nit úplně, nebo chcete přeskočit na pozdější etapu? Tohle postaví všechno až do konce etapy Provozujte to (kroky 1–10) najednou. Vložte, počkejte a pokračujte od Přidejte stav.

cd ~
kubectl create namespace shop --dry-run=client -o yaml | kubectl apply -f -
kubectl config set-context --current --namespace=shop
echo '<h1>Shop v2</h1>' > index.html
kubectl create configmap web-content --from-file=index.html \
  --dry-run=client -o yaml | kubectl apply -f -

kubectl apply -f - <<'YAML'
apiVersion: apps/v1
kind: Deployment
metadata:
  name: shop-web
  labels: { app: shop-web }
spec:
  replicas: 5
  selector:
    matchLabels: { app: shop-web }
  template:
    metadata:
      labels: { app: shop-web }
    spec:
      containers:
        - name: nginx
          image: nginx:1.27
          ports:
            - containerPort: 80
          resources:
            requests: { cpu: "50m", memory: "64Mi" }
            limits:   { memory: "128Mi" }
          readinessProbe:
            httpGet: { path: /, port: 80 }
            initialDelaySeconds: 3
            periodSeconds: 5
          volumeMounts:
            - name: content
              mountPath: /usr/share/nginx/html
      volumes:
        - name: content
          configMap: { name: web-content }
---
apiVersion: v1
kind: Service
metadata:
  name: shop-svc
spec:
  selector: { app: shop-web }
  ports:
    - port: 80
      targetPort: 80
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: shop-gw
spec:
  gatewayClassName: nginx
  listeners:
    - name: http
      protocol: HTTP
      port: 80
      allowedRoutes:
        namespaces: { from: All }
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: shop
spec:
  parentRefs:
    - name: shop-gw
  hostnames: ["shop.k8s.lab"]
  rules:
    - matches:
        - path: { type: PathPrefix, value: / }
      backendRefs:
        - name: shop-svc
          port: 80
YAML

kubectl rollout status deploy/shop-web
PORT=$(kubectl get svc -n shop -l gateway.networking.k8s.io/gateway-name=shop-gw \
  -o jsonpath='{.items[0].spec.ports[?(@.port==80)].nodePort}')
echo "gateway node port: $PORT"
curl -s -H 'Host: shop.k8s.lab' http://worker01:$PORT
Tip

Když se kdekoli zaseknete, odpověď je skoro vždy: kubectl describe daný objekt a přečíst si Events.