Cinquanta-cinc lliçons. srv-tramontana està instal·lat, enfortit, monitorable, copiat, automatitzat, xifrat en trànsit i amb una base de dades que ja no corre amb la configuració de fàbrica. Saps reconstruir-lo en cinquanta minuts, recuperar la base de dades a un segon concret, diagnosticar una latència amb eBPF i decidir amb números si cal alta disponibilitat.
I tot i així hi falta alguna cosa, perquè «funciona» i «està en producció» no són el mateix. Un sistema en producció és aquell del qual altres persones depenen sense saber que existeix: té un propietari, un objectiu de servei acordat, algú que el mira cada matí, alertes que s'atenen, un procediment quan es trenca i un registre de per què està com està. Res d'això no és programari; tot això és operació.
Aquesta lliçó tanca el curs posant aquella capa. La checklist completa amb la seva evidència, el monitoratge amb Prometheus i Grafana que va quedar ajornat des de 05-07, alertes que es puguin atendre sense odiar-les, la rutina diària, la revisió posterior a l'incident sense culpables, i el pressupost d'error que converteix el 99,8 % de 07-07 en una decisió executable. I els tres deutes que continuen oberts des del Mòdul 5, que es tanquen avui.
Contingut
- Què significa estar en producció
- La checklist de posada en producció
- Monitorar i alertar: no són el mateix
- Prometheus i node_exporter
- PromQL de l'imprescindible
- Mètriques pròpies: les que ja tens sense saber-ho
- Grafana i el panell mínim
- Alertes que es poden atendre
- Registres centralitzats: el deute de 05-06
- Operació diària, setmanal i mensual
- El quadern de guàrdia i el registre de canvis
- La revisió posterior a l'incident sense culpables
- Gestió del canvi i finestres de desplegament
- SLI, SLO i pressupost d'error
- Tancament dels deutes pendents
- Compliment: RGPD i retenció de dades
- Tancament del curs
Què significa estar en producció
| «Funciona a la meva màquina» | En producció | |
|---|---|---|
| Qui en depèn | Tu | Persones que no saben que existeix |
| Quan ha de funcionar | Quan el mires | Sempre |
| Si es trenca de matinada | S'arregla demà | Algú se n'assabenta i actua |
| Configuració | Al teu cap | En codi, versionada |
| Canvis | Quan ve de gust | Amb finestra i aprovació |
| Dades | Reproduïbles | Irreemplaçables |
| Objectiu de disponibilitat | Cap | Acordat i mesurat |
| Quan algú pregunta «va bé?» | «Crec que sí» | Un número |
L'última fila és la que resumeix la lliçó. La diferència entre un sistema aficionat i un de professional no és la tecnologia: és que davant de «va bé?» hi ha una dada, no una impressió.
I hi ha una prova pràctica, més útil que qualsevol definició:
Podries anar-te'n dues setmanes de vacances sense ordinador?
Per respondre que sí calen sis coses: que el sistema s'autorepari en les fallades previstes, que algú rebi les alertes, que aquell algú tingui procediments escrits que pugui seguir sense ser tu, que les còpies es verifiquin soles, que hi hagi un camí d'escalat, i que existeixi un registre de què s'ha canviat i per què. Aquesta lliçó construeix les sis.
La checklist de posada en producció
Cada fila porta evidència comprovable: una ordre que retorna un resultat, no una casella de consciència. Sense evidència, una checklist és una llista de bones intencions.
Infraestructura
| # | Requisit | Evidència | Estat |
|---|---|---|---|
| 1 | Sistema operatiu amb suport a llarg termini | lsb_release -d → Ubuntu 24.04 LTS |
✅ 01-04 |
| 2 | Configuració reproduïble en codi | ansible-playbook --check --diff sense canvis |
✅ 07-06 |
| 3 | Reconstrucció completa mesurada | Registre de l'últim assaig: 50 min | ✅ 07-06 |
| 4 | Serveis gestionats per systemd, no scripts solts | systemctl list-units --failed buit |
✅ 05-05 |
| 5 | Arrencada verificada després de reiniciar | systemd-analyze critical-chain sense fallades |
✅ 07-01 |
| 6 | Recursos amb marge mesurat | revisio_salut.sh → 0 |
✅ 05-07 |
| 7 | Entorn de proves equivalent | srv-tramontana-proves operatiu |
✅ 07-04 |
Seguretat
| # | Requisit | Evidència | Estat |
|---|---|---|---|
| 8 | Tallafocs de llista blanca | ufw status verbose |
✅ 06-03 |
| 9 | SSH sense contrasenya, sense root, amb fail2ban |
sshd -T | grep -E 'permitroot|passwordauth' |
✅ 06-02 |
| 10 | Xifratge en trànsit actiu | curl -sI https://... | grep strict-transport |
✅ 08-01 |
| 11 | Certificat vàlid i renovant-se sol | certbot certificates; revisar_certificat.sh |
✅ 06-05 |
| 12 | Secrets fora del codi i xifrats | pass ls; systemd-creds list |
✅ 06-05 |
| 13 | Servei amb mínim privilegi | systemd-analyze security tramontana → 1.6 |
✅ 05-05 |
| 14 | Confinament obligatori actiu | aa-status | grep tramontana (enforce) |
✅ 06-06 |
| 15 | Detecció de canvis en fitxers | aide --check; BD fora del servidor |
✅ 06-04 |
| 16 | Auditoria d'esdeveniments sensibles | auditctl -l amb regles carregades |
✅ 06-04 |
| 17 | Actualitzacions de seguretat al dia | apt list --upgradable | grep -c security → 0 |
✅ 06-06 |
| 18 | Revisió d'exposició externa | Índex de Lynis: 82 | ✅ 06-06 |
| 19 | Accés remot sense exposar serveis | wg show amb parells actius |
✅ 08-04 |
Dades
| # | Requisit | Evidència | Estat |
|---|---|---|---|
| 20 | RPO i RTO acordats per escrit | RPO 15 min, RTO 2 h | ✅ 07-06, 08-02 |
| 21 | Còpies automàtiques i verificades | comprovar_copia.sh → 0; restic check |
✅ 05-08 |
| 22 | Còpia fora del servidor (regla 3-2-1) | restic snapshots en repositori remot |
✅ 05-08 |
| 23 | Còpia externa immutable (append-only) |
Testimoni de només afegir | ⚠️ Es tanca avui |
| 24 | Restauració assajada amb temps mesurat | Manual d'operació RB-BD-02: 24 min | ✅ 08-02 |
| 25 | Recuperació a un punt en el temps | Arxivat de WAL: failed_count = 0 |
✅ 08-02 |
| 26 | Xifratge en repòs de la còpia | LUKS + xifratge de restic |
✅ 05-04, 06-05 |
| 27 | Política de retenció d'acord amb el RGPD | Definida i aplicada | ⚠️ Pendent: auditoria |
Observabilitat
| # | Requisit | Evidència | Estat |
|---|---|---|---|
| 28 | Registres persistents i rotats | journalctl --disk-usage; logrotate -d |
✅ 05-06 |
| 29 | Monitoratge continu amb històric | up{job="tramontana"} = 1 |
⚠️ Es tanca avui |
| 30 | Alertes que arriben a una persona | Ruta d'Alertmanager provada | ⚠️ Es tanca avui |
| 31 | Panell amb els quatre senyals daurats | Grafana operatiu | ⚠️ Es tanca avui |
| 32 | Extrem de salut significatiu | curl -s /salut | jq .estat |
✅ 07-07 |
Operació
| # | Requisit | Evidència | Estat |
|---|---|---|---|
| 33 | Objectiu de disponibilitat formal | SLO acordat amb la Marta | ⚠️ Es tanca avui |
| 34 | Manuals d'operació fora del servidor | Arxivador + repositori | ✅ 05-08, 08-02 |
| 35 | Rutina diària definida i executada | Taula de l'apartat 10 | ⚠️ Es tanca avui |
| 36 | Registre de canvis | git log de ~/tramontana-infra |
✅ 07-06 |
| 37 | Desplegament reversible i provat | desplegar.sh amb marxa enrere |
✅ 04-07 |
| 38 | Escalat definit: qui i quan | Apartat 8 | ⚠️ Es tanca avui |
Documentació i compliment
| # | Requisit | Evidència | Estat |
|---|---|---|---|
| 39 | Inventari de sistemes i responsables | docs/inventari.md |
⚠️ Pendent |
| 40 | Tots els accessos justificats | authorized_keys revisat |
⚠️ authorized_keys2: avui |
| 41 | Registre d'activitats de tractament | Document de RGPD | ⚠️ Pendent |
| 42 | Diagrama d'arquitectura actualitzat | docs/arquitectura.md |
✅ 07-07 |
Resum: 30 files complertes de 42. Nou es tanquen en aquesta lliçó; tres queden com a feina planificada amb data. Aquella xifra és en si mateixa l'evidència número 43: saber exactament què falta.
Monitorar i alertar: no són el mateix
És la distinció que evita l'error més car d'aquesta àrea.
| Monitorar | Alertar | |
|---|---|---|
| Pregunta que respon | «Què està passant i què va passar?» | «Algú ha de fer alguna cosa ara?» |
| Quan es consulta | Quan algú mira | Et busca a tu |
| Volum adequat | Tot el que es pugui mesurar | Molt poc |
| Cost d'un excés | Disc | Que s'ignorin les importants |
| Eina | Prometheus, Grafana | Alertmanager |
Monitora tot el que puguis; alerta de gairebé res. És contraintuïtiu i és l'única manera que les alertes serveixin. Un sistema que envia quaranta notificacions al dia és un sistema sense alertes, perquè ningú no les llegeix.
Els quatre senyals daurats
De 05-07, ara amb instrumentació real:
| Senyal | Què mesura | A Tramontana |
|---|---|---|
| Latència | Quant triga una petició | $upstream_response_time de 08-01 |
| Trànsit | Quanta demanda hi ha | Peticions per segon |
| Errors | Quina proporció falla | Respostes 5xx |
| Saturació | Com de ple està el sistema | CPU, memòria, connexions de PgBouncer |
Dos matisos que fan la diferència entre mesurar i mesurar bé:
La latència es mesura en percentils, mai en mitjana. Una mitjana de 80 ms pot amagar que l'1 % dels usuaris espera 4 segons. I cal separar la latència de les peticions correctes de la de les errònies: un error 500 retornat en 2 ms millora la mitjana i empitjora el servei.
La latència dels errors s'exclou de l'SLI. Si no, una caiguda total —on tot falla ràpid— apareixeria com una millora de rendiment.
I el mètode USE, complementari, per a cada recurs: Utilització, Saturació i Errors.
| Senyals daurats | Mètode USE | |
|---|---|---|
| Punt de vista | De l'usuari | Dels recursos |
| Respon a | «Està bé el servei?» | «Quin recurs és el coll d'ampolla?» |
| Quan es fa servir | Alertar | Diagnosticar |
Prometheus i node_exporter
Prometheus és una base de dades de sèries temporals que recol·lecta mètriques: en lloc que els serveis li enviïn dades, ell les demana periòdicament a extrems HTTP.
| Recol·lecció (Prometheus) | Enviament (StatsD, Graphite) | |
|---|---|---|
| Qui inicia | El servidor | El client |
| Detectar que un servei ha mort | Trivial: up == 0 |
Difícil: absència de dades |
| Configuració | Centralitzada | A cada client |
| Objectius efímers | Necessita descobriment | Natural |
Aquella segona fila és un avantatge enorme: amb recol·lecció, un servei caigut produeix immediatament up == 0, que és un senyal explícit.
$ sudo apt install prometheus prometheus-node-exporter
$ prometheus --version
prometheus, version 2.48.1 (branch: HEAD)Les unitats del paquet són raonables però no compleixen l'estàndard del curs. Drop-ins, com sempre:
# /etc/systemd/system/prometheus-node-exporter.service.d/override.conf
[Service]
# Escoltar NOMES a localhost: Prometheus corre a la mateixa maquina.
# Sense aixo, qualsevol a 10.0.2.0/24 llegeix metriques que revelen
# versio del nucli, sistemes de fitxers i processos.
ExecStart=
ExecStart=/usr/bin/prometheus-node-exporter \
--web.listen-address=127.0.0.1:9100 \
--collector.systemd \
--collector.textfile.directory=/var/lib/node_exporter/textfile \
--no-collector.wifi --no-collector.hwmon --no-collector.infiniband
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
NoNewPrivileges=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
SystemCallFilter=@system-service
SystemCallFilter=~@privileged @resources
CapabilityBoundingSet=
MemoryMax=256M
CPUQuota=20%$ sudo mkdir -p /var/lib/node_exporter/textfile
$ sudo chown prometheus:prometheus /var/lib/node_exporter/textfile
$ sudo systemctl daemon-reload && sudo systemctl restart prometheus-node-exporter
$ systemd-analyze security prometheus-node-exporter
→ Overall exposure level: 2.1 OK# /etc/prometheus/prometheus.yml
global:
scrape_interval: 15s # cada quant es demanen les metriques
evaluation_interval: 15s # cada quant s avaluen les regles
external_labels:
entorn: produccio
servidor: srv-tramontana
rule_files:
- /etc/prometheus/regles/*.yml
alerting:
alertmanagers:
- static_configs:
- targets: ['127.0.0.1:9093']
scrape_configs:
# El mateix Prometheus: si ell falla, cal saber-ho
- job_name: prometheus
static_configs:
- targets: ['127.0.0.1:9090']
# Metriques del sistema: CPU, memoria, disc, xarxa, systemd
- job_name: node
static_configs:
- targets: ['127.0.0.1:9100']
labels: {instancia: srv-tramontana}
# L aplicacio (requereix que el Luis exposi /metrics)
- job_name: tramontana
metrics_path: /metrics
static_configs:
- targets: ['127.0.0.1:8080']
# Descartar metriques d alta cardinalitat: una etiqueta amb l ID
# de reserva crearia una serie per reserva i rebentaria la memoria.
metric_relabel_configs:
- source_labels: [__name__]
regex: 'tramontana_booking_detail.*'
action: drop
- job_name: nginx
static_configs:
- targets: ['127.0.0.1:9113']
- job_name: postgres
static_configs:
- targets: ['127.0.0.1:9187']$ sudo promtool check config /etc/prometheus/prometheus.yml
Checking /etc/prometheus/prometheus.yml
SUCCESS: 1 rule files found
$ sudo systemctl reload prometheus
$ curl -s 'http://127.0.0.1:9090/api/v1/targets' | \
jq -r '.data.activeTargets[] | "\(.labels.job)\t\(.health)"'
prometheus up
node up
tramontana up
nginx up
postgres uppromtool check config és el nginx -t de Prometheus, i es fa servir igual: sempre abans de recarregar.
Sobre la retenció i el disc, que és la pregunta que sempre surt:
$ sudo du -sh /var/lib/prometheus/metrics2
412M /var/lib/prometheus/metrics2
# Estimacio: bytes ≈ series x (temps / interval) x ~2 bytes per mostra
# 4.000 series x (30 dies / 15 s) x 2 B ≈ 1,4 GB# /etc/default/prometheus
ARGS="--storage.tsdb.retention.time=30d --storage.tsdb.retention.size=4GB \
--web.listen-address=127.0.0.1:9090"Trenta dies basten per investigar incidents i veure tendències mensuals. Per a històric d'anys existeixen Thanos o Mimir, i per a Tramontana són innecessaris.
PromQL de l'imprescindible
PromQL espanta al principi i es redueix a cinc patrons que cobreixen el 90 % dels casos.
1. up: el més important i el més simple.
2. rate() sobre comptadors. Un comptador només puja. El seu valor absolut no diu res; el que importa és a quin ritme creix.
rate() gestiona correctament els reinicis: si el comptador torna a zero perquè el procés s'ha reiniciat, ho detecta i no produeix un valor negatiu absurd.
Regla:
rate()només sobre comptadors (_total). Per a mesuradors —memòria, temperatura, connexions— es fa servir el valor directament.
3. Agregacions i proporcions.
# Proporcio d errors 5xx sobre el total: el SENYAL D ERRORS
sum(rate(nginx_http_requests_total{status=~"5.."}[5m]))
/
sum(rate(nginx_http_requests_total[5m]))
# Us de CPU: 'idle' es el que sobra, aixi que 1 menys aixo
1 - avg(rate(node_cpu_seconds_total{mode="idle"}[5m]))
# Percentatge de memoria disponible
node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes * 1004. histogram_quantile(): els percentils de latència.
# p95 de latencia, en segons
histogram_quantile(0.95,
sum(rate(tramontana_request_duration_seconds_bucket[5m])) by (le))L'etiqueta le (less or equal) defineix els cubells de l'histograma i s'ha de conservar al by. És l'error més comú de PromQL: agregar sense by (le) i obtenir resultats sense sentit.
5. Predicció, que és el que converteix una alerta en útil.
# A aquest ritme, s omplira el disc en les properes 4 hores?
predict_linear(node_filesystem_avail_bytes{mountpoint="/"}[6h], 4*3600) < 0Alertar de «disc al 90 %» és alertar tard o alertar en va —un disc estable al 91 % no és un problema—. Alertar de «a aquest ritme s'omple en quatre hores» és accionable, que és la propietat que defineix una bona alerta.
Mètriques pròpies: les que ja tens sense saber-ho
Els node_exporter i companyia donen mètriques tècniques. Les que de debò importen són les que responen a preguntes del negoci, i la majoria ja les estàs calculant als scripts del curs — només cal exposar-les.
El mecanisme és el recol·lector de fitxers de text: qualsevol script deixa un .prom en un directori i node_exporter el publica.
#!/usr/bin/env bash
#
# metriques_tramontana.sh - Exposa metriques propies a Prometheus
#
# Recull el que els scripts del curs ja calculen i ho publica en
# format d exposicio de Prometheus. S executa cada 5 min.
#
set -euo pipefail
readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/lib/comuns.sh"
readonly SORTIDA=/var/lib/node_exporter/textfile/tramontana.prom
readonly DOMINI=reserves.tramontana.example
umask 022 # node_exporter ha de poder LLEGIR-LO
main() {
# Escriptura ATOMICA: si l script mor a mitges, node_exporter
# llegiria un fitxer truncat i descartaria totes les metriques.
local tmp; tmp="$(mktemp "${SORTIDA}.XXXXXX")"
trap 'rm -f "$tmp"' EXIT
{
# --- 1. Edat de l ultima copia correcta (05-08) ---
# La metrica de copies mes util NO es "s ha executat el
# temporitzador?" sino "quant fa que hi ha una copia VERIFICADA?".
echo '# HELP tramontana_backup_age_seconds Segons des de l ultima copia verificada'
echo '# TYPE tramontana_backup_age_seconds gauge'
if [[ -f /var/lib/tramontana/ultima-copia-correcta ]]; then
printf 'tramontana_backup_age_seconds %d\n' \
$(( $(date +%s) - $(stat -c %Y /var/lib/tramontana/ultima-copia-correcta) ))
else
printf 'tramontana_backup_age_seconds %d\n' 999999
fi
# --- 2. Dies fins a la caducitat del certificat (06-05) ---
echo '# HELP tramontana_certificate_days_left Dies fins a la caducitat del certificat TLS'
echo '# TYPE tramontana_certificate_days_left gauge'
local fi dies
if fi="$(echo | timeout 10 openssl s_client -connect "${DOMINI}:443" \
-servername "$DOMINI" 2>/dev/null | \
openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)"; then
dies=$(( ( $(date -d "$fi" +%s) - $(date +%s) ) / 86400 ))
printf 'tramontana_certificate_days_left %d\n' "$dies"
fi
# --- 3. Index de Lynis (06-06) ---
echo '# HELP tramontana_lynis_index Index d enfortiment de Lynis'
echo '# TYPE tramontana_lynis_index gauge'
if [[ -f /var/log/lynis-report.dat ]]; then
printf 'tramontana_lynis_index %s\n' \
"$(awk -F= '/^hardening_index=/{print $2}' /var/log/lynis-report.dat)"
fi
# --- 4. Retard de la replica de PostgreSQL (08-02) ---
# Quants bytes es perdrien si el primari caigues ARA.
echo '# HELP tramontana_replica_lag_bytes Retard de la replica en bytes'
echo '# TYPE tramontana_replica_lag_bytes gauge'
printf 'tramontana_replica_lag_bytes %s\n' \
"$(sudo -u postgres psql -tAc \
"SELECT coalesce(max(pg_wal_lsn_diff(sent_lsn,replay_lsn)),0)::bigint
FROM pg_stat_replication" 2>/dev/null || echo 0)"
# --- 5. Fallades d arxivat de WAL (08-02) ---
echo '# HELP tramontana_wal_archive_failures Fallades acumulades d arxivat de WAL'
echo '# TYPE tramontana_wal_archive_failures gauge'
printf 'tramontana_wal_archive_failures %s\n' \
"$(sudo -u postgres psql -tAc \
"SELECT failed_count FROM pg_stat_archiver" 2>/dev/null || echo 0)"
# --- 6. Estat de revisio_salut.sh (0/1/2) ---
echo '# HELP tramontana_health_status 0 correcte, 1 avis, 2 critic'
echo '# TYPE tramontana_health_status gauge'
local estat=0
"${SCRIPT_DIR}/revisio_salut.sh" >/dev/null 2>&1 || estat=$?
printf 'tramontana_health_status %d\n' "$estat"
# --- 7. Metriques de NEGOCI: les que li importen a la Marta ---
echo '# HELP tramontana_bookings_total Reserves registrades avui'
echo '# TYPE tramontana_bookings_total gauge'
printf 'tramontana_bookings_total %s\n' \
"$(sudo -u postgres psql -tAc \
"SELECT count(*) FROM app.reserves WHERE data = CURRENT_DATE" \
-d tramontana 2>/dev/null || echo 0)"
# --- 8. Marca de frescor: detecta que AQUEST script ha mort ---
echo '# HELP tramontana_metrics_generated_seconds Marca temporal de generacio'
echo '# TYPE tramontana_metrics_generated_seconds gauge'
printf 'tramontana_metrics_generated_seconds %d\n' "$(date +%s)"
} > "$tmp"
chmod 0644 "$tmp"
mv "$tmp" "$SORTIDA" # atomic
trap - EXIT
}
main "$@"$ sudo ~/scripts/metriques_tramontana.sh
$ curl -s http://127.0.0.1:9100/metrics | grep '^tramontana_'
tramontana_backup_age_seconds 21840
tramontana_certificate_days_left 71
tramontana_lynis_index 82
tramontana_replica_lag_bytes 0
tramontana_wal_archive_failures 0
tramontana_health_status 0
tramontana_bookings_total 14
tramontana_metrics_generated_seconds 1755518402Quatre decisions de disseny de l'script:
- Escriptura atòmica amb
mktempimv.node_exporterpot llegir el fitxer en qualsevol moment; un de truncat fa que en descarti tot el contingut. umask 022, en contra de la convenció general del curs:node_exportercorre com un altre usuari i necessita llegir-lo. És una excepció justificada, i per això està comentada.- La mètrica 8, la marca de frescor, és la que fa fiables les altres set. Sense ella, si aquest script deixa d'executar-se, Prometheus continuaria publicant els últims valors coneguts indefinidament i tot semblaria correcte per sempre. Amb ella es pot alertar que les mètriques estan rances.
- La mètrica de negoci,
tramontana_bookings_total, és la que a la Marta li importa i la que detecta la fallada més perillosa: la que no trenca res. Si el sistema respon 200 a tot però ningú no aconsegueix reservar, cap mètrica tècnica no ho revelarà.
I per a l'aplicació, el que el Luis ha d'exposar a /metrics:
# HELP tramontana_requests_total Peticions HTTP ateses
# TYPE tramontana_requests_total counter
tramontana_requests_total{metode="GET",ruta="/cases",codi="200"} 41822
# HELP tramontana_request_duration_seconds Durada de les peticions
# TYPE tramontana_request_duration_seconds histogram
tramontana_request_duration_seconds_bucket{le="0.05"} 38120
tramontana_request_duration_seconds_bucket{le="0.1"} 40911
tramontana_request_duration_seconds_bucket{le="0.5"} 41780
tramontana_request_duration_seconds_bucket{le="+Inf"} 41822
tramontana_request_duration_seconds_sum 1284.41
tramontana_request_duration_seconds_count 41822
# HELP tramontana_db_active_connections Connexions a la base de dades en us
# TYPE tramontana_db_active_connections gauge
tramontana_db_active_connections 12Grafana i el panell mínim
$ sudo install -m 0755 -d /etc/apt/keyrings
$ curl -fsSL https://apt.grafana.com/gpg.key | \
sudo gpg --dearmor -o /etc/apt/keyrings/grafana.gpg
$ echo "deb [signed-by=/etc/apt/keyrings/grafana.gpg] https://apt.grafana.com stable main" | \
sudo tee /etc/apt/sources.list.d/grafana.list
$ sudo apt update && sudo apt install grafana# /etc/grafana/grafana.ini (fragment)
[server]
http_addr = 127.0.0.1 # NO exposat: s hi arriba per Nginx o VPN
http_port = 3000
root_url = https://panel.tramontana.example/
[security]
admin_user = operador
# La contrasenya s injecta amb systemd-creds (06-05), no aqui
disable_gravatar = true
cookie_secure = true
cookie_samesite = strict
content_security_policy = true
[users]
allow_sign_up = falseI l'accés, aprofitant el que ja està muntat: per la VPN de 08-04, sense publicar res de nou.
El panell mínim, sis panells i ni un més:
| Panell | Consulta | Per què |
|---|---|---|
| Disponibilitat (30 d) | avg_over_time(up{job="tramontana"}[30d]) * 100 |
El número que la Marta pregunta |
| Latència p50/p95/p99 | histogram_quantile(...) |
Senyal daurat: latència |
| Peticions per segon | sum(rate(nginx_http_requests_total[5m])) |
Senyal daurat: trànsit |
| Proporció d'errors | 5xx sobre total | Senyal daurat: errors |
| Saturació | CPU, memòria, disc, connexions | Senyal daurat: saturació |
| Estat del sistema | Edat de còpia, dies de certificat, retard de rèplica, Lynis | El que es mira cada matí |
Un consell que contradiu l'instint: un panell amb quaranta gràfiques no es mira. El panell principal ha de cabre en una pantalla i respondre en tres segons a «va bé?». El detall viu en panells secundaris als quals s'arriba quan cal.
Alertes que es poden atendre
La regla
Tota alerta ha de requerir una acció humana immediata. Si el receptor no pot fer res, o pot esperar a demà, no és una alerta: és un panell o un informe.
La fatiga d'alertes és el mode de fallada característic del monitoratge, i la seva progressió és sempre la mateixa:
- Es configuren alertes per a tot, «per si de cas».
- N'arriben deu al dia, gairebé totes irrellevants.
- La gent comença a ignorar-les.
- Es crea un filtre de correu que les arxiva.
- Arriba l'alerta important i ningú no la veu.
El sistema queda pitjor que sense alertes, perquè hi ha una falsa sensació de vigilància. I el remei és contraintuïtiu: esborrar alertes.
Símptoma enfront de causa
| Alerta de símptoma | Alerta de causa | |
|---|---|---|
| Què detecta | Que el servei no funciona | Que un component falla |
| Exemple | «La taxa d'errors supera el 5 %» | «La CPU està al 90 %» |
| Falsos positius | Pocs | Molts: la CPU al 90 % pot ser normal |
| Cobertura | Detecta fallades imprevistes | Només les previstes |
| Ajuda a diagnosticar | Poc | Molt |
Regla: alerta de símptomes, diagnostica amb causes. Un for: 5m sobre la taxa d'errors detecta qualsevol fallada que afecti l'usuari, incloses les que ningú no va anticipar. Una alerta de CPU al 90 % dispara quan s'executa la còpia nocturna i el servei va perfectament.
Amb dues excepcions legítimes, que són alertes de causa i han d'existir perquè el seu símptoma arriba massa tard:
- El disc s'omplirà. Quan el símptoma apareix, el servei ja està caigut.
- El certificat caducarà. Quan el símptoma apareix, el web ja no carrega.
Totes dues comparteixen la propietat clau: avisen amb hores o dies d'antelació i l'acció és evident.
# /etc/prometheus/regles/tramontana.yml
groups:
- name: simptomes
interval: 30s
rules:
# --- CRITIQUES: desperten algu de matinada ---
- alert: ServiceDown
expr: up{job="tramontana"} == 0
for: 2m # 2 min eviten el soroll d un reinici
labels: {severity: critical, equip: operacions}
annotations:
summary: "Tramontana Reserves no respon"
description: "L objectiu {{ $labels.instance }} fa 2 min que no respon."
accio: "Veure manual RB-OPS-01. systemctl status tramontana; journalctl -u tramontana -n50"
runbook: "https://docs.tramontana.example/RB-OPS-01"
- alert: HighErrorRate
expr: |
sum(rate(nginx_http_requests_total{status=~"5.."}[5m]))
/ sum(rate(nginx_http_requests_total[5m])) > 0.05
for: 5m
labels: {severity: critical, equip: operacions}
annotations:
summary: "Mes del 5 % de les peticions falla"
description: "Taxa actual: {{ $value | humanizePercentage }}."
accio: "journalctl -t nginx_error -n 50; comprovar PostgreSQL"
- alert: LatencyDegraded
expr: |
histogram_quantile(0.95,
sum(rate(tramontana_request_duration_seconds_bucket[5m])) by (le)) > 2
for: 10m
labels: {severity: warning, equip: operacions}
annotations:
summary: "p95 de latencia per damunt de 2 s"
accio: "pg_stat_statements per total_exec_time; diagnostic_latencia.sh"
- name: causes_que_avisen_amb_temps
rules:
# --- Les dues excepcions justificades ---
- alert: DiskWillFill
expr: predict_linear(node_filesystem_avail_bytes{mountpoint="/"}[6h], 4*3600) < 0
for: 15m
labels: {severity: critical, equip: operacions}
annotations:
summary: "/ s omplira en menys de 4 hores al ritme actual"
accio: "du -sh /var/log/* /srv/*; revisar rotacio i purga"
- alert: CertificateExpiringSoon
expr: tramontana_certificate_days_left < 20
for: 1h
labels: {severity: warning, equip: operacions}
annotations:
summary: "El certificat caduca en {{ $value }} dies"
accio: "certbot renew --dry-run; revisar certbot.timer"
- name: dades
rules:
- alert: BackupTooOld
# Llindar a 2x l RPO acordat: no alerta per un retard normal
expr: tramontana_backup_age_seconds > 2*4*3600
for: 30m
labels: {severity: critical, equip: operacions}
annotations:
summary: "Sense copia verificada des de fa {{ $value | humanizeDuration }}"
accio: "Veure manual RB-BD-01. journalctl -u tramontana-copia"
- alert: WALArchivingFailing
# Alerta de causa deliberada: el simptoma (PostgreSQL aturat)
# apareix hores despres i aleshores no hi ha sortida facil.
expr: tramontana_wal_archive_failures > 0
for: 5m
labels: {severity: critical, equip: operacions}
annotations:
summary: "L arxivat de WAL esta fallant"
accio: "df -h /srv/tramontana/backups; veure incident del 2026-08-18"
- alert: ReplicaLagging
expr: tramontana_replica_lag_bytes > 100*1024*1024
for: 10m
labels: {severity: warning, equip: operacions}
annotations:
summary: "La replica va {{ $value | humanize1024 }}B per darrere"
- name: metamonitoratge
rules:
# L alerta que vigila el monitoratge. Sense ella, un script de
# metriques mort fa que TOT sembli correcte per sempre.
- alert: StaleMetrics
expr: time() - tramontana_metrics_generated_seconds > 1800
for: 5m
labels: {severity: warning, equip: operacions}
annotations:
summary: "Les metriques propies fa mes de 30 min que no s actualitzen"
accio: "systemctl status metriques-tramontana.timer"$ sudo promtool check rules /etc/prometheus/regles/tramontana.yml
SUCCESS: 10 rules found
# Provar una regla SENSE esperar que passi
$ sudo promtool test rules /etc/prometheus/proves/regles_test.yml
Unit Testing: SUCCESSAlertmanager: rutes i severitats
# /etc/alertmanager/alertmanager.yml
global:
resolve_timeout: 5m
route:
group_by: ['alertname', 'severity']
group_wait: 30s # esperar 30 s per si arriben alertes relacionades
group_interval: 5m
repeat_interval: 4h # recordatori cada 4 h si continua activa
receiver: correu-operacions
routes:
# Critiques: correu i, fora d horari, telefon
- matchers: [severity="critical"]
receiver: guardia
group_wait: 10s
repeat_interval: 1h
# Avisos: nomes correu, sense pressa
- matchers: [severity="warning"]
receiver: correu-operacions
repeat_interval: 12h
# Silenciar alertes derivades quan la causa arrel ja ha alertat
inhibit_rules:
- source_matchers: [alertname="ServiceDown"]
target_matchers: [severity=~"warning|critical"]
equal: [instance]
receivers:
- name: correu-operacions
email_configs:
- to: [email protected]
headers: {Subject: '[{{ .Status | toUpper }}] {{ .CommonLabels.alertname }}'}
- name: guardia
email_configs:
- to: [email protected]
webhook_configs:
- url: 'http://127.0.0.1:9095/telefon'Les inhibit_rules mereixen atenció: quan el servei cau, es dispararien també les alertes de latència, d'errors i de connexions a la base de dades. Rebre cinc notificacions del mateix incident és fatiga d'alertes en la seva forma més pura. La inhibició n'envia una: la causa.
Severitats i escalat
| Severitat | Criteri | Canal | Resposta |
|---|---|---|---|
| Crítica | El servei està caigut o hi ha risc de pèrdua de dades | Telèfon, 24×7 | Immediata |
| Avís | Degradat, o alguna cosa es trencarà en hores | Correu | Horari laboral |
| Informativa | Convé saber-ho | Panell | Revisió setmanal |
Nivell 1: operador (tu) -> 15 min sense resposta Nivell 2: Luis (desenvolupament) -> 30 min sense resposta Nivell 3: Marta (decisions de negoci) -> se l informa sempre en critiques
I el silenci programat durant el manteniment, sense el qual la finestra de desplegament genera una allau de notificacions:
$ amtool silence add alertname=~".*" --duration=1h \
--author=operador --comment="Finestra de manteniment: desplegament 3.3.0"
b3f19c8d-4a2e-4f91-b7c2-1e9d0f8a3b45
$ amtool silence expire b3f19c8d-4a2e-4f91-b7c2-1e9d0f8a3b45 # en acabarEls silencis es posen amb caducitat, sempre. Un silenci indefinit posat un dimarts és una alerta que no tornarà a sonar mai, i ningú no se'n recordarà.
Registres centralitzats: el deute de 05-06
A 05-06 va quedar una peça pendent: el journal és persistent i local. Amb un sol servidor funciona; així que n'hi ha dos —els nodes d'aplicació de 07-07, el balancejador, la rèplica— diagnosticar exigeix entrar a cada màquina i correlacionar a mà.
Per què cal centralitzar:
| Problema | Amb registres locals | Centralitzats |
|---|---|---|
| Correlacionar entre màquines | A mà, amb marques de temps | Una consulta |
| Un servidor compromès | L'atacant esborra les seves petjades | Ja són fora |
| Un servidor destruït | Es perden els registres | Conservats |
| Retenció legal | Per màquina | Una política |
| Cercar en 30 dies de registres | zgrep sobre fitxers rotats |
Índex |
Aquella segona fila és de seguretat pura i enllaça amb 06-04: si els registres només viuen a la màquina atacada, l'atacant els edita. És el mateix raonament pel qual la base de dades d'AIDE viu fora del servidor.
Loki com a opció lleugera:
| Loki | Elasticsearch/OpenSearch | journald remot | |
|---|---|---|---|
| Què indexa | Només les etiquetes, no el text | Tot el text | Res |
| Recursos | Molt baixos | Alts: 4-8 GB de RAM com a mínim | Mínims |
| Consulta | LogQL, semblant a PromQL | Molt potent | journalctl |
| Integració amb Grafana | Nativa: mateix panell | Bona | No |
| Cost d'emmagatzematge | Baix: objectes comprimits | Alt | Baix |
Loki és l'elecció per a Tramontana per dues raons: no indexa el text complet —que és el que fa car Elasticsearch— i comparteix etiquetes i sintaxi amb Prometheus, de manera que es pot saltar d'una gràfica de latència als registres d'aquell mateix instant al mateix panell. Aquella correlació és el que fa útil un sistema de registres.
# /etc/promtail/config.yml (agent que envia els registres)
clients:
- url: http://10.0.2.15:3100/loki/api/v1/push
scrape_configs:
- job_name: journal
journal:
max_age: 12h
labels: {job: systemd-journal, servidor: srv-tramontana}
relabel_configs:
- source_labels: ['__journal__systemd_unit']
target_label: unitat
- source_labels: ['__journal_priority_keyword']
target_label: nivell# Consulta LogQL: errors de l aplicacio en l ultima hora
{unitat="tramontana.service", nivell=~"err|crit"} |= "ERROR"
# Taxa d errors per minut, que es pot graficar al costat de les metriques
sum(rate({unitat="tramontana.service"} |= "ERROR" [5m]))I l'advertiment obligatori: els registres contenen dades personals —IP, identificadors d'usuari, de vegades correus—. Centralitzar-los multiplica l'abast d'una fuita. Retenció acotada, accés restringit i el mateix tractament que la base de dades.
Operació diària, setmanal i mensual
Els cinc minuts de cada matí
| # | Què es mira | On | Senyal d'alarma |
|---|---|---|---|
| 1 | Alertes actives | Alertmanager | Qualsevol sense atendre |
| 2 | Panell principal | Grafana | Anomalia respecte d'ahir |
| 3 | Edat de l'última còpia | Panell | > 24 h |
| 4 | Unitats fallides | systemctl --failed |
Qualsevol |
| 5 | Errors nous al registre | Loki o journalctl -p err |
Patró desconegut |
#!/usr/bin/env bash
# revisio_matinal.sh — els cinc minuts, en una ordre
set -euo pipefail
source "$(dirname "${BASH_SOURCE[0]}")/lib/comuns.sh"
main() {
printf '\n=== REVISIO MATINAL · %s ===\n\n' "$(date '+%F %T')"
printf '── Alertes actives ──\n'
amtool alert query --output=extended 2>/dev/null | head -10 || echo " (cap)"
printf '\n── Unitats fallides ──\n'
systemctl --failed --no-legend || echo " (cap)"
printf '\n── Estat general ──\n'
curl -s 'http://127.0.0.1:9090/api/v1/query?query=tramontana_health_status' | \
jq -r '.data.result[0].value[1] as $e |
" revisio_salut: " + (if $e=="0" then "correcte"
elif $e=="1" then "AVIS" else "CRITIC" end)'
curl -s 'http://127.0.0.1:9090/api/v1/query?query=tramontana_backup_age_seconds' | \
jq -r '" ultima copia: fa " + ((.data.result[0].value[1]|tonumber/3600|floor)|tostring) + " h"'
curl -s 'http://127.0.0.1:9090/api/v1/query?query=tramontana_certificate_days_left' | \
jq -r '" certificat: " + .data.result[0].value[1] + " dies"'
printf '\n── Errors de les ultimes 24 h ──\n'
journalctl -p err --since "24 hours ago" --no-pager -q | \
awk '{$1=$2=$3=""; print}' | sort | uniq -c | sort -rn | head -5
printf '\n── Disponibilitat (7 d) ──\n'
curl -s --data-urlencode 'query=avg_over_time(up{job="tramontana"}[7d])*100' \
'http://127.0.0.1:9090/api/v1/query' | \
jq -r '" " + (.data.result[0].value[1]|tonumber|.*100|round/100|tostring) + " %"'
printf '\n'
}
main "$@"$ ~/scripts/revisio_matinal.sh
=== REVISIO MATINAL · 2026-08-18 08:04:11 ===
── Alertes actives ──
(cap)
── Unitats fallides ──
(cap)
── Estat general ──
revisio_salut: correcte
ultima copia: fa 6 h
certificat: 71 dies
── Errors de les ultimes 24 h ──
3 tramontana[1204]: ERROR consulta lenta a /informes/facturacio
1 nginx_error: upstream timed out
── Disponibilitat (7 d) ──
99.94 %Setmanal (30 minuts) i mensual (2 hores)
| Freqüència | Tasca | Per què |
|---|---|---|
| Setmanal | Revisar tendències de 7 dies | Veure creixements lents |
| Setmanal | Actualitzacions de seguretat pendents | Finestra d'exposició |
| Setmanal | Top 5 de pg_stat_statements |
Consultes que es degraden |
| Setmanal | Espai al disc i creixement | Anticipar, no reaccionar |
| Setmanal | Registre de canvis de la setmana | Context per als incidents |
| Mensual | Restaurar un fitxer de la còpia | Verificar de debò |
| Mensual | Revisar i podar alertes | Fatiga d'alertes |
| Mensual | Lynis i AIDE | Deriva de configuració |
| Mensual | Revisar accessos: usuaris, claus SSH, parells de VPN | S'acumulen |
| Mensual | Consum del pressupost d'error | Decidir ritme de canvis |
| Trimestral | Assaig complet de PITR (RB-BD-02) | La còpia sense assaig és un fitxer |
| Semestral | Simulacre de reconstrucció | Validar l'RTO |
| Anual | Revisar SLO, model d'amenaces i arquitectura | El context canvia |
El quadern de guàrdia i el registre de canvis
El quadern de guàrdia és un fitxer de text on s'anota el que passa. Sona trivial i és l'eina que més temps estalvia en un incident, perquè respon a «això ja ens havia passat?».
# Quadern de guàrdia · srv-tramontana
## 2026-08-18
**08:04** Revisió matinal. Tot correcte. Disponibilitat 7 d: 99,94 %.
**11:20** [CANVI] Nginx com a servidor intermediari invers amb TLS. Tancat el
deute del xifratge en trànsit obert des de 06-05. HSTS amb max-age=300 a
propòsit; pujar a 2 anys el 25/08 després de verificar la renovació. Mesurat:
-78 % de bytes transferits, +19 ms al primer handshake.
**14:32** [INCIDENT] `DELETE` accidental sobre app.reserves (40.218 files).
Detectat 14:51. Recuperat per PITR en màquina de proves i reimportat.
Temps total 34 min. Sense pèrdua de dades. Posterior a l'incident: 20/08.
**16:10** [OBSERVACIÓ] El p95 puja a 1,4 s els dimarts entre les 16 i les 18 h.
Coincideix amb l'informe de facturació de la Marta. No és un problema avui;
vigilar.
## 2026-08-17
**03:14** [ALERTA] Espai a /var al 91 %. Causa: lv-backups ple →
archive_command falla → pg_wal creix. Resolt purgant WAL amb
pg_archivecleanup i ampliant el LV 10 GiB. **Alerta nova creada:
WALArchivingFailing (failed_count > 0).**Quatre regles perquè funcioni:
- S'escriu en el moment, no al final del dia. El que es posposa no s'escriu.
- Marques de temps sempre, per poder correlacionar amb les gràfiques.
- Les observacions valen tant com els incidents. L'entrada de les 16:10 és la que explicarà una alerta d'aquí a dos mesos.
- Viu fora del servidor —al repositori d'Ansible— perquè caldrà precisament quan el servidor no hi sigui.
El registre de canvis ja existeix: és el git log de ~/tramontana-infra. La disciplina és que tot canvi de configuració hi passi, perquè es pugui respondre a «què va canviar abans que comencés a fallar?».
$ cd ~/tramontana-infra && git log --oneline --since="7 days ago"
a4f19c8 Rol web: HSTS a 2 anys despres de verificar la renovacio
3e2b1d5 Rol bd: ajust de memoria per a 3,8 GB + PgBouncer
9c8a7f2 Alerta WALArchivingFailing despres de l incident del 17/08
1b4d6e3 Rol vpn: alta de marta-tauleta
# La pregunta clau durant un incident
$ git log --since="6 hours ago" --statLa revisió posterior a l'incident sense culpables
Un incident és una fallada del sistema, no d'una persona. Si algú va poder esborrar 40.000 files amb una ordre, el problema és que el sistema ho permetés sense fricció, no que aquella persona s'equivoqués.
Això no és amabilitat corporativa: és l'única manera d'obtenir informació verdadera. En una cultura on es busquen culpables, la gent amaga els errors, i aleshores els incidents es repeteixen perquè ningú no sap que van passar. La revisió sense culpables és una decisió d'enginyeria.
Revisió posterior a l'incident: esborrat accidental de reserves
Incident: INC-2026-003 · Data: 2026-08-18 · Severitat: Alta Durada de l'impacte: 34 min · Dades perdudes: cap Redactat per: Operacions · Revisat amb: Marta, Luis
1. Què va passar
Durant una neteja rutinària de dades antigues, es va executar a la base de dades de producció un
DELETEla clàusulaWHEREdel qual no era la prevista, i va eliminar 40.218 reserves històriques. Es va detectar 19 minuts després i es van recuperar mitjançant recuperació a un punt en el temps. No hi va haver pèrdua de dades ni interrupció del servei.2. Impacte
Dimensió Impacte Disponibilitat del servei Cap: el web va continuar funcionant Dades perdudes definitivament Cap Dades temporalment inaccessibles 40.218 reserves històriques, 34 min Clients afectats Cap (dades de consulta interna) Pressupost d'error consumit 0 % 3. Línia temporal
Hora Succés 14:30 S'inicia la neteja de dades anteriors a 2024 14:32 S'executa el DELETEamb la condició incorrecta14:51 Un informe retorna xifres anòmales: es detecta 14:53 S'atura tota escriptura addicional. S'obre l'incident 14:58 Es localitza la marca temporal exacta a pg_stat_statements15:02 Comença la restauració PITR a srv-tramontana-proves15:19 Recuperació completada i verificada a proves 15:24 Dades reimportades a producció amb psql -115:26 Verificació final. Incident tancat 4. Causa arrel: els cinc perquès
# Pregunta Resposta 1 Per què es van esborrar 40.218 reserves? Un DELETEambWHEREincorrecte2 Per què es va executar amb la condició incorrecta? Es va escriure a mà, sense SELECTprevi3 Per què es va escriure a mà? No existeix procediment per a neteges de dades 4 Per què no existeix? Mai no s'havia fet una neteja massiva 5 Per què va caldre ara? La taula d'auditoria va créixer sense política de retenció Causa arrel: l'absència d'una política de retenció de dades va obligar a una neteja manual improvisada, sense procediment ni salvaguardes.
I una observació sobre el mètode: els cinc perquès travessen la resposta fàcil —«algú es va equivocar en teclejar»— fins a arribar a una causa sistèmica i accionable. Quedar-se al perquè número 2 hauria produït la conclusió inútil de «cal anar amb més compte».
5. Què va funcionar bé
Aquesta secció és obligatòria i s'oblida sempre:
- La recuperació a un punt en el temps va funcionar exactament com es va assajar (RB-BD-02, 08-02). L'assaig trimestral va demostrar el seu valor al cap de tres dies.
- La restauració en màquina a part va preservar les transaccions posteriors. Restaurar sobre producció hauria perdut 19 minuts de reserves reals.
- La detecció en 19 minuts va ser raonable, encara que millorable.
- Es va avisar immediatament, sense amagar-ho. Això va permetre actuar de pressa.
6. Què es canvia
# Acció Responsable Data Estat 1 Política de retenció d' auditoriaacordada amb la MartaMarta + Ops 25/08 Pendent 2 Manual de neteges: SELECT count(*)obligatori abans de qualsevolDELETEOps 22/08 En curs 3 Rol svc_netejaambstatement_timeouti sense permís deDELETEmassiuOps 25/08 Pendent 4 Purga programada com a temporitzador idempotent i provat, no manual Ops 31/08 Pendent 5 Alerta si una taula perd més del 10 % de files en 5 min Ops 25/08 Pendent 6 psqlde producció amb\set ON_ERROR_ROLLBACK offi avís al promptOps 22/08 Fet Cap acció no és «anar amb més compte». Una acció correctiva que depèn de l'atenció humana no és una acció correctiva: és una expressió de desig.
7. Lliçons
- Els assaigs de recuperació es paguen sols. El de 08-02 es va fer el dia 15; el dia 18 va caldre de debò, i ningú no va haver d'improvisar.
- Una taula que creix sense política de retenció acaba forçant una operació arriscada. El creixement silenciós és un deute que es cobra de cop.
- Detectar en 19 minuts és molt per a un esborrat massiu. L'acció 5 ho baixarà a menys de 5.
Gestió del canvi i finestres de desplegament
La majoria de les interrupcions no vénen de fallades de maquinari: vénen de canvis. I tanmateix el canvi és necessari. Gestionar-lo és posar-hi fricció proporcional al risc, no impedir-lo.
| Tipus de canvi | Exemples | Aprovació | Finestra |
|---|---|---|---|
| Estàndard | Actualitzacions de seguretat, alta de VPN | Cap: preaprovat | Qualsevol moment |
| Normal | Desplegament de versió, canvi de configuració | Revisió d'un company | Finestra acordada |
| Major | Canvi d'esquema, versió major de PostgreSQL | Marta + pla de marxa enrere | Finestra planificada |
| D'emergència | Pedaç de vulnerabilitat crítica | Posterior | Immediata |
La finestra de Tramontana: dimarts i dimecres, de 10:00 a 12:00. I les raons de per què no es desplega el divendres a la tarda, que és la regla més famosa del sector:
- Els problemes apareixen amb càrrega real, hores després. Un divendres a les 18:00, aquella càrrega arriba el dilluns.
- La gent se'n va. Si alguna cosa falla a les 20:00 del divendres, qui ho va desplegar ja no hi és.
- El cap de setmana és quan més reserves es fan a Tramontana: el pitjor moment possible per a una fallada.
- Un incident el divendres s'arrossega el cap de setmana, amb la persona de guàrdia treballant en un canvi que no va fer.
La regla, formulada correctament: no es desplega si no hi ha temps suficient per endavant per detectar i revertir el problema amb l'equip disponible. El dimarts al matí ho compleix; el divendres a la tarda no.
Llista de verificació prèvia a cada desplegament:
| # | Comprovació |
|---|---|
| 1 | Està provat a srv-tramontana-proves? |
| 2 | Hi ha còpia recent i verificada? |
| 3 | Està escrit el pla de marxa enrere? |
| 4 | Les migracions d'esquema són compatibles cap enrere? |
| 5 | Està avisat qui hagi d'estar-ho? |
| 6 | Hi ha silenci d'alertes programat i amb caducitat? |
| 7 | Queda pressupost d'error? |
| 8 | Hi ha temps per endavant per revertir amb calma? |
SLI, SLO i pressupost d'error
Tres conceptes que es confonen i són diferents:
| SLI | SLO | SLA | |
|---|---|---|---|
| Què és | Un indicador mesurat | Un objectiu intern | Un acord contractual |
| Exemple | 99,94 % de peticions correctes | ≥ 99,8 % mensual | 99,5 %, amb penalització |
| Qui el fixa | El mesurament | L'equip amb el negoci | Legal i comercial |
| Si s'incompleix | — | Es canvia el ritme de treball | Hi ha conseqüències econòmiques |
L'SLO es posa sempre més exigent que l'SLA, per tenir marge abans d'incomplir el contracte.
L'SLO de Tramontana
Recollint el 99,8 % proposat a 07-07 i convertint-lo en números concrets:
SLI: proporció de peticions HTTP a
reserves.tramontana.examplerespostes amb codi < 500 i en menys de 2 segons.SLO: ≥ 99,8 % d'aquelles peticions, mesurat en finestra mòbil de 30 dies.
# L SLI, tal com es mesura
(
sum(rate(nginx_http_requests_total{status!~"5.."}[30d]))
- sum(rate(tramontana_request_duration_seconds_count{le="2"}[30d]))
) / sum(rate(nginx_http_requests_total[30d]))El pressupost d'error
Aquí hi ha la idea que canvia la manera de treballar:
Pressupost d'error = 100 % − SLO. És la quantitat de fallada que et pots permetre. No és un residu: és un recurs que es gasta.
| SLO | Pressupost | En 30 dies |
|---|---|---|
| 99,9 % | 0,1 % | 43 min 12 s |
| 99,8 % | 0,2 % | 1 h 26 min 24 s |
| 99,5 % | 0,5 % | 3 h 36 min |
Tramontana pot estar caiguda 1 h 26 min cada 30 dies i continuar complint el seu objectiu. Aquell temps és un pressupost que es pot gastar deliberadament: en desplegaments arriscats, en actualitzacions, en experiments.
I d'aquí surt una regla de gestió que substitueix les discussions eternes entre «cal anar més ràpid» i «cal ser més estables»:
| Pressupost consumit | Decisió |
|---|---|
| < 50 % | Es pot desplegar amb normalitat. Hi ha marge |
| 50-80 % | Desplegaments normals, més compte amb els majors |
| 80-100 % | Només canvis estàndard i correccions. Es congelen les funcions noves |
| Esgotat | Congelació total. Tot l'esforç a estabilitzar |
# Pressupost consumit en els ultims 30 dies, en percentatge
(1 - (
sum(rate(nginx_http_requests_total{status!~"5.."}[30d]))
/ sum(rate(nginx_http_requests_total[30d]))
)) / 0.002 * 100$ curl -s --data-urlencode 'query=(1 - (sum(rate(nginx_http_requests_total{status!~"5.."}[30d])) / sum(rate(nginx_http_requests_total[30d])))) / 0.002 * 100' \
http://127.0.0.1:9090/api/v1/query | jq -r '.data.result[0].value[1]'
31.431,4 % consumit: hi ha marge. Aquell número va al panell principal, i és el que converteix «despleguem?» d'una discussió d'opinions en una consulta de dades.
El més valuós del pressupost d'error, i sol sorprendre: és tan dolent esgotar-lo com no gastar-lo. Un equip que acaba el mes amb el 5 % consumit està sent massa conservador — podria haver desplegat més, lliurat més valor i assumit més risc dins del que s'ha acordat. La fiabilitat excessiva també té un cost: el de les funcions que no es van fer.
Tancament dels deutes pendents
Deute 1: la còpia externa no és append-only
El problema. restic puja les còpies amb un testimoni que també pot esborrar. Un atacant amb accés a srv-tramontana pot executar restic forget --prune i destruir tot l'històric. És exactament la manera d'operar del programari de segrest modern: primer esborra les còpies, després xifra les dades.
# La comprovacio que revela el deute
$ restic forget --keep-last 1 --dry-run
# Si aixo NO dona un error de permisos, el testimoni pot esborrar.La solució: un testimoni de només afegir, més un restic en mode --append-only al costat del repositori.
# 1. Testimoni nou al proveidor, SENSE permis d esborrat
# (Backblaze B2: "Write Only"; S3: politica sense s3:DeleteObject)
$ pass insert tramontana/restic-append-only
# 2. Bloqueig d objectes al proveidor: no es pot esborrar res
# durant 30 dies, ni tan sols amb credencials d administrador
$ b2 update-bucket --defaultRetentionMode compliance \
--defaultRetentionPeriod "30 days" tramontana-copies
# 3. El servidor fa servir el testimoni restringit
$ sudo sed -i 's|^RESTIC_TOKEN=.*|RESTIC_TOKEN_CMD="pass tramontana/restic-append-only"|' \
/etc/tramontana/copia.env
# 4. VERIFICAR que no pot esborrar
$ RESTIC_PASSWORD=$(pass restic/tramontana) restic forget --keep-last 1 --dry-run
Fatal: unable to remove files: AccessDenied
# 5. La purga es fa des d UNA ALTRA MAQUINA amb credencials diferents,
# mensualment, amb un testimoni que si que pot esborrar i que MAI no
# es al servidor copiat.El principi general, aplicable molt més enllà d'això: el sistema copiat no ha de poder destruir les seves còpies. Si pot, no són còpies: són una rèplica amb retard.
| Abans | Després | |
|---|---|---|
| El servidor pot esborrar còpies | Sí | No |
| Supervivent a un programari de segrest | No | Sí, 30 dies |
Supervivent a un rm accidental |
No | Sí |
| Qui purga | El mateix servidor | Una altra màquina, mensualment |
Checklist fila 23: tancada.
Deute 2: l'authorized_keys2 sense explicar
$ ls -la /home/operador/.ssh/
-rw------- 1 operador operador 742 gen 12 2025 authorized_keys
-rw------- 1 operador operador 381 mar 3 2025 authorized_keys2 # <-- ???
$ ssh-keygen -lf /home/operador/.ssh/authorized_keys2
2048 SHA256:Xk9m2pQ7... suport@proveidor-antic (RSA)Què és. authorized_keys2 és un fitxer obsolet d'OpenSSH 2.x, quan hi havia fitxers separats per a claus SSH-1 i SSH-2. Les versions modernes d'OpenSSH l'ignoren completament, tret que AuthorizedKeysFile l'esmenti explícitament.
$ sudo sshd -T | grep -i authorizedkeysfile
authorizedkeysfile .ssh/authorized_keys .ssh/authorized_keys2I aquí hi ha el problema: la configuració per defecte d'Ubuntu sí que l'inclou. Aquella clau RSA de 2048 bits, d'un proveïdor amb qui ja no es treballa, dona accés al servidor avui mateix.
És un exemple perfecte d'un risc real: no és una vulnerabilitat de programari, és un fitxer que ningú no va mirar en divuit mesos.
# 1. Investigar abans d esborrar: s ha fet servir?
$ sudo journalctl -u ssh --since "90 days ago" | \
grep 'Accepted publickey' | grep -o 'SHA256:[A-Za-z0-9+/]*' | sort -u
SHA256:aB3cD4eF5g... # la d operador
# L empremta d authorized_keys2 NO hi apareix: sense us en 90 dies
# 2. Preguntar a la Marta si el proveidor continua tenint relacio
# -> Resposta: contracte acabat el marc de 2025
# 3. Retirar, conservant prova (06-04)
$ sudo cp /home/operador/.ssh/authorized_keys2 \
/var/log/incidents/authorized_keys2.retirat-$(date +%F)
$ sudo shred -u /home/operador/.ssh/authorized_keys2
# 4. I la correccio estructural: que no pugui tornar a existir
$ sudo tee /etc/ssh/sshd_config.d/60-authorized-keys.conf <<'FI'
# Nomes un fitxer de claus autoritzades. authorized_keys2 es una resta
# d OpenSSH 2.x que Ubuntu continua incloent per defecte, i que permet
# que una clau oblidada d un tercer doni acces sense que ningu la vegi.
AuthorizedKeysFile .ssh/authorized_keys
FI
$ sudo sshd -t && sudo systemctl reload ssh
$ sudo sshd -T | grep -i authorizedkeysfile
authorizedkeysfile .ssh/authorized_keysI a Ansible, perquè sigui permanent i verificat:
- name: No permetre authorized_keys2
ansible.builtin.template:
src: 60-authorized-keys.conf.j2
dest: /etc/ssh/sshd_config.d/60-authorized-keys.conf
validate: '/usr/sbin/sshd -t -f %s'
notify: Recarregar ssh
- name: Comprovar que no existeixen fitxers de claus fora de politica
ansible.builtin.find:
paths: /home
patterns: 'authorized_keys2'
recurse: true
hidden: true
register: claus_extra
failed_when: claus_extra.matched > 0La lliçó de mètode: a l'inventari d'accessos, un fitxer que no es pot explicar és un fitxer que es retira. I l'error de fons va ser no tenir mai una revisió periòdica d'accessos — que ara és la fila mensual de la taula de l'apartat 10.
Checklist fila 40: tancada.
Deute 3: no hi ha objectiu formal de disponibilitat
Es tanca formalitzant el que es va proposar a 07-07:
Acord de nivell de servei intern · Tramontana Reserves Acordat el 18/08/2026 entre Operacions i Direcció (Marta Vidal). Revisió: agost de 2027.
Concepte Valor SLI Peticions amb codi < 500 i latència < 2 s SLO 99,8 % en finestra mòbil de 30 dies Pressupost d'error 1 h 26 min cada 30 dies Finestra de manteniment Dimarts i dimecres, 10:00-12:00 RPO 15 minuts RTO 2 hores Horari de guàrdia Laborable 8:00-20:00; crítiques 24×7 Revisió del compliment Mensual, al panell
Checklist fila 33: tancada.
Compliment: RGPD i retenció de dades
Tramontana guarda dades personals de clients: nom, contacte i dates d'estada. Això activa obligacions concretes del Reglament General de Protecció de Dades, i algunes són tècniques i són teves.
| Obligació | Estat a Tramontana |
|---|---|
| Registre d'activitats de tractament (art. 30) | ⚠️ Pendent |
| Mesures tècniques apropiades (art. 32) | ✅ Xifratge en trànsit (08-01) i en repòs (LUKS) |
| Minimització: només les dades necessàries | ⚠️ Revisar amb el Luis |
| Limitació del termini de conservació | ⚠️ auditoria sense política |
| Dret de supressió | ⚠️ Procediment no escrit |
| Dret d'accés i portabilitat | ⚠️ No automatitzat |
| Notificació de bretxes en 72 h | ✅ Detecció (06-04); procediment per escriure |
| Registre d'accessos a dades personals | ✅ auditd (06-04) |
I el punt que connecta directament amb l'incident de l'apartat 12: la taula auditoria ocupa 1.204 MB, més que totes les dades de reserves juntes, i ningú no ha decidit quant de temps es conserven. Això no és només un problema d'espai: conservar dades personals sense termini definit és un incompliment del principi de limitació del termini de conservació.
-- Proposta de retencio, a decidir amb la Marta i amb assessoria legal
-- Reserves: 6 anys (obligacio mercantil de conservacio comptable)
-- Auditoria tecnica: 12 mesos
-- Registres amb IP: 12 mesos
-- Dades de contacte de reserves cancelades: 1 any
CREATE OR REPLACE PROCEDURE app.purgar_retencio() LANGUAGE plpgsql AS $$
BEGIN
DELETE FROM app.auditoria WHERE creat_el < now() - interval '12 months';
UPDATE app.hostes SET telefon = NULL, email = NULL, anonimitzat = true
WHERE id IN (SELECT hoste_id FROM app.reserves
WHERE estat = 'cancelada' AND data < now() - interval '1 year')
AND anonimitzat = false;
RAISE NOTICE 'purga de retencio completada';
END $$;Anonimitzar en lloc d'esborrar conserva la utilitat estadística —quantes reserves hi va haver, de quines cases— sense conservar les dades personals. És la solució tècnicament correcta i la que més es passa per alt.
L'advertiment final, i és important: el RGPD no és un assumpte tècnic, és un assumpte legal amb implicacions tècniques. Res d'aquest apartat no substitueix l'assessoria professional. El que sí que és responsabilitat teva com a administrador és: que existeixin les mesures tècniques, que la retenció sigui aplicable amb una ordre, que hi hagi traçabilitat d'accessos, i que qui decideix —la Marta— tingui la informació per decidir. Un administrador que implementa una purga sense que ningú hagi decidit els terminis està prenent una decisió legal sense autoritat per fer-ho.
Errors Comuns i Consells
- Confondre monitorar amb alertar. Monitora-ho tot; alerta de gairebé res.
- Alertar de causes en lloc de símptomes. «CPU al 90 %» dispara quan corre la còpia. «Més del 5 % d'errors» detecta qualsevol fallada real.
- No podar alertes. Deu notificacions al dia equivalen a cap. La revisió mensual d'alertes és tan important com crear-les.
- Alertes sense acció. Si el receptor no pot fer res, és un panell, no una alerta. Tota alerta porta la seva
accioi el seu enllaç al manual d'operació. - Silencis sense caducitat. Un silenci indefinit és una alerta apagada per sempre.
- No monitorar el monitoratge. Un script de mètriques mort fa que tot sembli correcte indefinidament. La marca de frescor ho resol.
- Mitjanes de latència en lloc de percentils. Una mitjana de 80 ms amaga que l'1 % espera 4 segons.
- Incloure els errors a la latència. Una caiguda total, on tot falla ràpid, apareixeria com una millora de rendiment.
rate()sobre un mesurador. Només té sentit sobre comptadors (_total).- Agregar histogrames sense
by (le). És l'error clàssic de PromQL: els percentils surten sense sentit. - Etiquetes d'alta cardinalitat. Una etiqueta amb l'ID de reserva crea una sèrie per reserva i rebenta la memòria de Prometheus.
- Un panell amb quaranta gràfiques. No es mira. El principal ha de cabre en una pantalla.
- Revisions posteriors a l'incident que busquen culpables. Garanteixen que el següent incident s'amagui.
- Accions correctives del tipus «anar amb més compte». No són accions: són desitjos. Han de canviar el sistema.
- Desplegar el divendres a la tarda. Els problemes apareixen amb càrrega real i aleshores no hi ha ningú.
- Un SLO sense pressupost d'error. L'objectiu sense la seva conseqüència operativa no canvia cap decisió.
- Gastar el 5 % del pressupost d'error. Estàs sent massa conservador: podries haver lliurat més valor.
- Còpies que el mateix servidor pot esborrar. No són còpies: són una rèplica amb retard, i el programari de segrest ho sap.
- No revisar els accessos periòdicament. Un
authorized_keys2d'un proveïdor de fa any i mig dona accés avui. - Guardar dades personals sense termini definit. És un incompliment del RGPD, no només un problema d'espai.
- Consell de mètode. Davant de «va bé el sistema?», si la resposta no és un número, no estàs en producció.
Exercicis
Exercici 1
A les 03:47 es dispara HighErrorRate. Documenta la resposta completa a l'incident —des de la notificació fins al tancament— i redacta la revisió posterior a l'incident resultant.
Exercici 2
Dissenya la revisió mensual d'alertes: què es mesura de cada alerta, amb quins criteris es decideix eliminar-la, ajustar-la o mantenir-la, i aplica el mètode a les alertes configurades en aquesta lliçó.
Exercici 3
La Marta demana un informe d'estat del sistema per a la reunió anual amb els socis: què s'ha fet aquest any, en quina situació està i què es necessita. Redacta'l.
Solucions
Solució 1
03:47 — Notificació.
[CRITICAL] HighErrorRate Summary: Mes del 5 % de les peticions falla Description: Taxa actual: 34,2 %. Accio: journalctl -t nginx_error -n 50; comprovar PostgreSQL Runbook: https://docs.tramontana.example/RB-OPS-02
03:49 — Confirmar que el problema és real, abans de tocar res.
$ curl -sI https://reserves.tramontana.example/cases | head -1
HTTP/2 502
$ curl -s --data-urlencode 'query=sum(rate(nginx_http_requests_total{status=~"5.."}[5m])) / sum(rate(nginx_http_requests_total[5m]))' \
http://127.0.0.1:9090/api/v1/query | jq -r '.data.result[0].value[1]'
0.342Confirmat: un de cada tres usuaris rep un error. No és una falsa alarma.
03:51 — Acotar l'abast amb el panell, no endevinant.
| Panell | Lectura |
|---|---|
| Errors | 502 des de les 03:41 |
| Trànsit | Normal per a l'hora |
| Latència p95 | Va pujar a 8 s a les 03:38, abans dels errors |
| CPU / memòria | Normals |
| Connexions de PgBouncer | cl_waiting = 47 |
tramontana_health_status |
2 (crític) |
La latència va pujar tres minuts abans que els errors, i hi ha 47 clients esperant connexió. El coll d'ampolla és a la base de dades, no a l'aplicació ni a Nginx.
03:53 — El registre de canvis, que és la pregunta obligatòria.
Sense canvis recents. No és un desplegament: és alguna cosa que ha passat sola.
03:55 — Diagnòstic a la base de dades.
$ sudo -u postgres psql -x -c "
SELECT pid, state, wait_event_type, wait_event,
now()-xact_start AS durada, left(query,60) AS consulta
FROM pg_stat_activity WHERE state != 'idle'
ORDER BY xact_start LIMIT 3;"
-[ RECORD 1 ]---+--------------------------------------------
pid | 12844
state | active
wait_event_type | Lock
wait_event | transactionid
durada | 00:19:12
consulta | UPDATE app.disponibles SET estat = 'ocupada'
$ sudo -u postgres psql -c "
SELECT bloquejada.pid AS esperant, bloquejant.pid AS bloquejant,
left(bloquejant_act.query, 50) AS consulta_bloquejant
FROM pg_locks bloquejada
JOIN pg_locks bloquejant ON bloquejant.transactionid = bloquejada.transactionid
AND bloquejant.granted
JOIN pg_stat_activity bloquejant_act ON bloquejant_act.pid = bloquejant.pid
WHERE NOT bloquejada.granted;"
esperant | bloquejant | consulta_bloquejant
----------+------------+--------------------------------------------
12844 | 11902 | VACUUM FULL app.disponiblesCausa arrel trobada en vuit minuts. Un VACUUM FULL sobre app.disponibles té un blocatge ACCESS EXCLUSIVE que impedeix tota lectura i escriptura d'aquella taula. Les peticions s'acumulen esperant, esgoten el conjunt de connexions de PgBouncer, i l'aplicació retorna 502.
$ sudo -u postgres psql -c "SELECT pid, backend_start, application_name
FROM pg_stat_activity WHERE pid = 11902;"
pid | backend_start | application_name
-------+-------------------------------+------------------
11902 | 2026-08-19 03:30:12.441+02 | psqlapplication_name = psql i arrencat a les 03:30: algú el va llançar a mà. La consulta al quadern de guàrdia ho confirma —una nota de la tarda anterior: «disponibles amb 31,7 % de tuples morts, mirar».
03:58 — Mitigació.
# Cancel·lar la consulta (SIGINT). NO pg_terminate_backend, que talla
# la connexio de cop: cancel·lar es mes net i sol bastar.
$ sudo -u postgres psql -c "SELECT pg_cancel_backend(11902);"
pg_cancel_backend
-------------------
t
$ sleep 20 && curl -sI https://reserves.tramontana.example/cases | head -1
HTTP/2 200
$ sudo -u postgres psql -tAc "SELECT count(*) FROM pg_stat_activity
WHERE wait_event_type = 'Lock';"
004:00 — Verificar i tancar.
$ curl -s --data-urlencode 'query=sum(rate(nginx_http_requests_total{status=~"5.."}[5m])) / sum(rate(nginx_http_requests_total[5m]))' \
http://127.0.0.1:9090/api/v1/query | jq -r '.data.result[0].value[1]'
0.0021
$ amtool alert query alertname=HighErrorRate
(sense alertes actives)04:05 — Anotar al quadern mentre està fresc.
**03:47** [INCIDENT] HighErrorRate, 34 % de 502. Causa: VACUUM FULL
manual sobre app.disponibles llançat a les 03:30, blocatge ACCESS
EXCLUSIVE. Mitigat amb pg_cancel_backend a les 03:58. Durada de
l'impacte: 17 min. Revisió posterior el 21/08.Revisió posterior a l'incident: errors 502 per blocatge de VACUUM FULL
Incident: INC-2026-004 · Data: 2026-08-19 · Severitat: Crítica Durada de l'impacte: 17 min (03:41-03:58) · Detecció: 6 min
1. Què va passar
Un
VACUUM FULLexecutat manualment sobreapp.disponiblesva adquirir un blocatge exclusiu que va impedir tota lectura i escriptura de la taula durant 28 minuts. Les peticions es van acumular, es va esgotar el conjunt de connexions i l'aplicació va retornar errors 502 al 34 % de les peticions durant 17 minuts.2. Impacte
Dimensió Valor Durada de l'impacte 17 min Peticions fallides ~2.100 (34 % de 6.200) Reserves perdudes estimades 1-2 (matinada, trànsit baix) Pressupost d'error consumit 19,7 % del mes (17 de 86 min) Dades perdudes Cap 3. Línia temporal
Hora Succés 03:30 Es llança VACUUM FULL app.disponiblesmanualment03:38 El p95 de latència puja a 8 s (símptoma primerenc, sense alerta) 03:41 Primers 502: el conjunt de connexions de PgBouncer s'esgota 03:47 Es dispara HighErrorRatei arriba la notificació03:49 Es confirma el problema 03:51 S'acota amb el panell: la base de dades 03:55 S'identifica el blocatge i el VACUUM FULL03:58 pg_cancel_backend. Servei restablert04:00 Verificat. Incident tancat 4. Causa arrel: els cinc perquès
# Pregunta Resposta 1 Per què hi va haver 502? L'aplicació no va poder consultar la base de dades 2 Per què no va poder? Un blocatge ACCESS EXCLUSIVEsobreapp.disponibles3 Per què hi havia aquell blocatge? Un VACUUM FULLmanual4 Per què es va executar a mà en producció? Es va detectar 31,7 % de tuples morts i es va voler resoldre ràpid 5 Per què hi havia tant d'inflament? autovacuumno està ajustat per a aquella taulaCausa arrel:
autovacuumamb els valors per defecte (llindar del 20 %) no segueix el ritme d'escriptura d'app.disponibles. L'inflament resultant va motivar una intervenció manual amb una eina el blocatge de la qual no es va anticipar.Causa contribuent: no hi havia cap control que impedís executar una operació bloquejant en producció, ni alerta que avisés abans que els usuaris se'n veiessin afectats.
5. Què va funcionar bé
- L'alerta era de símptoma i va funcionar: va detectar una fallada que ningú no havia anticipat. Una alerta de causa sobre
VACUUMno existia i no hauria calgut.- El panell va acotar el problema en dos minuts. Sense ell, el diagnòstic hauria estat a cegues.
- El registre de canvis va descartar un desplegament en trenta segons.
- La mitigació va ser reversible: cancel·lar en lloc d'acabar la connexió.
- El quadern de guàrdia va donar el context —la nota del dia anterior— que va explicar qui i per què.
6. Què es canvia
# Acció Responsable Data Estat 1 Ajustar autovacuumd'app.disponiblesal 2 % (08-02)Ops 19/08 Fet 2 lock_timeout = 5sper defecte a la sessió depsqlde produccióOps 20/08 Fet 3 Instal·lar pg_repacki documentar-lo com **l'**eina per a l'inflamentOps 26/08 Pendent 4 Alerta LongLockWait(> 30 s) — avisa abans del símptomaOps 22/08 Pendent 5 Manual d'operació RB-BD-03: manteniment de base de dades, amb la llista d'operacions bloquejants Ops 26/08 Pendent 6 Panell amb tuples morts per taula, per no descobrir-ho per casualitat Ops 22/08 Pendent 7 Cap operació de manteniment manual en producció fora de finestra Equip 20/08 Acordat # Accio 2: /etc/postgresql/16/main/conf.d/50-proteccio.conf # Si una consulta no aconsegueix un blocatge en 5 s, falla en lloc # d esperar. Una consulta que falla rapid es infinitament preferible # a una que bloqueja el servei sencer. lock_timeout = 5s7. Lliçons
- La latència va pujar 9 minuts abans que els errors. Aquell és el marge que vam perdre per no tenir una alerta primerenca. L'acció 4 el recupera.
- La intenció era bona i el resultat va ser una caiguda. No és una fallada de la persona: és una fallada del sistema, que permetia executar una operació bloquejant en producció sense cap fricció. Les accions 2, 5 i 7 hi afegeixen aquella fricció.
- El 19,7 % del pressupost d'error en un incident de 17 minuts demostra per què l'SLO canvia decisions: amb dos incidents així, el mes entra en zona de precaució i els desplegaments es moderen.
VACUUM FULLno és «un vacuum més gran». Bloqueja la taula sencera. Mereix ser en un manual d'operació amb aquell advertiment en majúscules.
Solució 2
Per què existeix aquesta revisió. Les alertes es creen després de cada incident i gairebé mai no s'eliminen. En un any, un sistema acumula trenta alertes de les quals en serveixen cinc. La revisió mensual és el contrapès.
Les quatre mètriques que es mesuren de cada alerta:
| Mètrica | Com s'obté | Què revela |
|---|---|---|
| Freqüència | Vegades que es va disparar en 30 dies | Soroll o silenci |
| Precisió | (Dispars que van exigir acció) / (dispars totals) | Falsos positius |
| Sensibilitat | Incidents que va detectar / incidents reals | Buits de cobertura |
| Temps de reacció | Minuts fins a la primera acció humana | Si s'atén o s'ignora |
# Frequencia i durada de cada alerta en 30 dies, des de Prometheus
$ curl -s --data-urlencode \
'query=sum by (alertname) (count_over_time(ALERTS{alertstate="firing"}[30d]))' \
http://127.0.0.1:9090/api/v1/query | \
jq -r '.data.result[] | "\(.metric.alertname)\t\(.value[1])"' | sort -k2 -rn
BackupTooOld 412
LatencyDegraded 38
DiskWillFill 4
HighErrorRate 2
CertificateExpiringSoon 0
ServiceDown 0
WALArchivingFailing 0
StaleMetrics 0
ReplicaLagging 0L'arbre de decisió, aplicat a cada alerta:
S ha disparat en 30 dies?
├── NO
│ ├── Hauria detectat un incident real que hi va haver? → MANTENIR
│ ├── Cobreix un risc greu encara que rar? → MANTENIR
│ └── Cap de les dues? → CANDIDATA A ELIMINAR
└── SI
├── Precisio < 50 %? → AJUSTAR llindar o 'for', o ELIMINAR
├── Frequencia > 10/mes? → Es soroll: AJUSTAR
├── Ningu no hi va actuar mai? → NO ES UNA ALERTA: passar a panell
└── Precisio alta i baixa frequencia? → MANTENIRAplicació a les alertes d'aquesta lliçó:
| Alerta | Freq. | Precisió | Reacció | Decisió |
|---|---|---|---|---|
BackupTooOld |
412 | 0,2 % | Ignorada | AJUSTAR: trencada |
LatencyDegraded |
38 | 21 % | 45 min | AJUSTAR: massa sensible |
DiskWillFill |
4 | 100 % | 12 min | MANTENIR |
HighErrorRate |
2 | 100 % | 2 min | MANTENIR: exemplar |
ServiceDown |
0 | — | — | MANTENIR: risc màxim |
WALArchivingFailing |
0 | — | — | MANTENIR: va detectar l'incident del 17/08 |
CertificateExpiringSoon |
0 | — | — | MANTENIR: cost zero, risc alt |
ReplicaLagging |
0 | — | — | MANTENIR |
StaleMetrics |
0 | — | — | MANTENIR: vigila la vigilància |
Anàlisi i correcció de les dues problemàtiques:
BackupTooOld: 412 dispars, 0,2 % de precisió. És el cas de llibre de fatiga d'alertes: es disparava cada 30 minuts, totes les nits, entre l'execució de la còpia i la seva verificació. Quatre-centes dotze notificacions que ningú no llegia, i que a més van entrenar l'equip a ignorar el canal.
# Diagnostic: quan es dispara exactament?
$ curl -s --data-urlencode \
'query=ALERTS{alertname="BackupTooOld",alertstate="firing"}' \
http://127.0.0.1:9090/api/v1/query_range... | jq ...
# -> Totes entre les 02:30 i les 03:10 # ABANS: llindar massa ajustat i sense marge d execucio
# expr: tramontana_backup_age_seconds > 2*4*3600 # 8 h
# for: 30m
# DESPRES: 26 h cobreixen un cicle diari complet mes marge
# d execucio; 'for: 1h' evita el soroll de la finestra de copia.
- alert: BackupTooOld
expr: tramontana_backup_age_seconds > 26*3600
for: 1h
labels: {severity: critical}
annotations:
summary: "Sense copia verificada des de fa {{ $value | humanizeDuration }}"
accio: "Veure manual RB-BD-01"LatencyDegraded: 38 dispars, 21 % de precisió, 45 minuts fins a la reacció. Els 45 minuts són la dada reveladora: l'equip ja l'estava ignorant. Es dispara cada tarda durant l'informe de facturació de la Marta, que és lent i esperat.
# ABANS: p95 > 2 s durant 10 min -> capturava els informes
# DESPRES: dos canvis que la tornen util
# 1. Excloure la ruta d informes, que es lenta per disseny
# 2. Llindar i finestra mes laxos: 3 s durant 15 min
- alert: LatencyDegraded
expr: |
histogram_quantile(0.95, sum(rate(
tramontana_request_duration_seconds_bucket{ruta!~"/informes/.*"}[5m]
)) by (le)) > 3
for: 15m
labels: {severity: warning}I per als informes, que sí que convé vigilar però amb un altre llindar:
- alert: ReportsVerySlow
expr: |
histogram_quantile(0.95, sum(rate(
tramontana_request_duration_seconds_bucket{ruta=~"/informes/.*"}[5m]
)) by (le)) > 15
for: 30m
labels: {severity: warning}Resultat de la revisió:
| Abans | Després | |
|---|---|---|
| Notificacions al mes | 456 | ~8 |
| Precisió mitjana | 2 % | ~85 % |
| Alertes actives | 9 | 10 (una de nova, dues d'ajustades) |
| Temps mitjà de reacció | 45 min | Estimat < 5 min |
Quatre principis que resumeix aquesta revisió:
- Una alerta amb precisió baixa és pitjor que cap, perquè entrena l'equip a ignorar el canal on també arriben les bones.
- El temps de reacció mesura si l'alerta s'atén. Quaranta-cinc minuts signifiquen que s'ignora, i aquella dada és més honesta que qualsevol opinió.
- Una alerta que no es dispara mai no és inútil, si cobreix un risc greu i el seu cost és zero.
ServiceDownno s'ha disparat en un any i ha de continuar sent-hi. - Ajustar abans que eliminar. Les dues problemàtiques cobrien riscos reals; el problema era al llindar, no a la idea.
I una nota final de mètode: aquesta revisió s'anota al quadern de guàrdia, amb l'abans i el després de cada alerta modificada. D'aquí a sis mesos, quan algú es pregunti per què el llindar de BackupTooOld és 26 hores i no 8, la resposta estarà escrita.
Solució 3
Informe anual de sistemes · Tramontana Reserves
Per a: Marta Vidal i socis · De: Operacions de sistemes Exercici: 2026 · Data: 18 d'agost de 2026
Resum executiu
El sistema que sosté Tramontana Reserves ha passat, en l'últim any, de ser una màquina configurada a mà que ningú més que jo sabia reconstruir a una infraestructura documentada, automatitzada, vigilada i amb procediments escrits que una altra persona podria seguir.
Els tres canvis més importants, en una línia cadascun:
- Les dades dels nostres clients ja viatgen xifrades. Fins al juny no ho feien.
- Podem recuperar la base de dades a qualsevol instant concret, i ho hem assajat: 24 minuts.
- Reconstruir el servidor sencer va passar de 8 hores a 50 minuts, i està mesurat, no estimat.
Disponibilitat mesurada en els últims 30 dies: 99,94 %, per damunt de l'objectiu del 99,8 % que proposem formalitzar.
1. On érem i on som
Fa un any Avui Configuració del servidor Al meu cap En codi, versionat Reconstruir després d'un desastre 8 h estimades 50 min mesurats Dades que podríem perdre Fins a 24 h 15 minuts Recuperar un esborrat accidental Impossible sense perdre un dia Al segon anterior S'han provat les còpies? Mai Assaig trimestral documentat Xifratge del trànsit web No Sí Accés remot segur Només des de l'oficina Des de qualsevol lloc, per túnel xifrat Sabem si va bé? «Crec que sí» 99,94 %, en un panell Si alguna cosa falla de matinada Ens n'assabentem al matí Alerta al telèfon Documentació operativa Cap 6 procediments escrits i assajats
2. El que s'ha fet, agrupat
Fiabilitat. Automatització completa de la configuració: el servidor es reconstrueix des de zero amb una ordre, i això ha reduït el temps de recuperació davant d'un desastre de vuit hores a cinquanta minuts. És la millora que més ha augmentat la disponibilitat, i no ha costat un euro en equipament.
Protecció de dades. Còpies automàtiques verificades, xifrades, guardades fora del servidor i —des d'aquest mes— impossibles d'esborrar des del mateix servidor durant 30 dies, que és la defensa concreta contra un atac de segrest de dades. I la capacitat de rebobinar la base de dades a qualsevol instant, assajada.
Seguretat. Tallafocs restrictiu, detecció d'intrusions, auditoria d'accessos, contrasenyes i certificats guardats xifrats, i el servei funcionant amb els mínims permisos possibles. Una avaluació automàtica independent puntua el nostre nivell d'enfortiment en 82 sobre 100, quan la instal·lació per defecte ronda el 60.
Rendiment. Ajust de la base de dades a la màquina real, que ha reduït els accessos a disc un 93 %. I una consulta d'informes que trigava 3,8 segons ara triga 0,18.
Visibilitat. Aquest és el canvi més recent i el que més canvia el dia a dia: un panell que respon en tres segons a «va bé?», amb històric de 30 dies, i alertes que avisen abans que el problema afecti els clients.
3. Incidents de l'any
Data Què va passar Impacte Estat 17/08 El disc de còpies es va omplir i va bloquejar el registre de la base de dades Cap: detectat abans Resolt + alerta nova 18/08 Esborrat accidental de 40.218 reserves històriques Cap: recuperades en 34 min Resolt + 6 mesures 19/08 Errors durant 17 min per una operació de manteniment ~2.100 peticions fallides Resolt + 7 mesures Cap incident no va causar pèrdua de dades. Els tres es van analitzar amb el mateix mètode —què va passar, per què, què es canvia— i van generar quinze millores concretes, totes aplicades o amb data.
Vull destacar el del 18 d'agost: es van recuperar 40.000 reserves sense perdre res, fent servir un procediment que havíem assajat tres dies abans. Aquell assaig, que semblava burocràcia, es va pagar sol en 72 hores.
4. El que proposem acordar
Un objectiu de servei. Fins ara no en teníem cap, i sense ell no es pot decidir quant invertir. Proposem:
Concepte Proposta Què significa a la pràctica Disponibilitat 99,8 % mensual Com a màxim 1 h 26 min de fallada cada 30 dies Dades recuperables 15 minuts En el pitjor cas es perdrien 15 min de reserves Temps de recuperació 2 hores Des d'un desastre total fins al servei restablert Finestra de canvis Dimarts i dimecres al matí Mai divendres a la tarda ni cap de setmana Estem per damunt d'aquell objectiu avui (99,94 %). El proposem així a propòsit: un objectiu que ja es compleix amb folgança permet desplegar millores amb tranquil·litat, i un de massa exigent obligaria a frenar el desenvolupament del producte.
I una idea que vull explicar perquè canvia com decidim. Aquell 0,2 % de marge és un pressupost: el podem «gastar» en canvis i millores. Si un mes el consumim per sota de la meitat, continuem desplegant amb normalitat. Si ens acostem al límit, es congelen les funcions noves i es dedica l'esforç a estabilitzar. Converteix la discussió «anem massa ràpid?» en una consulta a un número. Aquest mes en portem consumit el 31 %.
5. El que fa falta
Prioritat alta, decisió de direcció:
- Política de retenció de dades. Guardem l'històric d'activitat de l'aplicació sense termini definit, i ja ocupa més que totes les reserves juntes. A més de ser un problema d'espai, conservar dades personals sense termini determinat no és defensable davant del Reglament de Protecció de Dades. Necessito que es decideixi quant de temps guardem cada cosa; ho aplico en una setmana. Recomano assessoria legal per fixar els terminis.
- Registre d'activitats de tractament. És un document que el RGPD exigeix i que no tenim. No és feina tècnica, però necessita la meva aportació.
Prioritat mitjana, inversió moderada:
- Segon servidor d'aplicació amb repartiment de càrrega. És la recomanació que vaig analitzar en detall: eliminaria les interrupcions per desplegament i per fallada d'una màquina, que són la majoria. Cost: dues màquines i una setmana de feina, més tres o quatre setmanes de desenvolupament previ amb el Luis. Ens portaria del 99,8 % actual a un marge molt més folgat.
- Rèplica de la base de dades. Justificada sobretot perquè permetria executar els informes pesants sense afectar el web, i de passada actualitzar sense tallar servei.
El que NO recomano, i vull deixar-ho per escrit:
- Arquitectura d'alta disponibilitat completa (cinc màquines, canvi automàtic de servidor). Analitzat amb números: costa diverses vegades més que l'opció 3 i aporta amb prou feines 670 € addicionals d'estalvi anual. I afegiria una complexitat que amb la plantilla actual no podem vigilar bé — un sistema sofisticat mal mantingut és menys fiable que un de senzill ben cuidat.
- Kubernetes, que és la tecnologia de moda. L'he muntat i avaluat a l'entorn de proves: per a la nostra mida consumiria entre dues i quatre setmanes de feina l'any només a mantenir-lo, per resoldre problemes que no tenim.
6. Riscos que assumim conscientment
Prefereixo que estiguin escrits que no pas que es descobreixin el dia dolent:
Risc Conseqüència Per què l'acceptem Una sola màquina d'aplicació Una fallada de maquinari = servei caigut fins a 2 h La proposta 3 ho resoldria Canvi manual de base de dades 5-15 min addicionals en aquell cas Automatitzar-ho exigeix 5 màquines i crearia un risc més gran Una sola persona amb coneixement operatiu Vacances o baixa = resposta lenta Mitigat en part: procediments escrits Dependència d'un proveïdor Una caiguda seva ens afecta sencers Habitual a la nostra escala El tercer és el que més em preocupa a mitjà termini, i és la raó que aquest any hagi dedicat tant d'esforç a escriure procediments: avui, algú amb coneixements tècnics generals podria seguir la majoria d'ells sense haver treballat mai al nostre sistema. Fa un any, no.
7. Conclusió
El sistema està en una situació considerablement millor que fa un any, i —el que més importa— està en una situació que puc demostrar amb dades en lloc d'afirmar amb confiança. Hem passat de «crec que va bé» a «99,94 %, aquí tens el panell».
El que queda pendent són sobretot decisions, no feina tècnica: quant de temps guardem les dades, quin nivell de servei ens comprometem a donar, i si invertim en el segon servidor. Amb aquelles tres decisions preses, tenim un sistema del qual es pot dependre.
Conclusió
El curs acaba on havia d'acabar: no en una eina més, sinó en la capa que converteix un servidor que funciona en un servei del qual es pot dependre. Tens una checklist de quaranta-dues files amb evidència comprovable a cadascuna, i saps exactament quines falten, que és una manera de saber més valuosa que creure que hi són totes. Tens monitoratge amb històric de trenta dies, alertes que es poden atendre perquè són poques i són de símptoma, i un panell que respon en tres segons a l'única pregunta que importa.
Sobretot tens criteri operatiu. Saps que monitorar i alertar no són el mateix i que cal fer molt del primer i molt poc del segon. Saps que una alerta amb precisió del 2 % és pitjor que cap, perquè entrena l'equip a ignorar el canal per on també arriben les bones. Saps que una revisió posterior a l'incident que busca culpables garanteix que el següent s'amagui, i que una acció correctiva del tipus «anar amb més compte» no és una acció sinó un desig. I saps que el pressupost d'error converteix «despleguem?» en una consulta a un número, amb la conclusió que sorprèn: gastar només el 5 % del pressupost significa haver estat massa conservador.
Els tres deutes estan tancats. La còpia externa ja no la pot esborrar el servidor que es copia, perquè un sistema que pot destruir les seves còpies no té còpies. L'authorized_keys2 va resultar ser el que solen ser aquestes coses: no una vulnerabilitat exòtica, sinó un fitxer d'un proveïdor de fa any i mig que continuava donant accés perquè ningú no l'havia mirat — i el que s'ha corregit no és el fitxer, és l'absència de revisió periòdica que va permetre que hi fos. I l'objectiu de disponibilitat existeix per fi, amb el seu número, la seva finestra i el seu pressupost.
I amb això es tanca el curs complet. Vuit mòduls.
Vas començar al Mòdul 1 sense saber què era un nucli, i vas muntar el sistema des d'una imatge d'instal·lació. Al 2 vas deixar de témer el terminal: permisos, inodes, enllaços i man com a primera parada. Al 3 el shell va deixar de ser un intèrpret d'ordres soltes i va passar a ser un llenguatge: canonades, find, awk, processos, senyals. Al 4 vas escriure scripts que no es trenquen —set -euo pipefail, trap, flock, idempotència—, i desplegar.sh amb la seva marxa enrere atòmica continua en producció cinquanta lliçons després. El 5 va ser l'administració de debò: usuaris, paquets amb fixació de versions, LVM, systemd enfortit, journal persistent, i unes còpies amb RPO i RTO acordats en lloc d'improvisats. El 6 va posar la seguretat com a disciplina i no com a llista de trucs: model d'amenaces, tallafocs de llista blanca, AIDE amb la seva base de dades fora, secrets xifrats i aquell certificat TLS que va esperar dos mòduls. El 7 va mirar sota el capó —arrencada, strace, perf, eBPF, el nucli, KVM, contenidors, Ansible— i va acabar amb l'anàlisi honesta de si calia alta disponibilitat. I el 8 ho va aplicar tot a construccions completes, de principi a fi.
El que t'endús d'aquí no és una llista d'ordres. És un mètode: mesurar abans i després, perquè un ajust sense mesurament és superstició. Provar a proves abans que a producció. --dry-run abans d'actuar. Còpia i diff -u abans d'editar. Validar abans de recarregar, sigui nginx -t, promtool check, testparm -s o pg_hba_file_rules. No tanquis mai la porta per la qual estàs entrant. Silenci si tot va bé. I dir sempre què protegeix i què no protegeix una mesura, que és el que separa un informe honest d'un argument de venda.
I una idea que ha aparegut a cada mòdul amb roba diferent: la resposta professional no sempre és que sí. No a l'arquitectura completa d'alta disponibilitat, no a Kubernetes, no a pujar max_connections, no a copiar sis terabytes al núvol. Saber dir que no, amb números i amb alternativa, val tant com saber-ho muntar.
Sobre com continuar. Les certificacions LPIC-1 i LPIC-2 o la RHCSA ordenen i acrediten el que ja saps; la RHCSA és pràctica i exigent, i preparar-la ensenya de debò. Les especialitzacions naturals des d'aquí són quatre: enginyeria de fiabilitat, que és aquesta última lliçó portada a escala; plataforma i núvol, on Kubernetes i la infraestructura com a codi són el dia a dia; ciberseguretat, prolongant el Mòdul 6; i bases de dades, prolongant 08-02. Participa a la comunitat —llistes, fòrums, un informe d'error ben escrit, una correcció de documentació— perquè explicar alguna cosa és la millor manera de descobrir si l'entens. I llegeix els canvis de les versions del nucli i de la teva distribució: és la manera més barata de no quedar-se enrere.
Però si només pots conservar una cosa, que sigui aquesta: mantén el laboratori. Aquella VM de proves, o el servidor multimèdia de 08-03, o el clúster de k3s que vas decidir no portar a producció. Un lloc propi on trencar coses a propòsit, provocar les fallades en horari laboral en lloc d'esperar-les, i provar el que encara no saps. Tot el que has après en aquest curs ho has après fent-ho, i aquella és l'única manera que hi ha. La diferència entre qui llegeix sobre sistemes i qui els administra no és el que ha estudiat: és el que ha trencat i ha tornat a aixecar. Encén la màquina i continua.
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
