A AWS vam aconseguir infraestructura seriosa amb poc esforç operatiu: ECS Fargate executa la imatge, RDS desa les dades, el balancejador reparteix i ningú no administra un servidor. La lliçó acabava amb una pregunta oberta: què passa quan la xarxa de Ribalta deixa de ser una aplicació i en són cinc, amb tres equips desplegant pel seu compte i l'ajuntament preguntant si això es pot moure a un altre proveïdor?

La resposta de la indústria és Kubernetes, l'orquestrador de contenidors que s'ha convertit en l'estàndard de facto. Aquesta lliçó l'aborda amb honestedat: primer explica quin problema resol i per què per a una sola aplicació com CicloUrbana sol ser complexitat no justificada, i després el fa bé, amb els manifestos complets i comentats de la xarxa de Ribalta. Aquí és on encaixen per fi dues peces que anem preparant des del mòdul 7: les sondes de salut de 07-01, que Kubernetes consulta de tres maneres diferents, i la imatge de 07-04, que és exactament la unitat que un Deployment desplega.

Contingut

  1. Quin problema resol un orquestrador, i quan compensa
  2. Arquitectura i objectes de Kubernetes
  3. Entorn de pràctica i kubectl
  4. Namespace, ConfigMap i Secret
  5. El Deployment de CicloUrbana
  6. Les tres sondes i els grups de salut de 07-01
  7. Service i Ingress amb TLS
  8. Migracions de Flyway: Job o initContainer
  9. Desplegament rolling, rollout status i rollout undo
  10. HPA i PodDisruptionBudget
  11. Empaquetar amb Helm
  12. PostgreSQL dins o fora del clúster
  13. Observabilitat i depuració d'un pod que no arrenca
  14. GitOps i Spring Cloud Kubernetes
  15. Errors Comuns i Consells
  16. Exercicis

  1. Quin problema resol un orquestrador, i quan compensa

Un orquestrador pren un conjunt de màquines i les presenta com un únic recurs computacional al qual se li declara un estat desitjat. En lloc de dir «arrenca aquest contenidor en aquesta màquina», es diu «vull tres còpies d'aquesta imatge, amb aquest límit de memòria, accessibles en aquest nom», i el sistema s'ocupa d'aconseguir-ho i de mantenir-ho malgrat les avaries.

Problema Com el resol Kubernetes
Un contenidor cau El controlador ho detecta i en crea un altre per tornar al nombre desitjat
Una màquina mor Els pods es reprogramen a les que queden
Repartir contenidors entre màquines El planificador els col·loca segons recursos, afinitats i restriccions
Descobriment de serveis Cada Service té nom DNS intern estable
Desplegament sense tall i escalat Deployment progressiu amb reversió; HorizontalPodAutoscaler segons mètriques
Configuració i secrets ConfigMap i Secret muntats com a variables o fitxers
Portabilitat entre proveïdors Els mateixos manifestos a EKS, GKE, AKS o en un servidor propi

L'advertiment honest, abans de continuar. Tot el d'aquesta taula ho feia també ECS a 08-03, amb una fracció de l'esforç. Kubernetes porta amb ell un vocabulari de desenes d'objectes, un pla de control que cal actualitzar, complements que cal instal·lar (controlador d'ingress, cert-manager, mètriques, agregador de logs), un model de xarxa no trivial i una superfície de seguretat àmplia. Per a una sola aplicació, ECS o un PaaS solen bastar i solen ser millors.

Kubernetes comença a compensar quan concorren diverses d'aquestes condicions: hi ha molts serveis (més de cinc o sis) que despleguen de manera independent; hi ha diversos equips que necessiten autonomia sense trepitjar-se, amb Namespace i quotes; s'exigeix portabilitat entre proveïdors o allotjament propi; cal automatització avançada —canary, GitOps, operadors—; i ja existeix una plataforma interna amb algú que la manté.

Per a CicloUrbana, la resposta sincera avui és: no cal. L'estudiem perquè és l'escenari de l'ajuntament a tres anys vista, perquè és la destinació de la imatge que ja sabem construir, i perquè les sondes de 07-01 només revelen tot el seu sentit aquí.

  1. Arquitectura i objectes de Kubernetes

flowchart TD
    subgraph CP["Pla de control (gestionat pel proveidor)"]
      API[API Server] --- ETCD[(etcd)]
      API --- SCHED[Scheduler]
      API --- CM[Controller Manager]
    end
    KUBECTL[kubectl / CI] --> API
    subgraph N1["Node 1"]
      K1[kubelet] --> P1[Pod ciclourbana]
      K1 --> P2[Pod ciclourbana]
    end
    subgraph N2["Node 2"]
      K2[kubelet] --> P3[Pod ciclourbana]
    end
    API --> K1
    API --> K2
    ING[Ingress Controller] --> SVC[Service ClusterIP]
    SVC --> P1 & P2 & P3

El model mental és senzill i convé fixar-lo: es declara l'estat desitjat a l'API Server, que el desa a etcd, i els controladors comparen contínuament allò desitjat amb allò real i actuen per reduir la diferència. No es donen ordres: es descriu un objectiu.

Objecte Què és A CicloUrbana
Pod La unitat mínima: un o diversos contenidors que comparteixen xarxa i volums Un pod = una instància de l'aplicació
ReplicaSet / Deployment El Deployment gestiona ReplicaSet i orquestra actualitzacions i reversions ciclourbana, 3 rèpliques
Service Nom DNS i IP virtual estables sobre un conjunt canviant de pods ciclourbana:8080 dins del clúster
Ingress Encaminament HTTP/HTTPS des de fora, amb TLS i per amfitrió o ruta ciclourbana.ribalta.example
ConfigMap Configuració no sensible, com a variables o fitxers Perfil actiu, URL de la base de dades
Secret Dades sensibles, codificades en base64 Contrasenya de PostgreSQL, secret JWT
Namespace Partició lògica del clúster amb noms i quotes propis ciclourbana-prod, ciclourbana-pre
HPA Ajusta el nombre de rèpliques segons mètriques Escalar per CPU entre 3 i 10
PVC Sol·licitud d'emmagatzematge persistent No el fem servir: la base de dades és fora
Job / CronJob Procés puntual o periòdic fins a completar-se Les migracions de Flyway

  1. Entorn de pràctica i kubectl

Per practicar sense cost no cal un clúster de pagament:

minikube (minikube start --cpus 4 --memory 6144) és la més completa, amb addons d'ingress i mètriques; kind (kind create cluster --name ribalta) és lleugera i rapidíssima, ideal per a la mateixa CI; i Docker Desktop només demana marcar la casella «Enable Kubernetes».

Avís de cost: un clúster gestionat (EKS, GKE, AKS) costa uns 70 $/mes només pel pla de control, més els nodes. Practica en local; si en crees un de gestionat, destrueix-lo el mateix dia.

Les ordres imprescindibles:

Ordre Què fa
kubectl apply -f manifest.yaml Crea o actualitza el declarat (mode declaratiu)
kubectl get pods -n ciclourbana-prod Llista pods i el seu estat
kubectl describe pod <nom> Detall i esdeveniments: la primera parada en depurar
kubectl logs <pod> -f Segueix el log; --previous per al contenidor que va morir
kubectl exec -it <pod> -- sh Obre una shell dins del contenidor
kubectl port-forward svc/ciclourbana 8080:8080 Túnel local sense exposar res
kubectl rollout status / undo deploy/ciclourbana Espera que acabi l'actualització; reverteix a l'anterior
kubectl get events --sort-by=.lastTimestamp Què ha passat al namespace

Una regla de treball: apply sobre fitxers versionats a Git, mai kubectl edit ni kubectl run en producció, perquè un canvi fet a mà es perd en el següent apply i ningú no sap que va existir.

  1. Namespace, ConfigMap i Secret

El primer manifest, 00-namespace.yaml, és trivial: un Namespace anomenat ciclourbana-prod amb les etiquetes projecte: ciclourbana i entorn: prod. Aïlla noms, permisos i quotes de l'entorn de preproducció.

# 01-configmap.yaml — configuracio NO sensible
apiVersion: v1
kind: ConfigMap
metadata:
  name: ciclourbana-config
  namespace: ciclourbana-prod
data:
  SPRING_PROFILES_ACTIVE: "prod"
  TZ: "Europe/Madrid"
  JAVA_TOOL_OPTIONS: "-XX:MaxRAMPercentage=75 -XX:+ExitOnOutOfMemoryError"
  SPRING_DATASOURCE_URL: "jdbc:postgresql://ciclourbana-prod.abc123xyz.eu-west-1.rds.amazonaws.com:5432/ciclourbana?sslmode=require"
  SPRING_DATASOURCE_USERNAME: "ciclourbana_app"
  SPRING_FLYWAY_ENABLED: "false"
  MANAGEMENT_SERVER_PORT: "8081"
  SERVER_FORWARD_HEADERS_STRATEGY: "framework"

Les claus són noms de variables d'entorn, que Spring Boot tradueix a propietats per relaxed binding (02-05): SPRING_DATASOURCE_URL → spring.datasource.url. Aquí sí que podem mantenir el port de gestió separat al 8081 de 07-01 —a diferència de Heroku— perquè un pod pot exposar diversos ports, i el Service decideix quins publica.

# 02-secret.yaml — MAI es versiona amb valors reals
apiVersion: v1
kind: Secret
metadata:
  name: ciclourbana-secrets
  namespace: ciclourbana-prod
type: Opaque
stringData:                       # stringData admet text pla; K8s el codifica en desar-lo
  SPRING_DATASOURCE_PASSWORD: "REEMPLACAR"
  JWT_SECRET: "REEMPLACAR"

Advertiment fonamental: un Secret de Kubernetes només està codificat en base64, no xifrat. Base64 no és xifratge: kubectl get secret ciclourbana-secrets -o jsonpath='{.data.JWT_SECRET}' | base64 -d retorna el valor en clar per a qualsevol amb permisos de lectura sobre el namespace. I per defecte es desa tal qual a etcd.

Solució Què aporta Cost
RBAC estricte Només qui ho necessita pot llegir Secret Gratis; imprescindible en qualsevol cas
Xifratge en repòs d'etcd El proveïdor xifra amb KMS el que hi ha a etcd A EKS/GKE és una casella del clúster
Sealed Secrets Es versiona un SealedSecret xifrat; només el controlador el desxifra Un controlador més; encaixa amb GitOps
External Secrets Operator Sincronitza Secret des de Secrets Manager, SSM o Vault Un operador més; la millor opció amb AWS (08-03)

Mai no es versiona un Secret amb valors reals. Es versiona la plantilla amb REEMPLACAR, i el valor real arriba per kubectl create secret --from-literal en la creació inicial, o —el correcte— per External Secrets des del magatzem de 08-03.

  1. El Deployment de CicloUrbana

# 03-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ciclourbana
  namespace: ciclourbana-prod
  labels: { app: ciclourbana }
spec:
  replicas: 3
  revisionHistoryLimit: 5
  selector:
    matchLabels: { app: ciclourbana }
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1              # com a maxim 1 pod extra durant l'actualitzacio
      maxUnavailable: 0        # mai per sota de 3 pods sans
  template:
    metadata:
      labels: { app: ciclourbana, version: "2.4.0" }
    spec:
      terminationGracePeriodSeconds: 60
      securityContext: { runAsNonRoot: true, runAsUser: 10001, fsGroup: 10001, seccompProfile: { type: RuntimeDefault } }
      containers:
        - name: ciclourbana
          image: ghcr.io/ajuntament-ribalta/ciclourbana:2.4.0
          imagePullPolicy: IfNotPresent
          ports:
            - { name: http,   containerPort: 8080 }
            - { name: gestio, containerPort: 8081 }
          envFrom:
            - configMapRef: { name: ciclourbana-config }
            - secretRef:    { name: ciclourbana-secrets }
          resources:
            requests: { cpu: "300m", memory: "768Mi" }
            limits:   { cpu: "1",    memory: "1Gi" }
          securityContext: { allowPrivilegeEscalation: false, readOnlyRootFilesystem: true, capabilities: { drop: ["ALL"] } }
          volumeMounts: [{ name: temporal, mountPath: /tmp }]
          startupProbe:                   # fins a 120 s per arrencar
            httpGet: { path: /actuator/health/liveness, port: gestio }
            periodSeconds: 5
            failureThreshold: 24
          livenessProbe:
            httpGet: { path: /actuator/health/liveness, port: gestio }
            periodSeconds: 10
            timeoutSeconds: 3
            failureThreshold: 3
          readinessProbe:
            httpGet: { path: /actuator/health/readiness, port: gestio }
            periodSeconds: 5
            timeoutSeconds: 3
            failureThreshold: 3
          lifecycle: { preStop: { exec: { command: ["sh", "-c", "sleep 10"] } } }
      volumes: [{ name: temporal, emptyDir: { sizeLimit: 256Mi } }]

Els punts que importen de debò:

resources i la seva relació amb MaxRAMPercentage. requests és el que el planificador reserva per col·locar el pod; limits és el sostre dur. Superar el límit de memòria no produeix una excepció: el kernel mata el procés (OOMKilled). Amb limits.memory: 1Gi i MaxRAMPercentage=75 del ConfigMap, el heap màxim és 768 MB i els 256 MB restants cobreixen metaespai, piles de fils, búfers directes i codi natiu. Posar el 100 % és la recepta garantida de l'OOMKilled sense traça. En CPU, limits: 1 significa un nucli: si es posa un límit baix, el throttling del kernel allarga l'arrencada de la JVM i pot fer fallar el startupProbe — motiu pel qual requests.cpu convé generós aquí.

securityContext en dos nivells. El del pod fixa l'usuari sense privilegis (coherent amb l'usuari no root de la imatge de 07-04); el del contenidor prohibeix escalar privilegis, elimina totes les capacitats del kernel i munta el sistema de fitxers arrel de només lectura. Això últim trenca l'aplicació si alguna cosa necessita escriure —Tomcat fa servir /tmp per a pujades i treball intern—, i per això es munta un emptyDir a /tmp: un volum efímer, propi de cada pod, que desapareix amb ell. És el factor 6 de 08-01 imposat pel sistema.

envFrom amb configMapRef i secretRef injecta d'una sola vegada totes les claus de tots dos objectes com a variables d'entorn, així que afegir una propietat és editar el ConfigMap sense tocar el Deployment. Contrapartida: canviar un ConfigMap no reinicia els pods, i la nova configuració no s'aplica fins a un kubectl rollout restart deploy/ciclourbana.

preStop i terminationGracePeriodSeconds, casats amb l'aturada ordenada. La seqüència completa en retirar un pod:

sequenceDiagram
    participant K as Kubernetes
    participant P as Pod CicloUrbana
    participant S as Service / Endpoints
    K->>S: eliminar el pod dels endpoints
    K->>P: executar preStop (sleep 10)
    Note over P: continua atenent peticions<br/>mentre el Service propaga el canvi
    K->>P: SIGTERM
    Note over P: graceful shutdown (01-05):<br/>acaba el que hi ha en curs, tanca executors i pool
    Note over K: si als 60 s continua viu -> SIGKILL

El sleep 10 del preStop no és un truc brut: és la solució estàndard al fet que l'eliminació de l'endpoint i l'enviament del SIGTERM passen en paral·lel, no en seqüència. Sense aquest marge, durant un parell de segons hi ha trànsit dirigit a un pod que ja s'està tancant, i els ciutadans veuen 502. I l'aritmètica ha de quadrar: terminationGracePeriodSeconds (60) > preStop (10) + timeout-per-shutdown-phase (40), o Kubernetes matarà el procés a mitja aturada ordenada.

  1. Les tres sondes i els grups de salut de 07-01

Aquest és l'apartat on 07-01 pren tot el seu sentit. Kubernetes fa tres preguntes diferents i actua diferent segons la resposta:

Sonda Pregunta Si falla, Kubernetes... Endpoint
startupProbe Ha acabat d'arrencar? Continua esperant; suspèn les altres dues /actuator/health/liveness
livenessProbe El procés és irrecuperable? Mata el contenidor i el reinicia /actuator/health/liveness
readinessProbe Pot atendre peticions ara? El treu del Service, sense matar-lo /actuator/health/readiness

I la configuració que ja vam escriure a 07-01 fa que aquests endpoints responguin el correcte:

management:
  server:
    port: 8081
  endpoint:
    health:
      probes:
        enabled: true
      group:
        readiness: { include: db }              # inclou la base de dades
        liveness:  { include: livenessState }   # NO inclou la base de dades

Per què confondre-les provoca reinicis en bucle. Suposa que algú posa la base de dades al grup liveness, o que apunta la livenessProbe a /actuator/health complet. RDS té un manteniment de 40 segons:

Els tres pods deixen d'arribar a la base de dades i /actuator/health retorna 503; la livenessProbe falla tres vegades seguides als tres; Kubernetes mata els tres contenidors alhora; arrenquen de nou, la base de dades continua en manteniment i tornen a morir; i llavors s'aplica el retrocés exponencial —CrashLoopBackOff, amb esperes de 10, 20, 40 segons fins a arribar a 5 minuts—. Una incidència de 40 segons es converteix en una de vint minuts, i és el mateix orquestrador qui l'allarga.

Amb la configuració correcta, el que passa és molt diferent: readiness falla, els pods surten del Service, l'Ingress retorna 503 durant els 40 segons —cosa que és honesta: l'aplicació de debò no pot treballar— i liveness continua en verd perquè el procés està perfectament sa. Quan la base de dades torna, readiness passa a UP i els pods reben trànsit una altra vegada. Sense ni un sol reinici.

I la startupProbe resol el conflicte clàssic de la JVM. Una aplicació Spring Boot amb Hibernate triga 25-45 segons a arrencar, i una livenessProbe amb failureThreshold: 3 i periodSeconds: 10 la mataria als 30, abans que arribés a estar viva. La solució antiga —un initialDelaySeconds gran— retarda la detecció de fallades durant tota la vida del pod; la startupProbe ho separa: dona fins a 120 segons per arrencar (24 × 5 s) i un cop superada cedeix el control a les altres dues, que ja poden ser agressives i detectar una fallada real en 30 segons.

  1. Service i Ingress amb TLS

# 04-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: ciclourbana
  namespace: ciclourbana-prod
spec:
  type: ClusterIP              # nomes accessible dins del cluster
  selector: { app: ciclourbana }
  ports:
    - { name: http, port: 8080, targetPort: http }

ClusterIP és deliberat: el Service no publica res a l'exterior, només dona un nom DNS intern estable (ciclourbana.ciclourbana-prod.svc.cluster.local) sobre els pods que estiguin ready a cada moment; qui exposa a l'exterior és l'Ingress. I fixa't que el port 8081 de gestió no es publica: les sondes el consulten directament al pod, així que Actuator queda inaccessible des de fora del clúster per construcció — la separació de ports de 07-01 fent la seva feina.

# 05-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ciclourbana
  namespace: ciclourbana-prod
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
  ingressClassName: nginx
  tls:
    - { hosts: [ciclourbana.ribalta.example], secretName: ciclourbana-tls }  # cert-manager el crea i renova
  rules:
    - host: ciclourbana.ribalta.example
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service: { name: ciclourbana, port: { name: http } }

Un Ingress és només una declaració: cal un controlador que la implementi (ingress-nginx, Traefik, o el del proveïdor). L'anotació cert-manager.io/cluster-issuer fa que cert-manager —un operador que cal instal·lar— sol·liciti el certificat a Let's Encrypt, validi el domini, desi el certificat al Secret indicat i el renovi automàticament abans de caducar. És l'equivalent d'ACM a 08-03.

Com que el controlador termina el TLS i parla HTTP amb els pods, continua fent falta allò de 08-01, que ja és al ConfigMap: SERVER_FORWARD_HEADERS_STRATEGY: framework.

  1. Migracions de Flyway: Job o initContainer

El ConfigMap va desactivar Flyway. La raó és la mateixa de 08-01 i 08-03, aquí més aguda: amb 3 rèpliques, tres pods arrencarien alhora i intentarien migrar simultàniament. Flyway ho serialitza amb un bloqueig a la base de dades, així que no corromp res, però els pods que esperen consumeixen la seva finestra d'arrencada, i si la migració falla els tres entren en CrashLoopBackOff i l'aplicació queda caiguda encara que la versió anterior funcionés.

# 06-job-migracio.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: ciclourbana-migracio-2-4-0      # el nom inclou la versio: cada desplegament, un Job
  namespace: ciclourbana-prod
spec:
  backoffLimit: 2                        # com a maxim 3 intents
  ttlSecondsAfterFinished: 3600          # s'autoelimina en 1 h
  template:
    spec:
      restartPolicy: Never               # un Job NO reinicia: falla o completa
      containers:
        - name: migracio
          image: ghcr.io/ajuntament-ribalta/ciclourbana:2.4.0     # LA MATEIXA imatge
          args:
            - "--spring.flyway.enabled=true"
            - "--spring.main.web-application-type=none"
          envFrom:
            - configMapRef: { name: ciclourbana-config }
            - secretRef:    { name: ciclourbana-secrets }
          resources: { requests: { cpu: "200m", memory: "512Mi" }, limits: { cpu: "1", memory: "1Gi" } }

Un Job executa el pod fins que acaba amb èxit. web-application-type=none aixeca el context sense Tomcat: Flyway migra i el procés surt. La seqüència del desplegament és llavors:

kubectl apply -f 06-job-migracio.yaml
kubectl wait --for=condition=complete --timeout=600s job/ciclourbana-migracio-2-4-0 -n ciclourbana-prod
kubectl set image deploy/ciclourbana ciclourbana=ghcr.io/ajuntament-ribalta/ciclourbana:2.4.0 -n ciclourbana-prod
kubectl rollout status deploy/ciclourbana -n ciclourbana-prod

Si el kubectl wait falla, no es toca el Deployment: la versió anterior continua servint Ribalta amb total normalitat.

L'alternativa de l'initContainer executa la migració dins de cada pod abans del contenidor principal. És més simple d'escriure, però N rèpliques executen N migracions: amb el bloqueig de Flyway no es corromp res, però es multiplica la feina, s'allarga l'arrencada de tots els pods i es perd la propietat més valuosa del Job —que una fallada de migració aturi el desplegament abans de tocar els pods vius—. El seu únic cas raonable és un Deployment amb una sola rèplica.

I el recordatori de sempre: durant el rolling update conviuen 2.3.0 i 2.4.0 contra l'esquema ja migrat, així que cada migració ha de ser compatible cap enrere — expand/contract de 04-08.

  1. Desplegament rolling, rollout status i rollout undo

Amb maxSurge: 1 i maxUnavailable: 0 sobre 3 rèpliques, Kubernetes crea un quart pod amb la versió nova, espera que la seva readinessProbe passi, l'afegeix al Service i només llavors en retira un de vell. I repeteix. En cap moment no hi ha menys de 3 pods sans atenent.

Els tres ajustos habituals: maxUnavailable: 0 amb maxSurge: 1 garanteix la capacitat a canvi d'un desplegament més lent i de necessitar marge al clúster; maxUnavailable: 1 amb maxSurge: 1 és més ràpid però admet un moment amb només 2 pods; i maxSurge: 3 és el més ràpid, duplicant el consum durant la transició.

kubectl rollout status  deploy/ciclourbana -n ciclourbana-prod   # espera i falla si no progressa
kubectl rollout history deploy/ciclourbana -n ciclourbana-prod   # revisions
kubectl rollout undo    deploy/ciclourbana [--to-revision=7]     # revertir
kubectl rollout restart deploy/ciclourbana                       # reiniciar sense canviar la imatge

rollout status és la peça que la canalització de 08-05 fa servir com a criteri d'èxit: retorna un codi diferent de zero si l'actualització no progressa en el termini (progressDeadlineSeconds, 600 s per defecte), cosa que permet automatitzar la reversió.

I l'advertiment de 08-01, que aquí és literal: rollout undo reverteix l'aplicació, no la base de dades. La migració aplicada continua aplicada. Si l'esquema no és compatible cap enrere, revertir empitjora la situació en lloc d'arreglar-la.

  1. HPA i PodDisruptionBudget

# 07-hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata: { name: ciclourbana, namespace: ciclourbana-prod }
spec:
  scaleTargetRef: { apiVersion: apps/v1, kind: Deployment, name: ciclourbana }
  minReplicas: 3
  maxReplicas: 10
  metrics:
    - type: Resource
      resource: { name: cpu, target: { type: Utilization, averageUtilization: 70 } }
  behavior:
    scaleUp:   { stabilizationWindowSeconds: 30 }
    scaleDown: { stabilizationWindowSeconds: 300 }

La relació entre l'HPA i resources és la clau i gairebé ningú no la veu a la primera: averageUtilization: 70 no significa el 70 % d'un nucli, sinó el 70 % de requests.cpu. Amb requests.cpu: 300m, l'objectiu és 210 milinuclis per pod. D'aquí, dues conseqüències pràctiques: si requests és molt baix, l'HPA escala constantment per res; si és molt alt, no escala mai encara que els pods pateixin. Sense requests.cpu, l'HPA no funciona en absolut i kubectl describe hpa mostra <unknown> a la columna de mètriques.

I com a 08-03: maxReplicas × maximum-pool-size + marge < max_connections de PostgreSQL. Amb 10 rèpliques i un pool de 10 són 100 connexions, més el Job de migració i l'administració. Cal comprovar-ho abans de pujar maxReplicas, no quan el pic de trànsit produeixi too many clients.

# 08-pdb.yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata: { name: ciclourbana, namespace: ciclourbana-prod }
spec:
  minAvailable: 2
  selector: { matchLabels: { app: ciclourbana } }

El PodDisruptionBudget protegeix davant de les interrupcions voluntàries: quan un administrador buida un node (kubectl drain) o l'autoescalador de nodes consolida màquines, Kubernetes respecta el pressupost i no retira pods si això deixés menys de 2 disponibles. No protegeix de fallades involuntàries —un node que mor s'endú els seus pods igualment— però elimina la causa més freqüent de caigudes evitables a Kubernetes: una actualització del clúster que buida dos nodes alhora i s'endú totes les rèpliques.

  1. Empaquetar amb Helm

Els vuit manifestos anteriors descriuen un entorn. Per a pre en calen vuit més de gairebé idèntics, amb diferent namespace, rèpliques, base de dades i domini: copiar i enganxar és la garantia que d'aquí a tres mesos pre i prod divergeixin sense que ningú sàpiga en què. Helm és el gestor de paquets de Kubernetes: converteix els manifestos en plantilles i separa els valors per entorn.

charts/ciclourbana/
├── Chart.yaml                  # nom, versio del chart, appVersion
├── values.yaml                 # valors per defecte
├── values-pre.yaml             # nomes el que canvia a pre
├── values-prod.yaml            # nomes el que canvia a prod
└── templates/                  # configmap, deployment, service, ingress,
    │                           # hpa, pdb, job-migracio...
    └── _helpers.tpl            # funcions de noms i etiquetes
# values.yaml
replicaCount: 3
image:   { repositori: ghcr.io/ajuntament-ribalta/ciclourbana, tag: "2.4.0" }
recursos: { requests: { cpu: 300m, memory: 768Mi }, limits: { cpu: "1", memory: 1Gi } }
ingress: { host: ciclourbana.ribalta.example }
config:  { perfil: prod, urlBaseDades: "jdbc:postgresql://ciclourbana-prod...:5432/ciclourbana?sslmode=require" }
autoescalat: { actiu: true, min: 3, max: 10, cpuObjectiu: 70 }
# values-pre.yaml — NOMES les diferencies
replicaCount: 1
recursos:
  requests: { cpu: 100m, memory: 512Mi }
  limits:   { cpu: 500m, memory: 768Mi }
ingress: { host: pre.ciclourbana.ribalta.example }
config:  { perfil: pre, urlBaseDades: "jdbc:postgresql://ciclourbana-pre...:5432/ciclourbana?sslmode=require" }
autoescalat: { actiu: false }

I la plantilla consumeix aquests valors:

# templates/deployment.yaml (fragment)
spec:
  replicas: {{ .Values.replicaCount }}
  template:
    spec:
      containers:
        - name: ciclourbana
          image: "{{ .Values.image.repositori }}:{{ .Values.image.tag }}"
          resources: {{- toYaml .Values.recursos | nindent 12 }}
helm lint ./charts/ciclourbana
helm template ciclourbana ./charts/ciclourbana -f values-pre.yaml   # veure el YAML final sense aplicar
helm install ciclourbana ./charts/ciclourbana -n ciclourbana-pre -f values-pre.yaml --create-namespace
helm upgrade ciclourbana ./charts/ciclourbana -n ciclourbana-prod -f values-prod.yaml \
  --set image.tag=2.4.1 --atomic --timeout 5m
helm rollback ciclourbana -n ciclourbana-prod      # a la revisio anterior

--atomic és l'opció que més tranquil·litat dona: si l'actualització no acaba bé en el termini, Helm reverteix automàticament a l'estat anterior. I --set image.tag=2.4.1 és la línia exacta que executarà la canalització de 08-05.

Alternativa: Kustomize, integrat a kubectl (kubectl apply -k), que no fa servir plantilles sinó pedaços sobre una base comuna:

Helm Kustomize
Mecanisme Plantilles Go amb valors Pedaços sobre manifestos base
Corba i instal·lació Mitjana; eina a part Baixa (YAML sobre YAML); inclòs a kubectl
Distribuir a tercers Sí: repositoris de charts No pensat per a això
Estat de la instal·lació Sí, amb historial i rollback No: només apply

Regla pràctica: Kustomize si només canvien uns pocs valors entre pre i prod; Helm si el chart té lògica, condicionals o cal distribuir-lo. Per a CicloUrbana, Helm compensa per --atomic, rollback i l'historial de versions.

  1. PostgreSQL dins o fora del clúster

Es pot executar PostgreSQL a Kubernetes amb un StatefulSet i un PVC, o amb un operador seriós com CloudNativePG o Zalando. Però:

Gestionada (RDS, Cloud SQL) Dins del clúster
Còpies, PITR i actualitzacions Automàtiques i provades pel proveïdor Tu les configures, tu les proves
Alta disponibilitat Multi-AZ amb commutació automàtica Un operador que cal entendre i operar
Rendiment de l'emmagatzematge Optimitzat Depèn de la StorageClass i la xarxa
Risc de pèrdua de dades Baix Alt si no es domina l'operador
Cost Més gran en factura Menor en factura, molt més gran en temps

El curs recomana sense matisos la base de dades gestionada. Kubernetes està dissenyat per a càrregues sense estat i llencables —pods que es maten i es recreen sense conseqüències— i una base de dades és exactament el contrari. Els operadors moderns són bons, però el dia que un PVC es corromp o una fallada de xarxa separa el primari de la rèplica, cal algú que sàpiga de PostgreSQL i de Kubernetes alhora. Aquest perfil és escàs, i l'ajuntament de Ribalta no el té.

Excepció raonable: PostgreSQL dins del clúster a dev i a les proves, on perdre les dades no costa res.

  1. Observabilitat i depuració d'un pod que no arrenca

kubectl logs deploy/ciclourbana -n ciclourbana-prod -f --tail=100
kubectl logs <pod> --previous          # el log del contenidor que va MORIR: clau en CrashLoopBackOff
kubectl get events -n ciclourbana-prod --sort-by=.lastTimestamp

Els logs de kubectl logs surten de l'stdout del contenidor i es perden quan el pod desapareix: en producció cal un agent que els reenviï a un agregador (Fluent Bit, Vector o el del proveïdor), que és el tema de 09-05.

El mètode de depuració, en aquest ordre exacte:

kubectl get pods -n ciclourbana-prod            # 1. en quin estat esta?
kubectl describe pod <pod> -n ciclourbana-prod  # 2. seccio Events, al final: gairebe sempre hi es
kubectl logs <pod> --previous                   # 3. si va reiniciar, el log del contenidor anterior
Estat Significat Causes habituals
Pending No s'ha pogut col·locar No hi ha node amb CPU/memòria suficients per als requests; PVC sense enllaçar
ImagePullBackOff No pot descarregar la imatge Etiqueta inexistent, registre privat sense imagePullSecrets, error de nom
CreateContainerConfigError No pot construir el contenidor El ConfigMap o el Secret referenciat no existeix
CrashLoopBackOff Arrenca i mor repetidament Excepció a l'arrencada, migració fallida, liveness mal configurada
Running però 0/1 READY Viu però no llest readinessProbe fallant: gairebé sempre no arriba a la base de dades
OOMKilled El kernel el va matar per memòria limits.memory baix o MaxRAMPercentage massa alt

OOMKilled en una JVM mereix explicació a part, perquè despista. No és un OutOfMemoryError de Java: no hi ha traça, no hi ha excepció, no hi ha res al log. El procés simplement desapareix i kubectl describe pod mostra Last State: Terminated, Reason: OOMKilled, Exit Code: 137, perquè la JVM va demanar al sistema més memòria de la que el cgroup permet i el kernel la va matar. Les causes, per freqüència: MaxRAMPercentage massa alt o absent (el heap creix fins al límit i la resta de la memòria de la JVM no hi cap); limits.memory insuficient per al que l'aplicació necessita de debò; i, en últim lloc, una fuita real, que convé diagnosticar amb -XX:+HeapDumpOnOutOfMemoryError i un volum on escriure el bolcat.

I la distinció pràctica: un OutOfMemoryError de Java sí que deixa traça al log i, amb ExitOnOutOfMemoryError (07-04), surt amb codi 1. Un OOMKilled surt amb 137 i no deixa res. Si veus 137, és el kernel; si veus 1 amb traça, és el heap.

Per depurar un pod amb readOnlyRootFilesystem i sense shell existeix kubectl debug -it <pod> --image=busybox --target=ciclourbana, que adjunta al pod en execució un contenidor efímer amb eines.

  1. GitOps i Spring Cloud Kubernetes

GitOps inverteix la direcció del desplegament: en lloc que la canalització empenyi canvis al clúster amb kubectl (cosa que exigeix donar-li credencials d'administrador), un agent dins del clúster vigila un repositori Git i aplica el que hi troba.

El flux queda: el desenvolupador obre un pull request sobre el repositori de manifestos, la canalització es limita a actualitzar image.tag en aquest repositori, i Argo CD —que viu dins del clúster— detecta el canvi, el sincronitza i compara contínuament l'estat real amb el declarat.

Avantatges concrets: Git és l'única font de veritat i el seu historial diu qui va desplegar què i quan; la reversió és un git revert; la canalització no necessita credencials del clúster, cosa que redueix molt la superfície d'atac (08-05); i l'agent detecta la deriva, marcant com a OutOfSync qualsevol canvi fet a mà. Les eines de referència són Argo CD (interfície web molt visual) i Flux (més lleuger, integrat amb Helm).

Spring Cloud Kubernetes, finalment, permet a l'aplicació llegir ConfigMap i Secret com a fonts de propietats, descobrir serveis per l'API de Kubernetes i recarregar configuració en calent. Sovint no cal, i és important saber per què: muntar el ConfigMap com a variables (el nostre envFrom) ja resol la configuració amb zero dependències; el descobriment el fa el DNS intern, que funciona amb qualsevol client HTTP —el RestClient de 07-06 crida http://patinets:8080 sense més—; i la recàrrega en calent afegeix complexitat davant d'un kubectl rollout restart, que és explícit, auditable i sempre funciona. A més obligaria a donar a l'aplicació permisos RBAC sobre l'API, ampliant la seva superfície d'atac. Val la pena amb desenes de serveis i configuració centralitzada; per a CicloUrbana, el ConfigMap per envFrom és la resposta correcta i manté l'artefacte agnòstic de la plataforma, com demana 08-01.

Errors Comuns i Consells

Posar la base de dades al grup liveness. Un manteniment de 40 segons reinicia tots els pods i provoca CrashLoopBackOff durant vint minuts. Vida = procés sa; disponibilitat = pot treballar.

No posar startupProbe. La livenessProbe mata la JVM abans que acabi d'arrencar i el pod mai no arriba a estar llest. I MaxRAMPercentage sense marge produeix OOMKilled amb codi 137 i res al log: deixa el 25 %.

Oblidar requests.cpu. L'HPA no pot calcular la utilització, mostra <unknown> i no escala mai. I readOnlyRootFilesystem sense muntar /tmp fa fallar Tomcat en escriure el seu directori de treball, amb un error d'arrencada poc evident.

Secrets en base64 donats per xifrats. Base64 es desfà amb una ordre. Fes servir RBAC estricte, xifratge en repòs i External Secrets o Sealed Secrets.

Canviar un ConfigMap esperant que s'apliqui sol. Els pods conserven les variables amb què van arrencar: cal kubectl rollout restart. I migrar des d'un initContainer amb diverses rèpliques significa N pods, N migracions, i una fallada que tomba el desplegament sencer en lloc d'aturar-lo abans.

Consell: kubectl describe pod abans que qualsevol altra cosa. La secció Events del final explica en text clar el 80 % dels problemes.

Consell: verifica abans d'aplicar. helm template mostra el YAML final resolt i kubectl apply --dry-run=server -f el valida contra l'API real sense crear res: els dos passos de verificació de la canalització.

Consell: etiqueta-ho tot amb app, version i entorn. Els selectors, les consultes de logs i les mètriques en depenen.

Exercicis

Exercici 1

Escriu el Deployment de CicloUrbana per a l'entorn pre: 1 rèplica, requests de 200m/512Mi i limits de 500m/768Mi, imatge 2.5.0-rc1, les tres sondes correctament configurades i securityContext endurit. Calcula quin valor de MaxRAMPercentage correspon i justifica cada failureThreshold sabent que a pre l'aplicació triga uns 50 segons a arrencar per tenir menys CPU.

Exercici 2

Es desplega la versió 2.4.0 i kubectl get pods mostra durant deu minuts:

NAME                           READY   STATUS             RESTARTS   AGE
ciclourbana-7d4b8c9f5-2xk9p    0/1     CrashLoopBackOff   6          9m
ciclourbana-7d4b8c9f5-8mq2w    0/1     CrashLoopBackOff   6          9m
ciclourbana-7d4b8c9f5-p4v7t    0/1     CrashLoopBackOff   6          9m

Descriu el procediment de diagnòstic pas a pas i desenvolupa les cinc causes més probables per a aquest símptoma concret, amb l'evidència que confirma cadascuna i la seva correcció. Tingues en compte que la versió 2.3.0 funcionava correctament fins fa deu minuts.

Exercici 3

L'ajuntament vol desplegar CicloUrbana a pre i prod des del mateix codi, amb una sola ordre per entorn i sense duplicar manifestos. Dissenya el chart de Helm: què va a values.yaml, què a values-pre.yaml i values-prod.yaml, com es parametritza el Job de migració perquè s'executi una vegada per versió, i quina és la seqüència completa d'ordres que executaria la canalització de 08-05 per desplegar la versió 2.4.1 en producció de manera segura i reversible.

Solucions

Solució 1

Partint del Deployment de l'apartat 5, canvien el namespace a ciclourbana-pre, replicas: 1, la imatge a 2.5.0-rc1 i aquests blocs:

          resources:
            requests: { cpu: "200m", memory: "512Mi" }
            limits:   { cpu: "500m", memory: "768Mi" }
          startupProbe:
            httpGet: { path: /actuator/health/liveness, port: gestio }
            periodSeconds: 5
            failureThreshold: 30          # 150 s: el triple dels 50 s mesurats
          livenessProbe:
            httpGet: { path: /actuator/health/liveness, port: gestio }
            periodSeconds: 10
            failureThreshold: 3           # 30 s per detectar un proces irrecuperable
          readinessProbe:
            httpGet: { path: /actuator/health/readiness, port: gestio }
            periodSeconds: 5
            failureThreshold: 3           # 15 s per sortir de rotacio

El securityContext endurit (runAsNonRoot, runAsUser: 10001, allowPrivilegeEscalation: false, readOnlyRootFilesystem: true, capabilities: drop: ["ALL"]), l'emptyDir a /tmp, el preStop i terminationGracePeriodSeconds: 60 es mantenen idèntics: no depenen de l'entorn.

MaxRAMPercentage. Amb limits.memory: 768Mi, el 75 % són 576 MB de heap i queden 192 MB per a la resta. En una JVM amb Hibernate i Spring Security, metaespai (~90 MB), piles de fils (~40 MB) i búfers directes ronden ja els 150-180 MB: és massa just. En contenidors petits cal ser més conservador: MaxRAMPercentage=60 (460 MB de heap, 308 MB de marge). És un principi general i contraintuïtiu: com més petit el contenidor, menor ha de ser el percentatge, perquè els costos fixos de la JVM no escalen amb la mida.

Els failureThreshold. El startupProbe és l'únic que ha d'acomodar l'arrencada: 50 s mesurats, però amb limits.cpu: 500m —mig nucli— la JVM pateix throttling i en un dia de càrrega pot trigar el doble, així que 30 intents × 5 s = 150 s donen un marge de tres vegades sense ser absurd. Un cop superat, liveness amb 3 × 10 s detecta un procés irrecuperable en 30 segons, i readiness amb 3 × 5 s treu el pod de rotació en 15. Amb replicas: 1, readiness fallant significa 503 per a tot pre, cosa que és correcta i honesta: no hi ha a qui enviar el trànsit.

Solució 2

Procediment. Primer kubectl describe pod ciclourbana-7d4b8c9f5-2xk9p -n ciclourbana-prod i llegir la secció Events i el camp Last State (raó i codi de sortida). Després kubectl logs ciclourbana-7d4b8c9f5-2xk9p --previous, que és la part que més s'oblida: sense --previous es demana el log del contenidor actual, que encara no ha escrit res. I en paral·lel, kubectl get events --sort-by=.lastTimestamp i kubectl rollout history per veure què va canviar respecte a 2.3.0.

Les cinc causes, amb la seva evidència:

1. La migració de Flyway falla i ddl-auto: validate rebutja l'esquema. Primera sospita, perquè 2.3.0 funcionava: el que va canviar és la versió i amb ella l'esquema esperat. Evidència: al log previ, FlywayException o Schema-validation: missing column [places_totals] in table [estacions]. Correcció: revisar el Job (kubectl logs job/ciclourbana-migracio-2-4-0); si va fallar i el Deployment es va actualitzar igualment, s'ha saltat el kubectl wait — revertir només si l'esquema continua sent compatible i arreglar la canalització perquè el desplegament depengui del Job.

2. OOMKilled. Evidència: Last State: Terminated, Reason: OOMKilled, Exit Code: 137, i el log previ no mostra cap error: l'aplicació arrencava amb normalitat i desapareix. Correcció: pujar limits.memory o baixar MaxRAMPercentage; si 2.4.0 va afegir una memòria cau o una consulta que carrega molts objectes, el límit s'ha quedat curt.

3. livenessProbe massa agressiva o mal apuntada. Evidència: a Events, Liveness probe failed: HTTP probe failed with statuscode: 503 seguit de Killing container, i al log previ l'aplicació arriba a Started CicloUrbanaApplication i mor poc després. Correcció: verificar que liveness inclou només livenessState i no db, i que hi ha startupProbe; si 2.4.0 arrenca més lenta que 2.3.0, la sonda que abans bastava ara no arriba.

4. Un Secret o ConfigMap que falta o va canviar. Evidència: l'estat seria CreateContainerConfigError si l'objecte no existeix; si existeix amb un valor malament, el log previ mostra Could not resolve placeholder 'JWT_SECRET' o una fallada d'autenticació contra PostgreSQL. Correcció: kubectl get secret ciclourbana-secrets -o yaml i comprovar que hi són totes les claus que 2.4.0 necessita — una propietat nova al codi i no afegida al ConfigMap produeix exactament això.

5. La imatge no és la que es creu. Evidència: un Image ID que no correspon, o exec format error al log previ (imatge arm64 construïda en Apple Silicon, 08-03). Correcció: reconstruir amb --platform linux/amd64 i fer servir etiquetes immutables.

Regla d'or per a aquest escenari: els tres pods fallen igual, així que no és un problema d'un node ni d'infraestructura; és la versió nova. I l'acció immediata correcta —abans d'investigar— és kubectl rollout undo deploy/ciclourbana, sempre que l'esquema continuï sent compatible cap enrere. Si la causa 1 és la real i la migració va ser destructiva, revertir no arregla res: és l'escenari de l'exercici 3 de 08-01.

Solució 3

Repartiment de valors. A values.yaml va tot el comú i el que rarament canvia: repositori de la imatge, ports, les tres sondes amb els seus llindars, securityContext, preStop, terminationGracePeriodSeconds, etiquetes i les anotacions de l'Ingress. A values-pre.yaml i values-prod.yaml, només les diferències: replicaCount, recursos, ingress.host, config.perfil, config.urlBaseDades, autoescalat i el nom del Secret. El criteri: si un valor és igual als dos entorns, no pot estar als fitxers per entorn, o abans o després divergiran sense motiu.

El Job parametritzat per versió. La clau és que el nom inclogui l'etiqueta de la imatge, perquè cada desplegament creï un Job diferent (Kubernetes rebutjaria recrear-ne un amb el mateix nom) i quedi el rastre de cada migració:

# templates/job-migracio.yaml (fragment)
metadata:
  name: {{ include "ciclourbana.fullname" . }}-migracio-{{ .Values.image.tag | replace "." "-" }}
  annotations:
    "helm.sh/hook": pre-upgrade,pre-install
    "helm.sh/hook-weight": "-5"
    "helm.sh/hook-delete-policy": before-hook-creation
spec:
  backoffLimit: 2
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: migracio
          image: "{{ .Values.image.repositori }}:{{ .Values.image.tag }}"
          args: ["--spring.flyway.enabled=true", "--spring.main.web-application-type=none"]

Els hooks de Helm són la peça elegant: pre-upgrade fa que Helm executi el Job i esperi que acabi amb èxit abans de tocar el Deployment. Si falla, helm upgrade avorta i els pods continuen amb la versió anterior — exactament la propietat que buscàvem a l'apartat 8, ara sense scripts intermedis.

Seqüència de la canalització per a 2.4.1:

# 1. Verificacio estatica, sense tocar el cluster
helm lint ./charts/ciclourbana
helm template ciclourbana ./charts/ciclourbana -f values-prod.yaml --set image.tag=2.4.1 | kubectl apply --dry-run=server -f -

# 2. Desplegar a pre i esperar que convergeixi
helm upgrade --install ciclourbana ./charts/ciclourbana -n ciclourbana-pre -f values-pre.yaml --set image.tag=2.4.1 --atomic --timeout 8m

# 3. Prova de fum contra pre
curl -fsS https://pre.ciclourbana.ribalta.example/actuator/health/readiness
curl -fsS https://pre.ciclourbana.ribalta.example/api/v1/estacions | jq 'length'

# 4. Aprovacio manual (entorn protegit de GitHub Actions, 08-05)

# 5. Produccio: el hook migra, i si falla no es toca el Deployment
helm upgrade ciclourbana ./charts/ciclourbana -n ciclourbana-prod -f values-prod.yaml --set image.tag=2.4.1 --atomic --timeout 10m

# 6. Verificar que corre el que es creu, i revertir si no
kubectl rollout status deploy/ciclourbana -n ciclourbana-prod
curl -fsS https://ciclourbana.ribalta.example/actuator/info | jq '.build.version'
helm rollback ciclourbana -n ciclourbana-prod

Per què és segur i reversible. --atomic reverteix sol si el desplegament no convergeix en el termini; el hook pre-upgrade garanteix que una migració fallida aturi el procés abans de tocar els pods vius; helm rollback torna a la revisió anterior en una ordre; l'/actuator/info amb build-info de 07-01 confirma quina versió corre de debò; i la condició de fons, la de sempre: la reversió només funciona si la migració de 2.4.1 és compatible cap enrere (expand/contract, 04-08).

Conclusió

CicloUrbana corre ara en un orquestrador, i ho fa sabent per què i quan això té sentit. La lliçó va començar amb un advertiment que convé no oblidar: per a una sola aplicació, ECS o un PaaS solen bastar i solen ser millors, i Kubernetes només compensa quan hi ha molts serveis, diversos equips autònoms, exigència de portabilitat o automatització avançada. Amb aquesta honestedat de partida, has construït la xarxa de Ribalta completa sobre el clúster.

Coneixes l'arquitectura —pla de control, etcd, planificador, controladors i kubelet— i el model mental que l'explica: es declara l'estat desitjat i els controladors redueixen la diferència amb la realitat. Manegues els objectes que importen i tens els manifestos comentats: un Namespace per entorn, un ConfigMap amb la configuració no sensible i un Secret amb l'advertiment clar que base64 no és xifratge, juntament amb les solucions reals —RBAC estricte, xifratge en repòs, Sealed Secrets i External Secrets Operator, la millor opció quan el magatzem ja és a AWS (08-03)—.

El Deployment reuneix tot el que el curs anava preparant: la imatge de 07-04, requests i limits casats amb MaxRAMPercentage —amb el principi que com més petit el contenidor, menor ha de ser el percentatge—, variables des de configMapRef i secretRef, un securityContext amb usuari no root, capacitats eliminades i sistema de fitxers de només lectura amb /tmp en un emptyDir, i el preStop de deu segons amb terminationGracePeriodSeconds folgat que fa encaixar l'aturada ordenada de 01-05 amb la propagació dels endpoints. I sobretot, les tres sondes: startupProbe perquè la JVM tingui temps d'arrencar sense que liveness la mati, livenessProbe sobre livenessState per reiniciar només allò irrecuperable, i readinessProbe sobre el grup que inclou db per sortir de rotació sense morir. Saps exactament com un manteniment de 40 segons de la base de dades es converteix en vint minuts de CrashLoopBackOff si es confonen, i per què la configuració de 07-01 ho evita.

Tens a més el Service ClusterIP que no publica el port de gestió, l'Ingress amb TLS renovat per cert-manager, les migracions en un Job que atura el desplegament si falla, el rolling update amb maxUnavailable: 0 i el seu rollout undo amb l'advertiment sobre la base de dades, l'HPA amb la seva relació amb requests.cpu que gairebé ningú no veu a la primera, el PodDisruptionBudget, i el chart de Helm amb values per entorn, hooks per a la migració i --atomic. Saps quan Kustomize és millor, per què la base de dades ha de continuar sent gestionada, com es depura un pod que no arrenca —describe primer, Events al final, logs --previous després— i què signifiquen CrashLoopBackOff, ImagePullBackOff i un OOMKilled amb codi 137 que no deixa rastre al log.

I aquí acaba el recorregut per les plataformes. CicloUrbana es pot desplegar en un PaaS, en contenidors gestionats o en un orquestrador, i en els tres casos les decisions de fons són les de 08-01. Però a les tres lliçons hi ha hagut un actor implícit que continuava sent humà: algú que executa git push heroku main, aws ecs update-service o helm upgrade des del seu portàtil, amb les seves credencials, esperant haver provat abans el que desplega. L'última lliçó del mòdul, Integració i Lliurament Continu, elimina aquest actor: una canalització que compila, executa les proves del mòdul 6 —inclosos els Testcontainers—, analitza la qualitat, construeix i escaneja la imatge, la publica etiquetada amb el SHA del commit, desplega a pre, passa les proves de fum i, després d'una aprovació explícita, la porta a producció sense que ningú toqui un servidor a mà.

Curs de Spring Boot

Mòdul 1: Introducció a Spring Boot

Mòdul 2: Conceptes bàsics de Spring Boot

Mòdul 3: Construint serveis web RESTful

Mòdul 4: Accés a dades amb Spring Boot

Mòdul 5: Seguretat a Spring Boot

Mòdul 6: Proves a Spring Boot

Mòdul 7: Funcions avançades de Spring Boot

Mòdul 8: Desplegament d'aplicacions Spring Boot

Mòdul 9: Rendiment i monitoratge

Mòdul 10: Millors pràctiques i consells

© Copyright 2026. Tots els drets reservats