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
- Quan un pod necessita diversos contenidors i quan això és un error
- Init containers: preparació seqüencial abans de l'arrencada
- Casos d'ús d'init containers a Rutas Norte
- Diagnòstic d'init containers fallits
- Sidecars natius: init containers amb
restartPolicy: Always - Els tres patrons clàssics: sidecar, ambaixador i adaptador
- Patró sidecar: exportador de mètriques al costat de
postgres-reserves - Patró ambaixador: la connexió amb la passarel·la de pagaments
- Patró adaptador: normalitzar el log de
worker-notificacions - Ordre d'arrencada i terminació, i el cost real
- 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 |
Sí | 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 |
Sí | 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 | Sí | 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:
- Han d'escalar junts sempre? Si la resposta és no, són dues càrregues de treball diferents.
- El segon serveix exclusivament el primer? Si altres pods també el necessiten, és un Service a part.
- Necessiten compartir
localhosto 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.
- 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 |
Sí |
| 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.0Els 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.
- 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: 128Mipg_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: 256MiAvantatge 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 EXISTSno n'hi ha prou: dosALTER TABLEsimultanis 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.
- 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.
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.
Init:0/2 sostingut quatre minuts: el primer init container (esperar-postgres) continua al seu bucle.
Esperant postgres-reserves...
encara no respon; reintent en 2 s
encara no respon; reintent en 2 s
encara no respon; reintent en 2 sLa 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.
Aplicant migració 2.5.0...
ERROR: column "canal_venda" of relation "reserves" already exists (SQLSTATE 42701)
migració fallidaDiagnò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ó.
- Sidecars natius: init containers amb
restartPolicy: Always
restartPolicy: AlwaysDurant 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: 9187Aquest ú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 | Sí |
Les quatre conseqüències pràctiques, en ordre d'importància:
- Arrenca abans que els contenidors principals, garantint que el proxy o l'exportador estiguin llestos quan l'aplicació comença a treballar.
- Continua viu durant tota la vida del pod i es reinicia si mor, cosa que un init container normal no fa.
- S'acaba després dels contenidors principals, així que un sidecar de logs captura els últims missatges del tancament.
- No impedeix que un Job acabi. Quan els contenidors de
containersacaben, el kubelet para els sidecars i el pod arriba aSucceeded. 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.
- 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:
localhostcompartit: tots els contenidors del pod comparteixen l'espai de noms de xarxa, així que es parlen per127.0.0.1sense 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
emptyDirmuntat 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]
- Patró sidecar: exportador de mètriques al costat de
postgres-reserves
postgres-reservesLa 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
nobodyi 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:
É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: metriquesComprovació:
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 -3pg_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.
- 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: 0400I 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.
- Patró adaptador: normalitzar el log de
worker-notificacions
worker-notificacionsworker-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:
{"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.
- 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:
- Els init containers normals s'executen seqüencialment i fins a acabar.
- Els sidecars natius arrenquen en l'ordre declarat, i n'hi ha prou que estiguin iniciats.
- Els contenidors principals arrenquen tots alhora, sense garantia d'ordre entre ells.
- A la terminació, primer els principals; després els sidecars, en ordre invers.
- 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 logsikubectl execja necessita la bandera-c.
Preguntes de control abans d'afegir un sidecar:
- 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í.
- Ho pot fer la mateixa aplicació amb una llibreria? De vegades cinc línies de codi substitueixen un contenidor de 60 MiB.
- 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
workerque cada 5 segons escrigui una línia en format propietari a/logs/sortida.log(per exemple2026-08-05 10:00:00 | ENVIAMENT_OK | reserva=4471). - Un sidecar natiu
adaptador(init container ambrestartPolicy: Always) que segueixi aquest fitxer i emeti cada línia per la seva sortida estàndard amb el prefix[adaptat]. - Un
emptyDirambsizeLimit: 64Micompartit.
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 sí 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: 128MiInit:0/1: l'init container està corrent i cap no s'ha completat.
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-demoEl 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: 64Mikubectl 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=4473La 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:
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: 64Mikubectl 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-devEl 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 45sEl 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-devConclusió
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
- Què és Kubernetes?
- Arquitectura de Kubernetes
- Conceptes i Terminologia Clau
- Configuració d'un Clúster de Kubernetes
- La CLI de Kubernetes: kubectl
- Objectes, Manifests YAML i el Model Declaratiu
- El Projecte del Curs: la Plataforma Rutas Norte
Mòdul 2: Components Principals de Kubernetes
- Pods
- ReplicaSets
- Deployments
- Actualitzacions, Rollbacks i Estratègies de Desplegament
- Serveis
- Namespaces
- Etiquetes, Selectors i Anotacions
Mòdul 3: Gestió de Configuració i Secrets
- ConfigMaps
- Secrets
- Variables d'Entorn
- Quotes i Límits de Recursos
- LimitRanges i Classes de Qualitat de Servei (QoS)
- ServiceAccounts i Accés a l'API des dels Pods
Mòdul 4: Xarxes a Kubernetes
- Xarxes de Clúster
- Tipus de Serveis
- DNS Intern i Descobriment de Serveis
- Controladors d'Ingress
- TLS i Gestió de Certificats amb cert-manager
- Polítiques de Xarxa
Mòdul 5: Emmagatzematge a Kubernetes
- Volums
- Volums Persistents
- Reclamacions de Volums Persistents
- Classes d'Emmagatzematge
- Aprovisionament Dinàmic, Expansió i Snapshots
- Còpies de Seguretat i Restauració de Dades
Mòdul 6: Conceptes Avançats de Kubernetes
- StatefulSets
- DaemonSets
- Treballs i CronJobs
- Init Containers, Sidecars i Patrons Multicontenidor
- Planificació: Afinitat, Taints i Toleracions
- Definicions de Recursos Personalitzats (CRDs)
- Operadors i el Patró Controlador
Mòdul 7: Monitoratge i Registre
- Verificacions de Salut i Sondes
- Servidor de Mètriques i kubectl top
- Monitoratge amb Prometheus
- Visualització i Alertes amb Grafana i Alertmanager
- Registre Centralitzat amb Elasticsearch, Fluentd i Kibana (EFK)
- Depuració d'Aplicacions i Esdeveniments del Clúster
Mòdul 8: Seguretat a Kubernetes
- Control d'Accés Basat en Rols (RBAC)
- Contextos de Seguretat i Enduriment del Contenidor
- Polítiques de Seguretat de Pods i Pod Security Standards
- Seguretat de Xarxa
- Seguretat d'Imatges
- Auditoria, Escaneig i Gestió de Vulnerabilitats
Mòdul 9: Escalat i Rendiment
- Autoescalat Horitzontal de Pods
- Autoescalat Vertical de Pods
- Autoescalat de Clúster
- Escalat per Esdeveniments i Mètriques Personalitzades amb KEDA
- Alta Disponibilitat: PodDisruptionBudgets i Topologia
- Ajust de Rendiment
Mòdul 10: Ecosistema i Eines de Kubernetes
- Minikube i Entorns Locals amb kind
- Kubeadm
- Helm
- Kustomize
- GitOps amb Argo CD i Flux
- Kubernetes Gestionat: EKS, AKS i GKE
Mòdul 11: Estudis de Cas i Aplicacions del Món Real
- Desplegament d'una Aplicació Web
- Execució d'Aplicacions amb Estat
- CI/CD amb Kubernetes
- Estratègies de Desplegament: Blue-Green i Canary
- Gestió Multi-Clúster
- Operació en Producció: Incidències, Runbooks i Costos
