Tens els conceptes i un clúster kind amb un Redis solt. Ara va la plataforma sencera: els quatre serveis d'Aurora Libros amb manifestos reals, les sondes de 06-01, l'enduriment de 05-03 i la imatge que va publicar el teu pipeline a 06-02. Al final, /llibres retornarà els nou títols des de dins del clúster.

Contingut

  1. Organització de k8s/ i el Namespace
  2. aurora-cache: Deployment i Service
  3. Per què aurora-db és un StatefulSet
  4. El StatefulSet amb volumeClaimTemplates
  5. Service headless i el ConfigMap amb init.sql
  6. aurora-api: imatge per digest i imagePullSecrets
  7. Les tres sondes al manifest
  8. requests, limits i les classes de QoS
  9. securityContext de Pod i de contenidor
  10. Configuració amb ConfigMap i Secret
  11. aurora-web: Deployment, Service i Ingress
  12. TLS amb cert-manager
  13. Kustomize: bases i overlays per entorn
  14. Posada en marxa i verificació
  15. Depuració: els estats d'un Pod
  16. Helm com a alternativa

  1. Organització de k8s/ i el Namespace

k8s/base/{namespace,config,cache,db,api,web}.yaml + kustomization.yaml
k8s/overlays/{staging,produccio}/kustomization.yaml

Un fitxer per servei, amb tots els seus objectes junts separats per ---. És més pràctic que un fitxer per objecte: per entendre aurora-api obres un únic fitxer i hi veus el seu Deployment, el seu Service i la seva configuració. El primer és el Namespace, un objecte de cinc línies (apiVersion: v1, kind: Namespace, metadata.name: aurora) que crea la partició on viurà tota la resta.

  1. aurora-cache: Deployment i Service

# k8s/base/cache.yaml
apiVersion: apps/v1
kind: Deployment
metadata: { name: aurora-cache }
spec:
  replicas: 1
  selector: { matchLabels: { app.kubernetes.io/name: aurora-cache } }
  template:
    metadata: { labels: { app.kubernetes.io/name: aurora-cache } }
    spec:
      containers:
        - name: redis
          image: redis:7-alpine
          args: ["--maxmemory","200mb","--maxmemory-policy","allkeys-lru"]
          ports: [{ containerPort: 6379, name: redis }]
          resources:
            requests: { memory: 64Mi, cpu: 50m }
            limits:   { memory: 256Mi, cpu: 500m }
          livenessProbe:  { tcpSocket: { port: redis }, initialDelaySeconds: 5 }
          readinessProbe: { exec: { command: ["redis-cli","ping"] }, periodSeconds: 5 }
---
apiVersion: v1
kind: Service
metadata: { name: aurora-cache }
spec:
  selector: { app.kubernetes.io/name: aurora-cache }
  ports: [{ port: 6379, targetPort: redis }]

Dos patrons que es repetiran a tots els manifestos. El primer: els ports es nomenen (name: redis) i després es referencien pel nom al Service i a les sondes; si algun dia canvies el número, el canvies en un sol lloc. El segon: l'etiqueta app.kubernetes.io/name és l'única que apareix al selector, i la resta en queden fora, perquè el selector és immutable i no convé lligar-lo a res que hagi de canviar. I fixa't que Redis aquí és una memòria cau prescindible: sense volum i sense persistència, perquè si el Pod mor l'API torna a servir amb origen: db fins que es reescalfa, que és exactament el que vas decidir a la sonda de readiness de 06-01.

  1. Per què aurora-db és un StatefulSet

Aspecte Deployment StatefulSet
Noms dels Pods Aleatoris (aurora-api-7d9f-x2k) Ordinals estables: aurora-db-0, -1
Identitat en recrear-se Una altra de diferent La mateixa, amb el seu mateix disc
Emmagatzematge Compartit o efímer Un PVC propi per Pod, volumeClaimTemplates
Ordre d'arrencada i aturada Tots alhora Seqüencial -0, -1; aturada en ordre invers
DNS per Pod No Sí, amb Service headless
Actualització Progressiva per ReplicaSet Ordinal, de major a menor

Un Deployment amb un volum per a PostgreSQL falla per dos motius. Primer, l'estratègia per defecte arrenca el Pod nou abans de matar el vell: durant uns segons hi hauria dos PostgreSQL escrivint sobre els mateixos fitxers, cosa que corromp les dades. Segon, si escalessis a dues rèpliques, totes dues compartirien el mateix PVC —o s'hi barallarien— sense cap coordinació.

Advertència. Executar una base de dades a Kubernetes és factible, però en producció exigeix un operador (CloudNativePG, Zalando Postgres Operator, Crunchy) que gestioni failover, còpies, replicació i actualitzacions de versió, o directament un servei gestionat fora del clúster. Un StatefulSet a pèl com el d'aquesta lliçó és correcte per aprendre i per a entorns de desenvolupament, però no cobreix la recuperació davant de desastres. Valida aquesta decisió amb el responsable d'infraestructura i de dades de la teva organització abans de posar-hi dades reals.

  1. El StatefulSet amb volumeClaimTemplates

# k8s/base/db.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata: { name: aurora-db }
spec:
  serviceName: aurora-db          # obligatori: el Service headless que dona DNS per Pod
  replicas: 1
  selector: { matchLabels: { app.kubernetes.io/name: aurora-db } }
  template:
    metadata: { labels: { app.kubernetes.io/name: aurora-db } }
    spec:
      securityContext: { fsGroup: 999 }      # el volum pertany al grup de l'usuari postgres
      containers:
        - name: postgres
          image: postgres:16-alpine
          ports: [{ containerPort: 5432, name: postgres }]
          env:
            - { name: POSTGRES_USER, value: aurora }
            - { name: POSTGRES_DB,   value: aurora_llibres }
            - { name: PGDATA,        value: /var/lib/postgresql/data/pgdata }
            - name: POSTGRES_PASSWORD
              valueFrom: { secretKeyRef: { name: aurora-secrets, key: DB_PASSWORD } }
          volumeMounts:
            - { name: dades, mountPath: /var/lib/postgresql/data }
            - { name: init,  mountPath: /docker-entrypoint-initdb.d, readOnly: true }
          resources:
            requests: { memory: 256Mi, cpu: 100m }
            limits:   { memory: 1Gi,   cpu: "2" }
          readinessProbe:
            exec: { command: ["pg_isready","-U","aurora","-d","aurora_llibres"] }
            periodSeconds: 5
          # liveness sense consultar la base: només "el procés viu"
          livenessProbe: { tcpSocket: { port: postgres }, initialDelaySeconds: 30 }
      volumes: [{ name: init, configMap: { name: aurora-init-sql } }]
  volumeClaimTemplates:                        # un PVC per Pod, creat automàticament
    - metadata: { name: dades }
      spec:
        accessModes: [ReadWriteOnce]
        resources: { requests: { storage: 10Gi } }

El PGDATA en un subdirectori no és un caprici: molts aprovisionadors creen un lost+found a l'arrel del volum i PostgreSQL es nega a inicialitzar-se en un directori que no estigui buit. Fer servir /data/pgdata evita aquesta fallada, que és de les que costen una tarda sencera. Per la seva banda, el volumeClaimTemplates genera un PVC anomenat dades-aurora-db-0, amb una propietat deliberada: esborrar el StatefulSet no esborra els seus PVC. És una xarxa de seguretat —les dades sobreviuen a un esborrat accidental— i també una font de sorpreses, perquè recrear el StatefulSet reenganxa el volum anterior amb tot el que hi hagués a dins.

  1. Service headless i el ConfigMap amb init.sql

---
apiVersion: v1
kind: Service
metadata: { name: aurora-db }
spec:
  clusterIP: None                 # headless: sense IP virtual, DNS directe a cada Pod
  selector: { app.kubernetes.io/name: aurora-db }
  ports: [{ port: 5432, targetPort: postgres }]

Un Service normal reparteix entre Pods, que és justament el que no vols amb una base de dades: cada rèplica és diferent i has de poder adreçar-te a una de concreta. Amb clusterIP: None, el DNS retorna directament les adreces dels Pods, i cadascun rep a més el seu nom estable: aurora-db-0.aurora-db.aurora.svc.cluster.local. Amb rèpliques de lectura, aquest nom és el que et permet apuntar les escriptures al primari.

L'init.sql amb els nou títols entra com a ConfigMap generat des del fitxer, amb kubectl create configmap aurora-init-sql --from-file=init.sql=db/init.sql -n aurora. Un límit que convé conèixer: un ConfigMap no pot passar d'1 MiB, perquè viu a etcd. Per a un init.sql de nou llibres en sobra, però un bolcat real no hi cap; en aquest cas es fa servir un initContainer que el descarregui d'un emmagatzematge d'objectes.

  1. aurora-api: imatge per digest i imagePullSecrets

# k8s/base/api.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: aurora-api
  labels: { app.kubernetes.io/name: aurora-api, app.kubernetes.io/version: "2.0.0" }
spec:
  replicas: 3
  revisionHistoryLimit: 5
  selector: { matchLabels: { app.kubernetes.io/name: aurora-api } }
  template:
    metadata: { labels: { app.kubernetes.io/name: aurora-api } }
    spec:
      imagePullSecrets: [{ name: ghcr-auroralibros }]
      terminationGracePeriodSeconds: 30       # > TERMINI_ATURADA_MS (15 s) de 06-01
      containers:
        - name: api
          image: ghcr.io/auroralibros/aurora-api:2.0.0@sha256:a1b2c3d4e5f60718293a4b5c6d7e8f90
          imagePullPolicy: IfNotPresent
          ports: [{ containerPort: 3000, name: http }]
kubectl create secret docker-registry ghcr-auroralibros -n aurora \
  --docker-server=ghcr.io --docker-username=auroralibros --docker-password="$TOKEN_GHCR"

Fixar el digest al costat de l'etiqueta és el que fa que el desplegament sigui immutable de debò: l'etiqueta documenta quina versió és i el digest garanteix quins bytes s'executen, encara que algú hagi mogut 2.0.0 al registre d'imatges. És la pràctica que exigeix qualsevol política d'admissió seriosa. Sobre imagePullPolicy hi ha un parany clàssic: amb l'etiqueta latest el valor per defecte és Always, i amb qualsevol altra és IfNotPresent; fixant el digest, IfNotPresent és correcte i evita descàrregues innecessàries, perquè un digest sempre identifica el mateix contingut.

  1. Les tres sondes al manifest

          startupProbe:                       # protegeix les altres dues durant l'arrencada
            httpGet: { path: /salut/arrencat, port: http }
            periodSeconds: 5
            failureThreshold: 12              # fins a 60 s per arrencar
          livenessProbe:                      # NO toca la BD: només "respon el procés?"
            httpGet: { path: /salut/viu, port: http }
            periodSeconds: 15
            timeoutSeconds: 3
            failureThreshold: 3
          readinessProbe:                     # SÍ que comprova dependències
            httpGet: { path: /salut/preparat, port: http }
            periodSeconds: 5
            failureThreshold: 2
          # marge extra per al drenatge del kube-proxy
          lifecycle: { preStop: { exec: { command: ["sleep","5"] } } }
Sonda Endpoint Si falla Temps fins a actuar
startupProbe /salut/arrencat Es reinicia el contenidor 12 × 5 s = 60 s
livenessProbe /salut/viu Es reinicia el contenidor 3 × 15 s = 45 s
readinessProbe /salut/preparat Es treu dels endpoints 2 × 5 s = 10 s

Els números estan triats amb una lògica: la readiness reacciona de pressa (10 s) perquè la seva conseqüència és barata i reversible —deixar d'enviar trànsit—, mentre que la liveness reacciona a poc a poc (45 s) perquè la seva conseqüència és cara i disruptiva —matar el procés—. Invertir aquests terminis és la recepta dels reinicis en cascada sota càrrega. I el preStop amb sleep 5 compleix aquí el mateix paper que l'espera de cinc segons que vas programar a 06-01: quan Kubernetes decideix acabar un Pod, n'elimina l'endpoint i envia SIGTERM alhora, i aquestes dues coses es propaguen a ritmes diferents per tots els kube-proxy del clúster. El sleep retarda el SIGTERM just el necessari perquè les taules d'encaminament s'actualitzin abans.

  1. requests, limits i les classes de QoS

Per a aurora-api, requests: { memory: 128Mi, cpu: 100m } i limits: { memory: 512Mi, cpu: "1" }. Els dos camps fan coses diferents:

Camp Per a què serveix Què passa si te'n passes
requests Planificació: el scheduler només col·loca el Pod on càpiga El Pod no es programa: Pending
limits.memory Sostre dur del cgroup OOMKilled: el procés mor
limits.cpu Quota de temps de CPU Throttling: s'alenteix, no mor
Classe de QoS Condició En quedar-se el node sense memòria
Guaranteed requests == limits a tots els contenidors Últim a ser expulsat
Burstable requests < limits Intermedi
BestEffort Sense requests ni limits Primer a ser expulsat

aurora-api queda com a Burstable, que és el correcte per a una API: reserva poc perquè hi càpiguen moltes rèpliques per node i pot pujar fins al límit als pics. aurora-db, en canvi, és candidata a Guaranteed en producció, perquè no vols que el kernel triï la teva base de dades quan hagi d'expulsar alguna cosa. Els números surten directament de la taula de docker stats de 06-01: 214 MiB de pic → límit de 512 MiB.

  1. securityContext de Pod i de contenidor

      securityContext:                        # nivell POD: s'aplica a tots els contenidors
        runAsNonRoot: true
        runAsUser: 1000
        runAsGroup: 1000
        fsGroup: 1000
        seccompProfile: { type: RuntimeDefault }
      containers:
        - name: api
          securityContext:                    # nivell CONTENIDOR: guanya sobre el del Pod
            allowPrivilegeEscalation: false   # equival a no-new-privileges
            readOnlyRootFilesystem: true      # equival a read_only
            capabilities: { drop: [ALL] }     # equival a cap_drop: [ALL]
          volumeMounts: [{ name: tmp, mountPath: /tmp }]
      # el tmpfs de 05-03, necessari perquè l'arrel és de només lectura
      volumes: [{ name: tmp, emptyDir: { medium: Memory, sizeLimit: 32Mi } }]

Això és, línia per línia, la traducció de l'enduriment del compose.prod.yaml de 05-03. L'única novetat és runAsNonRoot: true, que mereix atenció: no canvia l'usuari, sinó que verifica que la imatge no arrenqui com a root, i si ho fa el Pod falla en crear-se amb CreateContainerConfigError. És una comprovació barata que impedeix que una imatge mal construïda arribi a executar-se. I fsGroup resol el problema clàssic de permisos amb volums: Kubernetes canvia el grup propietari del contingut muntat a aquell GID, de manera que un procés sense privilegis pot escriure al seu PVC.

  1. Configuració amb ConfigMap i Secret

# k8s/base/config.yaml
apiVersion: v1
kind: ConfigMap
metadata: { name: aurora-config }
data:
  DB_HOST: aurora-db
  DB_USER: aurora
  DB_NAME: aurora_llibres
  DB_POOL_MAX: "10"
  REDIS_HOST: aurora-cache
  CACHE_TTL: "60"
  TERMINI_ATURADA_MS: "15000"
  LOG_NIVELL: info
---
# i al contenidor d'aurora-api:
#         envFrom: [{ configMapRef: { name: aurora-config } }]   # totes les claus
#         env:
#           - name: DB_PASSWORD
#             valueFrom: { secretKeyRef: { name: aurora-secrets, key: DB_PASSWORD } }

L'API no necessita ni una línia de canvi: els mateixos noms de variable que vas definir a config.js a 06-01 arriben ara des d'un ConfigMap en comptes de des de Compose, que és la recompensa d'haver tret tota la configuració de la imatge. Podries, a més, muntar el secret com a fitxer i fer servir DB_PASSWORD_FILE, més segur pel que vas veure a 06-03 sobre l'herència als subprocessos; aquí es fa servir secretKeyRef per brevetat, però en producció el volum és preferible i el teu codi ja ho suporta.

Un detall operatiu important: canviar un ConfigMap no reinicia els Pods. Per aplicar el canvi cal forçar-ho amb kubectl rollout restart deployment/aurora-api, o deixar que Kustomize generi el ConfigMap amb un sufix de hash, que és el que farem.

  1. aurora-web: Deployment, Service i Ingress

# k8s/base/web.yaml (extracte)
apiVersion: apps/v1
kind: Deployment
metadata: { name: aurora-web }
spec:
  replicas: 2
  selector: { matchLabels: { app.kubernetes.io/name: aurora-web } }
  template:
    metadata: { labels: { app.kubernetes.io/name: aurora-web } }
    spec:
      containers:
        - name: nginx
          image: nginx:alpine
          ports: [{ containerPort: 80, name: http }]
          volumeMounts: [{ name: contingut, mountPath: /usr/share/nginx/html, readOnly: true }]
          readinessProbe: { httpGet: { path: /, port: http }, periodSeconds: 5 }
          resources:
            requests: { memory: 16Mi, cpu: 10m }
            limits:   { memory: 64Mi, cpu: 200m }
      volumes: [{ name: contingut, configMap: { name: aurora-web-html } }]
---
apiVersion: v1
kind: Service
metadata: { name: aurora-web }
spec:
  selector: { app.kubernetes.io/name: aurora-web }
  ports: [{ port: 80, targetPort: http }]
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: aurora
  annotations: { nginx.ingress.kubernetes.io/proxy-read-timeout: "30" }
spec:
  ingressClassName: nginx
  rules:
    - host: llibres.aurora.example
      http:
        paths:
          - { path: /llibres, pathType: Prefix, backend: { service: { name: aurora-api, port: { number: 3000 } } } }
          - { path: /salut,   pathType: Prefix, backend: { service: { name: aurora-api, port: { number: 3000 } } } }
          - { path: /,        pathType: Prefix, backend: { service: { name: aurora-web, port: { number: 80 } } } }

Fixa't en el canvi d'arquitectura: a Compose, aurora-web feia de proxy invers cap a l'API. Aquí aquesta feina la fa l'Ingress, i Nginx queda reduït a servir fitxers estàtics. És el natural a Kubernetes: l'encaminament és responsabilitat de la plataforma, no del teu contenidor, i així pots escalar el frontal i l'API per separat.

  1. TLS amb cert-manager

  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  tls:
    - hosts: [llibres.aurora.example]
      secretName: aurora-tls          # cert-manager el crea i el renova tot sol

Amb cert-manager instal·lat i un ClusterIssuer configurat, aquestes cinc línies n'hi ha prou: l'operador veu l'anotació, sol·licita el certificat a Let's Encrypt, resol el desafiament ACME, desa el resultat al Secret aurora-tls i el renova automàticament abans que caduqui. És l'exemple canònic d'operador i del que Kubernetes afegeix sobre Swarm: una tasca recurrent que algú feia a mà es converteix en un controlador més. La configuració de l'emissor —producció o staging de Let's Encrypt, DNS-01 davant d'HTTP-01— és una decisió de plataforma que convé acordar amb el teu equip d'infraestructura.

  1. Kustomize: bases i overlays per entorn

Kustomize és la mateixa idea dels overrides de Compose de 04-06, amb la diferència que aquí forma part de kubectl: una base comuna i overlays que la pedacen per entorn, sense duplicar ni un fitxer ni fer servir plantilles.

# k8s/base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: aurora
resources: [namespace.yaml, config.yaml, cache.yaml, db.yaml, api.yaml, web.yaml]
commonLabels: { app.kubernetes.io/part-of: aurora-libros }
configMapGenerator:
  - { name: aurora-init-sql, files: [init.sql=../../db/init.sql] }  # el hash va al nom
---
# k8s/overlays/produccio/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: aurora
resources: [../../base]
images:
  - { name: ghcr.io/auroralibros/aurora-api, newTag: 2.0.0, digest: sha256:a1b2c3d4e5f60718293a4b5c6d7e8f90 }
replicas: [{ name: aurora-api, count: 3 }]
patches:
  - target: { kind: Deployment, name: aurora-api }
    patch: |
      - { op: replace, path: /spec/template/spec/containers/0/resources/limits/memory, value: 512Mi }
kubectl kustomize k8s/overlays/produccio | head -20    # veure el resultat sense aplicar
kubectl apply -k k8s/overlays/produccio

El configMapGenerator resol el problema del final de la secció 10: genera el ConfigMap amb un sufix derivat del hash del contingut (aurora-init-sql-7f9c2d) i actualitza automàticament la referència als Deployments. Com que el nom canvia, la plantilla del Pod canvia, i el desplegament progressiu es dispara tot sol quan edites la configuració. És exactament el comportament que vols i que a mà no passa mai.

  1. Posada en marxa i verificació

kubectl apply -k k8s/overlays/produccio
kubectl rollout status statefulset/aurora-db -n aurora --timeout=120s
kubectl rollout status deployment/aurora-api -n aurora --timeout=120s
kubectl get all -n aurora
NAME                              READY   STATUS    RESTARTS   AGE
pod/aurora-api-6d4f8b7c9-2xkpq    1/1     Running   0          63s   (×3)
pod/aurora-cache-5b7d9c8f4-hq3vn  1/1     Running   0          92s
pod/aurora-db-0                   1/1     Running   0          92s
pod/aurora-web-79c6d8f5b-k2mtx    1/1     Running   0          63s   (×2)

NAME                   TYPE        CLUSTER-IP      PORT(S)
service/aurora-api     ClusterIP   10.96.201.14    3000/TCP
service/aurora-cache   ClusterIP   10.96.184.22    6379/TCP
service/aurora-db      ClusterIP   None            5432/TCP
service/aurora-web     ClusterIP   10.96.77.108    80/TCP

L'aurora-db-0 amb el seu ordinal i el CLUSTER-IP None del Service headless confirmen que el StatefulSet està ben muntat.

kubectl port-forward -n aurora svc/aurora-api 8080:3000 &
curl -s localhost:8080/llibres | jq '{origen, total: (.llibres|length), primer: .llibres[0].titol}'
curl -s localhost:8080/llibres | jq -r '.origen'      # segona crida
# { "origen": "db", "total": 9, "primer": "El jardín de senderos que se bifurcan" }
# cache

Els nou títols hi són, i la segona crida retorna origen: cache: el cache-aside funciona exactament igual que al teu portàtil, ara repartit entre tres Pods d'API que parlen amb un Redis i un PostgreSQL pels seus Services. L'aplicació no ha canviat ni una línia des del mòdul 4.

  1. Depuració: els estats d'un Pod

Estat Què significa Causa habitual Diagnòstic
Pending No hi ha node on col·locar-lo requests massa altes, taints, PVC sense enllaçar kubectl describe pod → Events
ContainerCreating Preparant volums i xarxa PVC en espera, secret inexistent describe → Events
ImagePullBackOff No pot descarregar la imatge Nom mal escrit, falta imagePullSecrets describe → Events
CrashLoopBackOff Arrenca i mor en bucle Fallada de configuració, sonda mal posada logs --previous
OOMKilled Va superar limits.memory Límit baix o fuita de memòria describe → Last State
Error Va sortir amb codi diferent de 0 Excepció a l'arrencada logs
Running però 0/1 Viu però no preparat La readiness no passa describe → Events, logs
Terminating etern No acaba de morir No captura SIGTERM o hi ha un finalitzador describe, delete --force
kubectl describe pod aurora-api-6d4f8b7c9-2xkpq -n aurora | tail -15   # Events
kubectl logs aurora-api-6d4f8b7c9-2xkpq -n aurora --previous           # l'intent anterior
kubectl get events -n aurora --sort-by=.lastTimestamp | tail -10
kubectl debug -it aurora-api-6d4f8b7c9-2xkpq -n aurora --image=nicolaka/netshoot --target=api

kubectl debug resol el problema que tenies amb les imatges mínimes de 05-03: adjunta un contenidor efímer amb totes les eines al Pod ja en marxa, compartint-ne la xarxa i els namespaces, sense reiniciar res i sense necessitat que la imatge contingui un sh. És el netshoot del mòdul 3, aplicat al món de Kubernetes.

  1. Helm com a alternativa

Aspecte Kustomize Helm
Mecanisme Pedaços sobre YAML pla Plantilles Go amb valors
Integració i corba A kubectl (-k); suau Binari a part; més pronunciada
Programari de tercers Poc pràctic El seu punt fort: helm install ingress-nginx
Estat i rollback Amb kubectl rollout helm rollback amb historial propi

La combinació habitual és fer servir Helm per al programari de tercers —ingress-nginx, cert-manager, Prometheus— i Kustomize per al que és teu, que és el que fa Aurora Libros. Helm brilla quan vols distribuir la teva aplicació a tercers que necessiten parametritzar-la; per a una aplicació pròpia amb dos entorns, Kustomize evita convertir els teus manifestos en plantilles il·legibles.

Errors Habituals i Consells

  • Una base de dades en un Deployment. Durant l'actualització conviuen dos Pods escrivint sobre el mateix disc. StatefulSet amb stop-first, i en producció un operador.
  • Oblidar imagePullSecrets. ImagePullBackOff a tots els Pods amb un registre d'imatges privat. El secret és per namespace.
  • PGDATA a l'arrel del volum. El lost+found de l'aprovisionador impedeix inicialitzar PostgreSQL.
  • Liveness apuntant a /salut/preparat. Reinicis en cascada quan la base de dades tus. La liveness només mira el procés.
  • initialDelaySeconds curt sense startupProbe. El Pod es reinicia abans d'acabar d'arrencar, per sempre.
  • Editar un ConfigMap i esperar que s'apliqui. No reinicia res: rollout restart o configMapGenerator.
  • Consell: kubectl apply -k --dry-run=server al pipeline valida els manifestos contra l'API real abans de tocar el clúster, i kubectl rollout restart deployment/... és la manera correcta de "reiniciar" (no delete pod).
  • Consell: kubectl get events --sort-by=.lastTimestamp és la vista més útil quan alguna cosa falla i no saps ni quin objecte mirar.

Exercicis

Exercici 1. Desplega Aurora Libros completa al teu clúster kind i verifica-ho d'extrem a extrem: que aurora-db-0 té el seu PVC enllaçat, que /llibres retorna els nou títols i que la segona crida ve de la memòria cau.

Exercici 2. Diagnostica tres fallades provocades sense mirar els manifestos: un Pending per requests impossibles, un ImagePullBackOff i un CrashLoopBackOff per falta d'una variable obligatòria. Indica en cada cas quina comanda ho revela.

Exercici 3. Demostra que el StatefulSet conserva la identitat i les dades: escriu un registre a la base, esborra el Pod aurora-db-0 i comprova què sobreviu i què no.

Solucions

Solució 1.

kubectl apply -k k8s/overlays/produccio
kubectl wait --for=condition=ready pod -l app.kubernetes.io/name=aurora-api -n aurora --timeout=180s
kubectl get pvc -n aurora
# NAME                STATUS  VOLUME       CAPACITY  ACCESS MODES  STORAGECLASS
# dades-aurora-db-0   Bound   pvc-8f3c1a…  10Gi      RWO           standard

El nom dades-aurora-db-0 és la signatura del volumeClaimTemplates: <nom-de-la-plantilla>-<nom-del-statefulset>-<ordinal>. Si escalessis a dues rèpliques apareixeria dades-aurora-db-1, amb el seu propi disc, que és justament el que un Deployment no et pot donar.

kubectl port-forward -n aurora svc/aurora-api 8080:3000 &
curl -s localhost:8080/llibres | jq -r '.origen, (.llibres|length), .llibres[4].titol'
curl -s localhost:8080/llibres | jq -r '.origen'
kubectl exec -n aurora aurora-db-0 -- psql -U aurora -d aurora_llibres -tAc 'SELECT count(*) FROM llibres;'
# db / 9 / Nada     (primera crida)
# cache             (segona crida)
# 9                 (files a la taula)

La verificació d'extrem a extrem està completa: el ConfigMap amb init.sql es va executar en inicialitzar PostgreSQL, els nou títols són a la taula, i l'API els serveix des de la base de dades la primera vegada i des de Redis la segona. Però val la pena aturar-se en el que no ha calgut. No has tocat el codi de l'aplicació, ni el Dockerfile, ni les variables que espera. La imatge que executa el clúster és la mateixa que va construir el pipeline de 06-02 i la mateixa que corria al teu Compose: el contracte que vas establir a 06-01 —configuració per entorn, sondes separades, aturada ordenada— és el que ha permès que canviar de plataforma sigui només canviar qui injecta les variables.

Solució 2.

kubectl patch deploy aurora-api -n aurora --type=json \
  -p='[{"op":"replace","path":"/spec/template/spec/containers/0/resources/requests/memory","value":"64Gi"}]'
kubectl get pods -n aurora | grep Pending
kubectl describe pod -n aurora -l app.kubernetes.io/name=aurora-api | grep -A3 Events
# aurora-api-84c7d9f6b-nk4pz   0/1   Pending   0   22s
# Events:
#   Warning  FailedScheduling  no nodes available: 3 Insufficient memory

kubectl set image deploy/aurora-api api=ghcr.io/auroralibros/aurora-api:9.9.9 -n aurora
kubectl describe pod -n aurora -l app.kubernetes.io/name=aurora-api | grep -A2 'Failed'
#   Warning  Failed  Failed to pull image "...aurora-api:9.9.9": not found
#   Warning  Failed  Error: ErrImagePull        → ImagePullBackOff

kubectl patch cm aurora-config -n aurora --type=json -p='[{"op":"remove","path":"/data/DB_HOST"}]'
kubectl rollout restart deploy/aurora-api -n aurora
kubectl logs -n aurora -l app.kubernetes.io/name=aurora-api --previous --tail=2
# {"nivell":"error","missatge":"configuracio invalida",
#  "detall":"Falta la variable obligatoria DB_HOST (o la seva variant _FILE)"}
Fallada Estat Comanda que la revela On és la resposta
requests impossibles Pending describe pod Events: Insufficient memory
Imatge inexistent ImagePullBackOff describe pod Events: not found
Falta configuració CrashLoopBackOff logs --previous Logs del contenidor

La regla de diagnòstic que resumeix la taula és la que estalvia més temps a Kubernetes: si el contenidor no va arribar a arrencar mai, la resposta és als esdeveniments; si va arrencar i va morir, és als logs. En els dos primers casos kubectl logs no retorna res, i no perquè falli, sinó perquè no hi ha cap procés que hagi escrit res. Al tercer, describe només diu Back-off restarting failed container, cosa que no explica res; el --previous és imprescindible perquè el contenidor actual acaba de néixer i el que va fallar ja no existeix. I fixa't en la qualitat d'aquell tercer missatge: diu exactament quina variable falta, que és el rendiment del config.js de 06-01 en un entorn on depurar és molt més incòmode.

Solució 3.

kubectl exec -n aurora aurora-db-0 -- psql -U aurora -d aurora_llibres -c \
  "INSERT INTO llibres (titol, autor, preu) VALUES ('El Aleph (2a ed.)','J. L. Borges', 18.50);"
kubectl get pod aurora-db-0 -n aurora -o jsonpath='{.status.podIP}{"\n"}'
kubectl delete pod aurora-db-0 -n aurora
kubectl wait --for=condition=ready pod/aurora-db-0 -n aurora --timeout=120s
kubectl get pod aurora-db-0 -n aurora -o jsonpath='{.status.podIP}{"\n"}'
kubectl exec -n aurora aurora-db-0 -- psql -U aurora -d aurora_llibres -tAc "SELECT count(*) FROM llibres;"
# 10.244.2.9
# pod "aurora-db-0" deleted
# 10.244.1.14
# 10
Propietat Sobreviu? Per què
Nom del Pod i el seu DNS Ordinal fix: aurora-db-0.aurora-db continua apuntant-hi
PVC dades-aurora-db-0 Es reenganxa al Pod nou
Les dades (10 files) Viuen al PVC, no al Pod
IP del Pod No Canvia de 10.244.2.9 a 10.244.1.14
Node on corre No garantit Es pot reprogramar

El contrast amb l'exercici 2 de 06-04 és el punt de la lliçó. Allà, en esborrar un Pod d'un Deployment tornava un altre Pod amb un altre nom; aquí torna aurora-db-0, el mateix nom, el mateix DNS i, sobretot, el mateix disc, amb el desè llibre a dins. Que la IP canviï i no passi res demostra per què la base de dades es referencia sempre pel seu nom DNS i mai per adreça: aurora-db al ConfigMap continua resolent, així que les tres rèpliques de l'API es van reconnectar soles —amb el reintent amb backoff de 04-04— sense que toquessis res.

Ara l'advertència que aquest exercici no demostra i convé no oblidar: has sobreviscut a la mort d'un Pod, no a la d'un node ni a la d'un disc. Amb ReadWriteOnce, si el node que allotja el PVC queda inaccessible, el Pod es pot quedar en Pending fins que el volum s'alliberi. Això, i l'absència de failover automàtic, és exactament el que aporta un operador de PostgreSQL i el que cal resoldre abans de posar-hi dades de clients.

Conclusió

Aurora Libros sencera viu a Kubernetes. Has escrit els manifestos dels quatre serveis amb criteri: aurora-cache com a Deployment prescindible amb ports amb nom, aurora-db com a StatefulSet —amb el perquè clar: dos PostgreSQL sobre els mateixos fitxers corrompen les dades— amb els seus volumeClaimTemplates, el seu Service headless, el seu ConfigMap amb els nou títols i el PGDATA en un subdirectori per esquivar el lost+found. I amb l'advertència al seu lloc: això serveix per aprendre, però producció demana un operador o una base gestionada, validat amb el teu equip d'infraestructura.

El Deployment d'aurora-api recull tot el del mòdul: la imatge del pipeline fixada per digest amb el seu imagePullSecrets, les tres sondes de 06-01 apuntant a /salut/arrencat, /salut/viu i /salut/preparat amb terminis asimètrics expressament —readiness ràpida perquè és barata, liveness lenta perquè mata—, el preStop que compra els segons que necessita el kube-proxy, les requests i els limits trets del mesurament real amb la seva classe de QoS, i el securityContext que tradueix línia a línia l'enduriment de 05-03. La configuració entra per ConfigMap i Secret sense tocar ni una línia de l'aplicació, que és la recompensa d'haver-la tret de la imatge. aurora-web queda com a servidor estàtic i l'encaminament passa a l'Ingress, amb cert-manager renovant el TLS tot sol.

Tot s'aplica amb kubectl apply -k i Kustomize, amb una base i overlays per entorn, i el configMapGenerator que dispara el desplegament progressiu quan canvia la configuració. Has verificat que /llibres retorna els nou títols amb origen: db i cache a la segona crida, saps llegir la taula d'estats d'un Pod amb la regla que més temps estalvia —si no va arrencar mai, mira els esdeveniments; si va arrencar i va morir, mira els logs amb --previous—, tens kubectl debug per inspeccionar imatges sense shell, i saps quan fer servir Helm i quan Kustomize.

A la lliçó següent, Escalat i Balanceig de Càrrega, la plataforma aprèn a créixer. Veuràs per què l'API escala replicant i la base de dades no, com balancegen de debò kube-proxy i l'Ingress, i muntaràs un HorizontalPodAutoscaler sobre aurora-api que crea rèpliques només mentre generes càrrega real contra /llibres, mesurant la latència abans i després per saber on és el coll d'ampolla.

Docker: De Principiant a Avançat

Mòdul 1: Introducció a Docker

Mòdul 2: Treballant amb Imatges Docker

Mòdul 3: Contenidors Docker

Mòdul 4: Docker Compose

Mòdul 5: Conceptes Avançats de Docker

Mòdul 6: Docker en Producció

Mòdul 7: Ecosistema i Eines de Docker

© Copyright 2026. Tots els drets reservats