Durant deu mòduls hem anat coneixent les peces de Kubernetes per separat: Pods, Deployments, Services, ConfigMaps, Ingress, sondes, HPA, polítiques de xarxa, RBAC, Helm, Argo CD. Cadascuna tenia sentit per si mateixa, però cap no és suficient tota sola per posar una aplicació en producció. Aquest mòdul canvia l'enfocament: en lloc d'estudiar una peça, resolem un escenari complet de principi a fi.
Comencem pel cas més freqüent i, en aparença, més senzill: portar una aplicació web sense estat a producció. A Rutas Norte S.L. són dos components, botiga-web (nginx servint la SPA de venda de bitllets) i api-reserves (l'API REST en Node.js que consulta disponibilitat i confirma reserves). Cap dels dos no desa dades localment, així que no hi ha volums per gestionar ni commutació per error per coordinar. I tot i així, el conjunt d'objectes que calen perquè aquest desplegament sigui defensable en una revisió de producció és de tretze peces per component.
Aquesta lliçó és la referència de manifests de tot el curs. Els fitxers que hi apareixen són els que es donen per bons a les lliçons següents: quan a 11-03 la canalització actualitzi un digest, serà el d'aquests Deployments; quan a 11-04 es converteixi api-reserves en un Rollout, es partirà d'aquest manifest; quan a 11-06 s'escrigui el runbook de "la latència de l'API s'ha disparat", es referirà a aquestes sondes i a aquest HPA.
Contingut
- El que cal tenir resolt abans del primer manifest
- El conjunt complet d'objectes d'un component de producció
- Manifests complets d'
api-reserves - Manifests complets de
botiga-web - Ordre d'aplicació i per què importa
- Verificació per capes, de dins cap enfora
- La llista de comprovació de "a punt per a producció"
- L'error característic de cada capa
- El que cal tenir resolt abans del primer manifest
Un error comú és tractar Kubernetes com el lloc on s'arreglen els problemes de l'aplicació. No ho és. Kubernetes amplifica el que l'aplicació ja fa: si l'aplicació arrenca lenta, l'escalat serà lent; si no tanca netament, cada desplegament perdrà peticions; si desa la sessió en memòria local, les rèpliques s'entrebancaran entre elles.
Abans d'escriure la primera línia de YAML, aquests set punts han d'estar resolts al codi i a la imatge, no al clúster.
1.1. Imatge reproduïble i identificada per digest
La imatge s'ha de construir sempre igual a partir del mateix commit, i desplegar-se referenciada per digest, no per etiqueta mòbil.
| Referència | Exemple | Val en producció? |
|---|---|---|
| Etiqueta mòbil | api-reserves:latest |
No. Dos pods del mateix Deployment poden acabar amb codi diferent |
| Etiqueta semàntica | api-reserves:2.7.0 |
Només si el registre és immutable; una etiqueta es pot reescriure |
| Digest | api-reserves@sha256:3f9c... |
Sí. És l'única referència criptogràficament estable |
Ja vam veure a 08-05 com construir amb multietapa i signar amb Cosign. El requisit previ aquí és organitzatiu: el procés que desplega ha de conèixer el digest, i això ho garanteix la canalització que veurem a 11-03.
Per què importa: sense digest, un RollingUpdate que reemplaça pods al llarg de vint minuts pot agafar dos artefactes diferents si algú reescriu l'etiqueta a mitges. És una fallada gairebé impossible de diagnosticar després.
1.2. Configuració totalment externalitzada
Cap valor que canviï entre rutas-norte-dev, rutas-norte-pre i rutas-norte-pro no pot estar dins de la imatge. La mateixa imatge, byte a byte, ha de poder executar-se als tres entorns.
- El que canvia i no és secret → ConfigMap (URL internes, temps d'espera, nivell de log).
- El que canvia i és secret → Secret, poblat per External Secrets Operator des del magatzem corporatiu (10-05).
- El que no canvia → pot quedar-se a la imatge.
Per què importa: si la imatge porta configuració a dins, el que es prova a pre no és el que es desplega a pro, i tota la cadena de confiança que hem construït a 08-05 deixa de significar res.
1.3. Processos sense estat local
Sessions, fitxers temporals que hagin de sobreviure, comptadors en memòria compartits: res d'això no pot viure al pod. A Rutas Norte, la sessió de l'usuari es signa amb JWT i el carretó de la compra es desa a redis-cache.
Per què importa: l'HPA crea i destrueix rèpliques contínuament durant el pont de maig. Si l'estat viu al pod, l'usuari perd el carretó cada vegada que l'autoescalat redueix rèpliques.
1.4. Tancament net davant de SIGTERM
Quan Kubernetes retira un pod, envia SIGTERM al procés principal i espera terminationGracePeriodSeconds. L'aplicació ha de:
- Deixar d'acceptar peticions noves.
- Acabar les que té en curs.
- Tancar connexions a la base de dades i a Redis.
- Sortir amb codi 0.
// Fragment de l'arrencada d'api-reserves (server.js)
const servidor = app.listen(8080);
let tancant = false;
// /preparat retorna 503 tan bon punt comença el tancament: així l'Endpoint
// es retira ABANS que deixem d'acceptar connexions.
app.get('/preparat', (req, res) => {
if (tancant) return res.status(503).json({ estat: 'tancant' });
return res.status(200).json({ estat: 'preparat' });
});
process.on('SIGTERM', async () => {
tancant = true;
// Donem marge perquè kube-proxy i l'Ingress actualitzin les seves taules.
await new Promise((r) => setTimeout(r, 5000));
servidor.close(async () => {
await pool.end(); // connexions a postgres-reserves
await redis.quit();
process.exit(0);
});
});Aquesta pausa de cinc segons sembla un truc brut, i en certa manera ho és, però respon a una cosa real: la retirada de l'Endpoint i l'enviament de SIGTERM passen en paral·lel, no en seqüència. Sense la pausa, el pod deixa d'acceptar connexions mentre el balancejador encara n'hi envia, i l'usuari veu errors 502 a cada desplegament.
1.5. Logs estructurats a stdout
Com vam veure a 07-05, un objecte JSON per línia a stdout, sense fitxers de log, sense rotació pròpia. Camps mínims a Rutas Norte: ts, nivell, missatge, traca_id, ruta, estat, duracio_ms.
1.6. Endpoints de salut diferenciats
Dos endpoints amb semàntica diferent, no un de sol:
| Endpoint | Pregunta que respon | Què comprova a api-reserves |
|---|---|---|
/salut |
El procés és viu? | Només que el bucle d'esdeveniments respon |
/preparat |
Pot atendre trànsit ara? | Connexió a postgres-reserves i a redis-cache, i que no s'estigui tancant |
Per què importa: si /salut comprova la base de dades, una caiguda de PostgreSQL provoca que el kubelet reiniciï tots els pods de l'API en bucle, i converteix una degradació en una caiguda total.
1.7. Mètriques exposades
api-reserves exposa /metriques en format Prometheus, amb com a mínim el comptador de peticions per ruta i codi, i l'histograma de latència. És el que alimentarà l'SLO d'11-06 i l'anàlisi del canari d'11-04.
- El conjunt complet d'objectes d'un component de producció
Aquesta és la taula que convé tenir a mà en revisar qualsevol desplegament nou. Cada fila és un objecte, què aporta, i què passa exactament si falta.
| Objecte | Què aporta | Què passa si falta |
|---|---|---|
| Namespace | Frontera d'aïllament, quotes i polítiques per entorn | Tot cau a default, sense quota ni política; impossible separar entorns |
| ServiceAccount | Identitat del pod davant de l'API i davant del núvol | S'utilitza la default, amb permisos indeterminats i sense traçabilitat en auditoria |
| ConfigMap | Configuració no sensible, versionada a Git | Valors incrustats a la imatge o al Deployment; canviar-ne un obliga a reconstruir |
| Secret (via External Secrets) | Credencials sincronitzades des del magatzem corporatiu | Secrets a mà al clúster, sense rotació ni rastre de qui els hi ha posat |
| Deployment | Rèpliques, actualització controlada, securityContext, sondes, recursos, repartiment topològic |
Sense ell no hi ha pods gestionats: un pod solt no es recrea ni s'actualitza |
| Service | Nom DNS estable i balanceig intern | Cal descobrir IP de pod a mà; cada reinici trenca la connexió |
| Ingress amb TLS | Entrada des d'internet amb certificat | El servei no és accessible des de fora, o ho és per un NodePort sense xifrar |
| HPA | Ajust de rèpliques a la càrrega real | El pont de maig satura les rèpliques fixes o es paga de més tot l'any |
| PodDisruptionBudget | Mínim de rèpliques durant manteniments i drenatges | Un drain de nodes pot deixar el servei a zero rèpliques |
| NetworkPolicy | Només les converses autoritzades | Qualsevol pod compromès del clúster arriba a l'API i a la base de dades |
| ServiceMonitor | Prometheus descobreix i rasca les mètriques | El component no apareix a Grafana ni dispara alertes: està cec |
topologySpreadConstraints (al Deployment) |
Rèpliques repartides entre zones i nodes | Les tres rèpliques poden acabar al mateix node; aquest node cau i cau el servei |
| Sondes (al Deployment) | Trànsit només a pods sans i reinici dels penjats | S'enruta trànsit a pods que encara arrenquen; els penjats no es recuperen mai |
Tretze files. La temptació en desplegar alguna cosa nova és fer les cinc primeres i deixar les altres "per quan hi hagi temps". L'experiència diu que les que es deixen fora són exactament les que calen el dia de l'incident.
graph TB
subgraph ns["Namespace rutas-norte-pro"]
subgraph seg["Seguretat i identitat"]
SA[ServiceAccount]
NP[NetworkPolicy]
end
subgraph cfg["Configuració"]
CM[ConfigMap]
SEC[Secret via ESO]
end
subgraph carga["Càrrega de treball"]
DEP[Deployment<br/>sondes + recursos + spread]
HPA[HPA]
PDB[PodDisruptionBudget]
end
subgraph red["Exposició"]
SVC[Service]
ING[Ingress + TLS]
end
SM[ServiceMonitor]
end
SA --> DEP
CM --> DEP
SEC --> DEP
DEP --> SVC
SVC --> ING
HPA -.escala.-> DEP
PDB -.protegeix.-> DEP
NP -.filtra.-> DEP
SM -.rasca.-> SVC
- Manifests complets d'
api-reserves
api-reservesEls fitxers següents viuen a k8s/base/api-reserves/ del repositori de manifests, i les superposicions de Kustomize (10-04) ajusten rèpliques, recursos i domini per entorn. Aquí es mostren ja resolts per a rutas-norte-pro.
3.1. ServiceAccount i configuració
apiVersion: v1
kind: ServiceAccount
metadata:
name: api-reserves
namespace: rutas-norte-pro
annotations:
# Identitat federada a EKS (10-06): permet llegir del bucket de factures
# sense cap clau d'accés de llarga vida.
eks.amazonaws.com/role-arn: arn:aws:iam::111122223333:role/rutasnorte-pro-api-reserves
# El pod no necessita parlar amb l'API de Kubernetes: no li muntem el token.
automountServiceAccountToken: false
---
apiVersion: v1
kind: ConfigMap
metadata:
name: api-reserves-config
namespace: rutas-norte-pro
data:
NODE_ENV: "production"
LOG_NIVELL: "info"
# DNS intern: nom curt perquè som al mateix namespace.
REDIS_URL: "redis://redis-cache:6379"
# L'agrupador de connexions de CloudNativePG; el veurem a 11-02.
PG_HOST: "postgres-reserves-pooler-rw"
PG_PORT: "5432"
PG_BASE: "reserves"
PG_POOL_MAX: "20"
PAGAMENTS_URL: "https://pagos.proveedorexterno.example/v2"
PAGAMENTS_TIMEOUT_MS: "4000"
RESERVA_TTL_SEGONS: "900"
---
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: api-reserves-secrets
namespace: rutas-norte-pro
spec:
refreshInterval: 1h
secretStoreRef:
name: magatzem-rutasnorte
kind: ClusterSecretStore
target:
name: api-reserves-secrets # nom del Secret que es crearà
creationPolicy: Owner
data:
- secretKey: PG_USUARI
remoteRef: { key: pro/api-reserves/bd, property: usuari }
- secretKey: PG_PASSWORD
remoteRef: { key: pro/api-reserves/bd, property: password }
- secretKey: JWT_SIGNATURA
remoteRef: { key: pro/api-reserves/jwt, property: clau }
- secretKey: PAGAMENTS_API_KEY
remoteRef: { key: pro/api-reserves/pagaments, property: api_key }Dos detalls que sovint passen desapercebuts. El primer, automountServiceAccountToken: false: api-reserves no crida l'API de Kubernetes, així que muntar-li el token només afegeix superfície d'atac (08-01). El segon, refreshInterval: 1h: l'ExternalSecret torna a llegir del magatzem cada hora, de manera que una rotació de credencials es propaga sola; el que no fa per si sol és reiniciar els pods, i per això més avall fem servir una suma de comprovació a les anotacions.
3.2. Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-reserves
namespace: rutas-norte-pro
labels:
app.kubernetes.io/name: api-reserves
app.kubernetes.io/part-of: rutas-norte
app.kubernetes.io/component: backend
rutasnorte.example/equip: desenvolupament
spec:
# replicas NO es fixa aquí: ho governa l'HPA i Argo CD ho ignora
# amb ignoreDifferences (10-05). Si ho fixéssim, cada sincronització
# tornaria les rèpliques al valor del fitxer.
revisionHistoryLimit: 5
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 25%
maxUnavailable: 0 # mai no baixem del nombre actual de rèpliques sanes
selector:
matchLabels:
app.kubernetes.io/name: api-reserves
template:
metadata:
labels:
app.kubernetes.io/name: api-reserves
app.kubernetes.io/part-of: rutas-norte
rutasnorte.example/equip: desenvolupament
annotations:
# Canvia si canvia el ConfigMap: força un rollout en canviar configuració.
rutasnorte.example/config-checksum: "sha256-8f1d2a"
spec:
serviceAccountName: api-reserves
automountServiceAccountToken: false
terminationGracePeriodSeconds: 45 # > els 5 s d'espera + peticions en curs
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
seccompProfile:
type: RuntimeDefault
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway # preferim servei a simetria perfecta
labelSelector:
matchLabels:
app.kubernetes.io/name: api-reserves
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app.kubernetes.io/name: api-reserves
containers:
- name: api
# Digest, no etiqueta: l'escriu la canalització d'11-03.
image: registry.rutasnorte.example/rutasnorte/api-reserves@sha256:3f9c1b7e5a04c2d8f61b93ae7c05d2419e8f6ab3c4d5e6f708192a3b4c5d6e7f
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 8080
- name: metriques
containerPort: 9090
envFrom:
- configMapRef:
name: api-reserves-config
- secretRef:
name: api-reserves-secrets
env:
- name: POD_NOM
valueFrom:
fieldRef:
fieldPath: metadata.name
resources:
requests:
cpu: 200m
memory: 256Mi
limits:
memory: 512Mi # sense límit de CPU: evita l'estrangulament (09-06)
startupProbe:
# Protegeix l'arrencada: fins a 60 s (12 x 5 s) abans de donar per mort el pod.
httpGet: { path: /salut, port: http }
periodSeconds: 5
failureThreshold: 12
livenessProbe:
httpGet: { path: /salut, port: http }
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
readinessProbe:
httpGet: { path: /preparat, port: http }
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 2 # surt del balanceig ràpid
successThreshold: 1
lifecycle:
preStop:
exec:
# Xarxa de seguretat addicional a la gestió de SIGTERM de l'apartat 1.4.
command: ["/bin/sh", "-c", "sleep 5"]
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}Comentaris sobre les decisions menys òbvies:
maxUnavailable: 0ambmaxSurge: 25%: durant el desplegament mai no hi ha menys capacitat de la que hi havia. Costa un 25 % de recursos temporals; aproval la pena.- Sense
limits.cpu: com vam raonar a 09-06, un límit de CPU provoca estrangulament amb pics de latència al percentil 95 encara que el node estigui ociós. Amb lesrequestsben posades i ResourceQuota al namespace (03-04), el risc està acotat. readOnlyRootFilesystem: trueobliga a l'emptyDira/tmp. És una molèstia de cinc minuts que talla d'arrel mitja dotzena de tècniques d'atac (08-02).whenUnsatisfiable: ScheduleAnyway: ambDoNotSchedule, durant el pont de maig l'HPA demanaria rèpliques que no es poden col·locar si una zona és plena. Preferim rèpliques mal repartides a rèpliques inexistents.
3.3. Service, Ingress, HPA, PDB, NetworkPolicy i ServiceMonitor
apiVersion: v1
kind: Service
metadata:
name: api-reserves
namespace: rutas-norte-pro
labels:
app.kubernetes.io/name: api-reserves
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: api-reserves
ports:
- name: http
port: 80
targetPort: http
- name: metriques
port: 9090
targetPort: metriques
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-reserves
namespace: rutas-norte-pro
annotations:
cert-manager.io/cluster-issuer: letsencrypt-produccio
nginx.ingress.kubernetes.io/proxy-body-size: "1m"
nginx.ingress.kubernetes.io/limit-rps: "200"
spec:
ingressClassName: nginx
tls:
- hosts: ["api.rutasnorte.example"]
secretName: api-rutasnorte-tls # l'omple cert-manager (04-05)
rules:
- host: api.rutasnorte.example
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api-reserves
port:
name: http
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: api-reserves
namespace: rutas-norte-pro
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: api-reserves
minReplicas: 4
maxReplicas: 40 # dimensionat per al pont de maig
metrics:
- type: Resource
resource:
name: cpu
target: { type: Utilization, averageUtilization: 65 }
behavior:
scaleUp:
stabilizationWindowSeconds: 0 # pujar ràpid
policies:
- type: Percent
value: 100
periodSeconds: 30
scaleDown:
stabilizationWindowSeconds: 300 # baixar a poc a poc
policies:
- type: Percent
value: 25
periodSeconds: 60
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: api-reserves
namespace: rutas-norte-pro
spec:
minAvailable: 75%
selector:
matchLabels:
app.kubernetes.io/name: api-reserves
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: api-reserves
namespace: rutas-norte-pro
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: api-reserves
policyTypes: [Ingress, Egress]
ingress:
- from:
- namespaceSelector:
matchLabels: { kubernetes.io/metadata.name: ingress-nginx }
ports:
- { protocol: TCP, port: 8080 }
- from:
- namespaceSelector:
matchLabels: { kubernetes.io/metadata.name: monitoratge }
ports:
- { protocol: TCP, port: 9090 }
egress:
- to:
- podSelector:
matchLabels: { cnpg.io/cluster: postgres-reserves }
ports: [{ protocol: TCP, port: 5432 }]
- to:
- podSelector:
matchLabels: { app.kubernetes.io/name: redis-cache }
ports: [{ protocol: TCP, port: 6379 }]
- to:
- namespaceSelector:
matchLabels: { kubernetes.io/metadata.name: kube-system }
podSelector:
matchLabels: { k8s-app: kube-dns }
ports: [{ protocol: UDP, port: 53 }, { protocol: TCP, port: 53 }]
- to:
- ipBlock:
cidr: 0.0.0.0/0
except: ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"]
ports: [{ protocol: TCP, port: 443 }] # pagos.proveedorexterno.example
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: api-reserves
namespace: rutas-norte-pro
labels:
release: kube-prometheus-stack # etiqueta que el Prometheus selecciona
spec:
selector:
matchLabels:
app.kubernetes.io/name: api-reserves
endpoints:
- port: metriques
path: /metriques
interval: 30sLa regla d'egrés cap a 0.0.0.0/0 amb exclusions privades mereix un comentari: com vam veure a 04-06, les NetworkPolicy no entenen de noms DNS, així que no es pot escriure "deixa sortir cap a pagos.proveedorexterno.example". El que sí que es pot fer és permetre el 443 cap a internet excloent-ne els rangs privats, de manera que un pod compromès no pugui fer servir aquesta regla per moure's lateralment dins de la xarxa corporativa.
- Manifests complets de
botiga-web
botiga-webbotiga-web és més simple: nginx servint fitxers estàtics. No té secrets, no parla amb la base de dades i les seves sondes són trivials. Però necessita exactament la mateixa disciplina.
apiVersion: v1
kind: ConfigMap
metadata:
name: botiga-web-nginx
namespace: rutas-norte-pro
data:
default.conf: |
server {
listen 8080;
root /usr/share/nginx/html;
# SPA: qualsevol ruta desconeguda retorna index.html
location / { try_files $uri $uri/ /index.html; }
location = /salut { access_log off; return 200 "ok\n"; }
location ~* \.(js|css|woff2|png|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: botiga-web
namespace: rutas-norte-pro
labels:
app.kubernetes.io/name: botiga-web
app.kubernetes.io/part-of: rutas-norte
rutasnorte.example/equip: desenvolupament
spec:
revisionHistoryLimit: 5
strategy:
rollingUpdate: { maxSurge: 25%, maxUnavailable: 0 }
selector:
matchLabels: { app.kubernetes.io/name: botiga-web }
template:
metadata:
labels:
app.kubernetes.io/name: botiga-web
app.kubernetes.io/part-of: rutas-norte
spec:
serviceAccountName: botiga-web
automountServiceAccountToken: false
terminationGracePeriodSeconds: 30
securityContext:
runAsNonRoot: true
runAsUser: 10002
seccompProfile: { type: RuntimeDefault }
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels: { app.kubernetes.io/name: botiga-web }
containers:
- name: nginx
image: registry.rutasnorte.example/rutasnorte/botiga-web@sha256:a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90
ports: [{ name: http, containerPort: 8080 }]
resources:
requests: { cpu: 50m, memory: 64Mi }
limits: { memory: 128Mi }
readinessProbe:
httpGet: { path: /salut, port: http }
periodSeconds: 5
livenessProbe:
httpGet: { path: /salut, port: http }
periodSeconds: 15
lifecycle:
preStop: { exec: { command: ["/bin/sh", "-c", "sleep 5"] } }
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: { drop: ["ALL"] }
volumeMounts:
- { name: conf, mountPath: /etc/nginx/conf.d }
- { name: cache, mountPath: /var/cache/nginx }
- { name: run, mountPath: /var/run }
volumes:
- name: conf
configMap: { name: botiga-web-nginx }
- { name: cache, emptyDir: {} }
- { name: run, emptyDir: {} }
---
apiVersion: v1
kind: Service
metadata:
name: botiga-web
namespace: rutas-norte-pro
spec:
selector: { app.kubernetes.io/name: botiga-web }
ports: [{ name: http, port: 80, targetPort: http }]
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: botiga-web
namespace: rutas-norte-pro
annotations:
cert-manager.io/cluster-issuer: letsencrypt-produccio
nginx.ingress.kubernetes.io/from-to-www-redirect: "true"
spec:
ingressClassName: nginx
tls:
- hosts: ["www.rutasnorte.example"]
secretName: www-rutasnorte-tls
rules:
- host: www.rutasnorte.example
http:
paths:
- path: /
pathType: Prefix
backend:
service: { name: botiga-web, port: { name: http } }
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: botiga-web
namespace: rutas-norte-pro
spec:
scaleTargetRef: { apiVersion: apps/v1, kind: Deployment, name: botiga-web }
minReplicas: 3
maxReplicas: 15
metrics:
- type: Resource
resource:
name: cpu
target: { type: Utilization, averageUtilization: 70 }
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: botiga-web
namespace: rutas-norte-pro
spec:
minAvailable: 2
selector:
matchLabels: { app.kubernetes.io/name: botiga-web }Fixa't que readOnlyRootFilesystem: true amb nginx obliga a muntar emptyDir a /var/cache/nginx i /var/run, i que el listen és el 8080 i no el 80: un procés sense privilegis no pot obrir ports per sota del 1024.
- Ordre d'aplicació i per què importa
Amb Argo CD (10-05) l'ordre el resolen les ones de sincronització (argocd.argoproj.io/sync-wave), però convé entendre'n la lògica, perquè és la mateixa que s'aplica en desplegar a mà a dev o en depurar.
| Ona | Objectes | Motiu |
|---|---|---|
| -2 | Namespace, ResourceQuota, LimitRange | Res no existeix fora d'un namespace |
| -1 | ServiceAccount, RBAC, NetworkPolicy | La identitat i les regles han d'existir abans que la càrrega |
| 0 | ConfigMap, ExternalSecret | El Deployment falla en arrencar si el ConfigMap no hi és |
| 1 | Deployment, Service | La càrrega i el seu nom estable |
| 2 | Ingress, HPA, PDB, ServiceMonitor | Depenen que Service i Deployment existeixin |
Kubernetes és eventualment consistent: si apliques el Deployment abans que el ConfigMap, els pods entren en CreateContainerConfigError i es recuperen sols quan el ConfigMap apareix. L'ordre no és una obligació tècnica estricta, és una manera que el desplegament no generi alertes espúries ni minuts de confusió.
Hi ha dues excepcions on l'ordre sí que és obligatori: els CRD abans que els seus recursos personalitzats (aplicar un ServiceMonitor sense l'operador de Prometheus instal·lat dona error dur), i la NetworkPolicy abans que el pod en entorns amb deny-all, perquè si no el pod arrenca, falla la seva sonda de preparació contra la base de dades i entra en reinicis innecessaris.
- Verificació per capes, de dins cap enfora
Desplegat no és funcionant. Aquesta seqüència de vuit comprovacions va de la capa més interna a la més externa, i cada pas només té sentit si l'anterior ha passat. És la rutina que ha d'executar qui desplega, i la que estructura el runbook de "un desplegament ha sortit malament" d'11-06.
graph LR A[1 Pod arrenca] --> B[2 Contenidor a punt] B --> C[3 Endpoints poblats] C --> D[4 DNS resol] D --> E[5 Service respon<br/>des del clúster] E --> F[6 Ingress respon<br/>des de fora] F --> G[7 Certificat vàlid] G --> H[8 Mètriques rascades] H --> I[9 Alertes en silenci]
Capa 1: el pod arrenca
kubectl -n rutas-norte-pro rollout status deploy/api-reserves --timeout=5m
kubectl -n rutas-norte-pro get pods -l app.kubernetes.io/name=api-reserves -o wideNAME READY STATUS RESTARTS AGE NODE
api-reserves-7d4b8c9f5d-2xk9p 1/1 Running 0 62s ip-10-0-2-41
api-reserves-7d4b8c9f5d-8mqrt 1/1 Running 0 58s ip-10-0-3-17
api-reserves-7d4b8c9f5d-p4vzn 1/1 Running 0 55s ip-10-0-1-93
api-reserves-7d4b8c9f5d-w6hxl 1/1 Running 0 51s ip-10-0-2-88Si falla: kubectl describe pod i mirar els esdeveniments. ImagePullBackOff apunta al registre o al digest; CreateContainerConfigError, a un ConfigMap o Secret que no existeix; Pending, a recursos insuficients o a un topologySpreadConstraint impossible.
Capa 2: el contenidor està a punt
READY 1/1 ja ho diu, però convé distingir "arrencat" de "preparat":
kubectl -n rutas-norte-pro get pods -l app.kubernetes.io/name=api-reserves \
-o custom-columns='POD:.metadata.name,LLEST:.status.conditions[?(@.type=="Ready")].status,REINICIS:.status.containerStatuses[0].restartCount'Si un pod està Running però 0/1, la sonda de preparació falla: gairebé sempre api-reserves no aconsegueix connectar amb postgres-reserves o amb redis-cache. Es comprova amb kubectl logs i es resol mirant la NetworkPolicy o les credencials.
Capa 3: els Endpoints estan poblats
Aquest és el pas que més gent es salta i el que més vegades explica un 503.
kubectl -n rutas-norte-pro get endpointslice -l kubernetes.io/service-name=api-reserves -o yaml | grep -A3 addressesEndpoints buits amb pods sans significa selector desalineat: les etiquetes del spec.selector del Service no coincideixen amb les de template.metadata.labels. És una fallada silenciosa: cap objecte no dona error, simplement no arriba trànsit.
Capa 4: el DNS resol
kubectl -n rutas-norte-pro run depurar --rm -it --restart=Never \
--image=registry.rutasnorte.example/utils/netdebug:1.4 -- \
nslookup api-reserves.rutas-norte-pro.svc.cluster.localSi no resol: CoreDNS caigut, o una NetworkPolicy que no permet l'egrés al port 53 cap a kube-system (la fallada més habitual després d'implantar deny-all).
Capa 5: el Service respon des de dins del clúster
kubectl -n rutas-norte-pro run depurar --rm -it --restart=Never \
--image=registry.rutasnorte.example/utils/netdebug:1.4 -- \
curl -s -o /dev/null -w '%{http_code} %{time_total}s\n' \
http://api-reserves/salutSi el pod respon però el Service no, sospita del targetPort o de la NetworkPolicy d'ingrés.
Capa 6: l'Ingress respon des de fora
curl -s -o /dev/null -w 'http=%{http_code} tls=%{ssl_verify_result} t=%{time_total}s\n' \
https://api.rutasnorte.example/salutSi dona 404 des de nginx, revisa ingressClassName i el host. Si dona 503, l'Ingress apunta a un Service sense Endpoints: t'has saltat la capa 3.
Capa 7: el certificat és vàlid
kubectl -n rutas-norte-pro get certificate api-rutasnorte-tls
echo | openssl s_client -connect api.rutasnorte.example:443 -servername api.rutasnorte.example 2>/dev/null \
| openssl x509 -noout -issuer -datesNAME READY SECRET AGE
api-rutasnorte-tls True api-rutasnorte-tls 184d
issuer=C=US, O=Let's Encrypt, CN=R11
notBefore=Apr 18 09:12:44 2026 GMT
notAfter=Jul 17 09:12:43 2026 GMTUn Certificate en False durant més d'uns minuts sol ser el repte ACME que falla: mira l'Order i el Challenge (04-05).
Capa 8: Prometheus rasca les mètriques
kubectl -n monitoratge port-forward svc/prometheus-operated 9090:9090 &
curl -sG 'http://localhost:9090/api/v1/query' \
--data-urlencode 'query=up{job="api-reserves"}' | jq '.data.result[] | {pod: .metric.pod, valor: .value[1]}'{"pod":"api-reserves-7d4b8c9f5d-2xk9p","valor":"1"}
{"pod":"api-reserves-7d4b8c9f5d-8mqrt","valor":"1"}Un objectiu absent gairebé sempre és l'etiqueta release: del ServiceMonitor, que ha de coincidir amb el serviceMonitorSelector del Prometheus.
Capa 9: les alertes estan en silenci
curl -s http://alertmanager.rutas-norte.example/api/v2/alerts \
| jq '[.[] | select(.labels.service=="api-reserves")] | length'Zero alertes actives quinze minuts després del desplegament és el criteri real d'"ha anat bé". Abans d'això, les finestres de les regles (for: 10m) encara no han tancat.
- La llista de comprovació de "a punt per a producció"
Aquesta taula s'enganxa tal qual a la descripció de la petició de canvi que promociona a rutas-norte-pro. Cada línia la marca una persona, no un script.
| # | Comprovació | Com es verifica | OK |
|---|---|---|---|
| 1 | Imatge per digest, signada i escanejada sense crítiques | cosign verify + informe de Trivy a la canalització |
☐ |
| 2 | Configuració fora de la imatge | Diff de ConfigMap entre pre i pro |
☐ |
| 3 | Sense estat local al pod | Revisió de codi; sessió en JWT i carretó a Redis | ☐ |
| 4 | Tancament net verificat | Prova de càrrega durant un rollout restart: 0 errors 5xx |
☐ |
| 5 | Sondes diferenciades i amb startupProbe |
Manifest revisat | ☐ |
| 6 | requests i limits posats, sense límit de CPU |
Manifest revisat; recomanació de VPA consultada | ☐ |
| 7 | securityContext sense root, sense escalada, arrel de només lectura |
Kyverno en mode bloqueig ho garanteix (08-03) | ☐ |
| 8 | topologySpreadConstraints per zona i node |
Manifest revisat | ☐ |
| 9 | PDB coherent amb minReplicas de l'HPA |
minAvailable < minReplicas |
☐ |
| 10 | HPA amb min i max dimensionats per al pic previst |
Càlcul del pont de maig documentat | ☐ |
| 11 | NetworkPolicy d'ingrés i egrés explícites | kubectl describe netpol |
☐ |
| 12 | ServiceMonitor actiu i panell de Grafana existent | up{job=...} == 1 i enllaç al panell |
☐ |
| 13 | Alertes amb enllaç a runbook a les anotacions | Regla de PrometheusRule revisada | ☐ |
| 14 | Ingress amb TLS vàlid i renovació automàtica | Certificate en Ready |
☐ |
| 15 | Verificació per capes 1-9 executada després de desplegar | Sortida enganxada a la petició de canvi | ☐ |
La fila 9 és la que més incidències evita i la que més s'oblida: si l'HPA pot baixar a 2 rèpliques i el PDB exigeix minAvailable: 3, el drenatge d'un node es bloqueja indefinidament i el manteniment del clúster es queda penjat.
- L'error característic de cada capa
Cada capa falla d'una manera reconeixible. Saber el símptoma característic estalvia la meitat del temps de diagnòstic; el procediment profund és a 07-06.
| Capa | Símptoma | Causa més probable | Primera comanda |
|---|---|---|---|
| Pod | ImagePullBackOff |
Digest inexistent o credencial del registre | kubectl describe pod |
| Pod | Pending perllongat |
Sense recursos, o topologySpread amb DoNotSchedule |
kubectl describe pod (esdeveniments del planificador) |
| Pod | CrashLoopBackOff |
Falla en arrencar, o livenessProbe amb llindar molt curt |
kubectl logs --previous |
| Contenidor | Running però 0/1 |
Dependència no assolible des de /preparat |
kubectl logs + provar la dependència |
| Endpoints | Llista buida amb pods sans | Selector del Service desalineat | kubectl get endpointslice |
| DNS | NXDOMAIN des d'un pod |
CoreDNS, o egrés al 53 bloquejat | nslookup des d'un pod de depuració |
| Service | Connecta però dona temps d'espera | targetPort erroni o NetworkPolicy d'ingrés |
curl al Service des del clúster |
| Ingress | 404 de nginx | ingressClassName o host mal posats |
kubectl describe ing |
| Ingress | 503 | Service sense Endpoints a punt | tornar a la capa 3 |
| TLS | Avís del navegador | Certificate no Ready, repte ACME fallit |
kubectl describe certificate |
| Mètriques | Objectiu absent a Prometheus | Etiqueta release: del ServiceMonitor |
up{job="..."} |
| Alertes | Alerta que no s'apaga | Regla mal escrita o llindar irreal | Executar l'expressió a Prometheus |
Errors Comuns i Consells
- Desplegar sense
preStopni gestió deSIGTERMi culpar l'Ingress dels 502. És l'error més freqüent en passar dedeva producció. La prova definitiva: llança una càrrega sostinguda amb k6 i executakubectl rollout restarta mitges. Si apareix un sol 5xx, el tancament net no està bé. - Posar
livenessProbesobre un endpoint que comprova dependències. Converteix una degradació de la base de dades en un reinici massiu de tota l'API. La sonda de vida només ha de mirar cap endins. - Fixar
replicasal Deployment tenint HPA. Cada sincronització d'Argo CD torna les rèpliques al valor del fitxer, i desfà l'escalat. Es resol ambignoreDifferences(10-05) i traient el camp. - Un PDB amb
minAvailableigual al nombre de rèpliques. Bloqueja tot drenatge. Fes servir percentatges i comprova la coherència amb elminReplicasde l'HPA. - Oblidar l'etiqueta
release:al ServiceMonitor. El component es desplega perfectament i queda invisible per al monitoratge. Ningú no se n'adona fins al primer incident. - Consell: automatitza la llista de comprovació en el que es pugui, però mantén la revisió humana. Kyverno pot exigir les files 6, 7 i 11; ningú no pot exigir per política la fila 10, que requereix haver pensat en el pont de maig.
- Consell: desa la sortida de la verificació per capes a la petició de canvi. Quan alguna cosa falli tres setmanes després, saber que el dia del desplegament el certificat era vàlid i les mètriques es rascaven acota enormement la cerca.
Exercicis
Exercici 1: detectar objectes absents
Un company ha desplegat un component nou, promocions-web, a rutas-norte-pro amb aquests objectes: Deployment (3 rèpliques fixes, sense sondes), Service i Ingress. Enumera quins objectes falten dels tretze de la taula de l'apartat 2 i descriu, per a cadascun, l'escenari concret en què la seva absència causarà una incidència a Rutas Norte.
Exercici 2: verificació per capes d'una fallada real
Després de desplegar api-reserves a rutas-norte-pre, aquesta és la situació:
$ kubectl -n rutas-norte-pre get pods -l app.kubernetes.io/name=api-reserves
NAME READY STATUS RESTARTS AGE
api-reserves-6c9f7d8b4c-hh2ks 0/1 Running 0 4m
api-reserves-6c9f7d8b4c-tq5rl 0/1 Running 0 4m
$ curl -s -o /dev/null -w '%{http_code}\n' https://api-pre.rutasnorte.example/salut
503Indica en quina capa de les nou és la fallada, quines tres comandes executaries i en quin ordre, i quines són les dues causes més probables.
Exercici 3: coherència entre HPA i PDB
worker-notificacions té un HPA governat per KEDA amb minReplicaCount: 1 i maxReplicaCount: 20, i un PDB amb minAvailable: 2. L'equip de plataforma necessita drenar un node per actualitzar el clúster i el kubectl drain es queda penjat. Explica per què i proposa'n la correcció, justificant el valor triat.
Solucions
Solució 1. Falten deu objectes:
| Falta | Escenari d'incidència |
|---|---|
| ServiceAccount propi | Fa servir la default; l'auditoria no pot atribuir accions i hereta permisos no previstos |
| ConfigMap | La configuració va a la imatge: promocionar de pre a pro exigeix reconstruir, i trenca la traçabilitat del digest |
| Secret via ESO | Credencials posades a mà, sense rotació |
| Sondes | S'enruta trànsit a pods que encara arrenquen: 502 a cada desplegament |
securityContext |
Kyverno en mode bloqueig (08-03) rebutjarà el pod; si està en mode avís, corre com a root |
topologySpreadConstraints |
Les 3 rèpliques poden caure al mateix node; aquest node es perd i el servei també |
| HPA | Les 3 rèpliques fixes se saturen el pont de maig |
| PDB | Un drenatge pot endur-se les 3 rèpliques alhora |
| NetworkPolicy | Amb deny-all al namespace, ni tan sols arrencarà; sense ell, queda exposat a moviment lateral |
| ServiceMonitor | Sense mètriques ni alertes: el component és invisible durant el primer incident |
Solució 2. La fallada és a la capa 2 (contenidor no preparat), i el 503 de la capa 6 n'és una conseqüència: sense pods a punt, no hi ha Endpoints. Comandes, en ordre:
kubectl -n rutas-norte-pre logs -l app.kubernetes.io/name=api-reserves --tail=50
kubectl -n rutas-norte-pre describe pod -l app.kubernetes.io/name=api-reserves | grep -A10 Events
kubectl -n rutas-norte-pre get endpointslice -l kubernetes.io/service-name=api-reservesCauses més probables: (a) /preparat falla perquè no hi ha connexió amb postgres-reserves (credencial de l'ExternalSecret no sincronitzada a pre, o NetworkPolicy sense la regla d'egrés al 5432); (b) l'aplicació arrenca però triga més que la startupProbe, encara que en aquest cas el normal seria veure reinicis, que aquí són 0, cosa que reforça la hipòtesi (a).
Solució 3. Amb minReplicaCount: 1, KEDA pot tenir el worker a una sola rèplica en hores vall. El PDB exigeix que en quedin 2 de disponibles, condició impossible amb 1 rèplica total, així que drain no pot desallotjar el pod i espera indefinidament. Correcció: fer servir minAvailable: 1 o, millor, maxUnavailable: 1, que s'expressa en termes de quantes rèpliques es poden perdre i funciona correctament en tot el rang d'1 a 20. Alternativa complementària: pujar minReplicaCount a 2 si el negoci exigeix que les notificacions no quedin mai sense capacitat, assumint el cost de la rèplica permanent.
Conclusió
Hem recorregut, de principi a fi, el camí d'una aplicació web sense estat fins a producció: els set requisits que s'han de complir a l'aplicació abans de tocar el YAML, els tretze objectes que formen un component complet i què es perd amb cadascun que falti, els manifests íntegres d'api-reserves i botiga-web que serveixen de referència per a la resta del curs, l'ordre d'aplicació, la verificació per capes de dins cap enfora amb la comanda exacta de cada pas, la llista de comprovació revisable en una petició de canvi i el símptoma característic de la fallada de cada capa.
La lliçó clau és que "està desplegat" i "està a punt per a producció" són afirmacions molt diferents, i que la diferència entre totes dues són precisament els objectes i les comprovacions que resulta temptador deixar per a després.
Tot això ha estat relativament còmode per una raó: botiga-web i api-reserves no desen res. Es poden matar, moure i multiplicar sense conseqüències. A la propera lliçó, Execució d'Aplicacions amb Estat, entrem al terreny on això deixa de ser cert: postgres-reserves desa les dades personals dels clients de Rutas Norte, no es pot reemplaçar un pod alegrement, i la primera pregunta que cal respondre honestament és si aquesta base de dades hauria de viure a Kubernetes.
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
