Tot el que hem desplegat fins ara comparteix una premissa: el procés no ha d'acabar mai. botiga-web serveix pàgines indefinidament, api-reserves atén peticions indefinidament, el recol·lector de logs de la lliçó anterior llegeix fitxers indefinidament. Si algun d'aquests contenidors surt, encara que sigui amb codi 0, Kubernetes ho considera una anomalia i el reinicia.
Però una part important de la feina real d'una plataforma consisteix precisament a acabar: generar l'informe d'ocupació d'ahir a la nit, migrar l'esquema de la base de dades abans d'un desplegament, reprocessar un lot de bitllets amb un preu mal calculat. Per a aquestes càrregues Kubernetes té dos objectes: el Job, que executa alguna cosa fins que es completa amb èxit, i el CronJob, que crea Jobs segons un calendari.
Amb ells desplegarem per fi l'últim component pendent de Rutas Norte: informes-ocupacio, la tasca nocturna que consulta postgres-reserves i deixa un informe al disc.
Contingut
- Càrregues que acaben davant de càrregues que no acaben
- L'objecte Job: anatomia i camps de control
restartPolicy:Neverdavant d'OnFailure- Els tres patrons de Job
completionMode: Indexedi el repartiment determinista- L'objecte CronJob: calendari i política de concurrència
- Cas central:
informes-ocupaciocom a CronJob nocturn - Segon exemple: un Job de migració d'esquema
- Depuració de treballs fallits i neteja d'historial
- Càrregues que acaben davant de càrregues que no acaben
La diferència arrenca al mateix contenidor. Un procés de llarga durada es queda bloquejat en un bucle d'esdeveniments; un procés per lots fa la seva feina, escriu un resultat i crida exit(0).
Kubernetes distingeix tots dos casos pel controlador que gestiona el pod, no pel contingut de la imatge.
| Aspecte | Deployment / StatefulSet / DaemonSet | Job / CronJob |
|---|---|---|
| Durada esperada | Indefinida | Finita |
| Sortida amb codi 0 | Anomalia; es reinicia | Èxit; el Job es marca com a completat |
restartPolicy permesa |
Només Always |
Només Never o OnFailure |
| Estat terminal | No existeix | Complete o Failed |
| Què mesura l'èxit | Que continuï viu | Que hagi acabat bé N vegades |
| Recollida de resultats | Logs continus | Logs del pod acabat, fins que es netegen |
Un detall que sol sorprendre: el pod d'un Job acabat no desapareix. Queda en estat Completed, sense consumir CPU ni memòria, perquè en puguis llegir els logs. Aquest és també el motiu pel qual un clúster mal mantingut acumula milers de pods Completed, cosa que resoldrem a l'apartat 9.
graph LR
subgraph Indefinides
D[Deployment] --> P1[Pod Running sempre]
P1 -->|surt| P1
end
subgraph Finites
C[CronJob] -->|cada nit| J[Job]
J --> P2[Pod]
P2 -->|exit 0| OK[Completed]
P2 -->|exit != 0| RT[Reintent]
RT --> P2
RT -->|backoffLimit esgotat| KO[Failed]
end
- L'objecte Job: anatomia i camps de control
El Job més simple possible:
apiVersion: batch/v1
kind: Job
metadata:
name: calcul-tarifes
namespace: rutas-norte-dev
spec:
template:
spec:
restartPolicy: Never
containers:
- name: calcul
image: busybox:1.36
command: ["sh", "-c", "echo 'Recalculant tarifes...'; sleep 5; echo 'Fet'"]Observa que no hi ha selector: el Job genera el seu automàticament amb una etiqueta única (batch.kubernetes.io/controller-uid). És una de les poques càrregues on no cal declarar-lo, i és millor no intentar-ho: un selector escrit a mà que col·lideixi amb un altre Job provoca que un controlador adopti pods aliens.
Els camps que governen el comportament:
| Camp | Per defecte | Què fa |
|---|---|---|
completions |
1 | Quants pods han d'acabar amb èxit per donar el Job per completat |
parallelism |
1 | Quants pods poden córrer simultàniament |
backoffLimit |
6 | Quantes fallades es toleren abans de marcar el Job com a Failed |
activeDeadlineSeconds |
sense límit | Segons màxims des de l'inici; en superar-los el Job es talla encara que no hagi fallat |
ttlSecondsAfterFinished |
sense límit | Segons després d'acabar (amb èxit o no) abans que el Job i els seus pods s'esborrin sols |
completionMode |
NonIndexed |
NonIndexed o Indexed (apartat 5) |
suspend |
false |
Si és true, el Job no crea pods; serveix per encuar i alliberar després |
podFailurePolicy |
cap | Regles fines per codi de sortida (estable des de 1.31) |
backoffLimit i el retrocés exponencial
Quan un pod falla, el Job en crea un altre, però no immediatament: espera 10 s, després 20 s, 40 s, 80 s… fins a un màxim de 6 minuts. Aquesta espera creixent evita que una fallada permanent —una contrasenya incorrecta, un host inabastable— consumeixi el clúster amb milers d'intents per minut.
En arribar a backoffLimit fallades, el Job passa a Failed amb la raó BackoffLimitExceeded i deixa de crear pods.
Valor pràctic per a Rutas Norte:
- Tasques idempotents amb dependències de xarxa (consultar la passarel·la de pagaments):
backoffLimit: 6, el valor per defecte, té sentit; una fallada transitòria es recupera sola. - Migracions d'esquema:
backoffLimit: 0. Si falla, vols saber-ho i mirar-ho, no que es reintenti cinc vegades sobre una base de dades a mig migrar.
activeDeadlineSeconds
És un tallafoc temporal. Es compta des que el Job arrenca i inclou els reintents:
Si s'esgota, els pods actius s'acaben i el Job queda Failed amb raó DeadlineExceeded. És la protecció contra el procés que no falla però tampoc avança: una consulta bloquejada per un lock a postgres-reserves, per exemple. activeDeadlineSeconds té prioritat sobre backoffLimit: mana el que es compleixi primer.
ttlSecondsAfterFinished
Passat aquest temps des que el Job arriba a estat terminal, el controlador de TTL esborra el Job i els seus pods. És la manera correcta d'evitar l'acumulació d'objectes. Posa'l sempre en Jobs creats per automatismes; deixa temps suficient perquè algú pugui llegir els logs d'una fallada nocturna (24 hores és un bon valor).
restartPolicy: Never davant d'OnFailure
restartPolicy: Never davant d'OnFailureL'API només admet aquests dos valors a la plantilla d'un Job. Always es rebutja, perquè contradiu la idea mateixa d'una tasca que acaba.
La diferència entre tots dos no és cosmètica: canvia què es reinicia.
restartPolicy: Never |
restartPolicy: OnFailure |
|
|---|---|---|
| Què passa en fallar el contenidor | El pod queda Failed; el Job crea un pod nou |
El kubelet reinicia el contenidor dins del mateix pod |
| Comptador que s'incrementa | backoffLimit del Job |
restartCount del contenidor (i també backoffLimit) |
| Rastre que queda | Un pod per intent, tots consultables | Un sol pod; els logs d'intents previs es perden llevat que facis servir --previous |
| Node del reintent | Pot ser un altre | Sempre el mateix |
Volum emptyDir |
Buit a cada intent | Es conserva entre reinicis |
Exemple del que es veu amb Never:
NAME READY STATUS RESTARTS AGE
migracio-esquema-4kx2p 0/1 Error 0 3m
migracio-esquema-9dz7w 0/1 Error 0 2m
migracio-esquema-t6m1c 0/1 Error 0 1mTres pods, un per intent, cadascun amb els seus logs íntegres. Amb OnFailure veuries:
Un sol pod amb RESTARTS: 3, i per veure el log de l'intent anterior et caldria kubectl logs <pod> --previous.
Recomanació per a Rutas Norte: fes servir Never llevat de motiu concret. Conservar un pod per intent fa la depuració molt més honesta, i en una tasca nocturna que ha fallat a les 03:00 voldràs poder llegir exactament què va dir el primer intent. OnFailure és preferible quan l'arrencada del contenidor és molt costosa —descarregar un model, restaurar una memòria cau— i vols aprofitar l'estat d'un emptyDir entre reintents.
Un matís important: si el node sencer falla, el pod es recrea en tots dos casos, perquè qui actua és el controlador del Job, no el kubelet.
- Els tres patrons de Job
Amb completions i parallelism es construeixen tres patrons que cobreixen gairebé tota la feina per lots.
| Patró | completions |
parallelism |
Quan |
|---|---|---|---|
| Tasca única | 1 (o absent) | 1 (o absent) | Migració, informe, tasca puntual |
| Paral·lelisme fix | N | M (≤ N) | N unitats de treball conegudes per endavant |
| Cua de treball | absent | M | Els treballadors consumeixen d'una cua externa fins a buidar-la |
Patró 1: tasca única
El més habitual. Un pod, una vegada, amb èxit.
apiVersion: batch/v1
kind: Job
metadata:
name: informe-puntual-juliol
namespace: rutas-norte-dev
labels:
app: informes-ocupacio
app.kubernetes.io/part-of: rutas-norte
entorn: dev
spec:
backoffLimit: 2
activeDeadlineSeconds: 900
ttlSecondsAfterFinished: 86400
template:
metadata:
labels:
app: informes-ocupacio
entorn: dev
spec:
restartPolicy: Never
containers:
- name: generador
image: postgres:16.4
command: ["sh", "-c", "psql -h postgres-reserves -U rutasnorte -d reserves -c 'SELECT count(*) FROM reserves'"]Patró 2: paral·lelisme fix amb nombre de finalitzacions
Rutas Norte té 12 línies d'autobús i vol recalcular l'ocupació històrica de totes. Són 12 unitats de treball, i no vol més de 4 consultes simultànies contra postgres-reserves:
apiVersion: batch/v1
kind: Job
metadata:
name: recalcul-ocupacio-linies
namespace: rutas-norte-dev
spec:
completions: 12 # 12 pods han d'acabar bé
parallelism: 4 # com a molt 4 alhora
backoffLimit: 6
ttlSecondsAfterFinished: 86400
template:
spec:
restartPolicy: Never
containers:
- name: recalcul
image: busybox:1.36
command: ["sh", "-c", "echo 'Processant una línia'; sleep 10"]El Job va mantenint quatre pods vius: així que un acaba, arrenca el següent, fins a acumular 12 èxits. parallelism és el mecanisme de control de càrrega: sense ell, dotze consultes simultànies podrien saturar la base de dades.
Observant l'evolució:
NAME COMPLETIONS DURATION AGE
recalcul-ocupacio-linies 0/12 3s 3s
recalcul-ocupacio-linies 4/12 15s 15s
recalcul-ocupacio-linies 8/12 27s 27s
recalcul-ocupacio-linies 12/12 39s 39sEl problema d'aquest patró en la seva forma bàsica: tots els pods executen la mateixa ordre. No hi ha res que digui a cadascun quina línia li toca. Això ho resol l'apartat 5.
Patró 3: cua de treball
Si s'omet completions però es fixa parallelism, el Job entra en mode cua: arrenca M treballadors i considera la feina acabada quan un qualsevol d'ells surt amb èxit estant la resta ja finalitzada. Els treballadors s'han de coordinar per si mateixos a través d'una cua externa (Redis, RabbitMQ, una taula amb SELECT ... FOR UPDATE SKIP LOCKED).
apiVersion: batch/v1
kind: Job
metadata:
name: reproces-bitllets-cua
namespace: rutas-norte-dev
spec:
parallelism: 5 # sense completions: mode cua de treball
backoffLimit: 10
ttlSecondsAfterFinished: 86400
template:
spec:
restartPolicy: Never
containers:
- name: treballador
image: registry.rutasnorte.example/worker-bitllets:1.4.2
env:
- name: CUA_URL
value: "redis://redis-cache.rutas-norte-dev.svc.cluster.local:6379/3"
command: ["/app/consumir-cua"]Cada treballador treu un bitllet de la cua de redis-cache, el reprocessa i repeteix fins que la cua és buida, moment en què surt amb codi 0. És el patró més flexible i el que pitjor tolera errors de disseny: si un treballador mor a mitja unitat, aquesta unitat ha de tornar a la cua, i això ho ha de garantir l'aplicació.
completionMode: Indexed i el repartiment determinista
completionMode: Indexed i el repartiment deterministaEl mode indexat resol la mancança del patró 2: donar a cada pod un número que li digui quina porció de la feina li correspon.
apiVersion: batch/v1
kind: Job
metadata:
name: recalcul-ocupacio-linies
namespace: rutas-norte-dev
spec:
completionMode: Indexed # <- la clau
completions: 12
parallelism: 4
backoffLimit: 6
ttlSecondsAfterFinished: 86400
template:
spec:
restartPolicy: Never
containers:
- name: recalcul
image: busybox:1.36
command:
- sh
- -c
- |
LINIA=$(( JOB_COMPLETION_INDEX + 1 ))
echo "Pod amb índex $JOB_COMPLETION_INDEX -> recalculant la línia $LINIA de Rutas Norte"
sleep 5
echo "Línia $LINIA recalculada"Amb Indexed, Kubernetes assigna a cada pod un índex únic de 0 a completions - 1, i l'exposa de dues maneres:
- La variable d'entorn
JOB_COMPLETION_INDEX. - L'anotació
batch.kubernetes.io/job-completion-indexdel pod.
A més, els noms dels pods incorporen l'índex, cosa que fa l'observació trivial:
NAME READY STATUS RESTARTS AGE
recalcul-ocupacio-linies-0-h2k9x 0/1 Completed 0 62s
recalcul-ocupacio-linies-1-m4p2t 0/1 Completed 0 62s
recalcul-ocupacio-linies-2-w8j5r 0/1 Completed 0 61s
...
recalcul-ocupacio-linies-11-z3q7n 0/1 Completed 0 18sPropietats que fan Indexed molt superior al mode per defecte quan cal repartir feina:
- Determinisme: l'índex 7 processa sempre la línia 8, s'executi quan s'executi.
- Reintents correctes: si el pod de l'índex 7 falla, el reintent torna a rebre l'índex 7, no un altre. Cap unitat no es queda sense processar ni es processa dues vegades.
- Traçabilitat: el nom del pod et diu quina unitat de treball va mirar aquell pod.
Si necessites repartir un rang, l'índex n'hi ha prou per calcular-lo:
TOTAL=12000
PER_POD=$(( TOTAL / COMPLETIONS ))
DES_DE=$(( JOB_COMPLETION_INDEX * PER_POD ))
FINS_A=$(( DES_DE + PER_POD ))
echo "Processant reserves de la $DES_DE a la $FINS_A"
- L'objecte CronJob: calendari i política de concurrència
Un CronJob no executa res per si mateix: crea Jobs segons un calendari. I cada Job crea els seus pods. Són tres nivells, i entendre'ls evita molta confusió en depurar.
graph LR CJ[CronJob<br/>informes-ocupacio] -->|03:15 del dia 1| J1[Job informes-ocupacio-28935120] CJ -->|03:15 del dia 2| J2[Job informes-ocupacio-28936560] J1 --> P1[Pod ...-28935120-4kx2p] J2 --> P2[Pod ...-28936560-9dz7w]
La sintaxi del schedule
Cinc camps separats per espais, en el format cron de sempre:
┌───────────── minut (0 - 59)
│ ┌───────────── hora (0 - 23)
│ │ ┌───────────── dia del mes (1 - 31)
│ │ │ ┌───────────── mes (1 - 12)
│ │ │ │ ┌───────────── dia de la setmana (0 - 6, diumenge = 0)
│ │ │ │ │
* * * * *Operadors admesos a cada camp:
| Operador | Significat | Exemple |
|---|---|---|
* |
Qualsevol valor | * * * * * = cada minut |
, |
Llista | 0 8,14,20 * * * = a les 8, 14 i 20 |
- |
Rang | 0 9-17 * * 1-5 = cada hora de 9 a 17, de dilluns a divendres |
/ |
Pas | */15 * * * * = cada 15 minuts |
Exemples amb significat per a Rutas Norte:
| Expressió | Quan s'executa |
|---|---|
15 3 * * * |
Tots els dies a les 03:15 |
0 * * * * |
En punt de cada hora |
*/10 * * * * |
Cada 10 minuts |
0 4 * * 1 |
Els dilluns a les 04:00 |
0 5 1 * * |
El dia 1 de cada mes a les 05:00 |
30 2 * * 0 |
Els diumenges a les 02:30 |
timeZone
Sense aquest camp, el calendari s'interpreta en la zona horària del kube-controller-manager, que en la majoria de clústers és UTC. Per a Rutas Norte, que opera a Espanya, això significa que 15 3 * * * s'executaria a les 04:15 en horari d'estiu i a les 03:15 a l'hivern: una hora diferent segons l'època de l'any, justament el que no vols en una finestra nocturna.
El camp timeZone és estable des de Kubernetes 1.27 i accepta qualsevol identificador de la base de dades IANA. Fes-lo servir sempre; és una de les millores més útils i menys conegudes de l'API de batch.
concurrencyPolicy
Què passa si arriba l'hora de l'execució següent i l'anterior encara no ha acabat?
| Valor | Comportament | Quan fer-lo servir |
|---|---|---|
Allow (per defecte) |
Es crea el Job nou; conviuen | Tasques independents i lleugeres |
Forbid |
S'omet l'execució nova; es registra el salt | Tasques que no es poden solapar |
Replace |
Es cancel·la l'anterior i arrenca la nova | Tasques on només importa el resultat més recent |
El cas real que justifica Forbid a Rutas Norte: informes-ocupacio s'executa cada nit i fa consultes agregades pesades sobre postgres-reserves. Si una nit de pont el volum de reserves és tan gran que l'informe triga 26 hores, amb Allow la nit següent hi hauria dos informes agregant simultàniament sobre la mateixa base de dades: el doble de càrrega en el pitjor moment possible, i dos fitxers escrivint al mateix PVC. Amb Forbid, la segona execució simplement no es llança i l'esdeveniment queda registrat perquè algú investigui per què l'informe triga tant.
Replace seria adequat per a, per exemple, un recàlcul de disponibilitat de places cada cinc minuts: si el càlcul de les 10:00 encara corre a les 10:05, el seu resultat ja és obsolet i és millor cancel·lar-lo i començar amb dades fresques.
startingDeadlineSeconds i el clúster caigut
Suposem que el pla de control va estar caigut entre les 03:00 i les 05:00 per un manteniment, i el CronJob havia de disparar a les 03:15. Què passa en tornar?
- Sense
startingDeadlineSeconds: el controlador veu una execució perduda i la llança tan bon punt pot, a les 05:00. Un informe "de les 03:15" executant-se a les 05:00 pot ser inofensiu o desastrós segons què faci. - Amb
startingDeadlineSeconds: 600: l'execució només es llança si han passat menys de 600 segons des de l'hora prevista. A les 05:00 han passat 6.300 s, així que s'omet i es registra el salt.
Hi ha a més un mecanisme de protecció que convé conèixer: si el controlador detecta més de 100 execucions perdudes dins de la finestra de la deadline, deixa de programar el CronJob del tot i emet aquest esdeveniment:
Warning FailedNeedsStart cronjob-controller Cannot determine if job needs to be started:
too many missed start times (> 100). Set or decrease .spec.startingDeadlineSeconds or check
clock skewÉs el motiu pel qual un CronJob que no s'ha executat en mesos pot quedar-se permanentment mort. Fixar startingDeadlineSeconds a un valor raonable ho evita.
Historial i suspensió
spec:
successfulJobsHistoryLimit: 3 # per defecte 3
failedJobsHistoryLimit: 5 # per defecte 1
suspend: falseEl controlador conserva els N Jobs més recents de cada classe i esborra la resta amb els seus pods. Consell pràctic: puja failedJobsHistoryLimit. El valor per defecte d'1 significa que si l'informe falla tres nits seguides, només conserves els logs de l'última, justament quan voldries comparar les tres.
suspend: true atura la programació sense esborrar l'objecte. És la maniobra correcta durant una finestra de manteniment de postgres-reserves:
kubectl patch cronjob informes-ocupacio -n rutas-norte-pro -p '{"spec":{"suspend":true}}'
# ... manteniment ...
kubectl patch cronjob informes-ocupacio -n rutas-norte-pro -p '{"spec":{"suspend":false}}'Compte: mentre està suspès, les execucions perdudes no es recuperen en reprendre; el controlador reprèn a partir de l'hora prevista següent.
- Cas central:
informes-ocupacio com a CronJob nocturn
informes-ocupacio com a CronJob nocturnÉs el moment de desplegar el component que portem sis mòduls citant i mai creant. informes-ocupacio consulta cada matinada les reserves del dia anterior a postgres-reserves, calcula l'ocupació per línia i deixa un fitxer CSV en un PVC.
El PVC de l'informe
Els informes s'acumulen i es conserven un temps. Fem servir la classe estàndard, no la ràpida: no hi ha exigències de latència.
# k8s/base/informes-ocupacio-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: informes-ocupacio-dades
namespace: rutas-norte-pro
labels:
app: informes-ocupacio
app.kubernetes.io/part-of: rutas-norte
entorn: pro
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: rutasnorte-estandar
resources:
requests:
storage: 5GiLa ServiceAccount
Seguint la pràctica de 03-06: compte dedicat i sense token muntat, perquè el treball no parla amb l'API de Kubernetes.
# k8s/base/informes-ocupacio-sa.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: informes-ocupacio
namespace: rutas-norte-pro
labels:
app: informes-ocupacio
app.kubernetes.io/part-of: rutas-norte
entorn: pro
automountServiceAccountToken: falseLa NetworkPolicy
A 04-06 vam deixar rutas-norte-pro amb un deny-all per defecte i vam anar autoritzant conversa a conversa. informes-ocupacio és una conversa nova i sense aquesta política no travessarà el mur: els pods arrencaran, la consulta es quedarà penjada i el Job fallarà per activeDeadlineSeconds sense cap missatge que expliqui la causa. És una de les fallades més difícils de diagnosticar de tot el curs.
Calen dues regles: sortida des de l'informe i entrada a la base de dades.
# k8s/entorns/pro/informes-ocupacio-networkpolicy.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: informes-ocupacio-sortida-postgres
namespace: rutas-norte-pro
labels:
app: informes-ocupacio
app.kubernetes.io/part-of: rutas-norte
entorn: pro
spec:
podSelector:
matchLabels:
app: informes-ocupacio
entorn: pro
policyTypes: ["Egress"]
egress:
# Resolució DNS: sense això no es resol el nom del servei
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
# Accés a la base de dades
- to:
- podSelector:
matchLabels:
app: postgres-reserves
entorn: pro
ports:
- protocol: TCP
port: 5432
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: postgres-reserves-entrada-informes
namespace: rutas-norte-pro
labels:
app: postgres-reserves
app.kubernetes.io/part-of: rutas-norte
entorn: pro
spec:
podSelector:
matchLabels:
app: postgres-reserves
entorn: pro
policyTypes: ["Ingress"]
ingress:
- from:
- podSelector:
matchLabels:
app: informes-ocupacio
entorn: pro
ports:
- protocol: TCP
port: 5432La regla de DNS és la que més s'oblida. Amb deny-all, un pod sense permís de sortida al port 53 de CoreDNS no resol postgres-reserves i falla amb un error de host desconegut que sembla un problema de configuració de l'aplicació.
El CronJob
# k8s/base/informes-ocupacio-cronjob.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: informes-ocupacio
namespace: rutas-norte-pro
labels:
app: informes-ocupacio
app.kubernetes.io/part-of: rutas-norte
entorn: pro
spec:
schedule: "15 3 * * *"
timeZone: "Europe/Madrid"
concurrencyPolicy: Forbid
startingDeadlineSeconds: 600
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 5
suspend: false
jobTemplate:
metadata:
labels:
app: informes-ocupacio
app.kubernetes.io/part-of: rutas-norte
entorn: pro
spec:
backoffLimit: 2
activeDeadlineSeconds: 5400 # 90 minuts com a sostre absolut
ttlSecondsAfterFinished: 172800 # s'autoneteja a les 48 h
template:
metadata:
labels:
app: informes-ocupacio
app.kubernetes.io/part-of: rutas-norte
entorn: pro
spec:
restartPolicy: Never
serviceAccountName: informes-ocupacio
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 10001
fsGroup: 10001
containers:
- name: generador
image: postgres:16.4
command:
- /bin/bash
- -c
- |
set -euo pipefail
AHIR=$(date -d 'yesterday' +%Y-%m-%d)
SORTIDA="/informes/ocupacio-${AHIR}.csv"
echo "Generant informe d'ocupació de ${AHIR}"
psql -h postgres-reserves -U "${PGUSER}" -d reserves \
--csv --no-psqlrc -o "${SORTIDA}" <<SQL
SELECT l.codi AS linia,
COUNT(r.id) AS bitllets,
SUM(r.places) AS places_ocupades,
ROUND(100.0 * SUM(r.places) / NULLIF(SUM(s.capacitat), 0), 2) AS pct_ocupacio
FROM reserves r
JOIN sortides s ON s.id = r.sortida_id
JOIN linies l ON l.id = s.linia_id
WHERE s.data = DATE '${AHIR}'
GROUP BY l.codi
ORDER BY pct_ocupacio DESC;
SQL
echo "Informe escrit a ${SORTIDA} ($(wc -l < "${SORTIDA}") línies)"
find /informes -name 'ocupacio-*.csv' -mtime +90 -delete
echo "Informes de més de 90 dies eliminats"
env:
- name: PGUSER
valueFrom:
secretKeyRef:
name: postgres-reserves-credencials
key: usuari
- name: PGPASSWORD
valueFrom:
secretKeyRef:
name: postgres-reserves-credencials
key: password
- name: PGCONNECT_TIMEOUT
value: "10"
resources:
requests:
cpu: 200m
memory: 256Mi
limits:
cpu: "1"
memory: 512Mi
volumeMounts:
- name: informes
mountPath: /informes
volumes:
- name: informes
persistentVolumeClaim:
claimName: informes-ocupacio-dadesRepàs de les decisions, que resumeixen mitja dotzena de lliçons anteriors:
schedulea les 03:15 ambtimeZone: Europe/Madrid: fora de les hores de venda i amb hora estable tot l'any.concurrencyPolicy: Forbid: dos informes simultanis sobre la mateixa base de dades i el mateix PVC serien nocius.activeDeadlineSeconds: 5400: si la consulta es queda bloquejada per un lock, el Job mor als 90 minuts en comptes d'arrossegar-se fins al matí.backoffLimit: 2: un reintent és útil davant d'un tall de xarxa; cinc només prolonguen la càrrega.ttlSecondsAfterFinished: 172800més els límits d'historial: dues capes de neteja automàtica.- Credencials per Secret (03-02), mai al manifest.
PGPASSWORDiPGUSERsón variables quepsqlreconeix directament. - Recursos declarats: sense ells el pod seria
BestEffort(03-05) i el primer node amb pressió de memòria el desallotjaria. securityContextamb usuari no root ifsGroupper poder escriure al PVC.- Retenció de 90 dies aplicada al mateix script: el PVC és finit i els informes contenen dades agregades, no personals, però acumular sense límit és una fuita d'espai garantida.
Desplegar i provar sense esperar a les tres de la matinada
kubectl apply -f k8s/base/informes-ocupacio-pvc.yaml
kubectl apply -f k8s/base/informes-ocupacio-sa.yaml
kubectl apply -f k8s/entorns/pro/informes-ocupacio-networkpolicy.yaml
kubectl apply -f k8s/base/informes-ocupacio-cronjob.yaml
kubectl get cronjob informes-ocupacio -n rutas-norte-proNAME SCHEDULE TIMEZONE SUSPEND ACTIVE LAST SCHEDULE AGE
informes-ocupacio 15 3 * * * Europe/Madrid False 0 <none> 20sL'ordre imprescindible: disparar una execució manual a partir del CronJob.
kubectl wait --for=condition=complete job/informes-prova-manual \
-n rutas-norte-pro --timeout=600s
kubectl logs -n rutas-norte-pro job/informes-prova-manualGenerant informe d'ocupació de 2026-08-04
Informe escrit a /informes/ocupacio-2026-08-04.csv (13 línies)
Informes de més de 90 dies eliminatskubectl create job --from=cronjob/... copia el jobTemplate tal qual, així que prova exactament el que s'executarà de matinada, incloses la ServiceAccount, la NetworkPolicy i els volums. És la manera correcta de validar un CronJob nou.
- Segon exemple: un Job de migració d'esquema
Abans de desplegar la versió 2.5.0 d'api-reserves cal afegir una columna a la taula de reserves. És un cas de Job puntual amb exigències molt diferents de les de l'informe.
# k8s/entorns/pre/migracio-esquema-2-5-0.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: migracio-esquema-2-5-0
namespace: rutas-norte-pre
labels:
app: api-reserves
app.kubernetes.io/part-of: rutas-norte
entorn: pre
spec:
backoffLimit: 0 # una migració NO es reintenta a cegues
activeDeadlineSeconds: 600
ttlSecondsAfterFinished: 604800 # 7 dies: volem rastre de les migracions
template:
metadata:
labels:
app: api-reserves
entorn: pre
spec:
restartPolicy: Never
serviceAccountName: api-reserves
automountServiceAccountToken: false
containers:
- name: migracio
image: registry.rutasnorte.example/api-reserves-migracions:2.5.0
command:
- /bin/sh
- -c
- |
set -euo pipefail
echo "Migració 2.5.0 sobre reserves"
psql -h postgres-reserves -U "${PGUSER}" -d reserves -v ON_ERROR_STOP=1 <<'SQL'
BEGIN;
ALTER TABLE reserves
ADD COLUMN IF NOT EXISTS canal_venda text NOT NULL DEFAULT 'web';
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_reserves_canal
ON reserves (canal_venda);
INSERT INTO migracions (versio, aplicada_el)
VALUES ('2.5.0', now())
ON CONFLICT (versio) DO NOTHING;
COMMIT;
SQL
echo "Migració 2.5.0 aplicada"
env:
- 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: 256MiContrastos deliberats amb informes-ocupacio:
| Decisió | Informe nocturn | Migració d'esquema | Motiu |
|---|---|---|---|
backoffLimit |
2 | 0 | Reintentar sobre una base de dades a mig migrar pot empitjorar l'estat |
ttlSecondsAfterFinished |
48 h | 7 dies | El rastre de què va migrar i quan té valor d'auditoria |
| Idempotència | No aplica | IF NOT EXISTS, ON CONFLICT DO NOTHING |
Si algú rellança el Job, no ha de trencar res |
| Transacció | No | BEGIN/COMMIT amb ON_ERROR_STOP=1 |
O s'aplica tot o no s'aplica res |
ON_ERROR_STOP=1 és imprescindible: sense ell, psql continua després d'un error i retorna codi 0, i el Job apareixeria com a completat amb la migració a mitges. És un parany clàssic.
Un advertiment de disseny: aquest Job desacobla la migració del desplegament. Cal executar-lo, verificar i només llavors desplegar la versió 2.5.0 d'api-reserves. L'alternativa —encadenar la migració a l'arrencada de cada pod de l'aplicació mitjançant un initContainer— és el tema de la lliçó 06-04, i té els seus propis avantatges i inconvenients.
- Depuració de treballs fallits i neteja d'historial
Quan un treball nocturn falla, el diagnòstic segueix sempre el mateix camí: CronJob → Job → Pod → logs.
Pas 1: estat dels Jobs
NAME STATUS COMPLETIONS DURATION AGE
informes-ocupacio-29175300 Complete 1/1 94s 2d
informes-ocupacio-29176740 Complete 1/1 88s 1d
informes-ocupacio-29178180 Failed 0/1 7m12s 3hEl sufix numèric és la marca temporal programada, no un identificador aleatori: els Jobs s'ordenen cronològicament sols.
Pas 2: per què va fallar el Job
Name: informes-ocupacio-29178180
Parallelism: 1
Completions: 1
Pods Statuses: 0 Running / 0 Succeeded / 3 Failed
Conditions:
Type Status Reason Message
---- ------ ------ -------
Failed True BackoffLimitExceeded Job has reached the specified backoff limit
Events:
Type Reason Age From Message
---- ------ ---- -------------- -------
Normal SuccessfulCreate 3h job-controller Created pod: informes-ocupacio-29178180-4kx2p
Normal SuccessfulCreate 3h job-controller Created pod: informes-ocupacio-29178180-9dz7w
Normal SuccessfulCreate 3h job-controller Created pod: informes-ocupacio-29178180-t6m1c
Warning BackoffLimitExceeded 3h job-controller Job has reached the specified backoff limitLes raons més freqüents i què signifiquen:
| Raó | Significat | On mirar |
|---|---|---|
BackoffLimitExceeded |
Els pods van fallar més vegades de les permeses | Logs dels pods, especialment el primer |
DeadlineExceeded |
Es va esgotar activeDeadlineSeconds |
Bloqueig? Consulta lenta? Xarxa tallada? |
FailedCreate |
No es va poder crear el pod | ResourceQuota esgotada, PVC inexistent, SA sense permisos |
Pas 3: el pod correcte i els seus logs
Amb restartPolicy: Never hi ha un pod per intent. Mira el primer, que sol contenir l'error original sense soroll de reintents:
kubectl get pods -n rutas-norte-pro \
-l job-name=informes-ocupacio-29178180 \
--sort-by=.metadata.creationTimestampNAME READY STATUS RESTARTS AGE
informes-ocupacio-29178180-4kx2p 0/1 Error 0 3h
informes-ocupacio-29178180-9dz7w 0/1 Error 0 3h
informes-ocupacio-29178180-t6m1c 0/1 Error 0 3hGenerant informe d'ocupació de 2026-08-02
psql: error: connection to server at "postgres-reserves" (10.96.144.21), port 5432 failed:
Connection timed out
Is the server running on that host and accepting TCP/IP connections?Un temps d'espera esgotat cap a la base de dades, amb el nom resolt correctament, apunta gairebé sempre a una NetworkPolicy que no autoritza aquella conversa: és exactament el que passaria si haguéssim oblidat el manifest de l'apartat 7.
Dreceres útils:
# Logs de tots els pods del Job d'un cop
kubectl logs -n rutas-norte-pro job/informes-ocupacio-29178180 --all-containers --tail=50
# Codi de sortida exacte del contenidor
kubectl get pod informes-ocupacio-29178180-4kx2p -n rutas-norte-pro \
-o jsonpath='{.status.containerStatuses[0].state.terminated.exitCode}{"\n"}'
# Amb restartPolicy: OnFailure, el log de l'intent anterior
kubectl logs -n rutas-norte-pro <pod> --previousCodis de sortida que convé reconèixer:
| Codi | Causa habitual |
|---|---|
| 1 | Error genèric de l'aplicació |
| 2 | Ús incorrecte d'una ordre de shell |
| 126 | L'ordre existeix però no és executable (permisos) |
| 127 | Ordre no trobada (típic d'un command mal escrit) |
| 137 | SIGKILL: gairebé sempre OOMKilled, es va superar limits.memory |
| 143 | SIGTERM: terminació ordenada, sovint per activeDeadlineSeconds |
Neteja de l'historial
Amb ttlSecondsAfterFinished i els límits d'historial, la neteja és automàtica. Per al manteniment manual:
# Quants objectes acabats hi ha acumulats
kubectl get jobs -A --field-selector status.successful=1 --no-headers | wc -l
# Esborrar Jobs completats d'un namespace
kubectl delete jobs -n rutas-norte-pro --field-selector status.successful=1
# Esborrar pods Completed i Error de tot el clúster
kubectl delete pods -A --field-selector status.phase=Succeeded
kubectl delete pods -A --field-selector status.phase=FailedEsborrar un Job elimina també els seus pods, perquè el Job és el seu ownerReference (el mecanisme de propietat que vam veure a 02-02). Si vols conservar els pods per inspeccionar-los:
Errors Comuns i Consells
Posar restartPolicy: Always en un Job. L'API ho rebutja amb un missatge clar (spec.template.spec.restartPolicy: Unsupported value: "Always"). Copiar i enganxar d'un Deployment n'és la causa habitual.
Oblidar la zona horària. Sense timeZone, un schedule: "15 3 * * *" es dispara en UTC, i a l'estiu això són les 05:15 a Espanya: dins de la finestra de manteniment d'altres equips o justament quan comença a haver-hi trànsit. Posa sempre timeZone: "Europe/Madrid".
Confiar en el concurrencyPolicy per defecte. Allow està bé per a tasques trivials, però per a qualsevol treball que toqui la base de dades o escrigui en un PVC compartit és una font de corrupció silenciosa. Pensa activament quin valor vols.
Un CronJob que no arrenca mai i no diu per què. Mira l'esdeveniment FailedNeedsStart amb "too many missed start times". Passa després d'una aturada llarga i es resol fixant startingDeadlineSeconds.
failedJobsHistoryLimit: 1. El valor per defecte et deixa sense evidència de la primera fallada, que és la més informativa. Puja'l a 5 en qualsevol treball amb importància operativa.
No posar ttlSecondsAfterFinished. Un CronJob cada cinc minuts genera gairebé 300 objectes al dia. Sense TTL ni límits d'historial, en setmanes tindràs desenes de milers de Jobs i pods a etcd, i l'apiserver se'n ressentirà.
Oblidar la NetworkPolicy a rutas-norte-pro. El símptoma és un Connection timed out cap a un nom que resol bé. I recorda incloure-hi la regla de sortida a CoreDNS: sense ella la fallada és un could not translate host name, encara més desconcertant.
Ignorar les ResourceQuota. Els Jobs consumeixen quota del namespace (03-04). Un Job amb parallelism: 20 pot esgotar-la i bloquejar el desplegament d'api-reserves. El símptoma és un esdeveniment FailedCreate amb exceeded quota.
Consell: kubectl create job --from=cronjob/<nom>. És l'eina de validació imprescindible. No despleguis mai un CronJob nou sense haver-lo disparat manualment almenys una vegada.
Consell: fes els teus treballs idempotents. Pot ser que s'executin dues vegades: per un reintent, per una execució manual, per una recuperació després d'una caiguda. IF NOT EXISTS, ON CONFLICT DO NOTHING i sobreescriure en comptes d'annexar són la diferència entre un incident i un no-esdeveniment.
Consell: registra sempre el principi i el final. Un echo en començar i un altre en acabar amb el resultat converteixen els logs en una cosa útil. Un Job que només escriu quan falla és un Job l'èxit del qual no pots verificar.
Exercicis
Exercici 1: Job indexat que reparteix línies
A rutas-norte-dev, crea un Job anomenat ocupacio-per-linia en mode Indexed amb 6 finalitzacions i paral·lelisme 2, que faci servir busybox:1.36. Cada pod ha d'imprimir quina línia de Rutas Norte li ha tocat (línia = índex + 1), trigar 5 segons i acabar. Afegeix-hi ttlSecondsAfterFinished d'una hora.
Verifica que s'han creat els 6 pods amb els seus índexs i que mai no n'hi va haver més de 2 corrent alhora.
Exercici 2: CronJob amb Forbid i prova manual
Crea un CronJob resum-vendes a rutas-norte-dev que s'executi cada 5 minuts en horari de Madrid, amb concurrencyPolicy: Forbid, startingDeadlineSeconds: 120, historial de 2 èxits i 3 fallades, i ttlSecondsAfterFinished d'una hora a la seva plantilla. El contingut ha de simular un resum que triga 30 segons.
Dispara una execució manual sense esperar el calendari i comprova els logs. Després suspèn el CronJob.
Exercici 3: diagnosticar un Job fallit
Crea deliberadament un Job informe-trencat a rutas-norte-dev amb backoffLimit: 2 i restartPolicy: Never, el contenidor del qual intenti connectar a un host inexistent postgres-inexistent i falli. Després:
- Esbrina la raó per la qual el Job està en
Failed. - Localitza el pod del primer intent i llegeix-ne el log.
- Obtén el codi de sortida del contenidor.
- Neteja.
Solucions
Solució 1
# /tmp/ocupacio-per-linia.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: ocupacio-per-linia
namespace: rutas-norte-dev
labels:
app: informes-ocupacio
app.kubernetes.io/part-of: rutas-norte
entorn: dev
spec:
completionMode: Indexed
completions: 6
parallelism: 2
backoffLimit: 3
ttlSecondsAfterFinished: 3600
template:
metadata:
labels:
app: informes-ocupacio
entorn: dev
spec:
restartPolicy: Never
automountServiceAccountToken: false
containers:
- name: calcul
image: busybox:1.36
command:
- sh
- -c
- |
LINIA=$(( JOB_COMPLETION_INDEX + 1 ))
echo "índex=$JOB_COMPLETION_INDEX -> línia L$LINIA de Rutas Norte"
sleep 5
echo "línia L$LINIA calculada"
resources:
requests:
cpu: 20m
memory: 32Mi
limits:
cpu: 100m
memory: 64Mikubectl apply -f /tmp/ocupacio-per-linia.yaml
kubectl get pods -n rutas-norte-dev -l job-name=ocupacio-per-linia -wDurant l'execució mai no es veuen més de dos pods en Running alhora, perquè parallelism: 2 ho impedeix:
NAME READY STATUS RESTARTS AGE
ocupacio-per-linia-0-p4m2x 1/1 Running 0 3s
ocupacio-per-linia-1-r7k9d 1/1 Running 0 3s
ocupacio-per-linia-0-p4m2x 0/1 Completed 0 9s
ocupacio-per-linia-2-w3j5t 1/1 Running 0 1skubectl get job ocupacio-per-linia -n rutas-norte-dev
kubectl logs -n rutas-norte-dev ocupacio-per-linia-4-b8n6qNAME STATUS COMPLETIONS DURATION AGE
ocupacio-per-linia Complete 6/6 27s 30s
índex=4 -> línia L5 de Rutas Norte
línia L5 calculadaL'índex 4 processa la línia 5, de manera determinista i reproduïble. Sense completionMode: Indexed els sis pods haurien executat la mateixa ordre sense saber quina era la seva porció.
Solució 2
# /tmp/resum-vendes.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: resum-vendes
namespace: rutas-norte-dev
labels:
app: informes-ocupacio
app.kubernetes.io/part-of: rutas-norte
entorn: dev
spec:
schedule: "*/5 * * * *"
timeZone: "Europe/Madrid"
concurrencyPolicy: Forbid
startingDeadlineSeconds: 120
successfulJobsHistoryLimit: 2
failedJobsHistoryLimit: 3
jobTemplate:
spec:
backoffLimit: 1
activeDeadlineSeconds: 300
ttlSecondsAfterFinished: 3600
template:
metadata:
labels:
app: informes-ocupacio
entorn: dev
spec:
restartPolicy: Never
automountServiceAccountToken: false
containers:
- name: resum
image: busybox:1.36
command:
- sh
- -c
- |
echo "$(date '+%Y-%m-%d %H:%M:%S') inici del resum de vendes"
sleep 30
echo "bitllets venuts ahir: 1842 (dada ficticia)"
echo "resum completat"
resources:
requests:
cpu: 20m
memory: 32Mi
limits:
cpu: 100m
memory: 64MiNAME SCHEDULE TIMEZONE SUSPEND ACTIVE LAST SCHEDULE AGE
resum-vendes */5 * * * * Europe/Madrid False 0 <none> 8s# Execució manual sense esperar el calendari
kubectl create job -n rutas-norte-dev --from=cronjob/resum-vendes resum-manual
kubectl wait --for=condition=complete job/resum-manual -n rutas-norte-dev --timeout=120s
kubectl logs -n rutas-norte-dev job/resum-manual2026-08-05 18:47:02 inici del resum de vendes
bitllets venuts ahir: 1842 (dada ficticia)
resum completat# Suspendre i comprovar
kubectl patch cronjob resum-vendes -n rutas-norte-dev -p '{"spec":{"suspend":true}}'
kubectl get cronjob resum-vendes -n rutas-norte-devNAME SCHEDULE TIMEZONE SUSPEND ACTIVE LAST SCHEDULE AGE
resum-vendes */5 * * * * Europe/Madrid True 0 3m 6mAmb SUSPEND en True no es crearan més Jobs, però l'objecte i el seu historial continuen allà. És el que faries abans d'una finestra de manteniment de postgres-reserves.
Solució 3
# /tmp/informe-trencat.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: informe-trencat
namespace: rutas-norte-dev
labels:
app: informes-ocupacio
entorn: dev
spec:
backoffLimit: 2
activeDeadlineSeconds: 300
template:
metadata:
labels:
app: informes-ocupacio
entorn: dev
spec:
restartPolicy: Never
automountServiceAccountToken: false
containers:
- name: generador
image: postgres:16.4
command:
- sh
- -c
- |
echo "Connectant a la base de dades de reserves..."
psql -h postgres-inexistent -U rutasnorte -d reserves -c 'SELECT 1'
env:
- name: PGCONNECT_TIMEOUT
value: "5"
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mikubectl apply -f /tmp/informe-trencat.yaml
# Esperar uns dos minuts pel retrocés exponencial entre intents
kubectl get job informe-trencat -n rutas-norte-dev1. Raó de la fallada:
Conditions:
Type Status Reason Message
---- ------ ------ -------
Failed True BackoffLimitExceeded Job has reached the specified backoff limitTres intents (l'original més dos reintents amb backoffLimit: 2) i cap no va tenir èxit.
2. Pod del primer intent i el seu log:
kubectl get pods -n rutas-norte-dev -l job-name=informe-trencat \
--sort-by=.metadata.creationTimestamp \
-o custom-columns=POD:.metadata.name,ESTAT:.status.phase,CREAT:.metadata.creationTimestampPOD ESTAT CREAT
informe-trencat-2xh4m Failed 2026-08-05T18:52:03Z
informe-trencat-8kq7p Failed 2026-08-05T18:52:18Z
informe-trencat-v5n1w Failed 2026-08-05T18:52:49ZConnectant a la base de dades de reserves...
psql: error: could not translate host name "postgres-inexistent" to address:
Name or service not knownEl missatge could not translate host name indica una fallada de resolució DNS, no de connectivitat: el nom no existeix. Si el nom existís però la NetworkPolicy bloquegés el trànsit, veuríem Connection timed out. Distingir aquests dos missatges estalvia moltíssim temps de diagnòstic.
3. Codi de sortida:
kubectl get pod informe-trencat-2xh4m -n rutas-norte-dev \
-o jsonpath='{.status.containerStatuses[0].state.terminated.exitCode}{"\n"}'Codi 2: psql no va poder connectar. Un 137 hauria indicat OOMKilled i un 127 una ordre inexistent.
4. Neteja:
kubectl delete -f /tmp/informe-trencat.yaml
kubectl delete -f /tmp/resum-vendes.yaml
kubectl delete -f /tmp/ocupacio-per-linia.yaml
kubectl delete job -n rutas-norte-dev resum-manual --ignore-not-foundConclusió
Els Jobs i els CronJobs completen el catàleg de càrregues de treball de Kubernetes amb les que acaben. El Job executa pods fins a acumular completions èxits, amb parallelism com a control de càrrega, backoffLimit i activeDeadlineSeconds com a tallafocs, i ttlSecondsAfterFinished com a neteja automàtica. L'elecció entre restartPolicy: Never i OnFailure decideix si cada intent deixa el seu propi pod amb els seus logs —gairebé sempre el que vols— o si es reinicia el contenidor al lloc. Els tres patrons (tasca única, paral·lelisme fix, cua de treball) cobreixen la feina per lots, i completionMode: Indexed amb JOB_COMPLETION_INDEX converteix el segon en un repartiment determinista i reintentable.
El CronJob hi afegeix el calendari: cinc camps, timeZone per no dependre d'UTC, concurrencyPolicy: Forbid quan dues execucions simultànies serien perjudicials, startingDeadlineSeconds per decidir què fer amb les execucions perdudes i els límits d'historial per no omplir etcd.
Amb això Rutas Norte està completa: botiga-web, api-reserves, postgres-reserves sobre StatefulSet, redis-cache, worker-notificacions i ara informes-ocupacio generant cada matinada a les 03:15 el seu informe sobre un PVC, amb la seva ServiceAccount, els seus recursos i la NetworkPolicy que li obre pas a través del deny-all.
Tots els pods que hem escrit fins aquí tenen una cosa en comú: un únic contenidor. Però en desplegar la migració d'esquema va aparèixer una pregunta que vam deixar sense resposta: i si en comptes d'un Job separat volguéssim que la migració s'executés automàticament abans que arrenqui cada pod d'api-reserves? I si api-reserves hagués d'esperar que postgres-reserves acceptés connexions abans de considerar-se arrencada? I si volguéssim exportar mètriques de PostgreSQL sense modificar-ne la imatge? Tot això es resol posant més d'un contenidor al mateix pod, i és el tema de la lliçó següent: Init Containers, Sidecars i Patrons Multicontenidor.
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
