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
- Organització de
k8s/i elNamespace aurora-cache: Deployment i Service- Per què
aurora-dbés unStatefulSet - El
StatefulSetambvolumeClaimTemplates - Service headless i el
ConfigMapambinit.sql aurora-api: imatge per digest iimagePullSecrets- Les tres sondes al manifest
requests,limitsi les classes de QoSsecurityContextde Pod i de contenidor- Configuració amb
ConfigMapiSecret aurora-web: Deployment, Service iIngress- TLS amb cert-manager
- Kustomize: bases i overlays per entorn
- Posada en marxa i verificació
- Depuració: els estats d'un Pod
- Helm com a alternativa
- Organització de
k8s/ i el Namespace
k8s/ i el Namespacek8s/base/{namespace,config,cache,db,api,web}.yaml + kustomization.yaml
k8s/overlays/{staging,produccio}/kustomization.yamlUn 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.
aurora-cache: Deployment i Service
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.
- Per què
aurora-db és un StatefulSet
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
StatefulSeta 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.
- El
StatefulSet amb volumeClaimTemplates
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.
- Service headless i el
ConfigMap amb init.sql
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.
aurora-api: imatge per digest i imagePullSecrets
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.
- 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.
requests, limits i les classes de QoS
requests, limits i les classes de QoSPer 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.
securityContext de Pod i de contenidor
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.
- Configuració amb
ConfigMap i Secret
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.
aurora-web: Deployment, Service i Ingress
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.
- 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 solAmb 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.
- 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/produccioEl 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.
- 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 auroraNAME 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/TCPL'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" }
# cacheEls 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.
- 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=apikubectl 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.
- 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.StatefulSetambstop-first, i en producció un operador. - Oblidar
imagePullSecrets.ImagePullBackOffa tots els Pods amb un registre d'imatges privat. El secret és per namespace. PGDATAa l'arrel del volum. Ellost+foundde 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. initialDelaySecondscurt sensestartupProbe. El Pod es reinicia abans d'acabar d'arrencar, per sempre.- Editar un ConfigMap i esperar que s'apliqui. No reinicia res:
rollout restartoconfigMapGenerator. - Consell:
kubectl apply -k --dry-run=serveral pipeline valida els manifestos contra l'API real abans de tocar el clúster, ikubectl rollout restart deployment/...és la manera correcta de "reiniciar" (nodelete 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 standardEl 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 | Sí | Ordinal fix: aurora-db-0.aurora-db continua apuntant-hi |
PVC dades-aurora-db-0 |
Sí | Es reenganxa al Pod nou |
| Les dades (10 files) | Sí | 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
- Què és Docker?
- Instal·lant Docker
- Arquitectura de Docker
- Comandes Bàsiques de Docker
- Entenent les Imatges de Docker
- Creant el teu Primer Contenidor Docker
- El Projecte del Curs: la Plataforma Aurora Libros
Mòdul 2: Treballant amb Imatges Docker
- Docker Hub i Repositoris
- Construint Imatges Docker
- Conceptes Bàsics de Dockerfile
- Instruccions Avançades del Dockerfile
- Gestionant Imatges Docker
- Etiquetatge i Publicació d'Imatges
Mòdul 3: Contenidors Docker
- Executant Contenidors
- Cicle de Vida del Contenidor
- Gestionant Contenidors
- Inspecció i Depuració de Contenidors
- Xarxes a Docker
- Persistència de Dades amb Volums
- Límits de Recursos i Polítiques de Reinici
Mòdul 4: Docker Compose
- Introducció a Docker Compose
- Definint Serveis a Docker Compose
- Comandes de Docker Compose
- Aplicacions Multi-Contenidor
- Variables d'Entorn a Docker Compose
- Perfils, Overrides i Múltiples Entorns
- Desenvolupament Local amb Docker Compose
Mòdul 5: Conceptes Avançats de Docker
- Aprofundiment en Xarxes Docker
- Opcions d'Emmagatzematge Docker
- Millors Pràctiques de Seguretat a Docker
- Optimitzant Imatges Docker
- Builds Avançades amb BuildKit i Buildx
- Registre i Monitoratge a Docker
- El Runtime per Dins: Namespaces, Cgroups i Capes
Mòdul 6: Docker en Producció
- Preparar una Imatge per a Producció
- CI/CD amb Docker
- Orquestrant Contenidors amb Docker Swarm
- Introducció a Kubernetes
- Desplegant Contenidors Docker a Kubernetes
- Escalat i Balanceig de Càrrega
- Estratègies de Desplegament i Rollback
