Comencem pel que cap lliçó de Kubernetes no sol dir al primer paràgraf: per a Tramontana S.L., Kubernetes és desproporcionat. Una aplicació, una base de dades, tres persones i cinc-centes reserves al mes no justifiquen un orquestrador de contenidors distribuït. Muntar-lo aquí multiplicaria la complexitat operativa per cinc a canvi de resoldre problemes que Tramontana no té.

Aquesta lliçó ho dirà amb números a l'apartat 2, i tot i així la faràs sencera. Per tres raons que sí que se sostenen:

  1. És la tecnologia dominant del sector. Te la trobaràs a la teva propera feina, a la majoria de les ofertes d'ocupació d'administració de sistemes i pràcticament a qualsevol equip de plataforma.
  2. Entendre-la et fa millor administrador encara que no la facis servir. Kubernetes és una destil·lació de dècades de bones pràctiques —salut, desplegaments progressius, límits de recursos, configuració separada del codi— i aquelles idees s'apliquen igual amb systemd.
  3. Saber quan NO fer-la servir és part de saber-la fer servir. I això només es pot argumentar havent-la muntada.

Així que la construeixes a srv-tramontana-proves, amb k3s, i hi desplegues Tramontana Reserves de debò. Al final tornes a la pregunta amb coneixement de causa.

Contingut

  1. Objectiu, requisits previs i advertiment
  2. Quin problema resol Kubernetes i quan NO fer-lo servir
  3. Arquitectura: pla de control i nodes de treball
  4. Distribucions: kubeadm, k3s, microk8s i els gestionats
  5. Instal·lació de k3s amb un servidor i un agent
  6. kubectl i el kubeconfig
  7. Els objectes, en ordre de dependència
  8. Manifests complets per a Tramontana Reserves
  9. Desplegament progressiu i marxa enrere
  10. Seguretat: securityContext, NetworkPolicy, RBAC i PSS
  11. Diagnòstic: llegir esdeveniments i les quatre fallades habituals
  12. Automatització amb Ansible
  13. La complexitat operativa real

Objectiu, requisits previs i advertiment

Objectiu. Aixecar un clúster funcional de dos nodes, desplegar-hi Tramontana Reserves amb alta disponibilitat interna, exposició TLS, configuració i secrets separats, sondes de salut i límits de recursos; fer un desplegament sense interrupció i una marxa enrere; i saber diagnosticar les fallades habituals.

Requisits previs: contenidors i Docker (07-05), especialment espais de noms, cgroups, capabilities i Dockerfile multietapa; l'entorn de virtualització de 07-04; el servidor intermediari invers de 08-01; i la noció de sondes de salut de 07-07.

Advertiment operatiu, i va de debò: tot això es fa a srv-tramontana-proves. No s'instal·la Kubernetes a srv-tramontana. k3s modifica les regles d'iptables/nftables, gestiona les seves pròpies interfícies de xarxa i arrenca un entorn d'execució de contenidors propi; en un servidor de producció amb ufw, nftables, AppArmor i un servei en marxa, la interferència és real i difícil de desfer.

$ ssh [email protected] 'hostnamectl; free -m | head -2; nproc'
 Static hostname: srv-tramontana-proves
  Operating System: Ubuntu 24.04.1 LTS
            Kernel: Linux 6.8.0-41-generic
               total        used        free
Mem:            3891         412        3102
2

I una segona màquina virtual per al node de treball, creada amb cloud-init com a 07-04:

$ virt-install --name k8s-node2 --memory 2048 --vcpus 2 --disk size=20 \
    --cloud-init user-data=cloud-init-node2.yaml \
    --os-variant ubuntu24.04 --import --noautoconsole
$ virsh domifaddr k8s-node2
 vnet2  52:54:00:a1:b2:c4  ipv4  192.168.122.105/24

Quin problema resol Kubernetes i quan NO fer-lo servir

El problema real

Kubernetes va néixer per gestionar molts contenidors en moltes màquines de manera declarativa. Els problemes que resol són concrets:

Problema Sense Kubernetes Amb Kubernetes
En quina màquina arrenco aquest contenidor? Ho decideix una persona El planificador, segons recursos
Un contenidor mor Algú el reinicia Es reinicia sol
Una màquina mor Algú recol·loca les seves càrregues Es recol·loquen soles
Desplegar sense tallar servei Script de drenatge (07-07) Natiu: RollingUpdate
On és el servei X? IP fixa o DNS manual DNS intern automàtic
Escalar de 3 a 30 rèpliques Aprovisionar i configurar kubectl scale, segons
Configuració i secrets Fitxers per màquina ConfigMap i Secret, versionats
Estat desitjat del sistema Documentació i confiança El clúster reconcilia sol

Aquella última fila és la idea central. Kubernetes és un bucle de reconciliació: tu declares «vull tres rèpliques d'aquesta imatge amb aquests límits», i uns controladors comparen contínuament l'estat real amb el declarat i actuen per acostar-los. No dones ordres; descrius un objectiu. És el mateix principi d'idempotència d'Ansible a 07-06, però continu en lloc de puntual.

Quan NO fer-lo servir, i el cas de Tramontana

Senyal Kubernetes?
1-3 serveis, una o dues màquines No
Un equip sense persona dedicada a plataforma No
Càrrega estable i predictible No
Aplicació monolítica amb estat al disc local No, o amb molta feina
Desenes de serveis i equips Sí
Escalat freqüent i impredictible Sí
Desplegaments diversos cops al dia Sí
Multiinquilí amb aïllament Sí
Ja feu servir un proveïdor amb Kubernetes gestionat Probablement sí

L'anàlisi per a Tramontana, amb números:

Concepte Situació actual Amb Kubernetes
Serveis que cal orquestrar 1 aplicació + 1 base de dades Els mateixos
Màquines necessàries 1 (2 amb la rèplica) 3 com a mínim per a pla de control amb quòrum
Peces noves que cal mantenir — etcd, CNI, Ingress, certificats interns, CRD
Actualitzacions a l'any apt upgrade 3-4 versions menors, cadascuna amb notes de ruptura
Formació necessària Ja la tens 3-6 mesos fins a ser productiu
Temps de recuperació en una fallada 50 min (mesurat, 07-06) Depèn de si la fallada és del clúster
Benefici en disponibilitat ~99,8 % amb l'opció B de 07-07 El mateix, amb més complexitat

La conclusió és inequívoca: Kubernetes no aportaria a Tramontana res que no aporti l'opció B de 07-07 —dos nodes d'aplicació amb un balancejador—, i hi afegiria un pla de control sencer per mantenir. La recomanació professional continua sent la de 07-07.

I el corol·lari que convé interioritzar: Kubernetes no és l'evolució natural d'un sistema ben administrat. És una eina per a un problema d'escala concret. Un sistema de tres màquines ben mantingut amb systemd, Ansible i un balancejador és una arquitectura excel·lent, no una etapa immadura.

Arquitectura: pla de control i nodes de treball

graph TB
    subgraph CP["PLA DE CONTROL (node servidor)"]
        API["kube-apiserver<br/>única porta d'entrada<br/>valida, autentica, persisteix"]
        ETCD[("etcd<br/>base de dades clau-valor<br/>TOT l'estat viu aquí")]
        SCH["kube-scheduler<br/>decideix EN QUIN NODE<br/>va cada Pod"]
        CM["kube-controller-manager<br/>bucles de reconciliació<br/>real vs. desitjat"]
        API <--> ETCD
        SCH --> API
        CM --> API
    end
    subgraph N1["NODE DE TREBALL 1"]
        K1["kubelet<br/>agent: arrenca i vigila<br/>els Pods d'aquest node"]
        P1["kube-proxy<br/>regles de xarxa<br/>per als Services"]
        R1["containerd<br/>entorn d'execució de contenidors"]
        K1 --> R1
    end
    subgraph N2["NODE DE TREBALL 2"]
        K2["kubelet"]
        P2["kube-proxy"]
        R2["containerd"]
        K2 --> R2
    end
    K1 -.->|"«què em toca<br/>executar?»"| API
    K2 -.-> API
    P1 -.-> API
    P2 -.-> API
    USER["kubectl apply -f app.yaml"] --> API

El pla de control, les quatre peces que cal conèixer:

Component Què fa Si falla
kube-apiserver Única porta d'entrada. Autentica, autoritza, valida i escriu a etcd. Tot hi passa No es pot canviar res; el que ja corre continua corrent
etcd Base de dades clau-valor amb tot l'estat del clúster El clúster és irrecuperable sense còpia de seguretat
kube-scheduler Tria node per a cada Pod nou segons recursos, afinitats i restriccions Els Pods nous es queden en Pending
kube-controller-manager Bucles que reconcilien estat real i desitjat (rèpliques, nodes, endpoints) Res no s'autorepara

Els nodes de treball:

Component Què fa
kubelet Agent a cada node: pregunta a l'apiserver què li toca, arrenca contenidors, executa les sondes i informa de l'estat
kube-proxy Programa les regles de xarxa (iptables o IPVS) que fan funcionar els Services
entorn d'execució (containerd) Executa els contenidors. És el de 07-05, sense Docker pel mig

Dues observacions que ordenen el model mental:

Tot passa per l'apiserver. No hi ha comunicació directa entre components: el scheduler no parla amb el kubelet, escriu a l'apiserver que aquell Pod va a aquell node, i el kubelet ho llegeix. Aquella arquitectura de bus central és el que fa que el sistema sigui extensible i auditable.

Si el pla de control cau, la càrrega continua funcionant. Els kubelets continuen executant el que ja tenien i reiniciant el que caigui. El que es perd és la capacitat de canviar coses i de reaccionar a fallades de nodes sencers. És una propietat de disseny valuosa i sorprèn molta gent.

Distribucions: kubeadm, k3s, microk8s i els gestionats

kubeadm k3s microk8s minikube EKS/GKE/AKS
Qui el manté El projecte SUSE (Rancher) Canonical El projecte El proveïdor
Instal·lació Diversos passos Una ordre Un snap Una ordre Consola web
Mida del binari ~1 GB en peces ~70 MB, un de sol ~200 MB Variable —
RAM mínima realista 2 GB per node 512 MB 1 GB 2 GB —
Magatzem d'estat etcd SQLite per defecte, etcd opcional dqlite etcd Gestionat
Multinode real Sí Sí Sí No (és local) Sí
Certificat de conformitat Sí Sí Sí Sí Sí
Producció Sí Sí, amb matisos Sí No Sí
Porta Ingress i balancejador No Sí: Traefik + ServiceLB Amb complements Amb complements Sí
Cost del pla de control El maquinari El maquinari El maquinari — 70-100 €/mes

L'elecció és k3s, per quatre motius concrets:

  1. Cap a la màquina de proves. Amb 3,8 GB i 2 vCPU, kubeadm va just; k3s deixa lloc per desplegar-hi alguna cosa a sobre.
  2. Un binari i una ordre, sense deixar de ser Kubernetes certificat: els manifests són idèntics i el que s'aprèn es transfereix sense canvis.
  3. Porta l'imprescindible muntat: Traefik com a Ingress, ServiceLB per a Services de tipus LoadBalancer, local-path com a StorageClass i CoreDNS. Amb kubeadm caldria instal·lar i configurar cada peça, cosa que és instructiva i aquí és soroll.
  4. És producció real, no una joguina: es fa servir en desplegaments a la vora, en fàbriques i en dispositius.

El seu compromís: SQLite en lloc d'etcd per defecte, cosa que significa un sol node de pla de control. Per aprendre i per a molts casos reals, sobra; per a alta disponibilitat del pla de control cal passar a etcd encastat amb tres nodes.

Sobre els serveis gestionats (EKS, GKE, AKS): el proveïdor manté el pla de control —actualitzacions, certificats, etcd, còpies— i tu només poses nodes de treball. Costa uns 70-100 € al mes per clúster, i per a una empresa petita que realment necessiti Kubernetes sol ser l'opció sensata: la major part de la complexitat operativa de l'apartat 13 desapareix de la teva teulada.

Instal·lació de k3s amb un servidor i un agent

# ===== NODE SERVIDOR: srv-tramontana-proves (192.168.122.104) =====
$ ssh [email protected]

# Descarregar i REVISAR l script abans d executar-lo. Canalitzar un URL
# directament a sh es exactament el que 05-03 desaconsella.
$ curl -sfL https://get.k3s.io -o /tmp/k3s-install.sh
$ sha256sum /tmp/k3s-install.sh
$ less /tmp/k3s-install.sh

$ INSTALL_K3S_VERSION="v1.30.4+k3s1" \
  INSTALL_K3S_EXEC="server \
      --write-kubeconfig-mode 0644 \
      --disable traefik=false \
      --node-label rol=servidor \
      --tls-san 192.168.122.104" \
  sh /tmp/k3s-install.sh
[INFO]  Using v1.30.4+k3s1 as release
[INFO]  systemd: Starting k3s

$ sudo systemctl status k3s --no-pager | head -4
● k3s.service - Lightweight Kubernetes
     Active: active (running) since Tue 2026-08-18 15:02:11 CEST

Fixar la versió amb INSTALL_K3S_VERSION no és opcional. Sense ella s'instal·la l'última, i una màquina reconstruïda sis mesos després tindria una versió diferent: exactament el problema que Ansible va venir a resoldre a 07-06. És la mateixa política de fixació de versions de 05-03.

# El testimoni que autentica els nodes que s uneixin. Es un secret real:
# amb ell, qualsevol pot unir un node al cluster.
$ sudo cat /var/lib/rancher/k3s/server/node-token
K10a3f19c8d::server:8f2b1c4d5e6a7b8c9d0e1f2a3b4c5d6e
# ===== NODE AGENT: k8s-node2 (192.168.122.105) =====
$ ssh [email protected]
$ curl -sfL https://get.k3s.io -o /tmp/k3s-install.sh
$ INSTALL_K3S_VERSION="v1.30.4+k3s1" \
  K3S_URL="https://192.168.122.104:6443" \
  K3S_TOKEN="K10a3f19c8d::server:8f2b1c4d5e6a7b8c9d0e1f2a3b4c5d6e" \
  INSTALL_K3S_EXEC="agent --node-label rol=treball" \
  sh /tmp/k3s-install.sh
# ===== Verificacio, des del servidor =====
$ sudo k3s kubectl get nodes -o wide
NAME                    STATUS   ROLES                  AGE   VERSION        INTERNAL-IP
srv-tramontana-proves   Ready    control-plane,master   4m    v1.30.4+k3s1   192.168.122.104
k8s-node2               Ready    <none>                 1m    v1.30.4+k3s1   192.168.122.105

$ sudo k3s kubectl get pods -A
NAMESPACE     NAME                                     READY   STATUS      RESTARTS
kube-system   coredns-6799fbcd5-x8k2p                  1/1     Running     0
kube-system   local-path-provisioner-6c86858495-mn4qt  1/1     Running     0
kube-system   metrics-server-54fd9b65b-7jc9d           1/1     Running     0
kube-system   svclb-traefik-4a1b2c3d-p9k4m             2/2     Running     0
kube-system   traefik-7d764994d8-lm2xw                 1/1     Running     0

Recursos que consumeix el clúster buit, que és una dada honesta que convé tenir:

$ sudo k3s kubectl top nodes
NAME                    CPU(cores)   CPU%   MEMORY(bytes)   MEMORY%
srv-tramontana-proves   142m         7%     812Mi           21%
k8s-node2               38m          1%     284Mi           14%

812 MiB de RAM i un 7 % de CPU sense desplegar res. Amb kubeadm serien 1,5-2 GB. Aquell és el preu d'entrada de l'orquestració, i és part de l'argument de l'apartat 2.

kubectl i el kubeconfig

# Des del teu portatil, sense dependre de sudo al servidor
$ sudo apt install kubernetes-client   # o descarregar el binari oficial
$ mkdir -p ~/.kube
$ scp [email protected]:/etc/rancher/k3s/k3s.yaml ~/.kube/config-proves
$ sed -i 's/127.0.0.1/192.168.122.104/' ~/.kube/config-proves
$ chmod 0600 ~/.kube/config-proves
$ export KUBECONFIG=~/.kube/config-proves

El kubeconfig és un YAML amb tres llistes —clusters, users i contexts— i un context actiu. Conté credencials d'administrador del clúster, així que va amb permisos 600 i mai a un repositori:

# ~/.kube/config-proves (fragment)
clusters:
- cluster:
    certificate-authority-data: LS0tLS1CRUdJTiBD...   # la CA del cluster
    server: https://192.168.122.104:6443
  name: default
users:
- name: default
  user:
    client-certificate-data: LS0tLS1CRUdJTiBD...      # el teu certificat
    client-key-data: LS0tLS1CRUdJTiBS...              # LA TEVA CLAU PRIVADA
contexts:
- context: {cluster: default, user: default, namespace: tramontana}
  name: proves
current-context: proves

Les ordres que es fan servir el 95 % del temps:

Ordre Per a què
kubectl get <tipus> Llistar. -o wide afegeix columnes; -o yaml dona l'objecte complet
kubectl describe <tipus> <nom> Detall i esdeveniments: la primera ordre davant d'un problema
kubectl logs <pod> Registres. -f segueix; --previous mostra els del contenidor anterior
kubectl exec -it <pod> -- sh Shell dins del contenidor
kubectl apply -f <fitxer> Aplicar un manifest de manera declarativa
kubectl delete -f <fitxer> Eliminar el que declara
kubectl get events --sort-by=.lastTimestamp Què ha passat, en ordre
kubectl -n <namespace> Treballar en un altre namespace
$ kubectl get nodes
NAME                    STATUS   ROLES                  AGE   VERSION
srv-tramontana-proves   Ready    control-plane,master   12m   v1.30.4+k3s1
k8s-node2               Ready    <none>                 9m    v1.30.4+k3s1

# Autocompletat i un alies: es fa servir dotzenes de vegades al dia
$ echo 'source <(kubectl completion bash)' >> ~/.bashrc
$ echo 'alias k=kubectl; complete -o default -F __start_kubectl k' >> ~/.bashrc

--previous mereix una nota: quan un contenidor es reinicia en bucle, kubectl logs mostra els de l'intent actual, que sol estar buit perquè acaba d'arrencar. Els que expliquen la fallada són els de l'intent anterior, i només es veuen amb --previous. És el primer truc útil de diagnòstic a Kubernetes.

Els objectes, en ordre de dependència

Pod: la unitat, i per què gairebé mai no es crea a mà

Un Pod és un o diversos contenidors que comparteixen espai de xarxa (mateixa IP, mateix localhost), emmagatzematge i cicle de vida. És la unitat mínima que Kubernetes planifica.

$ kubectl run prova --image=nginx:1.27-alpine --restart=Never
$ kubectl get pod prova -o wide
NAME    READY   STATUS    RESTARTS   AGE   IP           NODE
prova   1/1     Running   0          8s    10.42.1.14   k8s-node2

$ kubectl delete pod prova
$ kubectl get pods
No resources found.

Ha desaparegut i no torna. Aquí hi ha la raó de no crear Pods directament: un Pod és efímer i ningú no el vigila. Si el node mor, el Pod mor amb ell. El que es crea és un objecte de nivell superior que garanteixi que existeixin N Pods, passi el que passi.

ReplicaSet i Deployment

Un ReplicaSet manté N rèpliques idèntiques. Tampoc no es crea a mà: el gestiona un Deployment, que a més sap fer desplegaments progressius i marxes enrere.

Deployment  →  gestiona ReplicaSets (un per versió)  →  creen Pods

En canviar la imatge, el Deployment crea un ReplicaSet nou i va reduint el vell mentre augmenta el nou. Els antics es conserven amb zero rèpliques, que és el que permet kubectl rollout undo.

Estratègia Comportament Quan
RollingUpdate (per defecte) Substitueix a poc a poc. Conviuen les dues versions L'habitual
Recreate Mata tot i arrenca el nou. Hi ha tall Quan dues versions no poden coexistir (migració d'esquema incompatible)
strategy:
  type: RollingUpdate
  rollingUpdate:
    maxSurge: 1          # quants Pods DE MES hi pot haver temporalment
    maxUnavailable: 0    # quants de MENYS. 0 = capacitat integra sempre

maxUnavailable: 0 amb maxSurge: 1 és la configuració de desplegament sense interrupció: primer arrenca un de nou i només quan està a punt es retira un de vell. És exactament el drenatge de connexions de 07-07, automatitzat.

Service i el DNS intern

Els Pods tenen IP i canvien constantment. Un Service dona un nom i una IP virtual estables a un conjunt de Pods, seleccionats per etiquetes.

Tipus Abast Ús
ClusterIP (per defecte) Només dins del clúster Comunicació entre serveis
NodePort Un port alt (30000-32767) a tots els nodes Proves; poc elegant
LoadBalancer Demana un balancejador extern Al núvol; a k3s ho serveix ServiceLB
ExternalName Àlies DNS a un nom extern Referenciar la BD de fora del clúster

Com funciona el DNS intern, que és una de les coses més elegants de Kubernetes: CoreDNS crea automàticament un registre per Service amb el patró

<servei>.<namespace>.svc.cluster.local
$ kubectl run dns-test --rm -it --image=busybox:1.36 --restart=Never -- \
      nslookup tramontana.tramontana.svc.cluster.local
Server:    10.43.0.10
Address:   10.43.0.10:53
Name:      tramontana.tramontana.svc.cluster.local
Address:   10.43.112.88

Des d'un Pod del mateix namespace n'hi ha prou amb tramontana; des d'un altre, tramontana.tramontana. Mai no es codifica una IP: es fa servir el nom, i el clúster se n'encarrega.

I el que passa per sota: kube-proxy programa regles a cada node que reescriuen la destinació cap a una de les IP de Pod darrere del Service. No hi ha cap procés balancejador; és el mateix nucli.

Ingress i la seva relació amb l'Nginx de 08-01

Un Service de tipus LoadBalancer per aplicació significa una IP pública per aplicació. Un Ingress fa de servidor intermediari invers HTTP compartit: encamina per nom de domini i per ruta cap a Services diferents, i acaba el TLS.

Un Ingress és només una declaració; cal un controlador que la implementi. k3s porta Traefik.

I aquí hi ha la connexió amb 08-01, que és la pregunta natural: un Ingress fa exactament el que vas fer amb Nginx —acabar el TLS, encaminar per domini, afegir capçaleres, limitar la taxa— amb dues diferències:

Nginx de 08-01 Ingress de Kubernetes
Configuració Fitxer a /etc/nginx/ Objecte declaratiu al clúster
Destinacions IP i port fixos a upstream Services, que segueixen els Pods
En desplegar nginx -s reload Res: els endpoints s'actualitzen sols
Certificats certbot al disc Secret, o cert-manager automàtic
Qui l'aplica systemd El controlador (Traefik, ingress-nginx)

De fet, el controlador més utilitzat és ingress-nginx, que és Nginx per dins generant la seva configuració a partir d'objectes del clúster. El que vas aprendre a 08-01 s'aplica literalment; només canvia qui escriu el fitxer.

ConfigMap i Secret, i l'avís seriós

ConfigMap Secret
Per a què Configuració no sensible Contrasenyes, claus, certificats
Emmagatzematge Text pla a etcd base64 a etcd
Mida màxima 1 MiB 1 MiB
Es pot muntar com a Variables d'entorn o fitxers Igual

Un Secret de Kubernetes és base64, NO xifrat. Base64 és una codificació, no criptografia: es desfà amb una ordre i sense cap clau.

$ kubectl create secret generic prova --from-literal=password='SuperSecreta123'
$ kubectl get secret prova -o jsonpath='{.data.password}' | base64 -d; echo
SuperSecreta123

Aquí està, en clar, amb una ordre i sense credencial addicional. Les conseqüències pràctiques, enllaçant directament amb 06-05:

  • No pugis mai un Secret a Git. Un fitxer YAML amb data: en base64 és un fitxer amb la contrasenya.
  • Per defecte es guarda sense xifrar a etcd. Qui llegeixi el fitxer d'etcd o la seva còpia de seguretat té tots els secrets del clúster.
  • RBAC és el que realment els protegeix: només qui tingui permís de lectura sobre Secrets els veu.

Les tres solucions reals, en ordre creixent de rigor:

Solució Com funciona Valoració
Xifratge en repòs d'etcd EncryptionConfiguration a l'apiserver Mínim indispensable en producció
Sealed Secrets Es xifren amb la clau pública del clúster; el YAML xifrat sí que pot anar a Git Molt pràctic
Magatzem extern (Vault, secrets del proveïdor) Els Pods els demanen en arrencar; mai no són a etcd El més robust

És la mateixa conclusió de 06-05 en un altre embolcall: els secrets no viuen al costat del codi, i pass amb systemd-creds resolia exactament aquest problema fora de Kubernetes.

PersistentVolumeClaim, StorageClass i Namespace

Un contenidor és efímer: el que escriu al seu sistema de fitxers desapareix en reiniciar-se. Un PersistentVolumeClaim (PVC) és una petició d'emmagatzematge persistent; una StorageClass defineix com s'aprovisiona.

$ kubectl get storageclass
NAME                   PROVISIONER             RECLAIMPOLICY   VOLUMEBINDINGMODE
local-path (default)   rancher.io/local-path   Delete          WaitForFirstConsumer

Avís sobre local-path: crea un directori al disc del node. És ràpid i té una limitació greu: el Pod queda lligat a aquell node. Si el node mor, les dades no són enlloc més. Per a emmagatzematge realment distribuït calen Longhorn, Ceph o l'emmagatzematge del proveïdor de núvol.

Un Namespace és una divisió lògica del clúster: separa noms, permet quotes de recursos i és la unitat natural de RBAC i NetworkPolicy.

Manifests complets per a Tramontana Reserves

# ~/tramontana-k8s/00-namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: tramontana
  labels:
    # Pod Security Standards (apartat 10): rebutja Pods privilegiats
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/enforce-version: latest
---
# Quota: impedeix que un error de configuracio consumeixi el cluster sencer
apiVersion: v1
kind: ResourceQuota
metadata:
  name: quota-tramontana
  namespace: tramontana
spec:
  hard:
    requests.cpu: "2"
    requests.memory: 2Gi
    limits.cpu: "4"
    limits.memory: 4Gi
    persistentvolumeclaims: "4"
# ~/tramontana-k8s/10-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: tramontana-config
  namespace: tramontana
data:
  # Es el /etc/tramontana/app.conf de sempre, com a objecte del cluster
  db_host: "10.0.2.15"
  db_port: "6432"              # PgBouncer (08-02)
  db_name: "tramontana"
  max_connexions: "80"
  timeout_consulta: "30"
  log_nivell: "info"
  escolta: "0.0.0.0"           # dins del Pod, no de l amfitrio
  port: "8080"
---
apiVersion: v1
kind: Secret
metadata:
  name: tramontana-secrets
  namespace: tramontana
type: Opaque
stringData:
  # stringData permet escriure en clar i Kubernetes ho codifica sol.
  # AQUEST FITXER NO VA A GIT. En produccio, Sealed Secrets o Vault.
  db_password: "MARCADOR_S_INJECTA_DES_DE_PASS"
# A la practica, el Secret es crea sense fitxer intermedi:
$ kubectl -n tramontana create secret generic tramontana-secrets \
      --from-literal=db_password="$(pass tramontana/db)" \
      --dry-run=client -o yaml | kubectl apply -f -

Aquell --dry-run=client -o yaml | kubectl apply -f - és el patró idiomàtic per crear o actualitzar un Secret sense que quedi a l'historial ni al disc.

# ~/tramontana-k8s/20-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: tramontana
  namespace: tramontana
  labels:
    app: tramontana
spec:
  replicas: 2
  revisionHistoryLimit: 5        # quants ReplicaSets vells cal conservar
  selector:
    matchLabels:
      app: tramontana

  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1                # un de mes durant el desplegament
      maxUnavailable: 0          # MAI menys capacitat de la declarada

  template:
    metadata:
      labels:
        app: tramontana
        version: "3.2.1"
    spec:
      # Repartir les repliques entre nodes diferents: si un node cau,
      # no se n van les dues alhora.
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: kubernetes.io/hostname
          whenUnsatisfiable: ScheduleAnyway
          labelSelector:
            matchLabels:
              app: tramontana

      # --- Seguretat a nivell de Pod (apartat 10) ---
      securityContext:
        runAsNonRoot: true
        runAsUser: 997             # svc-tramontana, l uid de sempre
        runAsGroup: 1002           # grup tramontana
        fsGroup: 1002
        seccompProfile:
          type: RuntimeDefault

      containers:
        - name: tramontana
          image: registre.tramontana.example/tramontana:3.2.1
          # MAI ':latest'. Una etiqueta mobil fa que dos Pods de la
          # mateixa "versio" executin codi diferent, i que una marxa
          # enrere no torni enlloc.
          imagePullPolicy: IfNotPresent

          ports:
            - name: http
              containerPort: 8080

          # --- Configuracio des del ConfigMap ---
          envFrom:
            - configMapRef:
                name: tramontana-config
          env:
            - name: TRAMONTANA_DB_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: tramontana-secrets
                  key: db_password

          # --- Recursos: la relacio amb els cgroups de 07-05 ---
          # requests: el que el PLANIFICADOR reserva per triar node.
          # limits:   el sostre real, imposat per cgroups v2.
          #   - Superar el limit de MEMORIA -> el nucli MATA el
          #     contenidor (OOMKilled): es un limit dur.
          #   - Superar el de CPU -> s ESTRANGULA (throttling), no es
          #     mata: el proces simplement va mes lent.
          resources:
            requests:
              cpu: 100m            # 0,1 nucli
              memory: 256Mi
            limits:
              cpu: 500m
              memory: 512Mi

          # --- Les tres sondes, i cadascuna respon a una cosa diferent ---

          # startupProbe: "ha acabat d arrencar?" Mentre falla,
          # les altres dues NO s executen. Es el que permet arrencades
          # lentes sense relaxar el liveness. 30 x 5 s = 150 s de marge.
          startupProbe:
            httpGet: {path: /salut, port: http}
            periodSeconds: 5
            failureThreshold: 30

          # readinessProbe: "pot atendre peticions ARA?"
          # Si falla, el Pod SURT del Service (deixa de rebre transit)
          # pero NO es reinicia. Es la porta del balancejador.
          readinessProbe:
            httpGet: {path: /salut, port: http}
            periodSeconds: 5
            timeoutSeconds: 2
            successThreshold: 1
            failureThreshold: 3

          # livenessProbe: "es viu o penjat?" Si falla, el kubelet
          # MATA I REINICIA el contenidor. Per aixo es la mes perillosa:
          # mal calibrada, provoca reinicis en cascada sota carrega.
          # Llindars MES LAXOS que els de readiness, sempre.
          livenessProbe:
            httpGet: {path: /salut, port: http}
            periodSeconds: 15
            timeoutSeconds: 3
            failureThreshold: 5

          # --- Seguretat a nivell de contenidor ---
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities:
              drop: ["ALL"]

          # Amb readOnlyRootFilesystem cal donar lloc escrivible
          # explicit per al que de debo ho necessiti.
          volumeMounts:
            - name: tmp
              mountPath: /tmp
            - name: cache
              mountPath: /var/cache/tramontana

          # Tancament ordenat: dona temps a acabar les peticions en
          # curs abans que arribi el SIGTERM.
          lifecycle:
            preStop:
              exec:
                command: ["/bin/sh", "-c", "sleep 5"]

      terminationGracePeriodSeconds: 30
      volumes:
        - name: tmp
          emptyDir: {sizeLimit: 64Mi}
        - name: cache
          emptyDir: {sizeLimit: 256Mi}
# ~/tramontana-k8s/30-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: tramontana
  namespace: tramontana
spec:
  type: ClusterIP
  selector:
    app: tramontana            # les etiquetes del Pod, no del Deployment
  ports:
    - name: http
      port: 80                 # port del Service
      targetPort: http         # port amb nom del contenidor
---
# La base de dades viu FORA del cluster (srv-tramontana). Aquest objecte
# li dona un nom intern, de manera que l aplicacio faci servir sempre
# "postgresql" i no una IP codificada.
apiVersion: v1
kind: Service
metadata:
  name: postgresql
  namespace: tramontana
spec:
  type: ExternalName
  externalName: srv-tramontana.intern
# ~/tramontana-k8s/40-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: tramontana
  namespace: tramontana
  annotations:
    traefik.ingress.kubernetes.io/router.entrypoints: websecure
    traefik.ingress.kubernetes.io/router.tls: "true"
    # Les capcaleres de seguretat de 08-01, com a anotacions
    traefik.ingress.kubernetes.io/router.middlewares: tramontana-seguretat@kubernetescrd
spec:
  ingressClassName: traefik
  tls:
    - hosts: [reserves.tramontana.example]
      secretName: tramontana-tls      # Secret de tipus kubernetes.io/tls
  rules:
    - host: reserves.tramontana.example
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: tramontana
                port: {name: http}
---
# Middleware de Traefik: l equivalent dels add_header de 08-01
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: seguretat
  namespace: tramontana
spec:
  headers:
    stsSeconds: 63072000
    stsIncludeSubdomains: true
    contentTypeNosniff: true
    frameDeny: true
    referrerPolicy: "strict-origin-when-cross-origin"
# El certificat de 06-05, com a Secret
$ sudo kubectl -n tramontana create secret tls tramontana-tls \
      --cert=/etc/letsencrypt/live/reserves.tramontana.example/fullchain.pem \
      --key=/etc/letsencrypt/live/reserves.tramontana.example/privkey.pem

# Desplegar-ho tot, en ordre
$ kubectl apply -f ~/tramontana-k8s/
namespace/tramontana created
resourcequota/quota-tramontana created
configmap/tramontana-config created
deployment.apps/tramontana created
service/tramontana created
ingress.networking.k8s.io/tramontana created

$ kubectl -n tramontana get pods -o wide
NAME                          READY   STATUS    RESTARTS   AGE   NODE
tramontana-7d8f9c4b5-k2m4p    1/1     Running   0          48s   srv-tramontana-proves
tramontana-7d8f9c4b5-x9n7q    1/1     Running   0          48s   k8s-node2

$ kubectl -n tramontana get endpoints tramontana
NAME         ENDPOINTS                           AGE
tramontana   10.42.0.18:8080,10.42.1.22:8080     51s

Les rèpliques han caigut en nodes diferents gràcies a topologySpreadConstraints, i el Service ja té els seus dos endpoints.

Les tres sondes mereixen un resum en taula, perquè confondre-les és l'error més car d'aquest apartat:

Sonda Pregunta Si falla Risc si està malament
startupProbe Ha acabat d'arrencar? Reinicia després d'esgotar el marge Sense ella, liveness mata arrencades lentes
readinessProbe Pot atendre ara? Surt del Service, no es reinicia Sense ella, arriba trànsit a un Pod que no està a punt
livenessProbe Està penjat? Mata i reinicia Massa agressiva: reinicis en cascada sota càrrega

I l'error clàssic, que apareix en produccions reals amb conseqüències greus: posar el mateix failureThreshold i periodSeconds a readiness i liveness. Sota càrrega, l'aplicació respon una mica més lenta, totes dues fallen, i el kubelet reinicia Pods que només estaven ocupats — cosa que redueix la capacitat, augmenta la càrrega dels restants i provoca una caiguda en cascada. Liveness sempre més laxa que readiness.

Desplegament progressiu i marxa enrere

# 1. Desplegar la versio 3.3.0. --record esta obsolet; es fa servir una
#    anotacio perque quedi constancia del motiu.
$ kubectl -n tramontana set image deployment/tramontana \
      tramontana=registre.tramontana.example/tramontana:3.3.0
$ kubectl -n tramontana annotate deployment/tramontana \
      kubernetes.io/change-cause="Versio 3.3.0: informes optimitzats"

# 2. Seguir-ho en directe
$ kubectl -n tramontana rollout status deployment/tramontana
Waiting for deployment "tramontana" rollout to finish: 1 out of 2 new replicas have been updated...
Waiting for deployment "tramontana" rollout to finish: 1 old replicas are pending termination...
deployment "tramontana" successfully rolled out

# 3. Durant el proces conviuen tots dos ReplicaSets
$ kubectl -n tramontana get rs
NAME                    DESIRED   CURRENT   READY   AGE
tramontana-7d8f9c4b5    0         0         0       18m    # 3.2.1
tramontana-9f2a1c8e7    2         2         2       42s    # 3.3.0

Zero peticions perdudes, i el mecanisme és el que ja coneixes de 07-07: maxUnavailable: 0 garanteix que mai no hi ha menys capacitat de la declarada, i la readinessProbe garanteix que un Pod no rep trànsit fins que respon. El drenatge que a 07-07 feies amb socat contra el sòcol d'HAProxy, aquí és una propietat del sistema.

# 4. La versio nova falla en produccio: marxa enrere
$ kubectl -n tramontana rollout history deployment/tramontana
REVISION  CHANGE-CAUSE
1         Versio 3.2.1 inicial
2         Versio 3.3.0: informes optimitzats

$ kubectl -n tramontana rollout undo deployment/tramontana
deployment.apps/tramontana rolled back

$ kubectl -n tramontana rollout status deployment/tramontana
deployment "tramontana" successfully rolled out

La marxa enrere va trigar onze segons, contra els minuts del desplegar.sh de 04-07 amb el seu ln -sfn. És l'argument més sòlid a favor de Kubernetes: la reversibilitat és una propietat del sistema, no un script que cal recordar-se d'escriure bé.

Amb la reserva que cal dir sempre: rollout undo reverteix el codi, no les dades. Si la versió 3.3.0 va executar una migració d'esquema, tornar a la 3.2.1 deixa l'aplicació vella contra una base de dades nova. És el mateix problema que a 04-07, i la solució és la mateixa: migracions compatibles cap enrere. Kubernetes no ho resol.

# 5. Escalar, que es trivial i per aixo crida l atencio
$ kubectl -n tramontana scale deployment/tramontana --replicas=4
$ kubectl -n tramontana get pods --no-headers | wc -l
4

Seguretat: securityContext, NetworkPolicy, RBAC i PSS

securityContext

Els valors per defecte de Kubernetes són permissius: sense securityContext, un contenidor corre com a root amb un conjunt ampli de capabilities. Tot el de 07-05 s'aplica aquí de manera declarativa:

Directiva Què impedeix Equivalent a 07-05
runAsNonRoot: true Que el contenidor corri com a root USER al Dockerfile
runAsUser: 997 — --user
allowPrivilegeEscalation: false Guanyar privilegis via SUID --security-opt no-new-privileges
readOnlyRootFilesystem: true Escriure al sistema de fitxers --read-only
capabilities.drop: ["ALL"] Totes les capacitats del nucli --cap-drop=ALL
seccompProfile: RuntimeDefault Crides al sistema perilloses --security-opt seccomp

readOnlyRootFilesystem: true és la que més atacs frustra a la pràctica: la majoria dels exploits necessiten escriure alguna cosa al disc per persistir o per descarregar l'etapa següent.

# Verificar que s aplica de debo
$ kubectl -n tramontana exec deploy/tramontana -- id
uid=997 gid=1002 groups=1002
$ kubectl -n tramontana exec deploy/tramontana -- touch /prova
touch: /prova: Read-only file system
command terminated with exit code 1

NetworkPolicy

Per defecte, tots els Pods del clúster poden parlar amb tots. És una xarxa plana, i en un clúster amb diverses aplicacions això significa que un Pod compromès arriba a qualsevol altre.

# ~/tramontana-k8s/50-networkpolicy.yaml
# 1. Denegar-HO TOT per defecte al namespace (llista blanca, 06-03)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: denegar-tot
  namespace: tramontana
spec:
  podSelector: {}                # tots els Pods del namespace
  policyTypes: [Ingress, Egress]
---
# 2. Permetre nomes el que cal
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: permetre-tramontana
  namespace: tramontana
spec:
  podSelector:
    matchLabels: {app: tramontana}
  policyTypes: [Ingress, Egress]
  ingress:
    # Nomes des de l Ingress (Traefik viu a kube-system)
    - from:
        - namespaceSelector:
            matchLabels: {kubernetes.io/metadata.name: kube-system}
      ports: [{protocol: TCP, port: 8080}]
  egress:
    # DNS: sense aixo, RES no funciona i la fallada desconcerta
    - to:
        - namespaceSelector:
            matchLabels: {kubernetes.io/metadata.name: kube-system}
      ports:
        - {protocol: UDP, port: 53}
        - {protocol: TCP, port: 53}
    # PostgreSQL, fora del cluster
    - to:
        - ipBlock: {cidr: 10.0.2.15/32}
      ports: [{protocol: TCP, port: 6432}]

Oblidar la regla de DNS és l'error número u amb NetworkPolicy. S'aplica la política, tot deixa de funcionar, i el símptoma —«no es pot resoldre el nom»— no apunta al tallafocs. Amb policyTypes: [Egress], el trànsit a CoreDNS també queda bloquejat.

Avís important per a k3s: el CNI per defecte (Flannel) no implementa NetworkPolicy, així que aquests objectes s'accepten i no fan res. Cal instal·lar k3s amb --flannel-backend=none i desplegar Calico, o fer servir --disable-network-policy=false segons la versió. Comprovar-ho és imprescindible: creure que tens segmentació quan no la tens és pitjor que no tenir-ne.

RBAC, breument

El control d'accés basat en rols fa servir quatre objectes:

Objecte Abast Què defineix
Role Un namespace Quins verbs sobre quins recursos
ClusterRole Tot el clúster Igual, global
RoleBinding Un namespace Qui té aquell Role
ClusterRoleBinding Tot el clúster Qui té aquell ClusterRole
# Luis: nomes lectura i registres al seu namespace. No pot veure Secrets.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: tramontana
  name: lector
rules:
  - apiGroups: ["", "apps"]
    resources: ["pods", "pods/log", "deployments", "services", "configmaps"]
    verbs: ["get", "list", "watch"]
  # 'secrets' NO hi apareix: llegir-los es llegir les contrasenyes (base64)
# Comprovar permisos, propis o d un altre
$ kubectl auth can-i get secrets -n tramontana --as=luis
no
$ kubectl auth can-i get pods -n tramontana --as=luis
yes

kubectl auth can-i és l'eina de verificació d'RBAC, i és la manera de comprovar que el que està prohibit està prohibit, igual que vas fer amb els rols de PostgreSQL a 08-02.

Pod Security Standards

Substitueixen les PodSecurityPolicy, retirades a la 1.25. Són tres perfils que s'apliquen per namespace amb etiquetes:

Perfil Què permet
privileged Tot. Sense restriccions
baseline Bloqueja el més perillós: privilegiat, hostNetwork, hostPath
restricted Exigeix runAsNonRoot, drop ALL, seccomp, sense escalada

L'enforce: restricted del namespace fa que l'apiserver rebutgi qualsevol Pod que no ho compleixi. És una xarxa de seguretat que no depèn de recordar posar el securityContext:

$ kubectl -n tramontana run dolent --image=nginx --privileged
Error from server (Forbidden): pods "dolent" is forbidden: violates PodSecurity
"restricted:latest": privileged (container "dolent" must not set
securityContext.privileged=true), allowPrivilegeEscalation != false,
unrestricted capabilities, runAsNonRoot != true, seccompProfile

Diagnòstic: llegir esdeveniments i les quatre fallades habituals

La primera ordre davant de qualsevol problema és kubectl describe, i el que importa és al final, a Events.

$ kubectl -n tramontana describe pod tramontana-7d8f9c4b5-k2m4p | tail -12
Events:
  Type     Reason     Age                From     Message
  ----     ------     ----               ----     -------
  Normal   Scheduled  2m                 default-scheduler  Successfully assigned...
  Normal   Pulling    2m                 kubelet  Pulling image "...:3.2.1"
  Normal   Pulled     1m                 kubelet  Successfully pulled image
  Normal   Created    1m                 kubelet  Created container tramontana
  Normal   Started    1m                 kubelet  Started container tramontana
  Warning  Unhealthy  30s (x3 over 40s)  kubelet  Readiness probe failed:
             Get "http://10.42.0.18:8080/salut": dial tcp: connect: connection refused

Aquella última línia explica el problema sencer: l'aplicació encara no escolta al 8080.

Les quatre fallades que veuràs de debò

1. CrashLoopBackOff — el contenidor arrenca, mor, i Kubernetes espera cada vegada més abans de reintentar (10 s, 20 s, 40 s... fins a 5 min).

$ kubectl -n tramontana get pods
NAME                         READY   STATUS             RESTARTS      AGE
tramontana-9f2a1c8e7-p4k9m   0/1     CrashLoopBackOff   5 (48s ago)   4m

# La clau: --previous. Sense ella veus els registres de l intent ACTUAL,
# que acaba d arrencar i esta buit.
$ kubectl -n tramontana logs tramontana-9f2a1c8e7-p4k9m --previous
FATAL: no s ha pogut connectar a la base de dades: password authentication
failed for user "svc_tramontana"

# Confirmar la causa
$ kubectl -n tramontana get secret tramontana-secrets -o jsonpath='{.data.db_password}' \
      | base64 -d | head -c8; echo '...'
MARCADOR...

Aquí està: el Secret té el marcador de posició del manifest, no la contrasenya real.

Causa de CrashLoopBackOff Com es confirma
Error de configuració o credencials logs --previous
Falta una dependència (BD inabastable) logs --previous + exec a un altre Pod
L'ordre del contenidor acaba describe: Exit Code: 0
livenessProbe massa agressiva describe: esdeveniments Unhealthy abans de cada reinici
OOMKilled describe: Reason: OOMKilled

2. ImagePullBackOff / ErrImagePull

$ kubectl -n tramontana describe pod tramontana-x | grep -A3 Events
  Warning  Failed  30s  kubelet  Failed to pull image
    "registre.tramontana.example/tramontana:3.3.1": failed to resolve reference:
    unexpected status: 401 Unauthorized
Missatge Causa Solució
401 Unauthorized Falta credencial del registre imagePullSecrets
not found / manifest unknown L'etiqueta no existeix (errada) Verificar amb crane ls o skopeo
no such host El DNS del node no resol el registre Comprovar al node, no al Pod
connection refused Registre caigut o tallafocs Comprovar des del node

3. Pending — el Pod existeix i cap node no l'accepta.

$ kubectl -n tramontana describe pod tramontana-y | tail -4
  Warning  FailedScheduling  20s  default-scheduler
    0/2 nodes are available: 1 Insufficient cpu, 1 Insufficient memory.
    preemption: 0/2 nodes are available: 2 No preemption victims found.

# Quant hi ha realment compromes a cada node
$ kubectl describe node k8s-node2 | grep -A6 'Allocated resources'
Allocated resources:
  Resource   Requests      Limits
  cpu        1750m (87%)   3200m (160%)
  memory     1894Mi (94%)  2560Mi (128%)

El planificador reserva segons requests, no segons l'ús real. Un node amb el 20 % de CPU utilitzada pot rebutjar Pods si les seves requests sumen el 95 %. És la confusió més habitual amb Pending, i per això posar requests generoses «per si de cas» malgasta el clúster sencer.

Altres causes de Pending: un PVC que no es pot aprovisionar, nodeSelector que no coincideix amb cap node, o taints sense la toleration corresponent.

4. OOMKilled — el nucli va matar el contenidor per superar el límit de memòria.

$ kubectl -n tramontana describe pod tramontana-z | grep -A4 'Last State'
    Last State:     Terminated
      Reason:       OOMKilled
      Exit Code:    137            # 128 + 9 (SIGKILL)

És el mateix mecanisme de cgroups de 07-05. Exit Code: 137 és la firma inconfusible. I la decisió no és automàtica: cal esbrinar si el límit és massa baix o si hi ha una fuita de memòria a l'aplicació — pujar el límit sense mirar només endarrereix el problema.

Caixa d'eines de diagnòstic

# Esdeveniments del namespace, en ordre cronologic
$ kubectl -n tramontana get events --sort-by=.lastTimestamp | tail -10

# Consum real (necessita metrics-server, que k3s porta)
$ kubectl -n tramontana top pods
NAME                         CPU(cores)   MEMORY(bytes)
tramontana-7d8f9c4b5-k2m4p   12m          184Mi

# Depurar la xarxa des de dins del cluster, sense tocar els Pods reals
$ kubectl -n tramontana run depurar --rm -it --image=nicolaka/netshoot \
      --restart=Never -- bash
depurar:~# nslookup tramontana
depurar:~# curl -sv http://tramontana/salut
depurar:~# nc -zv 10.0.2.15 6432

# Reenviar un port al portatil, sense exposar res
$ kubectl -n tramontana port-forward svc/tramontana 8080:80
$ curl -s localhost:8080/salut

port-forward és l'eina més utilitzada del dia a dia: permet arribar a un servei intern des del teu portàtil sense crear un Ingress ni un NodePort.

Automatització amb Ansible

Hi ha una temptació evident aquí i convé evitar-la: Ansible no ha de substituir kubectl apply. Els manifests ja són declaratius i idempotents; embolcallar-los amb Ansible hi afegeix una capa sense guanyar res. El repartiment correcte:

Capa Eina Per què
Preparar el sistema operatiu del node Ansible És configuració de màquina
Instal·lar i configurar k3s Ansible És un servei de systemd
Unir nodes al clúster Ansible Necessita el testimoni, que és un secret
Desplegar aplicacions kubectl apply o Helm Ja és declaratiu
Mantenir el clúster sincronitzat amb Git Argo CD o Flux (GitOps) És el patró del sector
# ~/tramontana-infra/roles/k3s/defaults/main.yml
---
k3s_version: "v1.30.4+k3s1"
k3s_url: "https://192.168.122.104:6443"
k3s_magatzem: sqlite
k3s_deshabilitar: []
k3s_extra_args_servidor: "--write-kubeconfig-mode 0644 --tls-san {{ ansible_default_ipv4.address }}"
# ~/tramontana-infra/roles/k3s/tasks/main.yml
---
- name: Guardia de seguretat — MAI en produccio
  ansible.builtin.assert:
    that: inventory_hostname != 'srv-tramontana'
    fail_msg: >-
      Aquest rol NO s ha d executar a srv-tramontana. k3s reescriu les
      regles de nftables i arrenca el seu propi entorn d execucio de
      contenidors; al servidor de produccio interferiria amb ufw,
      AppArmor i el servei tramontana.

- name: Requisits del sistema
  ansible.builtin.apt:
    name: [curl, iptables, apparmor-utils]
    state: present

- name: Descarregar l instal·lador de k3s
  ansible.builtin.get_url:
    url: https://get.k3s.io
    dest: /usr/local/src/k3s-install.sh
    mode: '0755'

- name: Instal·lar k3s al node SERVIDOR
  ansible.builtin.command:
    cmd: /usr/local/src/k3s-install.sh
    creates: /usr/local/bin/k3s        # idempotencia (07-06)
  environment:
    INSTALL_K3S_VERSION: "{{ k3s_version }}"
    INSTALL_K3S_EXEC: "server {{ k3s_extra_args_servidor }}"
  when: "'k3s_servidor' in group_names"

- name: Llegir el testimoni del node servidor
  ansible.builtin.slurp:
    src: /var/lib/rancher/k3s/server/node-token
  register: k3s_token_raw
  when: "'k3s_servidor' in group_names"

- name: Compartir el testimoni amb els agents
  ansible.builtin.set_fact:
    k3s_token: "{{ hostvars[groups['k3s_servidor'][0]]['k3s_token_raw']['content'] | b64decode | trim }}"
  no_log: true

- name: Instal·lar k3s als nodes AGENT
  ansible.builtin.command:
    cmd: /usr/local/src/k3s-install.sh
    creates: /usr/local/bin/k3s
  environment:
    INSTALL_K3S_VERSION: "{{ k3s_version }}"
    K3S_URL: "{{ k3s_url }}"
    K3S_TOKEN: "{{ k3s_token }}"
    INSTALL_K3S_EXEC: "agent"
  no_log: true
  when: "'k3s_agent' in group_names"

- name: Esperar que el node estigui Ready
  ansible.builtin.command: "k3s kubectl get node {{ ansible_hostname }} -o json"
  register: node
  until: >-
    (node.stdout | from_json).status.conditions
    | selectattr('type','eq','Ready') | map(attribute='status') | first == 'True'
  retries: 30
  delay: 10
  changed_when: false
  delegate_to: "{{ groups['k3s_servidor'][0] }}"

# --- Copia de seguretat del cluster, que es el que de debo importa ---
- name: Instal·lar l script de copia d etcd/SQLite
  ansible.builtin.copy:
    src: copia_k3s.sh
    dest: /usr/local/bin/copia_k3s.sh
    mode: '0750'
  when: "'k3s_servidor' in group_names"

- name: Temporitzador diari de copia de l estat del cluster
  ansible.builtin.copy:
    src: "{{ item }}"
    dest: "/etc/systemd/system/{{ item }}"
    mode: '0644'
  loop: [copia-k3s.service, copia-k3s.timer]
  notify: Recarregar systemd
  when: "'k3s_servidor' in group_names"

Aquell assert inicial és l'aplicació literal de l'advertiment de l'apartat 1, convertida en codi que no es pot ignorar per descuit. I la còpia de l'estat del clúster no és opcional: sense etcd o el seu SQLite, el clúster no existeix.

# roles/k3s/files/copia_k3s.sh (nucli)
$ k3s etcd-snapshot save --name diari           # amb etcd
# o, amb SQLite:
$ sqlite3 /var/lib/rancher/k3s/server/db/state.db ".backup '/srv/backups/k3s.db'"
$ restic backup /srv/backups/k3s.db --tag k3s

La complexitat operativa real

Aquí hi ha la part que les presentacions de Kubernetes ometen, i la que respon de debò a la pregunta de l'apartat 2.

1. Les actualitzacions són constants. Kubernetes publica tres versions menors a l'any i cadascuna rep suport uns catorze mesos. Això obliga a actualitzar el clúster almenys dues vegades l'any, indefinidament. Cada actualització implica llegir les notes de la versió buscant canvis de ruptura, actualitzar el pla de control i després els nodes, i verificar que les càrregues continuen funcionant.

2. Les versions d'API es retiren, i trenquen manifests que funcionaven.

Recurs API antiga API actual Retirada a
Ingress extensions/v1beta1 networking.k8s.io/v1 1.22
CronJob batch/v1beta1 batch/v1 1.25
PodSecurityPolicy policy/v1beta1 Eliminat: fer servir PSS 1.25
HorizontalPodAutoscaler autoscaling/v2beta2 autoscaling/v2 1.26

Un manifest escrit fa dos anys pot fallar després d'una actualització. Existeixen eines (pluto, kubent) per detectar-ho abans, i fer-les servir és obligatori.

3. etcd és delicat. És una base de dades distribuïda amb quòrum: necessita 3 o 5 nodes (nombre senar), és sensible a la latència de disc, i requereix compactació i desfragmentació periòdiques. Un etcd corromput sense còpia és un clúster perdut. I s'hi aplica tot el del quòrum de 07-07.

4. Els certificats interns caduquen. Kubernetes fa servir TLS mutu entre tots els seus components, amb una PKI pròpia i certificats que caduquen a l'any. kubeadm els renova en actualitzar, però un clúster que porta catorze mesos sense tocar-se deixa de funcionar de cop, amb errors d'autenticació desconcertants.

$ sudo kubeadm certs check-expiration | head -5
CERTIFICATE                EXPIRES                  RESIDUAL TIME
apiserver                  Aug 18, 2027 13:02 UTC   364d
etcd-server                Aug 18, 2027 13:02 UTC   364d

5. L'ecosistema es mou molt de pressa. Docker Shim retirat a la 1.24, PSP substituïdes per PSS, Ingress evolucionant cap a Gateway API. El que s'aprèn caduca més ràpid que en qualsevol altra àrea del sistema.

6. Diagnosticar exigeix entendre més capes. Una fallada pot ser a l'aplicació, el contenidor, el Pod, el Service, el CNI, l'Ingress, el DNS, el planificador o el kubelet. Tot el del Mòdul 7 continua sent necessari; Kubernetes afegeix capes, no les substitueix.

El cost anual estimat, en hores, que és la xifra que cal portar a una decisió:

Activitat Hores/any
Dues actualitzacions de versió menor 16-32
Manteniment d'etcd i les seves còpies 8-12
Actualitzar manifests per APIs retirades 4-8
Diagnòstic d'incidents específics del clúster 20-40
Formació contínua 20-40
Total 70-130 h/any

Entre dues i quatre setmanes de feina l'any, només per mantenir el clúster viu, sense comptar el desplegament d'aplicacions. Amb un servei gestionat, la meitat. Amb systemd i Ansible en tres màquines, pràcticament zero.

Aquell número és la resposta final a la pregunta de l'apartat 2, i també explica per què Kubernetes gestionat té tant d'èxit: per a la majoria de les empreses, 100 € al mes són més barats que 100 hores a l'any.

Errors Comuns i Consells

  • Fer servir :latest a la imatge. Dos Pods de la mateixa «versió» poden executar codi diferent, i rollout undo no torna enlloc. Etiquetes immutables, sempre.
  • Crear Pods solts en lloc de Deployments. Ningú no els vigila: si moren, no tornen.
  • livenessProbe igual o més estricta que readinessProbe. Sota càrrega es reinicien Pods sans i el servei cau en cascada. Liveness sempre més laxa.
  • No posar startupProbe en aplicacions d'arrencada lenta. La liveness mata el contenidor abans que acabi d'arrencar, en bucle.
  • Creure que un Secret està xifrat. És base64. kubectl get secret -o jsonpath | base64 -d i ja el tens.
  • Pujar Secrets a Git. El YAML amb data: és el fitxer amb la contrasenya. Sealed Secrets o un magatzem extern.
  • Oblidar la regla de DNS en una NetworkPolicy d'Egress. Tot deixa de funcionar i el símptoma no apunta al tallafocs.
  • Aplicar NetworkPolicy amb un CNI que no les implementa. Flannel a k3s les accepta i les ignora: creus que tens segmentació i no en tens.
  • requests iguals als limits i molt generoses. El planificador reserva per requests: malgastes el clúster i provoques Pending amb nodes buits.
  • Pujar el límit de memòria en veure un OOMKilled sense investigar. Pot ser una fuita, i només estàs endarrerint el problema.
  • kubectl logs sense --previous en un CrashLoopBackOff. Veus els registres de l'intent actual, que està buit.
  • No fer còpia d'etcd o del SQLite. Sense això, el clúster no es pot reconstruir.
  • Ignorar la caducitat dels certificats interns. Un clúster sense tocar catorze mesos deixa de funcionar de cop.
  • Instal·lar Kubernetes en un servidor de producció existent. Reescriu regles de xarxa i arrenca el seu propi entorn d'execució. Màquina dedicada.
  • Muntar Kubernetes per a tres serveis. És l'error d'arquitectura d'aquesta lliçó sencera: 70-130 h/any per resoldre problemes que no tens.
  • Consell de mètode. Davant de qualsevol fallada: kubectl describe i llegeix els Events de baix a dalt. Resol la majoria dels problemes abans de tocar res.

Exercicis

Exercici 1

Després de desplegar la versió 3.3.0, els Pods entren en CrashLoopBackOff i el servei queda caigut. Diagnostica el problema amb mètode, restableix el servei i explica què hauria evitat l'incident.

Exercici 2

Compara, per al cas de Tramontana, l'arquitectura de Kubernetes d'aquesta lliçó amb l'opció B recomanada a 07-07, amb criteris objectius, i emet una recomanació.

Exercici 3

Escriu un manifest de CronJob que executi la còpia de seguretat de la base de dades dins del clúster, amb les mateixes garanties que tramontana-copia.timer de 05-05, i explica què s'hi guanya i què s'hi perd respecte al temporitzador de systemd.

Solucions

Solució 1

Pas 1: la foto general, abans de tocar res.

$ kubectl -n tramontana get pods
NAME                          READY   STATUS             RESTARTS      AGE
tramontana-7d8f9c4b5-k2m4p    1/1     Running            0             3h    # 3.2.1
tramontana-9f2a1c8e7-p4k9m    0/1     CrashLoopBackOff   4 (52s ago)   3m    # 3.3.0

Primera dada important i tranquil·litzadora: gràcies a maxUnavailable: 0, el Pod de la versió 3.2.1 continua en marxa i atenent trànsit. El desplegament està bloquejat, no caigut. Això treu pressió i permet diagnosticar sense presses.

$ kubectl -n tramontana rollout status deployment/tramontana --timeout=10s
error: timed out waiting for the condition

$ kubectl -n tramontana get endpoints tramontana
NAME         ENDPOINTS          AGE
tramontana   10.42.0.18:8080    3h

Un sol endpoint: el Pod nou no ha arribat mai a Ready, així que el Service no li ha enviat mai trànsit. La readinessProbe ha fet exactament la seva feina.

Pas 2: els registres de l'intent anterior.

$ kubectl -n tramontana logs tramontana-9f2a1c8e7-p4k9m --previous
[2026-08-18T16:02:11Z] INFO  Tramontana Reserves 3.3.0 iniciant
[2026-08-18T16:02:11Z] INFO  llegint configuracio de l entorn
[2026-08-18T16:02:11Z] ERROR variable TRAMONTANA_CACHE_URL no definida
[2026-08-18T16:02:11Z] FATAL configuracio incompleta; avortant

Causa arrel en quatre línies. La versió 3.3.0 introdueix una variable de configuració nova —TRAMONTANA_CACHE_URL, per a la memòria cau d'informes— que no és al ConfigMap. L'aplicació falla ràpid i en clar, que és el comportament correcte.

Pas 3: confirmar i descartar altres causes.

$ kubectl -n tramontana describe pod tramontana-9f2a1c8e7-p4k9m | \
      grep -A6 'Last State'
    Last State:     Terminated
      Reason:       Error
      Exit Code:    1              # NO es 137: no es OOMKilled
      Started:      Tue, 18 Aug 2026 16:02:11 +0200
      Finished:     Tue, 18 Aug 2026 16:02:11 +0200

$ kubectl -n tramontana get configmap tramontana-config -o jsonpath='{.data}' | \
      jq 'keys'
["db_host","db_name","db_port","escolta","log_nivell","max_connexions",
 "port","timeout_consulta"]

Exit Code: 1 amb arrencada i parada al mateix segon descarta OOMKilled (137), sondes mal calibrades (hauria arribat a Running una estona) i problemes de xarxa (no va arribar a connectar amb res). I el ConfigMap confirma l'absència de la clau.

Pas 4: decidir. I la decisió correcta és tornar enrere primer.

$ kubectl -n tramontana rollout undo deployment/tramontana
deployment.apps/tramontana rolled back
$ kubectl -n tramontana rollout status deployment/tramontana
deployment "tramontana" successfully rolled out

Per què tornar enrere abans d'arreglar, encara que l'arranjament sembli trivial: el Deployment està en un estat inconsistent, amb un ReplicaSet nou intentant arrencar en bucle cada pocs segons. Cada reintent genera esdeveniments, consumeix recursos i embruta el diagnòstic. Deixar el sistema en un estat conegut i estable abans de corregir és la disciplina que evita empitjorar les coses — i és la mateixa lògica de l'arbre de decisió del manual d'operació de recuperació de 07-06.

Pas 5: l'arranjament, provat abans d'aplicar-lo.

$ kubectl -n tramontana patch configmap tramontana-config \
      --type merge -p '{"data":{"cache_url":"redis://cache.tramontana.svc:6379"}}'
# 20-deployment.yaml — el mapatge explicit de la variable
          env:
            - name: TRAMONTANA_CACHE_URL
              valueFrom:
                configMapKeyRef:
                  name: tramontana-config
                  key: cache_url
# Verificar en un Pod exhaurible ABANS de tocar el Deployment
$ kubectl -n tramontana run verificar --rm -it --restart=Never \
      --image=registre.tramontana.example/tramontana:3.3.0 \
      --overrides='{"spec":{"containers":[{"name":"verificar",
        "image":"registre.tramontana.example/tramontana:3.3.0",
        "envFrom":[{"configMapRef":{"name":"tramontana-config"}}],
        "env":[{"name":"TRAMONTANA_CACHE_URL","valueFrom":
          {"configMapKeyRef":{"name":"tramontana-config","key":"cache_url"}}}]}]}}' \
      -- /usr/bin/tramontana --check-config
configuracio valida
# Ara si
$ kubectl apply -f ~/tramontana-k8s/20-deployment.yaml
$ kubectl -n tramontana rollout status deployment/tramontana
deployment "tramontana" successfully rolled out
$ kubectl -n tramontana get pods
NAME                          READY   STATUS    RESTARTS   AGE
tramontana-3c7e9a2b1-h4j8k    1/1     Running   0          42s
tramontana-3c7e9a2b1-w2n5r    1/1     Running   0          28s

Un detall que cal conèixer: canviar un ConfigMap no reinicia els Pods que el fan servir com a variables d'entorn. Si hagués calgut:

$ kubectl -n tramontana rollout restart deployment/tramontana

Què hauria evitat l'incident, que és la pregunta que de debò importa:

Mesura Com ho hauria evitat
Provar el manifest en un namespace de proves La fallada hauria passat on no importa
Validar la configuració en arrencar la imatge Un --check-config a la construcció falla abans de desplegar
Notes de versió amb els canvis de configuració El Luis ha de documentar tota variable nova
Valor per defecte a l'aplicació Una variable nova amb valor raonable no trenca res
CI que apliqui els manifests a un clúster efímer La fallada es detecta a la revisió del codi
Alerta al primer CrashLoopBackOff No l'evita, però el detecta en un minut

I les tres lliçons de mètode:

  1. maxUnavailable: 0 va convertir una caiguda en un desplegament bloquejat. És la línia de configuració amb millor relació benefici/cost de tot el manifest, i mereix ser a qualsevol Deployment de producció.
  2. La readinessProbe va impedir que el Service enviés trànsit a un Pod trencat. Sense ella, la meitat de les peticions haurien fallat durant tres minuts.
  3. --previous va ser el que va donar la resposta. Sense aquella opció, kubectl logs retornava un contenidor acabat d'arrencar i buit, i el diagnòstic hauria trigat molt més.

Solució 2

Comparació amb criteris objectius, sobre el cas real de Tramontana: una aplicació monolítica, una base de dades, ~500 reserves al mes, dues persones tècniques.

Criteri Kubernetes (k3s) Opció B de 07-07 (2 nodes + HAProxy)
Màquines necessàries 3 (pla de control amb quòrum) o 2 amb punt únic de fallada 3 (2 app + 1 balancejador)
RAM base consumida 812 MiB només el clúster ~120 MiB (HAProxy + keepalived)
Desplegament sense interrupció Natiu, RollingUpdate Drenatge manual amb socat, script
Marxa enrere rollout undo, 11 s ln -sfn + reinici, ~2 min
Autoreparació d'un Pod Automàtica Restart=on-failure de systemd
Autoreparació davant la caiguda d'un node Automàtica, recol·loca HAProxy el treu; no recol·loca
Escalar a 4 rèpliques Una ordre, segons Aprovisionar màquina + Ansible, ~1 h
Corba d'aprenentatge 3-6 mesos Ja la tens
Manteniment anual 70-130 h ~15 h
Peces noves que cal dominar etcd, CNI, Ingress, RBAC, PSS, CRD Cap
Diagnòstic d'una fallada Més capes per descartar Les de sempre
Reconstrucció completa mesurada Sense mesurar; el clúster hi afegeix passos 50 min, mesurat
Disponibilitat assolible ~99,8-99,9 % ~99,8 %
Cost anual estimat 3 VM + 100 h de feina 3 VM + 15 h
Transferible a una altra feina Molt Moderat

Els quatre punts on Kubernetes guanya de debò, sense exagerar:

  1. Desplegament i marxa enrere. Onze segons davant de dos minuts, i sense script propi per mantenir. És una diferència real.
  2. Recol·locació davant la caiguda d'un node. HAProxy treu el node caigut del repartiment però no recupera la capacitat; Kubernetes arrenca les rèpliques que falten al node supervivent.
  3. Escalat. Una ordre davant d'una hora de feina. Només importa si l'escalat és freqüent, i a Tramontana no ho és.
  4. Ocupabilitat. És un argument personal legítim però no és un argument d'arquitectura per a l'empresa, i cal separar-los honestament.

Els cinc on perd:

  1. 812 MiB de RAM abans de desplegar res, en màquines de 3,8 GB. És més del 20 % del servidor dedicat a l'orquestració.
  2. 70-130 hores l'any de manteniment indefinit, davant d'unes 15.
  3. Tres a sis mesos fins a poder resoldre incidents amb solvència. Durant aquell període, la fiabilitat baixa.
  4. Més modes de fallada. És el mateix argument de 07-07: un sistema amb més peces té més maneres de trencar-se, i Kubernetes n'hi afegeix moltes.
  5. La base de dades continua fora. El problema difícil de 07-07 —l'estat— no el resol Kubernetes. PostgreSQL continuaria a srv-tramontana, amb la seva promoció manual i el seu RPO.

L'anàlisi que tanca la qüestió, i és el mateix raonament que a 07-07: totes dues opcions assoleixen aproximadament la mateixa disponibilitat, al voltant del 99,8 %, perquè la majoria de les interrupcions de Tramontana no vénen de la fallada d'una màquina sinó de desplegaments i manteniment, i totes dues els resolen. Kubernetes ho fa de manera més elegant i automàtica; l'opció B ho fa amb eines que l'equip ja domina.

Quan dues opcions donen el mateix resultat, guanya la més simple. I la diferència de manteniment —entre 55 i 115 hores l'any— equival a dues o tres setmanes de feina que no es dedicarien a millorar el producte.

Recomanació: mantenir l'opció B de 07-07 per a producció.

I mantenir el clúster de k3s a srv-tramontana-proves com a laboratori d'aprenentatge, amb cost marginal zero, perquè la màquina ja existeix. És on practicar sense risc i on estar preparat per a l'escenari que sí que canviaria la recomanació.

Els tres supòsits que la canviarien, escrits per endavant per poder-los reconèixer:

Canvi Per què canviaria la decisió
Tramontana es divideix en 5+ serveis L'orquestració comença a pagar-se sola
Es contracta Kubernetes gestionat al núvol El 60 % del manteniment desapareix
S'incorpora algú amb experiència real La corba d'aprenentatge deixa de ser un cost

I un advertiment final que convé deixar per escrit: la decisió correcta avui pot no ser-ho d'aquí a dos anys, i això no significa que avui estigui mal presa. Fixar una revisió anual és més professional que triar l'arquitectura que potser caldrà algun dia.

Solució 3

# ~/tramontana-k8s/60-cronjob-copia.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
  name: copia-basedades
  namespace: tramontana
spec:
  # 02:30, igual que tramontana-copia.timer (05-05).
  # COMPTE: l horari s interpreta a la ZONA HORARIA DEL CLUSTER, que
  # per defecte es UTC. timeZone (estable des de 1.27) ho resol; sense
  # ella, la copia s executaria a les 04:30 a l estiu.
  schedule: "30 2 * * *"
  timeZone: "Europe/Madrid"

  # L equivalent del flock de 04-07: si la copia d ahir continua
  # corrent, NO se n llanca una altra a sobre.
  concurrencyPolicy: Forbid

  # Si el cluster va estar caigut a les 02:30, s executa en tornar?
  # 3600 = nomes si ha passat menys d 1 h. Es l equivalent de
  # Persistent=true del temporitzador, pero ACOTAT: sense aquest camp,
  # un Job molt endarrerit es llancaria en un moment inoportu.
  startingDeadlineSeconds: 3600

  successfulJobsHistoryLimit: 7    # conservar 7 execucions correctes
  failedJobsHistoryLimit: 14       # i 14 de fallides: son les que es miren

  jobTemplate:
    spec:
      # Reintents davant de fallada, amb espera exponencial
      backoffLimit: 2
      # Sostre absolut: una copia penjada no bloqueja la seguent
      activeDeadlineSeconds: 3600
      # Neteja automatica del Job 3 dies despres
      ttlSecondsAfterFinished: 259200

      template:
        spec:
          restartPolicy: OnFailure

          securityContext:
            runAsNonRoot: true
            runAsUser: 997
            runAsGroup: 1002
            fsGroup: 1002
            seccompProfile: {type: RuntimeDefault}

          containers:
            - name: copia
              image: registre.tramontana.example/copia:1.4.0
              command: ["/usr/local/bin/copia_tramontana.sh"]

              env:
                - name: PGHOST
                  valueFrom:
                    configMapKeyRef: {name: tramontana-config, key: db_host}
                - name: PGDATABASE
                  valueFrom:
                    configMapKeyRef: {name: tramontana-config, key: db_name}
                - name: PGPASSWORD
                  valueFrom:
                    secretKeyRef: {name: tramontana-secrets, key: db_password}
                - name: RESTIC_REPOSITORY
                  valueFrom:
                    secretKeyRef: {name: restic-secrets, key: repositori}
                - name: RESTIC_PASSWORD
                  valueFrom:
                    secretKeyRef: {name: restic-secrets, key: password}

              resources:
                requests: {cpu: 100m, memory: 256Mi}
                # La copia comprimeix: necessita mes folganca que l app
                limits:   {cpu: "1",  memory: 1Gi}

              securityContext:
                allowPrivilegeEscalation: false
                readOnlyRootFilesystem: true
                capabilities: {drop: ["ALL"]}

              volumeMounts:
                - {name: treball, mountPath: /tmp}
                - {name: cache-restic, mountPath: /var/cache/restic}

          volumes:
            - name: treball
              emptyDir: {sizeLimit: 4Gi}
            # La memoria cau de restic accelera molt les copies
            # successives: persistir-la entre execucions es una
            # optimitzacio real.
            - name: cache-restic
              persistentVolumeClaim: {claimName: cache-restic}
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: cache-restic
  namespace: tramontana
spec:
  accessModes: [ReadWriteOnce]
  resources: {requests: {storage: 2Gi}}
# Provar SENSE esperar a les 02:30 (equivalent a systemctl start)
$ kubectl -n tramontana create job --from=cronjob/copia-basedades prova-manual
$ kubectl -n tramontana logs job/prova-manual -f
[2026-08-18 17:12:03] iniciant copia de PostgreSQL
[2026-08-18 17:12:41] copia base verificada: 1,7 GiB
[2026-08-18 17:14:12] restic: instantania a3f19c8d desada
[2026-08-18 17:14:20] copia completada

$ kubectl -n tramontana get cronjob
NAME              SCHEDULE     TIMEZONE        SUSPEND   ACTIVE   LAST SCHEDULE
copia-basedades   30 2 * * *   Europe/Madrid   False     0        8h

Què s'hi guanya i què s'hi perd, que és la part central de l'exercici:

CronJob de Kubernetes tramontana-copia.timer (systemd)
S'executa si un node cau Sí, en un altre node No: mor amb la màquina
Zona horària timeZone (1.27+); UTC per defecte Zona del sistema, natural
Exclusió mútua concurrencyPolicy: Forbid flock (04-07)
Execució endarrerida startingDeadlineSeconds Persistent=true
Historial d'execucions Objectes Job consultables journalctl -u
Reintents backoffLimit, natiu Restart= + RestartSec
Aïllament de recursos requests/limits CPUQuota, MemoryMax
Secrets Secret base64 systemd-creds xifrat (06-05)
Notificació de fallada Requereix monitoratge extern OnFailure=, natiu
Accés al disc de l'amfitrió Complicat, i amb raó Directe
Depuració kubectl logs job/... journalctl, o executar l'script
Peces necessàries Clúster + registre + imatge Un script i un fitxer .timer

El que s'hi guanya de debò són dues coses, i només dues:

  1. Resistència a la fallada del node. És l'argument fort: tramontana-copia.timer viu a srv-tramontana i si aquella màquina està apagada a les 02:30, no hi ha còpia aquella nit i ningú no se n'assabenta fins que comprovar_copia.sh ho detecta a les 08:00. El CronJob s'executa en qualsevol node disponible.
  2. Aïllament i límits reals, amb la mateixa facilitat. Encara que systemd també els dona amb MemoryMax, aquí vénen de sèrie.

El que s'hi perd, i pesa:

  1. Els secrets empitjoren. systemd-creds de 06-05 els guarda xifrats amb el TPM o una clau d'amfitrió; un Secret de Kubernetes és base64 a etcd. És un retrocés objectiu, tret que s'hi afegeixi xifratge en repòs o Vault.
  2. La notificació de fallada deixa de ser nativa. OnFailure= a systemd envia l'alerta directament; amb un CronJob cal monitorar kube_job_status_failed des de Prometheus (08-06). Una peça més.
  3. Accedir al disc de l'amfitrió es complica, i amb raó: muntar /srv/tramontana/backups en un Pod exigeix un hostPath, que les Pod Security Standards restricted prohibeixen. Caldria reescriure el flux perquè la còpia vagi directa a restic sense passar per disc local, cosa que és més neta però és feina.
  4. Cal construir i publicar una imatge amb l'script, pg_dump, restic i les seves dependències — i mantenir-la actualitzada. El temporitzador fa servir el que ja hi ha instal·lat.

Recomanació coherent amb la solució 2:

Mantenir tramontana-copia.timer a systemd. L'únic benefici real —resistència a la fallada del node— es pot aconseguir sense Kubernetes: executant el temporitzador també al segon node d'aplicació amb un blocatge compartit, o llançant-lo des d'una màquina diferent de la que es copia, cosa que a més és millor disseny perquè la còpia no depèn que el servidor copiat estigui viu.

I hi ha un argument addicional: la còpia de seguretat ha de continuar funcionant quan el clúster falla. Un mecanisme de còpia que depèn de la infraestructura que copia és exactament la classe d'acoblament que cal evitar. És el mateix principi pel qual el manual d'operació de 05-08 viu fora del servidor i la base de dades d'AIDE fora de la màquina.

Conclusió

Has muntat un clúster de Kubernetes de dos nodes, hi has desplegat Tramontana Reserves a sobre amb manifests complets, has fet un desplegament progressiu sense perdre una petició i una marxa enrere en onze segons. I saps què hi ha a sota: l'apiserver com a única porta d'entrada, etcd guardant tot l'estat, el planificador triant node segons les requests, els controladors reconciliant sense descans el real amb el declarat, i el kubelet executant a cada node el que li toca. Saps que si el pla de control cau, la càrrega continua funcionant — i per què.

Entens els objectes en el seu ordre de dependència i per què existeix cadascun. Que un Pod solt no el vigila ningú. Que el Deployment gestiona ReplicaSets i que allà hi viu rollout undo. Que un Service dona nom i IP estables a un blanc mòbil, i que el DNS intern ho fa transparent. Que un Ingress és l'Nginx de 08-01 escrit com a objecte declaratiu — de fet, el controlador més utilitzat és Nginx per dins. I saps que un Secret és base64, no xifrat, cosa que enllaça directament amb pass i systemd-creds de 06-05 i explica per què existeixen Sealed Secrets i Vault.

Domines les tres sondes i la diferència que les separa, que és el que més incidents causa en produccions reals: readiness treu del repartiment, liveness mata i reinicia, startup dona marge a l'arrencada — i liveness sempre més laxa que readiness, o els reinicis en cascada sota càrrega són qüestió de temps. Saps que requests és el que reserva el planificador i limits el que imposa el cgroup, amb l'asimetria clau: superar la memòria mata el contenidor, superar la CPU només l'estrangula. I saps llegir un CrashLoopBackOff amb --previous, un Pending mirant les requests compromeses del node, i un Exit Code: 137 com la firma de l'OOM killer.

Però el que més val d'aquesta lliçó és la conclusió amb què començava: per a Tramontana, Kubernetes és desproporcionat, i ara ho pots defensar amb números —812 MiB de RAM abans de desplegar res, 70 a 130 hores de manteniment l'any, tres a sis mesos de corba d'aprenentatge— en lloc d'amb una intuïció. Que la resposta professional a «hauríem de fer servir Kubernetes?» pugui ser «no, i això és per què» és exactament el mateix criteri amb què vas respondre a la Marta sobre l'alta disponibilitat a 07-07. Quan dues opcions donen el mateix resultat, guanya la més simple. I el clúster de proves es queda, perquè un laboratori on practicar sense risc val molt més que la decisió de no fer-lo servir en producció.

A 08-06 es tanca el curs. Portaràs tot el que has construït en vuit mòduls a una checklist de posada en producció d'unes trenta files, cadascuna amb la seva evidència comprovable — i allà es tanca formalment el deute del xifratge en trànsit que 08-01 va resoldre. Muntaràs per fi el monitoratge amb Prometheus i Grafana que va quedar ajornat a 05-07, amb els quatre senyals daurats, PromQL de l'imprescindible i mètriques pròpies: l'edat de l'última còpia, els dies fins a la caducitat del certificat, el retard de la rèplica. Configuraràs alertes que siguin accionables, que és el contrari del que fa gairebé tothom. Escriuràs el procediment d'operació diària, el quadern de guàrdia i la revisió posterior a l'incident sense culpables. Convertiràs el 99,8 % proposat a 07-07 en un pressupost d'error que decideix si es desplega o s'estabilitza. I tancaràs els tres deutes que continuen oberts des del Mòdul 5: la còpia externa que no és append-only, l'authorized_keys2 que ningú no ha explicat, i l'objectiu de disponibilitat que no es va formalitzar mai.

Curs de Linux: De Principiant a Administrador de Sistemes

Mòdul 1: Introducció a Linux

Mòdul 2: Comandes Bàsiques de Linux

Mòdul 3: Habilitats Avançades en la Línia de Comandes

Mòdul 4: Scripting en Shell

Mòdul 5: Administració del Sistema

Mòdul 6: Xarxes i Seguretat

Mòdul 7: Temes Avançats

Mòdul 8: Projectes Pràctics

© Copyright 2026. Tots els drets reservats