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:
- É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.
- 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.
- 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
- Objectiu, requisits previs i advertiment
- Quin problema resol Kubernetes i quan NO fer-lo servir
- Arquitectura: pla de control i nodes de treball
- Distribucions: kubeadm, k3s, microk8s i els gestionats
- Instal·lació de k3s amb un servidor i un agent
- kubectl i el kubeconfig
- Els objectes, en ordre de dependència
- Manifests complets per a Tramontana Reserves
- Desplegament progressiu i marxa enrere
- Seguretat: securityContext, NetworkPolicy, RBAC i PSS
- Diagnòstic: llegir esdeveniments i les quatre fallades habituals
- Automatització amb Ansible
- 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
2I 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/24Quin 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:
- Cap a la màquina de proves. Amb 3,8 GB i 2 vCPU,
kubeadmva just; k3s deixa lloc per desplegar-hi alguna cosa a sobre. - 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.
- Porta l'imprescindible muntat: Traefik com a Ingress, ServiceLB per a Services de tipus LoadBalancer,
local-pathcom a StorageClass i CoreDNS. Ambkubeadmcaldria instal·lar i configurar cada peça, cosa que és instructiva i aquí és soroll. - É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 CESTFixar 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 0Recursos 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-provesEl 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: provesLes 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.
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 sempremaxUnavailable: 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ó
$ 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.88Des 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
SuperSecreta123Aquí 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 WaitForFirstConsumerAví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 51sLes 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.0Zero 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 outLa 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
4Seguretat: 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 1NetworkPolicy
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
yeskubectl 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, seccompProfileDiagnò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 refusedAquella ú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/salutport-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 k3sLa 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 364d5. 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
:latesta la imatge. Dos Pods de la mateixa «versió» poden executar codi diferent, irollout undono torna enlloc. Etiquetes immutables, sempre. - Crear Pods solts en lloc de Deployments. Ningú no els vigila: si moren, no tornen.
livenessProbeigual o més estricta quereadinessProbe. Sota càrrega es reinicien Pods sans i el servei cau en cascada. Liveness sempre més laxa.- No posar
startupProbeen 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 -di 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.
requestsiguals alslimitsi molt generoses. El planificador reserva perrequests: malgastes el clúster i provoquesPendingamb nodes buits.- Pujar el límit de memòria en veure un
OOMKilledsense investigar. Pot ser una fuita, i només estàs endarrerint el problema. kubectl logssense--previousen unCrashLoopBackOff. 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 describei llegeix elsEventsde 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.0Primera 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 3hUn 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; avortantCausa 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 outPer 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 28sUn 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:
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:
maxUnavailable: 0va 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ó.- La
readinessProbeva impedir que el Service enviés trànsit a un Pod trencat. Sense ella, la meitat de les peticions haurien fallat durant tres minuts. --previousva ser el que va donar la resposta. Sense aquella opció,kubectl logsretornava 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:
- Desplegament i marxa enrere. Onze segons davant de dos minuts, i sense script propi per mantenir. És una diferència real.
- 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.
- Escalat. Una ordre davant d'una hora de feina. Només importa si l'escalat és freqüent, i a Tramontana no ho és.
- 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:
- 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ó.
- 70-130 hores l'any de manteniment indefinit, davant d'unes 15.
- Tres a sis mesos fins a poder resoldre incidents amb solvència. Durant aquell període, la fiabilitat baixa.
- 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.
- 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-provescom 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 8hQuè 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:
- Resistència a la fallada del node. És l'argument fort:
tramontana-copia.timerviu asrv-tramontanai si aquella màquina està apagada a les 02:30, no hi ha còpia aquella nit i ningú no se n'assabenta fins quecomprovar_copia.shho detecta a les 08:00. El CronJob s'executa en qualsevol node disponible. - Aïllament i límits reals, amb la mateixa facilitat. Encara que
systemdtambé els dona ambMemoryMax, aquí vénen de sèrie.
El que s'hi perd, i pesa:
- Els secrets empitjoren.
systemd-credsde 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. - La notificació de fallada deixa de ser nativa.
OnFailure=a systemd envia l'alerta directament; amb un CronJob cal monitorarkube_job_status_faileddes de Prometheus (08-06). Una peça més. - Accedir al disc de l'amfitrió es complica, i amb raó: muntar
/srv/tramontana/backupsen un Pod exigeix unhostPath, que les Pod Security Standardsrestrictedprohibeixen. Caldria reescriure el flux perquè la còpia vagi directa aresticsense passar per disc local, cosa que és més neta però és feina. - Cal construir i publicar una imatge amb l'script,
pg_dump,restici 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.timera 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
- Què és Linux?
- Història de Linux
- Distribucions de Linux
- Instal·lant Linux
- Primer Contacte amb el Sistema
- Estructura del Sistema de Fitxers de Linux
Mòdul 2: Comandes Bàsiques de Linux
- Introducció a la Línia de Comandes
- Obtenir Ajuda i Documentació del Sistema
- Navegant pel Sistema de Fitxers
- Operacions amb Fitxers i Directoris
- Visualització i Edició de Fitxers
- Enllaços Durs i Simbòlics
- Permisos i Propietat dels Fitxers
Mòdul 3: Habilitats Avançades en la Línia de Comandes
- L'Entorn del Shell: Variables, Àlies i Historial
- Ús de Comodins i Expressions Regulars
- Cerca de Fitxers i Contingut: find, locate i grep
- Canonades i Redirecció
- Processament de Text: cut, sort, uniq, sed i awk
- Gestió de Processos
- Programació de Tasques amb Cron
- Comandes de Xarxa
Mòdul 4: Scripting en Shell
- Introducció al Scripting en Shell
- Variables i Tipus de Dades
- Entrada, Sortida i Arguments d'un Script
- Estructures de Control
- Funcions i Biblioteques
- Depuració i Gestió d'Errors
- Scripts de Producció: Bones Pràctiques
Mòdul 5: Administració del Sistema
- Gestió d'Usuaris i Grups
- sudo i Permisos Especials
- Gestió de Paquets
- Gestió de Discs
- systemd i la Gestió de Serveis
- Registres del Sistema: journald i syslog
- Monitoratge del Sistema i Optimització del Rendiment
- Còpies de Seguretat i Restauració
Mòdul 6: Xarxes i Seguretat
- Configuració de Xarxes
- SSH i Accés Remot
- Tallafocs i Seguretat Perimetral
- Sistemes de Detecció d'Intrusions
- Gestió de Secrets i Certificats TLS
- Assegurant Sistemes Linux
Mòdul 7: Temes Avançats
- El Procés d'Arrencada i la Recuperació del Sistema
- Diagnòstic Avançat: strace, perf i eBPF
- Optimització del Nucli de Linux
- Virtualització amb Linux
- Contenidors de Linux i Docker
- Automatització amb Ansible
- Alta Disponibilitat i Balanceig de Càrrega
