Al final de la lliçó anterior van quedar tres preguntes obertes. Com fer que la migració d'esquema s'executi automàticament abans que arrenqui api-reserves, en comptes de com un Job a part? Com evitar que api-reserves entri en CrashLoopBackOff quan arrenca abans que postgres-reserves? Com exportar mètriques de PostgreSQL sense tocar la imatge oficial?

Les tres es responen igual: posant més d'un contenidor al mateix pod.

Des de la lliçó 02-01 sabem que un pod és un grup de contenidors que comparteixen espai de noms de xarxa —i per tant localhost—, volums i cicle de vida. Fins ara no havíem aprofitat aquesta capacitat: tots els pods de Rutas Norte tenen un sol contenidor. En aquesta lliçó la farem servir amb criteri, que és la part difícil: la majoria de pods multicontenidor mal dissenyats neixen de posar dues aplicacions juntes perquè "van relacionades", i això sempre acaba malament.

Contingut

  1. Quan un pod necessita diversos contenidors i quan això és un error
  2. Init containers: preparació seqüencial abans de l'arrencada
  3. Casos d'ús d'init containers a Rutas Norte
  4. Diagnòstic d'init containers fallits
  5. Sidecars natius: init containers amb restartPolicy: Always
  6. Els tres patrons clàssics: sidecar, ambaixador i adaptador
  7. Patró sidecar: exportador de mètriques al costat de postgres-reserves
  8. Patró ambaixador: la connexió amb la passarel·la de pagaments
  9. Patró adaptador: normalitzar el log de worker-notificacions
  10. Ordre d'arrencada i terminació, i el cost real

  1. Quan un pod necessita diversos contenidors i quan això és un error

La regla d'or, formulada amb precisió:

Dos processos van al mateix pod quan estan tan acoblats que no té sentit escalar-los, actualitzar-los ni executar-los per separat, i un d'ells existeix únicament per servir l'altre.

La conseqüència immediata de compartir pod és que comparteixen destí: es programen al mateix node, escalen junts, es reinicien junts i s'actualitzen junts. Si api-reserves necessita 5 rèpliques i worker-notificacions en necessita 2, posar-los al mateix pod obliga a tenir-ne 5 de cadascun. Això és un error de disseny, no una optimització.

Situació Mateix pod? Per què
api-reserves + exportador de les seves mètriques L'exportador no serveix de res sense la seva aplicació; escalen alhora
api-reserves + worker-notificacions No Escalen diferent, es despleguen diferent, són dues aplicacions
worker-notificacions + adaptador de logs L'adaptador processa el fitxer local d'aquell procés concret
botiga-web + postgres-reserves No Cicles de vida i necessitats d'emmagatzematge radicalment diferents
Procés principal + proxy cap a un servei extern El proxy és infraestructura local del procés
Recol·lector de logs de tot el node No Això és un DaemonSet (06-02)

Tres preguntes de control abans d'afegir un contenidor a un pod existent:

  1. Han d'escalar junts sempre? Si la resposta és no, són dues càrregues de treball diferents.
  2. El segon serveix exclusivament el primer? Si altres pods també el necessiten, és un Service a part.
  3. Necessiten compartir localhost o un sistema de fitxers? Si no, no hi ha motiu tècnic per ajuntar-los.

Kubernetes ofereix dues categories de contenidor auxiliar:

  • initContainers: s'executen abans, en ordre, un rere l'altre, i han d'acabar amb èxit.
  • Contenidors auxiliars que acompanyen el principal durant tota la vida del pod: els sidecars.

  1. Init containers: preparació seqüencial abans de l'arrencada

Un init container és un contenidor que s'executa fins a completar-se abans que arrenqui cap contenidor principal. Si n'hi ha diversos, s'executen en l'ordre en què apareixen al manifest, cadascun esperant que l'anterior acabi amb èxit.

graph LR
  A[Pod programat] --> I1[initContainer 1<br/>esperar-postgres]
  I1 -->|exit 0| I2[initContainer 2<br/>migrar-esquema]
  I2 -->|exit 0| C[containers<br/>api arrenca]
  I1 -->|exit != 0| R1[reinici de l'initContainer]
  R1 --> I1

Propietats que els distingeixen dels contenidors normals:

Propietat Init container Contenidor principal
Moment d'execució Abans de tots els principals Després de tots els init
Execució Seqüencial, un rere l'altre Tots en paral·lel
Ha d'acabar Sí, amb codi 0 No; acabar és una anomalia
Si falla Es reintenta segons la restartPolicy del pod Es reinicia segons la restartPolicy
Sondes No admet readinessProbe ni startupProbe
Imatge Sol ser diferent i amb més eines La de l'aplicació

Aquesta última fila és més útil del que sembla: l'init container pot fer servir una imatge amb psql, curl o git que la imatge de producció no té, sense engreixar la imatge final ni ampliar-ne la superfície d'atac.

Sintaxi bàsica:

spec:
  template:
    spec:
      initContainers:
        - name: primer
          image: busybox:1.36
          command: ["sh", "-c", "echo preparant; sleep 2"]
        - name: segon
          image: busybox:1.36
          command: ["sh", "-c", "echo llest"]
      containers:
        - name: api
          image: registry.rutasnorte.example/api-reserves:2.5.0

Els init containers també consumeixen recursos i participen en el càlcul del que el pod demana al scheduler. La fórmula efectiva és:

petició del pod = max( petició més gran entre els initContainers ,
                       suma de peticions dels containers )

És a dir, un init container que demana 2 GiB obliga el scheduler a trobar un node amb 2 GiB lliures, encara que l'aplicació només necessiti 256 MiB. Convé mantenir-los lleugers.

  1. Casos d'ús d'init containers a Rutas Norte

Cas 1: esperar que postgres-reserves accepti connexions

Quan s'aixeca un entorn des de zero, api-reserves pot arrencar abans que la base de dades estigui llesta, fallar en connectar, sortir i entrar en CrashLoopBackOff. Acaba recuperant-se, però l'arrencada triga minuts i els esdeveniments s'omplen de soroll que emmascara problemes reals.

      initContainers:
        - name: esperar-postgres
          image: postgres:16.4
          command:
            - /bin/sh
            - -c
            - |
              echo "Esperant postgres-reserves..."
              until pg_isready -h postgres-reserves -p 5432 -U "${PGUSER}" -t 3; do
                echo "  encara no respon; reintent en 2 s"
                sleep 2
              done
              echo "postgres-reserves accepta connexions"
          env:
            - name: PGUSER
              valueFrom:
                secretKeyRef:
                  name: postgres-reserves-credencials
                  key: usuari
          resources:
            requests:
              cpu: 50m
              memory: 64Mi
            limits:
              cpu: 100m
              memory: 128Mi

pg_isready és una utilitat de la mateixa imatge de PostgreSQL que retorna 0 si el servidor accepta connexions. El bucle no té límit d'intents a propòsit: el pod es quedarà en Init:0/1 indefinidament, cosa que a kubectl get pods és un senyal claríssim de "la base de dades no està disponible", molt millor que un CrashLoopBackOff la causa del qual s'ha d'anar a buscar als logs.

Una nota de disseny honesta: això no substitueix que l'aplicació gestioni la reconnexió. Si postgres-reserves cau dues hores després, l'init container ja no hi és per ajudar. És una comoditat per a l'arrencada, no una garantia de resiliència.

Cas 2: executar la migració d'esquema

Aquí reprenem la pregunta que va deixar oberta la lliçó 06-03. La migració pot anar en un init container en comptes d'en un Job separat:

      initContainers:
        - name: esperar-postgres
          # ... com a dalt ...
        - name: migrar-esquema
          image: registry.rutasnorte.example/api-reserves-migracions:2.5.0
          command: ["/app/migrar", "--fins=2.5.0"]
          env:
            - name: PGHOST
              value: postgres-reserves
            - name: PGUSER
              valueFrom:
                secretKeyRef:
                  name: postgres-reserves-credencials
                  key: usuari
            - name: PGPASSWORD
              valueFrom:
                secretKeyRef:
                  name: postgres-reserves-credencials
                  key: password
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
            limits:
              cpu: 500m
              memory: 256Mi

Avantatge evident: la migració va acoblada al desplegament. És impossible desplegar el codi 2.5.0 sense haver migrat, perquè el contenidor de l'aplicació no arrenca si l'init container no acaba bé.

Però hi ha un parany important que cal conèixer:

Amb 4 rèpliques d'api-reserves, l'init container de migració s'executa 4 vegades, potencialment en paral·lel durant una actualització.

Conseqüències i mitigacions:

  • La migració ha de ser idempotent i estar protegida per un lock. IF NOT EXISTS no n'hi ha prou: dos ALTER TABLE simultanis es poden bloquejar mútuament. Una eina de migracions seriosa (Flyway, Liquibase, golang-migrate) pren un lock d'aplicació a la base de dades i les altres instàncies esperen.
  • Si la migració triga minuts, cada rèplica paga aquesta espera, i el desplegament s'allarga.
Enfocament Avantatge Inconvenient
Job separat (06-03) S'executa una vegada; control explícit Cal recordar-se de llançar-lo abans; es pot oblidar
initContainer al Deployment Impossible desplegar sense migrar S'executa per rèplica; exigeix lock i idempotència

Recomanació per a Rutas Norte: Job separat per a migracions grans o destructives (índexs sobre milions de files, canvis de tipus), initContainer per a migracions petites i idempotents del dia a dia.

Cas 3: preparar fitxers en un emptyDir compartit

botiga-web serveix HTML estàtic des de nginx, i la plantilla del peu de pàgina canvia segons l'entorn. En comptes de construir tres imatges, un init container genera el fitxer en un volum compartit:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: botiga-web
  namespace: rutas-norte-pro
  labels:
    app: botiga-web
    app.kubernetes.io/part-of: rutas-norte
    entorn: pro
spec:
  replicas: 3
  selector:
    matchLabels:
      app: botiga-web
      entorn: pro
  template:
    metadata:
      labels:
        app: botiga-web
        app.kubernetes.io/part-of: rutas-norte
        entorn: pro
    spec:
      automountServiceAccountToken: false
      initContainers:
        - name: preparar-contingut
          image: busybox:1.36
          command:
            - sh
            - -c
            - |
              set -eu
              cp /plantilles/index.html /public/index.html
              sed -i "s|__ENTORN__|${ENTORN}|g" /public/index.html
              sed -i "s|__NODE__|${NODE}|g"     /public/index.html
              echo "Contingut preparat per a l'entorn ${ENTORN}"
          env:
            - name: ENTORN
              value: "pro"
            - name: NODE
              valueFrom:
                fieldRef:
                  fieldPath: spec.nodeName
          resources:
            requests:
              cpu: 20m
              memory: 32Mi
            limits:
              cpu: 100m
              memory: 64Mi
          volumeMounts:
            - name: plantilles
              mountPath: /plantilles
              readOnly: true
            - name: public
              mountPath: /public
      containers:
        - name: nginx
          image: nginx:1.27.2-alpine
          ports:
            - name: http
              containerPort: 80
          resources:
            requests:
              cpu: 50m
              memory: 64Mi
            limits:
              cpu: 300m
              memory: 128Mi
          volumeMounts:
            - name: public
              mountPath: /usr/share/nginx/html
              readOnly: true
      volumes:
        - name: plantilles
          configMap:
            name: botiga-web-plantilles
        - name: public
          emptyDir: {}

El mecanisme és exactament l'emptyDir de 05-01: un volum buit creat amb el pod, compartit per tots els seus contenidors. L'init container escriu, nginx llegeix en només lectura. Quan el pod mor, l'emptyDir desapareix, i tant se val: es regenera a l'arrencada següent.

  1. Diagnòstic d'init containers fallits

Els init containers tenen els seus propis estats a la columna STATUS, i saber llegir-los estalvia molt de temps.

kubectl get pods -n rutas-norte-pro -l app=api-reserves
STATUS Significat
Init:0/2 Executant el primer init container de dos; cap de completat
Init:1/2 El primer va acabar bé; executant el segon
Init:Error Un init container va sortir amb codi diferent de 0 i restartPolicy: Never
Init:CrashLoopBackOff Un init container falla repetidament amb restartPolicy: Always
PodInitializing Tots els init van acabar; arrencant els principals
Running Tot en marxa

Un cas real: api-reserves encallada perquè la base de dades no respon.

NAME                            READY   STATUS     RESTARTS   AGE
api-reserves-6c8f9d4b7-jm2xq    0/1     Init:0/2   0          4m

Init:0/2 sostingut quatre minuts: el primer init container (esperar-postgres) continua al seu bucle.

kubectl logs -n rutas-norte-pro api-reserves-6c8f9d4b7-jm2xq -c esperar-postgres --tail=4
Esperant postgres-reserves...
  encara no respon; reintent en 2 s
  encara no respon; reintent en 2 s
  encara no respon; reintent en 2 s

La clau és a -c <nom>. Sense aquesta bandera, kubectl logs intenta llegir el contenidor principal, que encara no existeix, i respon amb un error confús.

Un altre cas: la migració falla.

NAME                            READY   STATUS       RESTARTS      AGE
api-reserves-7f4d2b9c8-k3n5p    0/1     Init:Error   3 (52s ago)   3m
kubectl logs -n rutas-norte-pro api-reserves-7f4d2b9c8-k3n5p -c migrar-esquema --previous
Aplicant migració 2.5.0...
ERROR: column "canal_venda" of relation "reserves" already exists (SQLSTATE 42701)
migració fallida

Diagnòstic: la migració no és idempotent. Hi faltava l'IF NOT EXISTS.

Ordres útils per inspeccionar init containers:

# Noms dels init containers d'un pod
kubectl get pod <pod> -n <ns> \
  -o jsonpath='{range .spec.initContainers[*]}{.name}{"\n"}{end}'

# Estat detallat de cadascun
kubectl get pod <pod> -n <ns> -o jsonpath='{.status.initContainerStatuses}' | python3 -m json.tool

# Vista completa, amb la secció "Init Containers" separada
kubectl describe pod <pod> -n <ns>

A la sortida de describe, els init containers apareixen en un bloc Init Containers: abans del bloc Containers:, cadascun amb el seu estat, el seu codi de sortida i la seva raó de terminació.

  1. Sidecars natius: init containers amb restartPolicy: Always

Durant anys, un sidecar era simplement "un altre contenidor més a la llista containers". Funcionava, però tenia dos defectes greus que Kubernetes 1.29 va resoldre i que són estables des de 1.33.

Els dos problemes del sidecar clàssic

Problema 1: no hi ha ordre d'arrencada. Tots els contenidors de containers arrenquen en paral·lel. Si el sidecar és un proxy pel qual l'aplicació ha de sortir a la xarxa, i l'aplicació arrenca abans que el proxy, les primeres peticions fallen.

Problema 2: els Jobs no acabaven mai. Un Job el pod del qual té un sidecar era un problema sense solució neta. El contenidor principal acaba amb èxit, però el sidecar continua viu, així que el pod no arriba mai a Succeeded i el Job es queda penjat indefinidament. L'única sortida eren apedaçaments lletjos: fitxers sentinella en un emptyDir, o que el contenidor principal matés el sidecar per l'API.

La solució: restartPolicy: Always en un init container

      initContainers:
        - name: exportador-metriques
          image: prometheuscommunity/postgres-exporter:v0.16.0
          restartPolicy: Always      # <- això el converteix en sidecar natiu
          ports:
            - name: metriques
              containerPort: 9187

Aquest únic camp canvia completament la semàntica del contenidor:

Init container normal Sidecar natiu (restartPolicy: Always) Contenidor de containers
Moment d'arrencada En ordre, abans que tot En ordre, abans dels principals En paral·lel amb els altres
Bloqueja el següent? Sí, fins a acabar No: n'hi ha prou que estigui iniciat No aplica
Durada Acaba Viu tot el pod Viu tot el pod
Si surt Es passa al següent Es reinicia Es reinicia
En acabar el pod Ja no hi és Es para després dels principals Es para en paral·lel
Bloqueja la finalització d'un Job? No No

Les quatre conseqüències pràctiques, en ordre d'importància:

  1. Arrenca abans que els contenidors principals, garantint que el proxy o l'exportador estiguin llestos quan l'aplicació comença a treballar.
  2. Continua viu durant tota la vida del pod i es reinicia si mor, cosa que un init container normal no fa.
  3. S'acaba després dels contenidors principals, així que un sidecar de logs captura els últims missatges del tancament.
  4. No impedeix que un Job acabi. Quan els contenidors de containers acaben, el kubelet para els sidecars i el pod arriba a Succeeded. Això desbloqueja tota la feina per lots amb sidecars: un CronJob amb proxy, amb exportador de mètriques o amb adaptador de logs simplement funciona.

Una precisió sobre l'ordre: el pod no espera que el sidecar acabi (no ho farà mai), sinó que estigui iniciat —i que passi la seva startupProbe si en té—. A diferència dels init containers normals, els sidecars sí que admeten sondes, tema de la lliçó 07-01.

Regla de decisió senzilla: si el contenidor auxiliar ha d'estar llest abans que l'aplicació, o si el pod és d'un Job, fes servir sidecar natiu. En els altres casos, un contenidor normal a containers continua sent perfectament vàlid.

  1. Els tres patrons clàssics: sidecar, ambaixador i adaptador

Els tres noms vénen de l'article fundacional de Brendan Burns i Dave Oppenheimer sobre patrons de disseny per a sistemes distribuïts. Es distingeixen per cap a on va el flux i què transformen.

Patró Què fa Direcció del flux Exemple a Rutas Norte
Sidecar Afegeix una capacitat que l'aplicació no té, sense modificar-la Lateral: observa o complementa Exportador de mètriques de postgres-reserves
Ambaixador (ambassador) Intermedia la sortida cap a un servei extern Aplicació → exterior Proxy cap a pagos.proveedorexterno.example
Adaptador (adapter) Normalitza la sortida de l'aplicació a un format estàndard Aplicació → exterior, transformant Convertir el log propietari de worker-notificacions a JSON

Una altra manera de memoritzar-ho:

  • El sidecar afegeix alguna cosa.
  • L'ambaixador simplifica el que l'aplicació veu del món exterior.
  • L'adaptador simplifica el que el món exterior veu de l'aplicació.

Tots tres es recolzen en els mateixos dos mecanismes, que ja coneixes de 02-01:

  • localhost compartit: tots els contenidors del pod comparteixen l'espai de noms de xarxa, així que es parlen per 127.0.0.1 sense passar per la xarxa del clúster, sense DNS i sense latència apreciable. Corol·lari: dos contenidors del mateix pod no poden fer servir el mateix port.
  • Volums compartits: un emptyDir muntat als dos permet passar fitxers. És el canal del patró adaptador.
graph TB
  subgraph POD[Pod]
    direction LR
    APP[Contenidor principal]
    SC[Sidecar<br/>afegeix capacitat]
    EM[Ambaixador<br/>proxy de sortida]
    AD[Adaptador<br/>normalitza format]
    APP <-->|localhost| SC
    APP -->|localhost:8080| EM
    APP -->|emptyDir| AD
  end
  EM -->|TLS + reintents| EXT[pagos.proveedorexterno.example]
  SC -->|:9187/metrics| PROM[Prometheus 07-03]
  AD -->|stdout JSON| LOGS[Recol·lector 06-02]

  1. Patró sidecar: exportador de mètriques al costat de postgres-reserves

La imatge postgres:16.4 no exposa mètriques en format Prometheus. Modificar-la seria un error: perdríem les actualitzacions oficials i hauríem de mantenir una imatge pròpia. La solució és un sidecar que es connecta a PostgreSQL per localhost, executa consultes d'estat i publica el resultat a /metrics.

Afegim el sidecar al StatefulSet que vam construir a 06-01:

# k8s/base/postgres-reserves-statefulset.yaml (fragment)
spec:
  template:
    spec:
      serviceAccountName: postgres-reserves
      automountServiceAccountToken: false
      initContainers:
        - name: exportador-metriques
          image: prometheuscommunity/postgres-exporter:v0.16.0
          restartPolicy: Always          # sidecar natiu
          ports:
            - name: metriques
              containerPort: 9187
          env:
            # localhost: el sidecar comparteix la xarxa del contenidor de PostgreSQL
            - name: DATA_SOURCE_URI
              value: "localhost:5432/reserves?sslmode=disable"
            - name: DATA_SOURCE_USER
              valueFrom:
                secretKeyRef:
                  name: postgres-reserves-credencials
                  key: usuari
            - name: DATA_SOURCE_PASS
              valueFrom:
                secretKeyRef:
                  name: postgres-reserves-credencials
                  key: password
          resources:
            requests:
              cpu: 20m
              memory: 48Mi
            limits:
              cpu: 100m
              memory: 96Mi
          securityContext:
            runAsNonRoot: true
            runAsUser: 65534
            readOnlyRootFilesystem: true
            allowPrivilegeEscalation: false
            capabilities:
              drop: ["ALL"]
      containers:
        - name: postgres
          image: postgres:16.4
          # ... la resta igual que a 06-01 ...

Punts que expliquen el patró:

  • DATA_SOURCE_URI: localhost:5432: no hi ha nom de servei ni DNS. El sidecar parla amb PostgreSQL a través del bucle local del pod. És l'avantatge fonamental: latència mínima, sense exposar el port 5432 a la xarxa del clúster per a això, i sense necessitat de NetworkPolicy addicional, perquè el trànsit ni tan sols surt del pod.
  • Sidecar natiu: arrenca abans que PostgreSQL. En principi no és imprescindible aquí, però garanteix que no perdem les mètriques dels primers segons de vida, que són justament les de l'arrencada i la recuperació del WAL.
  • Recursos modestos i separats: 20m de CPU i 48 MiB. Se sumen als de PostgreSQL en el càlcul del scheduler.
  • Enduriment propi: el sidecar corre com a nobody i amb sistema de fitxers de només lectura. No necessita res més, i així una fallada a l'exportador no compromet el contenidor de la base de dades.

Nota important sobre QoS: a 03-05 vam establir que postgres-reserves és de classe Guaranteed. Perquè el pod ho continuï sent, tots els seus contenidors —sidecar inclòs— han de tenir requests iguals a limits. Amb els valors de dalt (20m/100m, 48Mi/96Mi) el pod passaria a Burstable. Si volem conservar Guaranteed, cal igualar-los:

          resources:
            requests:
              cpu: 100m
              memory: 96Mi
            limits:
              cpu: 100m
              memory: 96Mi

És una conseqüència poc intuïtiva d'afegir sidecars que convé tenir present.

El Service que exposa les mètriques:

apiVersion: v1
kind: Service
metadata:
  name: postgres-reserves-metriques
  namespace: rutas-norte-pro
  labels:
    app: postgres-reserves
    app.kubernetes.io/part-of: rutas-norte
    entorn: pro
spec:
  selector:
    app: postgres-reserves
    entorn: pro
  ports:
    - name: metriques
      port: 9187
      targetPort: metriques

Comprovació:

kubectl exec -n rutas-norte-pro postgres-reserves-0 -c exportador-metriques -- \
  wget -qO- localhost:9187/metrics | grep -E '^pg_up|^pg_stat_database_numbackends' | head -3
pg_up 1
pg_stat_database_numbackends{datname="reserves"} 14

pg_up 1 confirma que l'exportador pot connectar; numbackends dona les connexions actives. Aquestes mètriques són les que Prometheus recollirà a la lliçó 07-03; aquí només n'hem muntat la font.

  1. Patró ambaixador: la connexió amb la passarel·la de pagaments

api-reserves cobra a través de pagos.proveedorexterno.example, un servei extern amb les complicacions habituals: TLS mutu amb certificat de client, reintents amb retrocés, tallacircuits quan el proveïdor va lent, límit de peticions per segon i un endpoint de proves diferent a cada entorn.

Ficar tota aquesta lògica a api-reserves significa escriure-la en Node.js, mantenir-la, provar-la i tornar-la a fer si demà apareix un segon proveïdor. L'ambaixador la treu del codi: un proxy local que escolta a localhost i s'ocupa de tot.

# k8s/base/api-reserves-deployment.yaml (fragment)
spec:
  template:
    spec:
      serviceAccountName: api-reserves
      automountServiceAccountToken: false
      initContainers:
        - name: ambaixador-pagaments
          image: envoyproxy/envoy:v1.31.3
          restartPolicy: Always        # sidecar natiu: ha d'estar llest abans que l'API
          args: ["-c", "/etc/envoy/envoy.yaml", "--log-level", "warn"]
          ports:
            - name: pagaments-local
              containerPort: 8081
          resources:
            requests:
              cpu: 50m
              memory: 64Mi
            limits:
              cpu: 200m
              memory: 128Mi
          securityContext:
            runAsNonRoot: true
            runAsUser: 65534
            allowPrivilegeEscalation: false
            capabilities:
              drop: ["ALL"]
          volumeMounts:
            - name: config-ambaixador
              mountPath: /etc/envoy
              readOnly: true
            - name: certificats-pagaments
              mountPath: /etc/certificats
              readOnly: true
      containers:
        - name: api
          image: registry.rutasnorte.example/api-reserves:2.5.0
          env:
            # L'aplicació parla HTTP pla contra localhost. Res més.
            - name: PASSARELLA_PAGAMENTS_URL
              value: "http://127.0.0.1:8081"
          ports:
            - name: http
              containerPort: 3000
          resources:
            requests:
              cpu: 200m
              memory: 256Mi
            limits:
              cpu: "1"
              memory: 512Mi
      volumes:
        - name: config-ambaixador
          configMap:
            name: ambaixador-pagaments-config
        - name: certificats-pagaments
          secret:
            secretName: pagaments-certificat-client
            defaultMode: 0400

I la configuració del proxy, resumida a l'essencial:

# k8s/base/ambaixador-pagaments-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: ambaixador-pagaments-config
  namespace: rutas-norte-pro
data:
  envoy.yaml: |
    static_resources:
      listeners:
        - name: pagaments_local
          address:
            socket_address: { address: 127.0.0.1, port_value: 8081 }
          filter_chains:
            - filters:
                - name: envoy.filters.network.http_connection_manager
                  typed_config:
                    "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
                    stat_prefix: pagaments
                    route_config:
                      virtual_hosts:
                        - name: pagaments
                          domains: ["*"]
                          routes:
                            - match: { prefix: "/" }
                              route:
                                cluster: passarella_externa
                                timeout: 8s
                                retry_policy:
                                  retry_on: "5xx,connect-failure,reset"
                                  num_retries: 3
                    http_filters:
                      - name: envoy.filters.http.router
                        typed_config:
                          "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
      clusters:
        - name: passarella_externa
          connect_timeout: 3s
          type: LOGICAL_DNS
          circuit_breakers:
            thresholds:
              - max_connections: 50
                max_pending_requests: 20
          load_assignment:
            cluster_name: passarella_externa
            endpoints:
              - lb_endpoints:
                  - endpoint:
                      address:
                        socket_address:
                          address: pagos.proveedorexterno.example
                          port_value: 443
          transport_socket:
            name: envoy.transport_sockets.tls
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
              sni: pagos.proveedorexterno.example
              common_tls_context:
                tls_certificates:
                  - certificate_chain: { filename: /etc/certificats/tls.crt }
                    private_key: { filename: /etc/certificats/tls.key }

El que hi ha guanyat Rutas Norte:

Responsabilitat Abans: a api-reserves Ara: a l'ambaixador
TLS mutu amb certificat de client Codi Node.js + gestió de fitxers Configuració declarativa
Reintents davant de 5xx i talls Llibreria i lògica pròpia retry_policy
Tallacircuits Llibreria i lògica pròpia circuit_breakers
Temps d'espera Constants repartides pel codi timeout en un sol lloc
Endpoint diferent per entorn Variable d'entorn i condicionals Un ConfigMap per entorn
Rotació del certificat Redesplegament de l'aplicació Canvi del Secret

I sobretot: api-reserves fa un POST HTTP pla a http://127.0.0.1:8081/cobraments i ja està. A les proves locals de desenvolupament, aquest endpoint pot ser un simulador; l'aplicació no nota la diferència.

La NetworkPolicy de 04-06 que autoritza la sortida a la passarel·la continua aplicant-se al pod, no al contenidor, així que no canvia: el pod sencer necessita permís de sortida al port 443 extern.

Un aclariment necessari: si aquesta idea s'aplica a tot el trànsit de tots els pods, amb un pla de control que distribueix la configuració, ja no se'n diu ambaixador sinó malla de serveis (Istio, Linkerd). Això és territori de la lliçó 08-04; aquí resolem un cas concret sense adoptar una plataforma sencera.

  1. Patró adaptador: normalitzar el log de worker-notificacions

worker-notificacions és un component heretat que escriu en un fitxer, amb un format propi i amb traces multilínia quan hi ha una excepció:

2026-08-05 03:14:22 | ENVIAMENT_OK | reserva=4471 | [email protected] | ms=312
2026-08-05 03:14:25 | ENVIAMENT_ERR | reserva=4472 | [email protected] | causa=SMTP timeout
  at smtp.enviar (smtp.js:88)
  at cua.processar (cua.js:41)

El recol·lector de logs del DaemonSet de 06-02 llegeix la sortida estàndard dels contenidors, no fitxers arbitraris, i encara que els llegís, aquest format no és consultable: no hi ha camps, i una excepció es parteix en tres entrades sense relació.

L'adaptador resol les dues coses: llegeix el fitxer des d'un volum compartit, uneix les línies de continuació i emet JSON estructurat per la seva pròpia sortida estàndard, on el recol·lector sí que el recull.

# k8s/base/worker-notificacions-deployment.yaml (fragment)
spec:
  template:
    spec:
      automountServiceAccountToken: false
      initContainers:
        - name: adaptador-logs
          image: fluent/fluent-bit:3.1.9
          restartPolicy: Always      # sidecar natiu: es para DESPRÉS del worker
          resources:
            requests:
              cpu: 30m
              memory: 48Mi
            limits:
              cpu: 100m
              memory: 96Mi
          volumeMounts:
            - name: logs-worker
              mountPath: /logs
              readOnly: true
            - name: config-adaptador
              mountPath: /fluent-bit/etc
              readOnly: true
      containers:
        - name: worker
          image: registry.rutasnorte.example/worker-notificacions:1.9.3
          env:
            - name: LOG_FITXER
              value: /logs/notificacions.log
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
            limits:
              cpu: 500m
              memory: 256Mi
          volumeMounts:
            - name: logs-worker
              mountPath: /logs
      volumes:
        - name: logs-worker
          emptyDir:
            sizeLimit: 256Mi     # sense límit, un log desbocat omple el disc del node
        - name: config-adaptador
          configMap:
            name: adaptador-logs-config
# k8s/base/adaptador-logs-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: adaptador-logs-config
  namespace: rutas-norte-pro
data:
  fluent-bit.conf: |
    [SERVICE]
        Flush        3
        Log_Level    error
        Parsers_File parsers.conf

    [INPUT]
        Name              tail
        Path              /logs/notificacions.log
        Tag               notificacions
        Parser            worker_rutasnorte
        Multiline.parser  worker_traca
        Refresh_Interval  5

    [FILTER]
        Name    record_modifier
        Match   notificacions
        Record  component worker-notificacions
        Record  entorn    pro

    [OUTPUT]
        Name   stdout
        Match  notificacions
        Format json_lines

  parsers.conf: |
    [PARSER]
        Name        worker_rutasnorte
        Format      regex
        Regex       ^(?<time>[\d-]+ [\d:]+) \| (?<nivell>\w+) \| reserva=(?<reserva>\d+) \| desti=(?<desti>[^ ]+) \|(?<resta>.*)$
        Time_Key    time
        Time_Format %Y-%m-%d %H:%M:%S

    [MULTILINE_PARSER]
        Name          worker_traca
        Type          regex
        Flush_Timeout 1000
        Rule          "start_state"  "^\d{4}-\d{2}-\d{2} "  "cont"
        Rule          "cont"         "^  at "               "cont"

Resultat a la sortida estàndard de l'adaptador, que és el que el recol·lector del node s'emporta:

kubectl logs -n rutas-norte-pro deployment/worker-notificacions -c adaptador-logs --tail=1
{"date":1754363665.0,"nivell":"ENVIAMENT_ERR","reserva":"4472","desti":"[email protected]","resta":" causa=SMTP timeout\n  at smtp.enviar (smtp.js:88)\n  at cua.processar (cua.js:41)","component":"worker-notificacions","entorn":"pro"}

L'excepció viatja sencera en un sol registre, amb els seus camps separats i etiquetada amb el component i l'entorn. A 07-05 això permetrà consultes del tipus "tots els ENVIAMENT_ERR de la reserva 4472".

Aquí el sidecar natiu aporta una cosa concreta i important: com que s'acaba després del contenidor principal, captura els últims missatges que worker-notificacions escriu durant el seu tancament ordenat amb SIGTERM (02-01). Amb un contenidor normal a containers, tots dos reben SIGTERM alhora i aquestes últimes línies —sovint les que expliquen per què es va aturar— es perden.

El sizeLimit: 256Mi de l'emptyDir no és opcional: un emptyDir sense límit escriu al disc del node fins a omplir-lo, i llavors el node entra en disk-pressure i desallotja pods aliens.

  1. Ordre d'arrencada i terminació, i el cost real

La seqüència completa

Amb init containers i sidecars natius, el cicle de vida d'un pod queda així:

sequenceDiagram
    participant K as kubelet
    participant I as initContainers normals
    participant S as sidecars (restartPolicy Always)
    participant C as containers principals
    K->>I: arrenca en ordre; espera que cadascun acabi amb èxit
    I-->>K: exit 0
    K->>S: arrenca en ordre; espera que estiguin iniciats
    S-->>K: iniciat (i startupProbe superada si n'hi ha)
    K->>C: arrenca tots en paral·lel
    Note over C: vida útil del pod
    K->>C: SIGTERM als principals
    C-->>K: acabats (o vençut el període de gràcia)
    K->>S: SIGTERM als sidecars, en ordre invers
    S-->>K: acabats

Punts a retenir:

  1. Els init containers normals s'executen seqüencialment i fins a acabar.
  2. Els sidecars natius arrenquen en l'ordre declarat, i n'hi ha prou que estiguin iniciats.
  3. Els contenidors principals arrenquen tots alhora, sense garantia d'ordre entre ells.
  4. A la terminació, primer els principals; després els sidecars, en ordre invers.
  5. El terminationGracePeriodSeconds és del pod, no de cada contenidor: és el pressupost total per a tot el tancament.

El cost real

Cada sidecar és un contenidor més per cada pod, i aquesta multiplicació és fàcil de subestimar. Números de Rutas Norte en producció:

Component Rèpliques Sidecar CPU sidecar Memòria sidecar Total CPU Total memòria
api-reserves 6 Ambaixador de pagaments 50m 64Mi 300m 384Mi
worker-notificacions 3 Adaptador de logs 30m 48Mi 90m 144Mi
postgres-reserves 1 Exportador de mètriques 100m 96Mi 100m 96Mi
Total 0,49 CPU 624Mi

Mig nucli i 600 MiB només en contenidors auxiliars. En un pic de pont festiu, amb api-reserves autoescalada a 20 rèpliques (09-01), només l'ambaixador ja són 1 CPU i 1,25 GiB.

I hi ha costos menys visibles:

  • Cada sidecar és una imatge que cal mantenir, escanejar i actualitzar (08-05). Tres sidecars són tres cadenes de subministrament més.
  • Cada sidecar pot fallar i, amb restartPolicy: Always, entrar en bucle de reinici arrossegant el pod sencer.
  • Cada sidecar suma temps a l'arrencada, i els natius el sumen de manera seqüencial abans de l'aplicació.
  • La depuració es complica: tot kubectl logs i kubectl exec ja necessita la bandera -c.

Preguntes de control abans d'afegir un sidecar:

  1. Ho pot fer un DaemonSet, un per node en comptes d'un per pod? Per a logs i mètriques de node, gairebé sempre sí.
  2. Ho pot fer la mateixa aplicació amb una llibreria? De vegades cinc línies de codi substitueixen un contenidor de 60 MiB.
  3. Justifica el sidecar el seu cost multiplicat pel nombre màxim de rèpliques? Calcula el pitjor cas, no l'habitual.

Errors Comuns i Consells

Oblidar -c <contenidor> a kubectl logs i kubectl exec. Amb diversos contenidors, kubectl exigeix saber quin. L'error a container name must be specified és inequívoc, però quan hi ha init containers el missatge pot despistar perquè el contenidor principal encara no existeix.

Posar un init container en bucle infinit sense visibilitat. Un until ... done sense echo deixa el pod en Init:0/1 sense cap log que expliqui l'espera. Imprimeix sempre alguna cosa a cada iteració.

Init containers pesants. El scheduler reserva el màxim entre els init containers, així que un que demani 2 GiB obliga a trobar un node amb 2 GiB lliures encara que l'aplicació necessiti 256 MiB. Mantén-los petits.

Migracions en initContainer sense lock. Amb N rèpliques, la migració s'executa N vegades, potencialment en paral·lel. Sense idempotència i sense lock d'aplicació, el resultat és una base de dades a mig migrar. Fes servir una eina de migracions que prengui lock, o un Job separat (06-03).

Dos contenidors del mateix pod escoltant al mateix port. Comparteixen l'espai de xarxa, així que el segon falla amb address already in use. Porta un registre de quin port fa servir cada contenidor auxiliar (3000 l'API, 8081 l'ambaixador, 9187 l'exportador...).

Sidecar clàssic en un pod de Job. És el parany històric: el Job no acaba mai. La solució a 1.29+ és un sidecar natiu amb restartPolicy: Always com a init container.

emptyDir sense sizeLimit per a logs. Un log que creix sense control omple el disc del node i provoca disk-pressure, amb desallotjament de pods que no hi tenien res a veure. Posa sempre sizeLimit.

Trencar la classe QoS en afegir un sidecar. Un pod és Guaranteed només si tots els seus contenidors tenen requests == limits. Afegir un sidecar amb valors diferents degrada el pod a Burstable i canvia la seva prioritat de desallotjament (03-05).

Consell: anomena els contenidors per la seva funció, no per la seva tecnologia. ambaixador-pagaments és molt més útil en una alerta a les tres de la matinada que envoy.

Consell: kubectl describe pod és la millor vista. Mostra Init Containers: i Containers: en blocs separats, amb l'estat, el codi de sortida i la raó de cadascun. És més ràpid que encadenar jsonpath.

Consell: kubectl logs --all-containers=true bolca tot el pod de cop, útil per reconstruir la seqüència d'una arrencada problemàtica.

Exercicis

Exercici 1: init container que espera una dependència

A rutas-norte-dev, crea un Deployment api-demo amb una rèplica de nginx:1.27.2-alpine i un init container que esperi que existeixi i respongui un Service anomenat dependencia-demo. Aplica el Deployment abans de crear la dependència i observa l'estat del pod. Després crea la dependència (un Deployment i un Service amb nginx) i comprova que api-demo arrenca.

Exercici 2: patró adaptador amb emptyDir compartit

A rutas-norte-dev, crea un Deployment worker-demo amb:

  • Un contenidor principal worker que cada 5 segons escrigui una línia en format propietari a /logs/sortida.log (per exemple 2026-08-05 10:00:00 | ENVIAMENT_OK | reserva=4471).
  • Un sidecar natiu adaptador (init container amb restartPolicy: Always) que segueixi aquest fitxer i emeti cada línia per la seva sortida estàndard amb el prefix [adaptat].
  • Un emptyDir amb sizeLimit: 64Mi compartit.

Verifica que el sidecar arrenca abans que el principal i que emet les línies.

Exercici 3: sidecar natiu en un Job

Crea a rutas-norte-dev un Job informe-amb-sidecar el pod del qual tingui un contenidor principal que trigui 15 segons i acabi, i un sidecar natiu que escrigui alguna cosa cada 3 segons indefinidament. Comprova que el Job que arriba a Complete. Després raona què hauria passat si el sidecar estigués declarat a containers en comptes de com a init container amb restartPolicy: Always.

Solucions

Solució 1

# /tmp/api-demo.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-demo
  namespace: rutas-norte-dev
  labels:
    app: api-demo
    app.kubernetes.io/part-of: rutas-norte
    entorn: dev
spec:
  replicas: 1
  selector:
    matchLabels:
      app: api-demo
      entorn: dev
  template:
    metadata:
      labels:
        app: api-demo
        app.kubernetes.io/part-of: rutas-norte
        entorn: dev
    spec:
      automountServiceAccountToken: false
      initContainers:
        - name: esperar-dependencia
          image: busybox:1.36
          command:
            - sh
            - -c
            - |
              echo "Esperant dependencia-demo:80..."
              until wget -q -T 2 -O /dev/null http://dependencia-demo:80 2>/dev/null; do
                echo "  encara no respon; reintent en 3 s"
                sleep 3
              done
              echo "dependencia-demo disponible"
          resources:
            requests:
              cpu: 10m
              memory: 16Mi
            limits:
              cpu: 50m
              memory: 32Mi
      containers:
        - name: nginx
          image: nginx:1.27.2-alpine
          ports:
            - containerPort: 80
          resources:
            requests:
              cpu: 50m
              memory: 64Mi
            limits:
              cpu: 200m
              memory: 128Mi
kubectl apply -f /tmp/api-demo.yaml
kubectl get pods -n rutas-norte-dev -l app=api-demo
NAME                        READY   STATUS     RESTARTS   AGE
api-demo-5b7c9d6f4-x8k2m    0/1     Init:0/1   0          35s

Init:0/1: l'init container està corrent i cap no s'ha completat.

kubectl logs -n rutas-norte-dev -l app=api-demo -c esperar-dependencia --tail=3
Esperant dependencia-demo:80...
  encara no respon; reintent en 3 s
  encara no respon; reintent en 3 s

Ara la dependència:

kubectl create deployment dependencia-demo -n rutas-norte-dev --image=nginx:1.27.2-alpine
kubectl label deployment dependencia-demo -n rutas-norte-dev app=dependencia-demo entorn=dev --overwrite
kubectl expose deployment dependencia-demo -n rutas-norte-dev --port=80

kubectl wait --for=condition=ready pod -l app=api-demo -n rutas-norte-dev --timeout=120s
kubectl get pods -n rutas-norte-dev -l app=api-demo
NAME                        READY   STATUS    RESTARTS   AGE
api-demo-5b7c9d6f4-x8k2m    1/1     Running   0          2m10s
kubectl logs -n rutas-norte-dev -l app=api-demo -c esperar-dependencia --tail=1
dependencia-demo disponible

El pod va passar d'Init:0/1 a PodInitializing i després a Running sense cap reinici. Aquest RESTARTS: 0 és la millora davant de deixar que l'aplicació entri en CrashLoopBackOff mentre espera.

Solució 2

# /tmp/worker-demo.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: worker-demo
  namespace: rutas-norte-dev
  labels:
    app: worker-demo
    app.kubernetes.io/part-of: rutas-norte
    entorn: dev
spec:
  replicas: 1
  selector:
    matchLabels:
      app: worker-demo
      entorn: dev
  template:
    metadata:
      labels:
        app: worker-demo
        app.kubernetes.io/part-of: rutas-norte
        entorn: dev
    spec:
      automountServiceAccountToken: false
      initContainers:
        - name: adaptador
          image: busybox:1.36
          restartPolicy: Always          # sidecar natiu
          command:
            - sh
            - -c
            - |
              echo "[adaptador] arrencat abans que el worker"
              touch /logs/sortida.log
              tail -F /logs/sortida.log | while read -r LINIA; do
                echo "[adaptat] $LINIA"
              done
          resources:
            requests:
              cpu: 10m
              memory: 16Mi
            limits:
              cpu: 50m
              memory: 32Mi
          volumeMounts:
            - name: logs
              mountPath: /logs
      containers:
        - name: worker
          image: busybox:1.36
          command:
            - sh
            - -c
            - |
              N=4471
              while true; do
                echo "$(date '+%Y-%m-%d %H:%M:%S') | ENVIAMENT_OK | reserva=$N" >> /logs/sortida.log
                N=$(( N + 1 ))
                sleep 5
              done
          resources:
            requests:
              cpu: 20m
              memory: 32Mi
            limits:
              cpu: 100m
              memory: 64Mi
          volumeMounts:
            - name: logs
              mountPath: /logs
      volumes:
        - name: logs
          emptyDir:
            sizeLimit: 64Mi
kubectl apply -f /tmp/worker-demo.yaml
kubectl wait --for=condition=ready pod -l app=worker-demo -n rutas-norte-dev --timeout=120s

POD=$(kubectl get pod -n rutas-norte-dev -l app=worker-demo -o jsonpath='{.items[0].metadata.name}')
kubectl logs -n rutas-norte-dev "$POD" -c adaptador --tail=4
[adaptador] arrencat abans que el worker
[adaptat] 2026-08-05 19:14:02 | ENVIAMENT_OK | reserva=4471
[adaptat] 2026-08-05 19:14:07 | ENVIAMENT_OK | reserva=4472
[adaptat] 2026-08-05 19:14:12 | ENVIAMENT_OK | reserva=4473

La primera línia confirma l'ordre: el sidecar va imprimir el seu missatge d'arrencada abans que el worker escrivís res, perquè els sidecars natius arrenquen abans que els contenidors de containers. El contenidor principal, en canvi, no imprimeix res per la seva sortida estàndard:

kubectl logs -n rutas-norte-dev "$POD" -c worker --tail=3
(sense sortida)

Tot el seu registre va al fitxer, i només arriba al recol·lector del node gràcies a l'adaptador. Aquest és exactament el propòsit del patró.

Solució 3

# /tmp/informe-amb-sidecar.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: informe-amb-sidecar
  namespace: rutas-norte-dev
  labels:
    app: informes-ocupacio
    entorn: dev
spec:
  backoffLimit: 1
  ttlSecondsAfterFinished: 3600
  template:
    metadata:
      labels:
        app: informes-ocupacio
        entorn: dev
    spec:
      restartPolicy: Never
      automountServiceAccountToken: false
      initContainers:
        - name: metriques-lot
          image: busybox:1.36
          restartPolicy: Always      # sidecar natiu: NO impedeix que el Job acabi
          command:
            - sh
            - -c
            - 'while true; do echo "[metriques] batec $(date +%H:%M:%S)"; sleep 3; done'
          resources:
            requests:
              cpu: 10m
              memory: 16Mi
            limits:
              cpu: 50m
              memory: 32Mi
      containers:
        - name: generador
          image: busybox:1.36
          command:
            - sh
            - -c
            - 'echo "generant informe d''ocupació..."; sleep 15; echo "informe generat"'
          resources:
            requests:
              cpu: 20m
              memory: 32Mi
            limits:
              cpu: 100m
              memory: 64Mi
kubectl apply -f /tmp/informe-amb-sidecar.yaml
kubectl wait --for=condition=complete job/informe-amb-sidecar -n rutas-norte-dev --timeout=120s
kubectl get job informe-amb-sidecar -n rutas-norte-dev
NAME                  STATUS     COMPLETIONS   DURATION   AGE
informe-amb-sidecar   Complete   1/1           19s        25s

El Job arriba a Complete en 19 segons tot i que el sidecar continuava imprimint batecs indefinidament.

POD=$(kubectl get pod -n rutas-norte-dev -l job-name=informe-amb-sidecar -o jsonpath='{.items[0].metadata.name}')
kubectl logs -n rutas-norte-dev "$POD" -c metriques-lot --tail=3
kubectl get pod "$POD" -n rutas-norte-dev
[metriques] batec 19:22:14
[metriques] batec 19:22:17
[metriques] batec 19:22:20

NAME                        READY   STATUS      RESTARTS   AGE
informe-amb-sidecar-4kx2p   0/2     Completed   0          45s

El pod està Completed amb els seus dos contenidors aturats.

Què hauria passat amb el sidecar a containers: el contenidor generador hauria acabat amb èxit als 15 segons, però metriques-lot hauria continuat viu. Un pod només arriba a la fase Succeeded quan tots els seus contenidors han acabat, així que el pod s'hauria quedat indefinidament en Running amb 1/2 contenidors llestos, i el Job en 0/1 completions per sempre. Només activeDeadlineSeconds ho hauria tallat, i amb estat Failed.

Aquest era exactament el problema històric que els sidecars natius van resoldre.

# Neteja
kubectl delete -f /tmp/informe-amb-sidecar.yaml
kubectl delete -f /tmp/worker-demo.yaml
kubectl delete -f /tmp/api-demo.yaml
kubectl delete deployment,service dependencia-demo -n rutas-norte-dev

Conclusió

Un pod amb diversos contenidors és una eina potent i fàcil de fer servir malament. La regla que la governa és que els processos comparteixin pod només quan estiguin tan acoblats que no tingui sentit escalar-los ni desplegar-los per separat, i quan un existeixi per servir l'altre.

Els init containers s'executen en ordre, fins a completar-se, abans que arrenqui cap contenidor principal, i a Rutas Norte ens serveixen per esperar postgres-reserves, aplicar migracions petites i idempotents, i preparar contingut en un emptyDir compartit. Els seus estats —Init:0/2, Init:Error, Init:CrashLoopBackOff— són diagnòstics per si mateixos, i kubectl logs -c <nom> és l'ordre que cal interioritzar.

Els sidecars natius de Kubernetes 1.29+ són init containers amb restartPolicy: Always: arrenquen abans que els contenidors principals, viuen durant tot el pod, es reinicien si moren, es paren després dels principals i —cosa que va tancar un problema d'anys— no impedeixen que un Job acabi.

Els tres patrons clàssics es distingeixen per la direcció del flux: el sidecar afegeix una capacitat (l'exportador de mètriques de postgres-reserves que Prometheus consumirà a 07-03); l'ambaixador intermedia la sortida cap a l'exterior (el proxy que s'ocupa de TLS mutu, reintents i tallacircuits contra pagos.proveedorexterno.example); l'adaptador normalitza el que l'aplicació produeix (el conversor a JSON del log propietari de worker-notificacions). Tots tres es recolzen en localhost i en volums compartits, i tots tres costen recursos multiplicats pel nombre de pods, cosa que cal calcular en el pitjor cas d'escalat.

Fins aquí hem decidit què s'executa i com es compon cada pod. La pregunta que no hem tocat és on: fins ara el kube-scheduler ha col·locat els nostres pods on ha volgut, i això ha bastat. Però postgres-reserves hauria d'estar en un node amb disc SSD, les rèpliques d'api-reserves no haurien de compartir node —si aquell node cau, cau l'API sencera—, i les càrregues d'anàlisi no haurien de competir amb la venda de bitllets. Tot això es controla amb afinitat, taints i toleracions, i és el tema de la lliçó següent: Planificació.

Curs de Kubernetes

Mòdul 1: Introducció a Kubernetes

Mòdul 2: Components Principals de Kubernetes

Mòdul 3: Gestió de Configuració i Secrets

Mòdul 4: Xarxes a Kubernetes

Mòdul 5: Emmagatzematge a Kubernetes

Mòdul 6: Conceptes Avançats de Kubernetes

Mòdul 7: Monitoratge i Registre

Mòdul 8: Seguretat a Kubernetes

Mòdul 9: Escalat i Rendiment

Mòdul 10: Ecosistema i Eines de Kubernetes

Mòdul 11: Estudis de Cas i Aplicacions del Món Real

Mòdul 12: Preparació per a la Certificació de Kubernetes

© Copyright 2026. Tots els drets reservats