Cincuenta y cinco lecciones. srv-tramontana está instalado, endurecido, monitorizable, respaldado, automatizado, cifrado en tránsito y con una base de datos que ya no corre con la configuración de fábrica. Sabes reconstruirlo en cincuenta minutos, recuperar la base de datos a un segundo concreto, diagnosticar una latencia con eBPF y decidir con números si hace falta alta disponibilidad.
Y aun así falta algo, porque «funciona» y «está en producción» no son lo mismo. Un sistema en producción es aquel del que otras personas dependen sin saber que existe: tiene un dueño, un objetivo de servicio acordado, alguien que lo mira cada mañana, alertas que se atienden, un procedimiento cuando se rompe y un registro de por qué está como está. Nada de eso es software; todo eso es operación.
Esta lección cierra el curso poniendo esa capa. La checklist completa con su evidencia, la monitorización con Prometheus y Grafana que quedó aplazada desde 05-07, alertas que se puedan atender sin odiarlas, la rutina diaria, la revisión post-incidente sin culpables, y el presupuesto de error que convierte el 99,8 % de 07-07 en una decisión ejecutable. Y las tres deudas que siguen abiertas desde el Módulo 5, que se cierran hoy.
Contenido
- Qué significa estar en producción
- La checklist de puesta en producción
- Monitorizar y alertar: no son lo mismo
- Prometheus y node_exporter
- PromQL de lo imprescindible
- Métricas propias: las que ya tienes sin saberlo
- Grafana y el panel mínimo
- Alertas que se pueden atender
- Registros centralizados: la deuda de 05-06
- Operación diaria, semanal y mensual
- El cuaderno de guardia y el registro de cambios
- La revisión post-incidente sin culpables
- Gestión del cambio y ventanas de despliegue
- SLI, SLO y presupuesto de error
- Cierre de las deudas pendientes
- Cumplimiento: RGPD y retención de datos
- Cierre del curso
Qué significa estar en producción
| «Funciona en mi máquina» | En producción | |
|---|---|---|
| Quién depende de ello | Tú | Personas que no saben que existe |
| Cuándo tiene que funcionar | Cuando lo miras | Siempre |
| Si se rompe de madrugada | Se arregla mañana | Alguien se entera y actúa |
| Configuración | En tu cabeza | En código, versionada |
| Cambios | Cuando apetece | Con ventana y aprobación |
| Datos | Reproducibles | Irreemplazables |
| Objetivo de disponibilidad | Ninguno | Acordado y medido |
| Cuando alguien pregunta «¿va bien?» | «Creo que sí» | Un número |
La última fila es la que resume la lección. La diferencia entre un sistema aficionado y uno profesional no es la tecnología: es que ante «¿va bien?» hay un dato, no una impresión.
Y hay una prueba práctica, más útil que cualquier definición:
¿Podrías irte dos semanas de vacaciones sin ordenador?
Para responder que sí hacen falta seis cosas: que el sistema se autorrepare en los fallos previstos, que alguien reciba las alertas, que ese alguien tenga procedimientos escritos que pueda seguir sin ser tú, que las copias se verifiquen solas, que haya un camino de escalado, y que exista un registro de qué se ha cambiado y por qué. Esta lección construye las seis.
La checklist de puesta en producción
Cada fila lleva evidencia comprobable: un comando que devuelve un resultado, no una casilla de conciencia. Sin evidencia, una checklist es una lista de buenas intenciones.
Infraestructura
| # | Requisito | Evidencia | Estado |
|---|---|---|---|
| 1 | Sistema operativo con soporte a largo plazo | lsb_release -d → Ubuntu 24.04 LTS |
✅ 01-04 |
| 2 | Configuración reproducible en código | ansible-playbook --check --diff sin cambios |
✅ 07-06 |
| 3 | Reconstrucción completa medida | Registro del último ensayo: 50 min | ✅ 07-06 |
| 4 | Servicios gestionados por systemd, no scripts sueltos | systemctl list-units --failed vacío |
✅ 05-05 |
| 5 | Arranque verificado tras reinicio | systemd-analyze critical-chain sin fallos |
✅ 07-01 |
| 6 | Recursos con margen medido | revision_salud.sh → 0 |
✅ 05-07 |
| 7 | Entorno de pruebas equivalente | srv-tramontana-pruebas operativo |
✅ 07-04 |
Seguridad
| # | Requisito | Evidencia | Estado |
|---|---|---|---|
| 8 | Cortafuegos de lista blanca | ufw status verbose |
✅ 06-03 |
| 9 | SSH sin contraseña, sin root, con fail2ban |
sshd -T | grep -E 'permitroot|passwordauth' |
✅ 06-02 |
| 10 | Cifrado en tránsito activo | curl -sI https://... | grep strict-transport |
✅ 08-01 |
| 11 | Certificado válido y renovándose solo | certbot certificates; revisar_certificado.sh |
✅ 06-05 |
| 12 | Secretos fuera del código y cifrados | pass ls; systemd-creds list |
✅ 06-05 |
| 13 | Servicio con mínimo privilegio | systemd-analyze security tramontana → 1.6 |
✅ 05-05 |
| 14 | Confinamiento obligatorio activo | aa-status | grep tramontana (enforce) |
✅ 06-06 |
| 15 | Detección de cambios en ficheros | aide --check; BD fuera del servidor |
✅ 06-04 |
| 16 | Auditoría de eventos sensibles | auditctl -l con reglas cargadas |
✅ 06-04 |
| 17 | Actualizaciones de seguridad al día | apt list --upgradable | grep -c security → 0 |
✅ 06-06 |
| 18 | Revisión de exposición externa | Índice de Lynis: 82 | ✅ 06-06 |
| 19 | Acceso remoto sin exponer servicios | wg show con pares activos |
✅ 08-04 |
Datos
| # | Requisito | Evidencia | Estado |
|---|---|---|---|
| 20 | RPO y RTO acordados por escrito | RPO 15 min, RTO 2 h | ✅ 07-06, 08-02 |
| 21 | Copias automáticas y verificadas | comprobar_copia.sh → 0; restic check |
✅ 05-08 |
| 22 | Copia fuera del servidor (regla 3-2-1) | restic snapshots en repositorio remoto |
✅ 05-08 |
| 23 | Copia externa inmutable (append-only) |
Token de solo añadir | ⚠️ Se cierra hoy |
| 24 | Restauración ensayada con tiempo medido | Runbook RB-BD-02: 24 min | ✅ 08-02 |
| 25 | Recuperación a un punto en el tiempo | Archivado de WAL: failed_count = 0 |
✅ 08-02 |
| 26 | Cifrado en reposo de la copia | LUKS + cifrado de restic |
✅ 05-04, 06-05 |
| 27 | Política de retención acorde con RGPD | Definida y aplicada | ⚠️ Pendiente: auditoria |
Observabilidad
| # | Requisito | Evidencia | Estado |
|---|---|---|---|
| 28 | Registros persistentes y rotados | journalctl --disk-usage; logrotate -d |
✅ 05-06 |
| 29 | Monitorización continua con histórico | up{job="tramontana"} = 1 |
⚠️ Se cierra hoy |
| 30 | Alertas que llegan a una persona | Ruta de Alertmanager probada | ⚠️ Se cierra hoy |
| 31 | Panel con los cuatro golden signals | Grafana operativo | ⚠️ Se cierra hoy |
| 32 | Extremo de salud significativo | curl -s /salud | jq .estado |
✅ 07-07 |
Operación
| # | Requisito | Evidencia | Estado |
|---|---|---|---|
| 33 | Objetivo de disponibilidad formal | SLO acordado con Marta | ⚠️ Se cierra hoy |
| 34 | Runbooks fuera del servidor | Archivador + repositorio | ✅ 05-08, 08-02 |
| 35 | Rutina diaria definida y ejecutada | Tabla del apartado 10 | ⚠️ Se cierra hoy |
| 36 | Registro de cambios | git log de ~/tramontana-infra |
✅ 07-06 |
| 37 | Despliegue reversible y probado | desplegar.sh con rollback |
✅ 04-07 |
| 38 | Escalado definido: quién y cuándo | Apartado 8 | ⚠️ Se cierra hoy |
Documentación y cumplimiento
| # | Requisito | Evidencia | Estado |
|---|---|---|---|
| 39 | Inventario de sistemas y responsables | docs/inventario.md |
⚠️ Pendiente |
| 40 | Todos los accesos justificados | authorized_keys revisado |
⚠️ authorized_keys2: hoy |
| 41 | Registro de actividades de tratamiento | Documento de RGPD | ⚠️ Pendiente |
| 42 | Diagrama de arquitectura actualizado | docs/arquitectura.md |
✅ 07-07 |
Resumen: 30 filas cumplidas de 42. Nueve se cierran en esta lección; tres quedan como trabajo planificado con fecha. Esa cifra es en sí misma la evidencia número 43: saber exactamente qué falta.
Monitorizar y alertar: no son lo mismo
Es la distinción que evita el error más caro de esta área.
| Monitorizar | Alertar | |
|---|---|---|
| Pregunta que responde | «¿Qué está pasando y qué pasó?» | «¿Alguien tiene que hacer algo ahora?» |
| Cuándo se consulta | Cuando alguien mira | Te busca a ti |
| Volumen adecuado | Todo lo que se pueda medir | Muy poco |
| Coste de un exceso | Disco | Que se ignoren las importantes |
| Herramienta | Prometheus, Grafana | Alertmanager |
Monitoriza todo lo que puedas; alerta de casi nada. Es contraintuitivo y es la única forma de que las alertas sirvan. Un sistema que envía cuarenta notificaciones al día es un sistema sin alertas, porque nadie las lee.
Los cuatro golden signals
De 05-07, ahora con instrumentación real:
| Señal | Qué mide | En Tramontana |
|---|---|---|
| Latencia | Cuánto tarda una petición | $upstream_response_time de 08-01 |
| Tráfico | Cuánta demanda hay | Peticiones por segundo |
| Errores | Qué proporción falla | Respuestas 5xx |
| Saturación | Cuán lleno está el sistema | CPU, memoria, conexiones de PgBouncer |
Dos matices que hacen la diferencia entre medir y medir bien:
La latencia se mide en percentiles, nunca en media. Una media de 80 ms puede esconder que el 1 % de los usuarios espera 4 segundos. Y hay que separar la latencia de las peticiones correctas de la de las erróneas: un error 500 devuelto en 2 ms mejora la media y empeora el servicio.
La latencia de los errores se excluye del SLI. Si no, una caída total —donde todo falla rápido— aparecería como una mejora de rendimiento.
Y el método USE, complementario, para cada recurso: Utilización, Saturación y Errores.
| Golden signals | Método USE | |
|---|---|---|
| Punto de vista | Del usuario | De los recursos |
| Responde a | «¿Está bien el servicio?» | «¿Qué recurso es el cuello de botella?» |
| Cuándo se usa | Alertar | Diagnosticar |
Prometheus y node_exporter
Prometheus es una base de datos de series temporales que recolecta métricas: en lugar de que los servicios le envíen datos, él los pide periódicamente a extremos HTTP.
| Recolección (Prometheus) | Envío (StatsD, Graphite) | |
|---|---|---|
| Quién inicia | El servidor | El cliente |
| Detectar que un servicio ha muerto | Trivial: up == 0 |
Difícil: ausencia de datos |
| Configuración | Centralizada | En cada cliente |
| Objetivos efímeros | Necesita descubrimiento | Natural |
Esa segunda fila es una ventaja enorme: con recolección, un servicio caído produce inmediatamente up == 0, que es una señal explícita.
$ sudo apt install prometheus prometheus-node-exporter
$ prometheus --version
prometheus, version 2.48.1 (branch: HEAD)Las unidades del paquete son razonables pero no cumplen el estándar del curso. Drop-ins, como siempre:
# /etc/systemd/system/prometheus-node-exporter.service.d/override.conf
[Service]
# Escuchar SOLO en localhost: Prometheus corre en la misma maquina.
# Sin esto, cualquiera en 10.0.2.0/24 lee metricas que revelan
# version del kernel, sistemas de ficheros y procesos.
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 cuánto se piden las métricas
evaluation_interval: 15s # cada cuánto se evalúan las reglas
external_labels:
entorno: produccion
servidor: srv-tramontana
rule_files:
- /etc/prometheus/reglas/*.yml
alerting:
alertmanagers:
- static_configs:
- targets: ['127.0.0.1:9093']
scrape_configs:
# El propio Prometheus: si él falla, hay que saberlo
- job_name: prometheus
static_configs:
- targets: ['127.0.0.1:9090']
# Métricas del sistema: CPU, memoria, disco, red, systemd
- job_name: node
static_configs:
- targets: ['127.0.0.1:9100']
labels: {instancia: srv-tramontana}
# La aplicación (requiere que Luis exponga /metrics)
- job_name: tramontana
metrics_path: /metrics
static_configs:
- targets: ['127.0.0.1:8080']
# Descartar métricas de alta cardinalidad: una etiqueta con el ID
# de reserva crearía una serie por reserva y reventaría la memoria.
metric_relabel_configs:
- source_labels: [__name__]
regex: 'tramontana_reserva_detalle.*'
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 es el nginx -t de Prometheus, y se usa igual: siempre antes de recargar.
Sobre la retención y el disco, que es la pregunta que siempre surge:
$ sudo du -sh /var/lib/prometheus/metrics2
412M /var/lib/prometheus/metrics2
# Estimacion: bytes ≈ series x (tiempo / intervalo) x ~2 bytes por muestra
# 4.000 series x (30 dias / 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"Treinta días bastan para investigar incidentes y ver tendencias mensuales. Para histórico de años existen Thanos o Mimir, y para Tramontana son innecesarios.
PromQL de lo imprescindible
PromQL asusta al principio y se reduce a cinco patrones que cubren el 90 % de los casos.
1. up: lo más importante y lo más simple.
2. rate() sobre contadores. Un contador solo sube. Su valor absoluto no dice nada; lo que importa es a qué ritmo crece.
rate() maneja correctamente los reinicios: si el contador vuelve a cero porque el proceso se reinició, lo detecta y no produce un valor negativo absurdo.
Regla:
rate()solo sobre contadores (_total). Para medidores —memoria, temperatura, conexiones— se usa el valor directamente.
3. Agregaciones y proporciones.
# Proporcion de errores 5xx sobre el total: la SEÑAL DE ERRORES
sum(rate(nginx_http_requests_total{status=~"5.."}[5m]))
/
sum(rate(nginx_http_requests_total[5m]))
# Uso de CPU: 'idle' es lo que sobra, asi que 1 menos eso
1 - avg(rate(node_cpu_seconds_total{mode="idle"}[5m]))
# Porcentaje de memoria disponible
node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes * 1004. histogram_quantile(): los percentiles de latencia.
# p95 de latencia, en segundos
histogram_quantile(0.95,
sum(rate(tramontana_duracion_peticion_seconds_bucket[5m])) by (le))La etiqueta le (less or equal) define los cubos del histograma y debe conservarse en el by. Es el error más común de PromQL: agregar sin by (le) y obtener resultados sin sentido.
5. Predicción, que es lo que convierte una alerta en útil.
# ¿A este ritmo, se llenará el disco en las próximas 4 horas?
predict_linear(node_filesystem_avail_bytes{mountpoint="/"}[6h], 4*3600) < 0Alertar de «disco al 90 %» es alertar tarde o alertar en vano —un disco estable al 91 % no es un problema—. Alertar de «a este ritmo se llena en cuatro horas» es accionable, que es la propiedad que define una buena alerta.
Métricas propias: las que ya tienes sin saberlo
Los node_exporter y compañía dan métricas técnicas. Las que de verdad importan son las que responden a preguntas del negocio, y la mayoría ya las estás calculando en los scripts del curso — solo hay que exponerlas.
El mecanismo es el recolector de ficheros de texto: cualquier script deja un .prom en un directorio y node_exporter lo publica.
#!/usr/bin/env bash
#
# metricas_tramontana.sh - Expone metricas propias a Prometheus
#
# Recoge lo que los scripts del curso ya calculan y lo publica en
# formato de exposicion de Prometheus. Se ejecuta cada 5 min.
#
set -euo pipefail
readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/lib/comunes.sh"
readonly SALIDA=/var/lib/node_exporter/textfile/tramontana.prom
readonly DOMINIO=reservas.tramontana.example
umask 022 # node_exporter debe poder LEERLO
main() {
# Escritura ATOMICA: si el script muere a medias, node_exporter
# leeria un fichero truncado y descartaria todas las metricas.
local tmp; tmp="$(mktemp "${SALIDA}.XXXXXX")"
trap 'rm -f "$tmp"' EXIT
{
# --- 1. Edad de la ultima copia correcta (05-08) ---
# La metrica de copias mas util NO es "¿se ejecuto el timer?"
# sino "¿cuanto hace que hay una copia VERIFICADA?".
echo '# HELP tramontana_copia_edad_segundos Segundos desde la ultima copia verificada'
echo '# TYPE tramontana_copia_edad_segundos gauge'
if [[ -f /var/lib/tramontana/ultima-copia-correcta ]]; then
printf 'tramontana_copia_edad_segundos %d\n' \
$(( $(date +%s) - $(stat -c %Y /var/lib/tramontana/ultima-copia-correcta) ))
else
printf 'tramontana_copia_edad_segundos %d\n' 999999
fi
# --- 2. Dias hasta la caducidad del certificado (06-05) ---
echo '# HELP tramontana_certificado_dias Dias hasta la caducidad del certificado TLS'
echo '# TYPE tramontana_certificado_dias gauge'
local fin dias
if fin="$(echo | timeout 10 openssl s_client -connect "${DOMINIO}:443" \
-servername "$DOMINIO" 2>/dev/null | \
openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)"; then
dias=$(( ( $(date -d "$fin" +%s) - $(date +%s) ) / 86400 ))
printf 'tramontana_certificado_dias %d\n' "$dias"
fi
# --- 3. Indice de Lynis (06-06) ---
echo '# HELP tramontana_lynis_indice Indice de endurecimiento de Lynis'
echo '# TYPE tramontana_lynis_indice gauge'
if [[ -f /var/log/lynis-report.dat ]]; then
printf 'tramontana_lynis_indice %s\n' \
"$(awk -F= '/^hardening_index=/{print $2}' /var/log/lynis-report.dat)"
fi
# --- 4. Retraso de la replica de PostgreSQL (08-02) ---
# Cuantos bytes se perderian si el primario cayera AHORA.
echo '# HELP tramontana_replica_retraso_bytes Retraso de la replica en bytes'
echo '# TYPE tramontana_replica_retraso_bytes gauge'
printf 'tramontana_replica_retraso_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. Fallos de archivado de WAL (08-02) ---
echo '# HELP tramontana_wal_archivado_fallos Fallos acumulados de archivado de WAL'
echo '# TYPE tramontana_wal_archivado_fallos gauge'
printf 'tramontana_wal_archivado_fallos %s\n' \
"$(sudo -u postgres psql -tAc \
"SELECT failed_count FROM pg_stat_archiver" 2>/dev/null || echo 0)"
# --- 6. Estado de revision_salud.sh (0/1/2) ---
echo '# HELP tramontana_salud_estado 0 correcto, 1 aviso, 2 critico'
echo '# TYPE tramontana_salud_estado gauge'
local estado=0
"${SCRIPT_DIR}/revision_salud.sh" >/dev/null 2>&1 || estado=$?
printf 'tramontana_salud_estado %d\n' "$estado"
# --- 7. Metricas de NEGOCIO: las que le importan a Marta ---
echo '# HELP tramontana_reservas_total Reservas registradas hoy'
echo '# TYPE tramontana_reservas_total gauge'
printf 'tramontana_reservas_total %s\n' \
"$(sudo -u postgres psql -tAc \
"SELECT count(*) FROM app.reservas WHERE fecha = CURRENT_DATE" \
-d tramontana 2>/dev/null || echo 0)"
# --- 8. Marca de frescura: detecta que ESTE script ha muerto ---
echo '# HELP tramontana_metricas_generadas_segundos Marca temporal de generacion'
echo '# TYPE tramontana_metricas_generadas_segundos gauge'
printf 'tramontana_metricas_generadas_segundos %d\n' "$(date +%s)"
} > "$tmp"
chmod 0644 "$tmp"
mv "$tmp" "$SALIDA" # atomico
trap - EXIT
}
main "$@"$ sudo ~/scripts/metricas_tramontana.sh
$ curl -s http://127.0.0.1:9100/metrics | grep '^tramontana_'
tramontana_copia_edad_segundos 21840
tramontana_certificado_dias 71
tramontana_lynis_indice 82
tramontana_replica_retraso_bytes 0
tramontana_wal_archivado_fallos 0
tramontana_salud_estado 0
tramontana_reservas_total 14
tramontana_metricas_generadas_segundos 1755518402Cuatro decisiones de diseño del script:
- Escritura atómica con
mktempymv.node_exporterpuede leer el fichero en cualquier momento; uno truncado hace que descarte todo su contenido. umask 022, en contra de la convención general del curso:node_exportercorre como otro usuario y necesita leerlo. Es una excepción justificada, y por eso está comentada.- La métrica 8, la marca de frescura, es la que hace fiables a las otras siete. Sin ella, si este script deja de ejecutarse, Prometheus seguiría publicando los últimos valores conocidos indefinidamente y todo parecería correcto para siempre. Con ella se puede alertar de que las métricas están rancias.
- La métrica de negocio,
tramontana_reservas_total, es la que a Marta le importa y la que detecta el fallo más peligroso: el que no rompe nada. Si el sistema responde 200 a todo pero nadie consigue reservar, ninguna métrica técnica lo revelará.
Y para la aplicación, lo que Luis debe exponer en /metrics:
# HELP tramontana_peticiones_total Peticiones HTTP atendidas
# TYPE tramontana_peticiones_total counter
tramontana_peticiones_total{metodo="GET",ruta="/casas",codigo="200"} 41822
# HELP tramontana_duracion_peticion_seconds Duracion de las peticiones
# TYPE tramontana_duracion_peticion_seconds histogram
tramontana_duracion_peticion_seconds_bucket{le="0.05"} 38120
tramontana_duracion_peticion_seconds_bucket{le="0.1"} 40911
tramontana_duracion_peticion_seconds_bucket{le="0.5"} 41780
tramontana_duracion_peticion_seconds_bucket{le="+Inf"} 41822
tramontana_duracion_peticion_seconds_sum 1284.41
tramontana_duracion_peticion_seconds_count 41822
# HELP tramontana_bd_conexiones_activas Conexiones a la base de datos en uso
# TYPE tramontana_bd_conexiones_activas gauge
tramontana_bd_conexiones_activas 12Grafana y el panel mínimo
$ 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 (fragmento)
[server]
http_addr = 127.0.0.1 # NO expuesto: se llega por Nginx o VPN
http_port = 3000
root_url = https://panel.tramontana.example/
[security]
admin_user = operador
# La contrasena se inyecta con systemd-creds (06-05), no aqui
disable_gravatar = true
cookie_secure = true
cookie_samesite = strict
content_security_policy = true
[users]
allow_sign_up = falseY el acceso, aprovechando lo montado: por la VPN de 08-04, sin publicar nada nuevo.
El panel mínimo, seis paneles y ni uno más:
| Panel | Consulta | Por qué |
|---|---|---|
| Disponibilidad (30 d) | avg_over_time(up{job="tramontana"}[30d]) * 100 |
El número que Marta pregunta |
| Latencia p50/p95/p99 | histogram_quantile(...) |
Golden signal: latencia |
| Peticiones por segundo | sum(rate(nginx_http_requests_total[5m])) |
Golden signal: tráfico |
| Proporción de errores | 5xx sobre total | Golden signal: errores |
| Saturación | CPU, memoria, disco, conexiones | Golden signal: saturación |
| Estado del sistema | Edad de copia, días de certificado, retraso de réplica, Lynis | Lo que se mira cada mañana |
Un consejo que contradice el instinto: un panel con cuarenta gráficas no se mira. El panel principal debe caber en una pantalla y responder en tres segundos a «¿va bien?». El detalle vive en paneles secundarios a los que se llega cuando hace falta.
Alertas que se pueden atender
La regla
Toda alerta debe requerir una acción humana inmediata. Si el receptor no puede hacer nada, o puede esperar a mañana, no es una alerta: es un panel o un informe.
La fatiga de alertas es el modo de fallo característico de la monitorización, y su progresión es siempre la misma:
- Se configuran alertas para todo, «por si acaso».
- Llegan diez al día, casi todas irrelevantes.
- La gente empieza a ignorarlas.
- Se crea un filtro de correo que las archiva.
- Llega la alerta importante y nadie la ve.
El sistema queda peor que sin alertas, porque hay una falsa sensación de vigilancia. Y el remedio es contraintuitivo: borrar alertas.
Síntoma frente a causa
| Alerta de síntoma | Alerta de causa | |
|---|---|---|
| Qué detecta | Que el servicio no funciona | Que un componente falla |
| Ejemplo | «La tasa de errores supera el 5 %» | «La CPU está al 90 %» |
| Falsos positivos | Pocos | Muchos: la CPU al 90 % puede ser normal |
| Cobertura | Detecta fallos imprevistos | Solo los previstos |
| Ayuda a diagnosticar | Poco | Mucho |
Regla: alerta de síntomas, diagnostica con causas. Un for: 5m sobre la tasa de errores detecta cualquier fallo que afecte al usuario, incluidos los que nadie anticipó. Una alerta de CPU al 90 % dispara cuando se ejecuta la copia nocturna y el servicio va perfectamente.
Con dos excepciones legítimas, que son alertas de causa y deben existir porque su síntoma llega demasiado tarde:
- El disco se va a llenar. Cuando el síntoma aparece, el servicio ya está caído.
- El certificado va a caducar. Cuando el síntoma aparece, la web ya no carga.
Ambas comparten la propiedad clave: avisan con horas o días de antelación y la acción es evidente.
# /etc/prometheus/reglas/tramontana.yml
groups:
- name: sintomas
interval: 30s
rules:
# --- CRITICAS: despiertan a alguien de madrugada ---
- alert: ServicioCaido
expr: up{job="tramontana"} == 0
for: 2m # 2 min evita el ruido de un reinicio
labels: {severidad: critica, equipo: operaciones}
annotations:
resumen: "Tramontana Reservas no responde"
descripcion: "El objetivo {{ $labels.instance }} lleva 2 min sin responder."
accion: "Ver runbook RB-OPS-01. systemctl status tramontana; journalctl -u tramontana -n50"
runbook: "https://docs.tramontana.example/RB-OPS-01"
- alert: TasaErroresAlta
expr: |
sum(rate(nginx_http_requests_total{status=~"5.."}[5m]))
/ sum(rate(nginx_http_requests_total[5m])) > 0.05
for: 5m
labels: {severidad: critica, equipo: operaciones}
annotations:
resumen: "Más del 5 % de las peticiones falla"
descripcion: "Tasa actual: {{ $value | humanizePercentage }}."
accion: "journalctl -t nginx_error -n 50; comprobar PostgreSQL"
- alert: LatenciaDegradada
expr: |
histogram_quantile(0.95,
sum(rate(tramontana_duracion_peticion_seconds_bucket[5m])) by (le)) > 2
for: 10m
labels: {severidad: aviso, equipo: operaciones}
annotations:
resumen: "p95 de latencia por encima de 2 s"
accion: "pg_stat_statements por total_exec_time; diagnostico_latencia.sh"
- name: causas_que_avisan_con_tiempo
rules:
# --- Las dos excepciones justificadas ---
- alert: DiscoSeLlenara
expr: predict_linear(node_filesystem_avail_bytes{mountpoint="/"}[6h], 4*3600) < 0
for: 15m
labels: {severidad: critica, equipo: operaciones}
annotations:
resumen: "/ se llenará en menos de 4 horas al ritmo actual"
accion: "du -sh /var/log/* /srv/*; revisar rotacion y purga"
- alert: CertificadoCaduca
expr: tramontana_certificado_dias < 20
for: 1h
labels: {severidad: aviso, equipo: operaciones}
annotations:
resumen: "El certificado caduca en {{ $value }} días"
accion: "certbot renew --dry-run; revisar certbot.timer"
- name: datos
rules:
- alert: CopiaAntigua
# Umbral en 2x el RPO acordado: no alerta por un retraso normal
expr: tramontana_copia_edad_segundos > 2*4*3600
for: 30m
labels: {severidad: critica, equipo: operaciones}
annotations:
resumen: "Sin copia verificada desde hace {{ $value | humanizeDuration }}"
accion: "Ver runbook RB-BD-01. journalctl -u tramontana-respaldo"
- alert: ArchivadoWALFallando
# Alerta de causa deliberada: el sintoma (PostgreSQL detenido)
# aparece horas despues y para entonces no hay salida facil.
expr: tramontana_wal_archivado_fallos > 0
for: 5m
labels: {severidad: critica, equipo: operaciones}
annotations:
resumen: "El archivado de WAL está fallando"
accion: "df -h /srv/tramontana/backups; ver incidente del 2026-08-18"
- alert: ReplicaRetrasada
expr: tramontana_replica_retraso_bytes > 100*1024*1024
for: 10m
labels: {severidad: aviso, equipo: operaciones}
annotations:
resumen: "La réplica va {{ $value | humanize1024 }}B por detrás"
- name: metamonitorizacion
rules:
# La alerta que vigila a la monitorizacion. Sin ella, un script
# de metricas muerto hace que TODO parezca correcto para siempre.
- alert: MetricasRancias
expr: time() - tramontana_metricas_generadas_segundos > 1800
for: 5m
labels: {severidad: aviso, equipo: operaciones}
annotations:
resumen: "Las métricas propias llevan más de 30 min sin actualizarse"
accion: "systemctl status metricas-tramontana.timer"$ sudo promtool check rules /etc/prometheus/reglas/tramontana.yml
SUCCESS: 10 rules found
# Probar una regla SIN esperar a que ocurra
$ sudo promtool test rules /etc/prometheus/pruebas/reglas_test.yml
Unit Testing: SUCCESSAlertmanager: rutas y severidades
# /etc/alertmanager/alertmanager.yml
global:
resolve_timeout: 5m
route:
group_by: ['alertname', 'severidad']
group_wait: 30s # esperar 30 s por si llegan alertas relacionadas
group_interval: 5m
repeat_interval: 4h # recordatorio cada 4 h si sigue activa
receiver: correo-operaciones
routes:
# Críticas: correo y, fuera de horario, teléfono
- matchers: [severidad="critica"]
receiver: guardia
group_wait: 10s
repeat_interval: 1h
# Avisos: solo correo, sin prisa
- matchers: [severidad="aviso"]
receiver: correo-operaciones
repeat_interval: 12h
# Silenciar alertas derivadas cuando la causa raíz ya alertó
inhibit_rules:
- source_matchers: [alertname="ServicioCaido"]
target_matchers: [severidad=~"aviso|critica"]
equal: [instance]
receivers:
- name: correo-operaciones
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/telefono'Las inhibit_rules merecen atención: cuando el servicio cae, se dispararían también las alertas de latencia, de errores y de conexiones a la base de datos. Recibir cinco notificaciones del mismo incidente es fatiga de alertas en su forma más pura. La inhibición envía una: la causa.
Severidades y escalado
| Severidad | Criterio | Canal | Respuesta |
|---|---|---|---|
| Crítica | El servicio está caído o hay riesgo de pérdida de datos | Teléfono, 24×7 | Inmediata |
| Aviso | Degradado, o algo se romperá en horas | Correo | Horario laboral |
| Informativa | Conviene saberlo | Panel | Revisión semanal |
Nivel 1: operador (tú) -> 15 min sin respuesta Nivel 2: Luis (desarrollo) -> 30 min sin respuesta Nivel 3: Marta (decisiones de negocio) -> se le informa siempre en críticas
Y el silencio programado durante el mantenimiento, sin el cual la ventana de despliegue genera una avalancha de notificaciones:
$ amtool silence add alertname=~".*" --duration=1h \
--author=operador --comment="Ventana de mantenimiento: despliegue 3.3.0"
b3f19c8d-4a2e-4f91-b7c2-1e9d0f8a3b45
$ amtool silence expire b3f19c8d-4a2e-4f91-b7c2-1e9d0f8a3b45 # al terminarLos silencios se ponen con caducidad, siempre. Un silencio indefinido puesto un martes es una alerta que no volverá a sonar nunca, y nadie se acordará.
Registros centralizados: la deuda de 05-06
En 05-06 quedó una pieza pendiente: el journal es persistente y local. Con un solo servidor funciona; en cuanto hay dos —los nodos de aplicación de 07-07, el balanceador, la réplica— diagnosticar exige entrar en cada máquina y correlacionar a mano.
Por qué hace falta centralizar:
| Problema | Con registros locales | Centralizados |
|---|---|---|
| Correlacionar entre máquinas | A mano, con marcas de tiempo | Una consulta |
| Un servidor comprometido | El atacante borra sus huellas | Ya están fuera |
| Un servidor destruido | Se pierden los registros | Conservados |
| Retención legal | Por máquina | Una política |
| Buscar en 30 días de logs | zgrep sobre ficheros rotados |
Índice |
Esa segunda fila es de seguridad pura y enlaza con 06-04: si los registros solo viven en la máquina atacada, el atacante los edita. Es el mismo razonamiento por el que la base de datos de AIDE vive fuera del servidor.
Loki como opción ligera:
| Loki | Elasticsearch/OpenSearch | journald remoto | |
|---|---|---|---|
| Qué indexa | Solo las etiquetas, no el texto | Todo el texto | Nada |
| Recursos | Muy bajos | Altos: 4-8 GB de RAM mínimo | Mínimos |
| Consulta | LogQL, parecido a PromQL | Muy potente | journalctl |
| Integración con Grafana | Nativa: mismo panel | Buena | No |
| Coste de almacenamiento | Bajo: objetos comprimidos | Alto | Bajo |
Loki es la elección para Tramontana por dos razones: no indexa el texto completo —que es lo que hace a Elasticsearch caro— y comparte etiquetas y sintaxis con Prometheus, de modo que se puede saltar de una gráfica de latencia a los registros de ese mismo instante en el mismo panel. Esa correlación es lo que hace útil un sistema de registros.
# /etc/promtail/config.yml (agente que envía los registros)
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: unidad
- source_labels: ['__journal_priority_keyword']
target_label: nivel# Consulta LogQL: errores de la aplicacion en la ultima hora
{unidad="tramontana.service", nivel=~"err|crit"} |= "ERROR"
# Tasa de errores por minuto, que se puede graficar junto a las metricas
sum(rate({unidad="tramontana.service"} |= "ERROR" [5m]))Y la advertencia obligatoria: los registros contienen datos personales —IP, identificadores de usuario, a veces correos—. Centralizarlos multiplica el alcance de una fuga. Retención acotada, acceso restringido y el mismo tratamiento que la base de datos.
Operación diaria, semanal y mensual
Los cinco minutos de cada mañana
| # | Qué se mira | Dónde | Señal de alarma |
|---|---|---|---|
| 1 | Alertas activas | Alertmanager | Cualquiera sin atender |
| 2 | Panel principal | Grafana | Anomalía frente a ayer |
| 3 | Edad de la última copia | Panel | > 24 h |
| 4 | Unidades fallidas | systemctl --failed |
Cualquiera |
| 5 | Errores nuevos en el registro | Loki o journalctl -p err |
Patrón desconocido |
#!/usr/bin/env bash
# revision_matutina.sh — los cinco minutos, en un comando
set -euo pipefail
source "$(dirname "${BASH_SOURCE[0]}")/lib/comunes.sh"
main() {
printf '\n=== REVISIÓN MATUTINA · %s ===\n\n' "$(date '+%F %T')"
printf '── Alertas activas ──\n'
amtool alert query --output=extended 2>/dev/null | head -10 || echo " (ninguna)"
printf '\n── Unidades fallidas ──\n'
systemctl --failed --no-legend || echo " (ninguna)"
printf '\n── Estado general ──\n'
curl -s 'http://127.0.0.1:9090/api/v1/query?query=tramontana_salud_estado' | \
jq -r '.data.result[0].value[1] as $e |
" revision_salud: " + (if $e=="0" then "correcto"
elif $e=="1" then "AVISO" else "CRITICO" end)'
curl -s 'http://127.0.0.1:9090/api/v1/query?query=tramontana_copia_edad_segundos' | \
jq -r '" ultima copia: hace " + ((.data.result[0].value[1]|tonumber/3600|floor)|tostring) + " h"'
curl -s 'http://127.0.0.1:9090/api/v1/query?query=tramontana_certificado_dias' | \
jq -r '" certificado: " + .data.result[0].value[1] + " dias"'
printf '\n── Errores de las últimas 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── Disponibilidad (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/revision_matutina.sh
=== REVISIÓN MATUTINA · 2026-08-18 08:04:11 ===
── Alertas activas ──
(ninguna)
── Unidades fallidas ──
(ninguna)
── Estado general ──
revision_salud: correcto
ultima copia: hace 6 h
certificado: 71 dias
── Errores de las últimas 24 h ──
3 tramontana[1204]: ERROR consulta lenta en /informes/facturacion
1 nginx_error: upstream timed out
── Disponibilidad (7 d) ──
99.94 %Semanal (30 minutos) y mensual (2 horas)
| Frecuencia | Tarea | Por qué |
|---|---|---|
| Semanal | Revisar tendencias de 7 días | Ver crecimientos lentos |
| Semanal | Actualizaciones de seguridad pendientes | Ventana de exposición |
| Semanal | Top 5 de pg_stat_statements |
Consultas que se degradan |
| Semanal | Espacio en disco y crecimiento | Anticipar, no reaccionar |
| Semanal | Registro de cambios de la semana | Contexto para incidentes |
| Mensual | Restaurar un fichero de la copia | Verificar de verdad |
| Mensual | Revisar y podar alertas | Fatiga de alertas |
| Mensual | Lynis y AIDE | Deriva de configuración |
| Mensual | Revisar accesos: usuarios, claves SSH, pares de VPN | Se acumulan |
| Mensual | Consumo del presupuesto de error | Decidir ritmo de cambios |
| Trimestral | Ensayo completo de PITR (RB-BD-02) | La copia sin ensayo es un fichero |
| Semestral | Simulacro de reconstrucción | Validar el RTO |
| Anual | Revisar SLO, modelo de amenazas y arquitectura | El contexto cambia |
El cuaderno de guardia y el registro de cambios
El cuaderno de guardia es un fichero de texto donde se anota lo que pasa. Suena trivial y es la herramienta que más tiempo ahorra en un incidente, porque responde a «¿esto ya nos había pasado?».
# Cuaderno de guardia · srv-tramontana
## 2026-08-18
**08:04** Revisión matutina. Todo correcto. Disponibilidad 7 d: 99,94 %.
**11:20** [CAMBIO] Nginx como proxy inverso con TLS. Cerrada la deuda del
cifrado en tránsito abierta desde 06-05. HSTS con max-age=300 a propósito;
subir a 2 años el 25/08 tras verificar la renovación. Medido: -78 % de bytes
transferidos, +19 ms en el primer handshake.
**14:32** [INCIDENTE] `DELETE` accidental sobre app.reservas (40.218 filas).
Detectado 14:51. Recuperado por PITR en máquina de pruebas y reimportado.
Tiempo total 34 min. Sin pérdida de datos. Post-incidente: 20/08.
**16:10** [OBSERVACIÓN] p95 sube a 1,4 s los martes entre 16 y 18 h. Coincide
con el informe de facturación de Marta. No es un problema hoy; vigilar.
## 2026-08-17
**03:14** [ALERTA] Espacio en /var al 91 %. Causa: lv-backups lleno →
archive_command falla → pg_wal crece. Resuelto purgando WAL con
pg_archivecleanup y ampliando el LV 10 GiB. **Alerta nueva creada:
ArchivadoWALFallando (failed_count > 0).**Cuatro reglas para que funcione:
- Se escribe en el momento, no al final del día. Lo que se pospone no se escribe.
- Marcas de tiempo siempre, para poder correlacionar con las gráficas.
- Las observaciones valen tanto como los incidentes. La entrada de las 16:10 es la que explicará una alerta dentro de dos meses.
- Vive fuera del servidor —en el repositorio de Ansible— porque hará falta precisamente cuando el servidor no esté.
El registro de cambios ya existe: es el git log de ~/tramontana-infra. La disciplina es que todo cambio de configuración pase por ahí, para que se pueda responder a «¿qué cambió antes de que empezara a fallar?».
$ cd ~/tramontana-infra && git log --oneline --since="7 days ago"
a4f19c8 Rol web: HSTS a 2 años tras verificar renovación
3e2b1d5 Rol basedatos: ajuste de memoria para 3,8 GB + PgBouncer
9c8a7f2 Alerta ArchivadoWALFallando tras el incidente del 17/08
1b4d6e3 Rol vpn: alta de marta-tableta
# La pregunta clave durante un incidente
$ git log --since="6 hours ago" --statLa revisión post-incidente sin culpables
Un incidente es un fallo del sistema, no de una persona. Si alguien pudo borrar 40.000 filas con un comando, el problema es que el sistema lo permitiera sin fricción, no que esa persona se equivocara.
Esto no es amabilidad corporativa: es la única forma de obtener información verdadera. En una cultura donde se buscan culpables, la gente oculta los errores, y entonces los incidentes se repiten porque nadie sabe que ocurrieron. La revisión sin culpables es una decisión de ingeniería.
Revisión post-incidente: borrado accidental de reservas
Incidente: INC-2026-003 · Fecha: 2026-08-18 · Severidad: Alta Duración del impacto: 34 min · Datos perdidos: ninguno Redactado por: Operaciones · Revisado con: Marta, Luis
1. Qué pasó
Durante una limpieza rutinaria de datos antiguos, se ejecutó en la base de datos de producción un
DELETEcuya cláusulaWHEREno era la prevista, eliminando 40.218 reservas históricas. Se detectó 19 minutos después y se recuperaron mediante recuperación a un punto en el tiempo. No hubo pérdida de datos ni interrupción del servicio.2. Impacto
Dimensión Impacto Disponibilidad del servicio Ninguno: la web siguió funcionando Datos perdidos definitivamente Ninguno Datos temporalmente inaccesibles 40.218 reservas históricas, 34 min Clientes afectados Ninguno (datos de consulta interna) Presupuesto de error consumido 0 % 3. Línea temporal
Hora Suceso 14:30 Se inicia la limpieza de datos anteriores a 2024 14:32 Se ejecuta el DELETEcon la condición incorrecta14:51 Un informe devuelve cifras anómalas: se detecta 14:53 Se detiene toda escritura adicional. Se abre el incidente 14:58 Se localiza la marca temporal exacta en pg_stat_statements15:02 Comienza la restauración PITR en srv-tramontana-pruebas15:19 Recuperación completada y verificada en pruebas 15:24 Datos reimportados a producción con psql -115:26 Verificación final. Incidente cerrado 4. Causa raíz: los cinco porqués
# Pregunta Respuesta 1 ¿Por qué se borraron 40.218 reservas? Un DELETEconWHEREincorrecto2 ¿Por qué se ejecutó con la condición incorrecta? Se escribió a mano, sin SELECTprevio3 ¿Por qué se escribió a mano? No existe procedimiento para limpiezas de datos 4 ¿Por qué no existe? Nunca se había hecho una limpieza masiva 5 ¿Por qué hizo falta ahora? La tabla de auditoría creció sin política de retención Causa raíz: la ausencia de una política de retención de datos obligó a una limpieza manual improvisada, sin procedimiento ni salvaguardas.
Y una observación sobre el método: los cinco porqués atraviesan la respuesta fácil —«alguien se equivocó al teclear»— hasta llegar a una causa sistémica y accionable. Quedarse en el porqué número 2 habría producido la conclusión inútil de «hay que tener más cuidado».
5. Qué funcionó bien
Esta sección es obligatoria y se olvida siempre:
- La recuperación a un punto en el tiempo funcionó exactamente como se ensayó (RB-BD-02, 08-02). El ensayo trimestral demostró su valor a los tres días.
- La restauración en máquina aparte preservó las transacciones posteriores. Restaurar sobre producción habría perdido 19 minutos de reservas reales.
- La detección en 19 minutos fue razonable, aunque mejorable.
- Se avisó de inmediato, sin ocultarlo. Eso permitió actuar rápido.
6. Qué se cambia
# Acción Responsable Fecha Estado 1 Política de retención de auditoriaacordada con MartaMarta + Ops 25/08 Pendiente 2 Runbook de limpiezas: SELECT count(*)obligatorio antes de cualquierDELETEOps 22/08 En curso 3 Rol svc_limpiezaconstatement_timeouty sin permiso deDELETEmasivoOps 25/08 Pendiente 4 Purga programada como timer idempotente y probado, no manual Ops 31/08 Pendiente 5 Alerta si una tabla pierde más del 10 % de filas en 5 min Ops 25/08 Pendiente 6 psqlde producción con\set ON_ERROR_ROLLBACK offy aviso en el promptOps 22/08 Hecho Ninguna acción es «tener más cuidado». Una acción correctiva que depende de la atención humana no es una acción correctiva: es una expresión de deseo.
7. Lecciones
- Los ensayos de recuperación se pagan solos. El de 08-02 se hizo el día 15; el día 18 hizo falta de verdad, y nadie tuvo que improvisar.
- Una tabla que crece sin política de retención acaba forzando una operación arriesgada. El crecimiento silencioso es una deuda que se cobra de golpe.
- Detectar en 19 minutos es mucho para un borrado masivo. La acción 5 lo bajará a menos de 5.
Gestión del cambio y ventanas de despliegue
La mayoría de las interrupciones no vienen de fallos de hardware: vienen de cambios. Y sin embargo el cambio es necesario. Gestionarlo es ponerle fricción proporcional al riesgo, no impedirlo.
| Tipo de cambio | Ejemplos | Aprobación | Ventana |
|---|---|---|---|
| Estándar | Actualizaciones de seguridad, alta de VPN | Ninguna: preaprobado | Cualquier momento |
| Normal | Despliegue de versión, cambio de configuración | Revisión de un compañero | Ventana acordada |
| Mayor | Cambio de esquema, versión mayor de PostgreSQL | Marta + plan de vuelta atrás | Ventana planificada |
| De emergencia | Parche de vulnerabilidad crítica | Posterior | Inmediata |
La ventana de Tramontana: martes y miércoles, de 10:00 a 12:00. Y las razones de por qué no se despliega el viernes por la tarde, que es la regla más famosa del sector:
- Los problemas aparecen con carga real, horas después. Un viernes a las 18:00, esa carga llega el lunes.
- La gente se va. Si algo falla a las 20:00 del viernes, quien lo desplegó ya no está.
- El fin de semana es cuando más reservas se hacen en Tramontana: el peor momento posible para un fallo.
- Un incidente el viernes se arrastra el fin de semana, con la persona de guardia trabajando en un cambio que no hizo.
La regla, formulada correctamente: no se despliega si no hay tiempo suficiente por delante para detectar y revertir el problema con el equipo disponible. El martes por la mañana cumple; el viernes por la tarde no.
Lista de verificación previa a cada despliegue:
| # | Comprobación |
|---|---|
| 1 | ¿Está probado en srv-tramontana-pruebas? |
| 2 | ¿Hay copia reciente y verificada? |
| 3 | ¿Está escrito el plan de vuelta atrás? |
| 4 | ¿Las migraciones de esquema son compatibles hacia atrás? |
| 5 | ¿Está avisado quien deba estarlo? |
| 6 | ¿Hay silencio de alertas programado y con caducidad? |
| 7 | ¿Queda presupuesto de error? |
| 8 | ¿Hay tiempo por delante para revertir con calma? |
SLI, SLO y presupuesto de error
Tres conceptos que se confunden y son distintos:
| SLI | SLO | SLA | |
|---|---|---|---|
| Qué es | Un indicador medido | Un objetivo interno | Un acuerdo contractual |
| Ejemplo | 99,94 % de peticiones correctas | ≥ 99,8 % mensual | 99,5 %, con penalización |
| Quién lo fija | La medición | El equipo con negocio | Legal y comercial |
| Si se incumple | — | Se cambia el ritmo de trabajo | Hay consecuencias económicas |
El SLO se pone siempre más exigente que el SLA, para tener margen antes de incumplir el contrato.
El SLO de Tramontana
Recogiendo el 99,8 % propuesto en 07-07 y convirtiéndolo en números concretos:
SLI: proporción de peticiones HTTP a
reservas.tramontana.examplerespondidas con código < 500 y en menos de 2 segundos.SLO: ≥ 99,8 % de esas peticiones, medido en ventana móvil de 30 días.
# El SLI, tal cual se mide
(
sum(rate(nginx_http_requests_total{status!~"5.."}[30d]))
- sum(rate(tramontana_duracion_peticion_seconds_count{le="2"}[30d]))
) / sum(rate(nginx_http_requests_total[30d]))El presupuesto de error
Aquí está la idea que cambia la forma de trabajar:
Presupuesto de error = 100 % − SLO. Es la cantidad de fallo que te puedes permitir. No es un residuo: es un recurso que se gasta.
| SLO | Presupuesto | En 30 días |
|---|---|---|
| 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 puede estar caída 1 h 26 min cada 30 días y seguir cumpliendo su objetivo. Ese tiempo es un presupuesto que se puede gastar deliberadamente: en despliegues arriesgados, en actualizaciones, en experimentos.
Y de ahí sale una regla de gestión que sustituye a las discusiones eternas entre «hay que ir más rápido» y «hay que ser más estables»:
| Presupuesto consumido | Decisión |
|---|---|
| < 50 % | Se puede desplegar con normalidad. Hay margen |
| 50-80 % | Despliegues normales, más cuidado con los mayores |
| 80-100 % | Solo cambios estándar y correcciones. Se congelan las funciones nuevas |
| Agotado | Congelación total. Todo el esfuerzo a estabilizar |
# Presupuesto consumido en los ultimos 30 dias, en porcentaje
(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 % consumido: hay margen. Ese número va al panel principal, y es lo que convierte «¿desplegamos?» de una discusión de opiniones en una consulta de datos.
Lo más valioso del presupuesto de error, y suele sorprender: es tan malo agotarlo como no gastarlo. Un equipo que termina el mes con el 5 % consumido está siendo demasiado conservador — podría haber desplegado más, entregado más valor y asumido más riesgo dentro de lo acordado. La fiabilidad excesiva también tiene un coste: el de las funciones que no se hicieron.
Cierre de las deudas pendientes
Deuda 1: la copia externa no es append-only
El problema. restic sube las copias con un token que también puede borrar. Un atacante con acceso a srv-tramontana puede ejecutar restic forget --prune y destruir todo el histórico. Es exactamente el modo de operar del ransomware moderno: primero borra las copias, luego cifra los datos.
# La comprobación que revela la deuda
$ restic forget --keep-last 1 --dry-run
# Si esto NO da un error de permisos, el token puede borrar.La solución: un token de solo añadir, más un restic en modo --append-only en el lado del repositorio.
# 1. Token nuevo en el proveedor, SIN permiso de borrado
# (Backblaze B2: "Write Only"; S3: politica sin s3:DeleteObject)
$ pass insert tramontana/restic-append-only
# 2. Bloqueo de objetos en el proveedor: nada se puede borrar
# durante 30 dias, ni siquiera con credenciales de administrador
$ b2 update-bucket --defaultRetentionMode compliance \
--defaultRetentionPeriod "30 days" tramontana-copias
# 3. El servidor usa el token restringido
$ sudo sed -i 's|^RESTIC_TOKEN=.*|RESTIC_TOKEN_CMD="pass tramontana/restic-append-only"|' \
/etc/tramontana/respaldo.env
# 4. VERIFICAR que no puede borrar
$ RESTIC_PASSWORD=$(pass restic/tramontana) restic forget --keep-last 1 --dry-run
Fatal: unable to remove files: AccessDenied
# 5. La purga se hace desde OTRA MAQUINA con credenciales distintas,
# mensualmente, con un token que sí puede borrar y que NUNCA está
# en el servidor respaldado.El principio general, aplicable mucho más allá de esto: el sistema respaldado no debe poder destruir sus copias. Si puede, no son copias: son una réplica con retraso.
| Antes | Después | |
|---|---|---|
| El servidor puede borrar copias | Sí | No |
| Superviviente a un ransomware | No | Sí, 30 días |
Superviviente a un rm accidental |
No | Sí |
| Quién purga | El propio servidor | Otra máquina, mensualmente |
Checklist fila 23: cerrada.
Deuda 2: el authorized_keys2 sin explicar
$ ls -la /home/operador/.ssh/
-rw------- 1 operador operador 742 ene 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... soporte@proveedor-antiguo (RSA)Qué es. authorized_keys2 es un fichero obsoleto de OpenSSH 2.x, cuando había ficheros separados para claves SSH-1 y SSH-2. Las versiones modernas de OpenSSH lo ignoran por completo, salvo que AuthorizedKeysFile lo mencione explícitamente.
$ sudo sshd -T | grep -i authorizedkeysfile
authorizedkeysfile .ssh/authorized_keys .ssh/authorized_keys2Y ahí está el problema: la configuración por defecto de Ubuntu sí lo incluye. Esa clave RSA de 2048 bits, de un proveedor con el que ya no se trabaja, da acceso al servidor hoy mismo.
Es un ejemplo perfecto de un riesgo real: no es una vulnerabilidad de software, es un fichero que nadie miró en dieciocho meses.
# 1. Investigar antes de borrar: ¿se ha usado?
$ sudo journalctl -u ssh --since "90 days ago" | \
grep 'Accepted publickey' | grep -o 'SHA256:[A-Za-z0-9+/]*' | sort -u
SHA256:aB3cD4eF5g... # la de operador
# La huella de authorized_keys2 NO aparece: sin uso en 90 dias
# 2. Preguntar a Marta si el proveedor sigue teniendo relación
# -> Respuesta: contrato terminado en marzo de 2025
# 3. Retirar, conservando prueba (06-04)
$ sudo cp /home/operador/.ssh/authorized_keys2 \
/var/log/incidentes/authorized_keys2.retirado-$(date +%F)
$ sudo shred -u /home/operador/.ssh/authorized_keys2
# 4. Y la corrección estructural: que no vuelva a poder existir
$ sudo tee /etc/ssh/sshd_config.d/60-authorized-keys.conf <<'EOF'
# Solo un fichero de claves autorizadas. authorized_keys2 es un resto
# de OpenSSH 2.x que Ubuntu sigue incluyendo por defecto, y que permite
# que una clave olvidada de un tercero de acceso sin que nadie la vea.
AuthorizedKeysFile .ssh/authorized_keys
EOF
$ sudo sshd -t && sudo systemctl reload ssh
$ sudo sshd -T | grep -i authorizedkeysfile
authorizedkeysfile .ssh/authorized_keysY en Ansible, para que sea permanente y verificado:
- name: No permitir 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: Recargar ssh
- name: Comprobar que no existen ficheros de claves fuera de política
ansible.builtin.find:
paths: /home
patterns: 'authorized_keys2'
recurse: true
hidden: true
register: claves_extra
failed_when: claves_extra.matched > 0La lección de método: en el inventario de accesos, un fichero que no se puede explicar es un fichero que se retira. Y el error de fondo fue no tener nunca una revisión periódica de accesos — que ahora es la fila mensual de la tabla del apartado 10.
Checklist fila 40: cerrada.
Deuda 3: no hay objetivo formal de disponibilidad
Se cierra formalizando lo propuesto en 07-07:
Acuerdo de nivel de servicio interno · Tramontana Reservas Acordado el 18/08/2026 entre Operaciones y Dirección (Marta Vidal). Revisión: agosto de 2027.
Concepto Valor SLI Peticiones con código < 500 y latencia < 2 s SLO 99,8 % en ventana móvil de 30 días Presupuesto de error 1 h 26 min cada 30 días Ventana de mantenimiento Martes y miércoles, 10:00-12:00 RPO 15 minutos RTO 2 horas Horario de guardia Laborable 8:00-20:00; críticas 24×7 Revisión del cumplimiento Mensual, en el panel
Checklist fila 33: cerrada.
Cumplimiento: RGPD y retención de datos
Tramontana guarda datos personales de clientes: nombre, contacto y fechas de estancia. Eso activa obligaciones concretas del Reglamento General de Protección de Datos, y algunas son técnicas y son tuyas.
| Obligación | Estado en Tramontana |
|---|---|
| Registro de actividades de tratamiento (art. 30) | ⚠️ Pendiente |
| Medidas técnicas apropiadas (art. 32) | ✅ Cifrado en tránsito (08-01) y en reposo (LUKS) |
| Minimización: solo los datos necesarios | ⚠️ Revisar con Luis |
| Limitación del plazo de conservación | ⚠️ auditoria sin política |
| Derecho de supresión | ⚠️ Procedimiento no escrito |
| Derecho de acceso y portabilidad | ⚠️ No automatizado |
| Notificación de brechas en 72 h | ✅ Detección (06-04); procedimiento por escribir |
| Registro de accesos a datos personales | ✅ auditd (06-04) |
Y el punto que conecta directamente con el incidente del apartado 12: la tabla auditoria ocupa 1.204 MB, más que todos los datos de reservas juntos, y nadie ha decidido cuánto tiempo se conservan. Eso no es solo un problema de espacio: conservar datos personales sin plazo definido es un incumplimiento del principio de limitación del plazo de conservación.
-- Propuesta de retención, a decidir con Marta y con asesoría legal
-- Reservas: 6 años (obligación mercantil de conservación contable)
-- Auditoría técnica: 12 meses
-- Registros con IP: 12 meses
-- Datos de contacto de reservas canceladas: 1 año
CREATE OR REPLACE PROCEDURE app.purgar_retencion() LANGUAGE plpgsql AS $$
BEGIN
DELETE FROM app.auditoria WHERE creado_en < now() - interval '12 months';
UPDATE app.huespedes SET telefono = NULL, email = NULL, anonimizado = true
WHERE id IN (SELECT huesped_id FROM app.reservas
WHERE estado = 'cancelada' AND fecha < now() - interval '1 year')
AND anonimizado = false;
RAISE NOTICE 'purga de retencion completada';
END $$;Anonimizar en lugar de borrar conserva la utilidad estadística —cuántas reservas hubo, de qué casas— sin conservar los datos personales. Es la solución técnicamente correcta y la que más se pasa por alto.
La advertencia final, y es importante: el RGPD no es un asunto técnico, es un asunto legal con implicaciones técnicas. Nada de este apartado sustituye a asesoría profesional. Lo que sí es responsabilidad tuya como administrador es: que existan las medidas técnicas, que la retención sea aplicable con una orden, que haya trazabilidad de accesos, y que quien decide —Marta— tenga la información para decidir. Un administrador que implementa una purga sin que nadie haya decidido los plazos está tomando una decisión legal sin autoridad para hacerlo.
Errores Comunes y Consejos
- Confundir monitorizar con alertar. Monitoriza todo; alerta de casi nada.
- Alertar de causas en lugar de síntomas. «CPU al 90 %» dispara cuando corre la copia. «Más del 5 % de errores» detecta cualquier fallo real.
- No podar alertas. Diez notificaciones al día equivalen a ninguna. La revisión mensual de alertas es tan importante como crearlas.
- Alertas sin acción. Si el receptor no puede hacer nada, es un panel, no una alerta. Toda alerta lleva su
acciony su enlace al runbook. - Silencios sin caducidad. Un silencio indefinido es una alerta apagada para siempre.
- No monitorizar la monitorización. Un script de métricas muerto hace que todo parezca correcto indefinidamente. La marca de frescura lo resuelve.
- Medias de latencia en lugar de percentiles. Una media de 80 ms esconde que el 1 % espera 4 segundos.
- Incluir los errores en la latencia. Una caída total, donde todo falla rápido, aparecería como mejora de rendimiento.
rate()sobre un medidor. Solo tiene sentido sobre contadores (_total).- Agregar histogramas sin
by (le). Es el error clásico de PromQL: los percentiles salen sin sentido. - Etiquetas de alta cardinalidad. Una etiqueta con el ID de reserva crea una serie por reserva y revienta la memoria de Prometheus.
- Un panel con cuarenta gráficas. No se mira. El principal debe caber en una pantalla.
- Post-incidentes que buscan culpables. Garantizan que el siguiente incidente se oculte.
- Acciones correctivas del tipo «tener más cuidado». No son acciones: son deseos. Deben cambiar el sistema.
- Desplegar el viernes por la tarde. Los problemas aparecen con carga real y para entonces no hay nadie.
- Un SLO sin presupuesto de error. El objetivo sin su consecuencia operativa no cambia ninguna decisión.
- Gastar el 5 % del presupuesto de error. Estás siendo demasiado conservador: podrías haber entregado más valor.
- Copias que el propio servidor puede borrar. No son copias: son una réplica con retraso, y el ransomware lo sabe.
- No revisar los accesos periódicamente. Un
authorized_keys2de un proveedor de hace año y medio da acceso hoy. - Guardar datos personales sin plazo definido. Es un incumplimiento del RGPD, no solo un problema de espacio.
- Consejo de método. Ante «¿va bien el sistema?», si la respuesta no es un número, no estás en producción.
Ejercicios
Ejercicio 1
A las 03:47 se dispara TasaErroresAlta. Documenta la respuesta completa al incidente —desde la notificación hasta el cierre— y redacta la revisión post-incidente resultante.
Ejercicio 2
Diseña la revisión mensual de alertas: qué se mide de cada alerta, con qué criterios se decide eliminarla, ajustarla o mantenerla, y aplica el método a las alertas configuradas en esta lección.
Ejercicio 3
Marta pide un informe de estado del sistema para la reunión anual con los socios: qué se ha hecho este año, en qué situación está y qué se necesita. Redáctalo.
Soluciones
Solución 1
03:47 — Notificación.
[CRITICA] TasaErroresAlta Resumen: Más del 5 % de las peticiones falla Descripción: Tasa actual: 34,2 %. Acción: journalctl -t nginx_error -n 50; comprobar PostgreSQL Runbook: https://docs.tramontana.example/RB-OPS-02
03:49 — Confirmar que el problema es real, antes de tocar nada.
$ curl -sI https://reservas.tramontana.example/casas | 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.342Confirmado: uno de cada tres usuarios recibe un error. No es una falsa alarma.
03:51 — Acotar el alcance con el panel, no adivinando.
| Panel | Lectura |
|---|---|
| Errores | 502 desde las 03:41 |
| Tráfico | Normal para la hora |
| Latencia p95 | Subió a 8 s a las 03:38, antes de los errores |
| CPU / memoria | Normales |
| Conexiones de PgBouncer | cl_waiting = 47 |
tramontana_salud_estado |
2 (crítico) |
La latencia subió tres minutos antes que los errores, y hay 47 clientes esperando conexión. El cuello de botella está en la base de datos, no en la aplicación ni en Nginx.
03:53 — El registro de cambios, que es la pregunta obligatoria.
Sin cambios recientes. No es un despliegue: es algo que ha ocurrido solo.
03:55 — Diagnóstico en la base de datos.
$ sudo -u postgres psql -x -c "
SELECT pid, state, wait_event_type, wait_event,
now()-xact_start AS duracion, 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
duracion | 00:19:12
consulta | UPDATE app.disponibles SET estado = 'ocupada'
$ sudo -u postgres psql -c "
SELECT bloqueada.pid AS esperando, bloqueante.pid AS bloqueando,
left(bloqueante_act.query, 50) AS consulta_bloqueante
FROM pg_locks bloqueada
JOIN pg_locks bloqueante ON bloqueante.transactionid = bloqueada.transactionid
AND bloqueante.granted
JOIN pg_stat_activity bloqueante_act ON bloqueante_act.pid = bloqueante.pid
WHERE NOT bloqueada.granted;"
esperando | bloqueando | consulta_bloqueante
-----------+------------+--------------------------------------------
12844 | 11902 | VACUUM FULL app.disponiblesCausa raíz encontrada en ocho minutos. Un VACUUM FULL sobre app.disponibles tiene un bloqueo ACCESS EXCLUSIVE que impide toda lectura y escritura de esa tabla. Las peticiones se acumulan esperando, agotan el pool de PgBouncer, y la aplicación devuelve 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 y arrancado a las 03:30: alguien lo lanzó a mano. La consulta al cuaderno de guardia lo confirma —una nota de la tarde anterior: «disponibles con 31,7 % de tuplas muertas, mirar».
03:58 — Mitigación.
# Cancelar la consulta (SIGINT). NO pg_terminate_backend, que corta
# la conexión de golpe: cancelar es más limpio y suele bastar.
$ sudo -u postgres psql -c "SELECT pg_cancel_backend(11902);"
pg_cancel_backend
-------------------
t
$ sleep 20 && curl -sI https://reservas.tramontana.example/casas | head -1
HTTP/2 200
$ sudo -u postgres psql -tAc "SELECT count(*) FROM pg_stat_activity
WHERE wait_event_type = 'Lock';"
004:00 — Verificar y cerrar.
$ 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=TasaErroresAlta
(sin alertas activas)04:05 — Anotar en el cuaderno mientras está fresco.
**03:47** [INCIDENTE] TasaErroresAlta, 34 % de 502. Causa: VACUUM FULL
manual sobre app.disponibles lanzado a las 03:30, bloqueo ACCESS
EXCLUSIVE. Mitigado con pg_cancel_backend a las 03:58. Duración del
impacto: 17 min. Post-incidente el 21/08.Revisión post-incidente: errores 502 por bloqueo de VACUUM FULL
Incidente: INC-2026-004 · Fecha: 2026-08-19 · Severidad: Crítica Duración del impacto: 17 min (03:41-03:58) · Detección: 6 min
1. Qué pasó
Un
VACUUM FULLejecutado manualmente sobreapp.disponiblesadquirió un bloqueo exclusivo que impidió toda lectura y escritura de la tabla durante 28 minutos. Las peticiones se acumularon, se agotó el pool de conexiones y la aplicación devolvió errores 502 al 34 % de las peticiones durante 17 minutos.2. Impacto
Dimensión Valor Duración del impacto 17 min Peticiones fallidas ~2.100 (34 % de 6.200) Reservas perdidas estimadas 1-2 (madrugada, tráfico bajo) Presupuesto de error consumido 19,7 % del mes (17 de 86 min) Datos perdidos Ninguno 3. Línea temporal
Hora Suceso 03:30 Se lanza VACUUM FULL app.disponiblesmanualmente03:38 El p95 de latencia sube a 8 s (síntoma temprano, sin alerta) 03:41 Primeros 502: el pool de PgBouncer se agota 03:47 Se dispara TasaErroresAltay llega la notificación03:49 Se confirma el problema 03:51 Se acota con el panel: la base de datos 03:55 Se identifica el bloqueo y el VACUUM FULL03:58 pg_cancel_backend. Servicio restablecido04:00 Verificado. Incidente cerrado 4. Causa raíz: los cinco porqués
# Pregunta Respuesta 1 ¿Por qué hubo 502? La aplicación no pudo consultar la base de datos 2 ¿Por qué no pudo? Un bloqueo ACCESS EXCLUSIVEsobreapp.disponibles3 ¿Por qué había ese bloqueo? Un VACUUM FULLmanual4 ¿Por qué se ejecutó a mano en producción? Se detectó 31,7 % de tuplas muertas y se quiso resolver rápido 5 ¿Por qué había tanta hinchazón? autovacuumno está ajustado para esa tablaCausa raíz:
autovacuumcon los valores por defecto (umbral del 20 %) no sigue el ritmo de escritura deapp.disponibles. La hinchazón resultante motivó una intervención manual con una herramienta cuyo bloqueo no se anticipó.Causa contribuyente: no había ningún control que impidiera ejecutar una operación bloqueante en producción, ni alerta que avisara antes de que los usuarios se vieran afectados.
5. Qué funcionó bien
- La alerta era de síntoma y funcionó: detectó un fallo que nadie había anticipado. Una alerta de causa sobre
VACUUMno existía y no habría hecho falta.- El panel acotó el problema en dos minutos. Sin él, el diagnóstico habría sido a ciegas.
- El registro de cambios descartó un despliegue en treinta segundos.
- La mitigación fue reversible: cancelar en lugar de terminar la conexión.
- El cuaderno de guardia dio el contexto —la nota del día anterior— que explicó quién y por qué.
6. Qué se cambia
# Acción Responsable Fecha Estado 1 Ajustar autovacuumdeapp.disponiblesal 2 % (08-02)Ops 19/08 Hecho 2 lock_timeout = 5spor defecto en la sesión depsqlde producciónOps 20/08 Hecho 3 Instalar pg_repacky documentarlo como la herramienta para hinchazónOps 26/08 Pendiente 4 Alerta EsperaBloqueoProlongada(> 30 s) — avisa antes del síntomaOps 22/08 Pendiente 5 Runbook RB-BD-03: mantenimiento de base de datos, con la lista de operaciones bloqueantes Ops 26/08 Pendiente 6 Panel con tuplas muertas por tabla, para no descubrirlo por casualidad Ops 22/08 Pendiente 7 Ninguna operación de mantenimiento manual en producción fuera de ventana Equipo 20/08 Acordado # Acción 2: /etc/postgresql/16/main/conf.d/50-proteccion.conf # Si una consulta no consigue un bloqueo en 5 s, falla en lugar de # esperar. Una consulta que falla rápido es infinitamente preferible # a una que bloquea el servicio entero. lock_timeout = 5s7. Lecciones
- La latencia subió 9 minutos antes que los errores. Ese es el margen que perdimos por no tener una alerta temprana. La acción 4 lo recupera.
- La intención era buena y el resultado fue una caída. No es un fallo de la persona: es un fallo del sistema, que permitía ejecutar una operación bloqueante en producción sin ninguna fricción. Las acciones 2, 5 y 7 añaden esa fricción.
- El 19,7 % del presupuesto de error en un incidente de 17 minutos demuestra por qué el SLO cambia decisiones: con dos incidentes así, el mes entra en zona de precaución y los despliegues se moderan.
VACUUM FULLno es «un vacuum más grande». Bloquea la tabla entera. Merece estar en un runbook con esa advertencia en mayúsculas.
Solución 2
Por qué existe esta revisión. Las alertas se crean tras cada incidente y casi nunca se eliminan. En un año, un sistema acumula treinta alertas de las que cinco sirven. La revisión mensual es el contrapeso.
Las cuatro métricas que se miden de cada alerta:
| Métrica | Cómo se obtiene | Qué revela |
|---|---|---|
| Frecuencia | Veces que se disparó en 30 días | Ruido o silencio |
| Precisión | (Disparos que exigieron acción) / (disparos totales) | Falsos positivos |
| Sensibilidad | Incidentes que detectó / incidentes reales | Huecos de cobertura |
| Tiempo de reacción | Minutos hasta la primera acción humana | Si se atiende o se ignora |
# Frecuencia y duracion de cada alerta en 30 dias, desde 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
CopiaAntigua 412
LatenciaDegradada 38
DiscoSeLlenara 4
TasaErroresAlta 2
CertificadoCaduca 0
ServicioCaido 0
ArchivadoWALFallando 0
MetricasRancias 0
ReplicaRetrasada 0El árbol de decisión, aplicado a cada alerta:
¿Se disparó en 30 días?
├── NO
│ ├── ¿Habría detectado un incidente real que hubo? → MANTENER
│ ├── ¿Cubre un riesgo grave aunque raro? → MANTENER
│ └── ¿Ninguna de las dos? → CANDIDATA A ELIMINAR
└── SÍ
├── ¿Precisión < 50 %? → AJUSTAR umbral o 'for', o ELIMINAR
├── ¿Frecuencia > 10/mes? → Es ruido: AJUSTAR
├── ¿Nadie actuó nunca? → NO ES UNA ALERTA: pasar a panel
└── ¿Precisión alta y baja frecuencia? → MANTENERAplicación a las alertas de esta lección:
| Alerta | Frec. | Precisión | Reacción | Decisión |
|---|---|---|---|---|
CopiaAntigua |
412 | 0,2 % | Ignorada | AJUSTAR: rota |
LatenciaDegradada |
38 | 21 % | 45 min | AJUSTAR: demasiado sensible |
DiscoSeLlenara |
4 | 100 % | 12 min | MANTENER |
TasaErroresAlta |
2 | 100 % | 2 min | MANTENER: ejemplar |
ServicioCaido |
0 | — | — | MANTENER: riesgo máximo |
ArchivadoWALFallando |
0 | — | — | MANTENER: detectó el incidente del 17/08 |
CertificadoCaduca |
0 | — | — | MANTENER: coste cero, riesgo alto |
ReplicaRetrasada |
0 | — | — | MANTENER |
MetricasRancias |
0 | — | — | MANTENER: vigila a la vigilancia |
Análisis y corrección de las dos problemáticas:
CopiaAntigua: 412 disparos, 0,2 % de precisión. Es el caso de libro de fatiga de alertas: se disparaba cada 30 minutos, todas las noches, entre la ejecución de la copia y su verificación. Cuatrocientas doce notificaciones que nadie leía, y que además entrenaron al equipo a ignorar el canal.
# Diagnostico: ¿cuando se dispara exactamente?
$ curl -s --data-urlencode \
'query=ALERTS{alertname="CopiaAntigua",alertstate="firing"}' \
http://127.0.0.1:9090/api/v1/query_range... | jq ...
# -> Todas entre las 02:30 y las 03:10 # ANTES: umbral demasiado ajustado y sin margen de ejecución
# expr: tramontana_copia_edad_segundos > 2*4*3600 # 8 h
# for: 30m
# DESPUÉS: 26 h cubre un ciclo diario completo más margen de
# ejecución; 'for: 1h' evita el ruido de la ventana de copia.
- alert: CopiaAntigua
expr: tramontana_copia_edad_segundos > 26*3600
for: 1h
labels: {severidad: critica}
annotations:
resumen: "Sin copia verificada desde hace {{ $value | humanizeDuration }}"
accion: "Ver runbook RB-BD-01"LatenciaDegradada: 38 disparos, 21 % de precisión, 45 minutos hasta la reacción. Los 45 minutos son el dato revelador: el equipo ya la estaba ignorando. Se dispara cada tarde durante el informe de facturación de Marta, que es lento y esperado.
# ANTES: p95 > 2 s durante 10 min -> capturaba los informes
# DESPUÉS: dos cambios que la vuelven útil
# 1. Excluir la ruta de informes, que es lenta por diseño
# 2. Umbral y ventana más laxos: 3 s durante 15 min
- alert: LatenciaDegradada
expr: |
histogram_quantile(0.95, sum(rate(
tramontana_duracion_peticion_seconds_bucket{ruta!~"/informes/.*"}[5m]
)) by (le)) > 3
for: 15m
labels: {severidad: aviso}Y para los informes, que sí conviene vigilar pero con otro umbral:
- alert: InformesMuyLentos
expr: |
histogram_quantile(0.95, sum(rate(
tramontana_duracion_peticion_seconds_bucket{ruta=~"/informes/.*"}[5m]
)) by (le)) > 15
for: 30m
labels: {severidad: aviso}Resultado de la revisión:
| Antes | Después | |
|---|---|---|
| Notificaciones al mes | 456 | ~8 |
| Precisión media | 2 % | ~85 % |
| Alertas activas | 9 | 10 (una nueva, dos ajustadas) |
| Tiempo medio de reacción | 45 min | Estimado < 5 min |
Cuatro principios que resume esta revisión:
- Una alerta con precisión baja es peor que ninguna, porque entrena al equipo a ignorar el canal donde también llegan las buenas.
- El tiempo de reacción mide si la alerta se atiende. Cuarenta y cinco minutos significa que se ignora, y ese dato es más honesto que cualquier opinión.
- Una alerta que nunca se dispara no es inútil, si cubre un riesgo grave y su coste es cero.
ServicioCaidono se ha disparado en un año y debe seguir ahí. - Ajustar antes que eliminar. Las dos problemáticas cubrían riesgos reales; el problema estaba en el umbral, no en la idea.
Y una nota final de método: esta revisión se anota en el cuaderno de guardia, con el antes y el después de cada alerta modificada. Dentro de seis meses, cuando alguien se pregunte por qué el umbral de CopiaAntigua es 26 horas y no 8, la respuesta estará escrita.
Solución 3
Informe anual de sistemas · Tramontana Reservas
Para: Marta Vidal y socios · De: Operaciones de sistemas Ejercicio: 2026 · Fecha: 18 de agosto de 2026
Resumen ejecutivo
El sistema que sostiene Tramontana Reservas ha pasado, en el último año, de ser una máquina configurada a mano que nadie más que yo sabía reconstruir a una infraestructura documentada, automatizada, vigilada y con procedimientos escritos que otra persona podría seguir.
Los tres cambios más importantes, en una línea cada uno:
- Los datos de nuestros clientes ya viajan cifrados. Hasta junio no lo hacían.
- Podemos recuperar la base de datos a cualquier instante concreto, y lo hemos ensayado: 24 minutos.
- Reconstruir el servidor entero pasó de 8 horas a 50 minutos, y está medido, no estimado.
Disponibilidad medida en los últimos 30 días: 99,94 %, por encima del objetivo del 99,8 % que proponemos formalizar.
1. Dónde estábamos y dónde estamos
Hace un año Hoy Configuración del servidor En mi cabeza En código, versionado Reconstruir tras un desastre 8 h estimadas 50 min medidos Datos que podríamos perder Hasta 24 h 15 minutos Recuperar un borrado accidental Imposible sin perder un día Al segundo anterior ¿Se han probado las copias? Nunca Ensayo trimestral documentado Cifrado del tráfico web No Sí Acceso remoto seguro Solo desde la oficina Desde cualquier sitio, por túnel cifrado ¿Sabemos si va bien? «Creo que sí» 99,94 %, en un panel Si algo falla de madrugada Nos enteramos por la mañana Alerta al teléfono Documentación operativa Ninguna 6 procedimientos escritos y ensayados
2. Lo que se ha hecho, agrupado
Fiabilidad. Automatización completa de la configuración: el servidor se reconstruye desde cero con un comando, y eso ha reducido el tiempo de recuperación ante un desastre de ocho horas a cincuenta minutos. Es la mejora que más ha aumentado la disponibilidad, y no ha costado un euro en equipamiento.
Protección de datos. Copias automáticas verificadas, cifradas, guardadas fuera del servidor y —desde este mes— imposibles de borrar desde el propio servidor durante 30 días, que es la defensa concreta contra un ataque de secuestro de datos. Y la capacidad de rebobinar la base de datos a cualquier instante, ensayada.
Seguridad. Cortafuegos restrictivo, detección de intrusiones, auditoría de accesos, contraseñas y certificados guardados cifrados, y el servicio funcionando con los mínimos permisos posibles. Una evaluación automática independiente puntúa nuestro nivel de endurecimiento en 82 sobre 100, cuando la instalación por defecto ronda el 60.
Rendimiento. Ajuste de la base de datos a la máquina real, que ha reducido los accesos a disco un 93 %. Y una consulta de informes que tardaba 3,8 segundos ahora tarda 0,18.
Visibilidad. Este es el cambio más reciente y el que más cambia el día a día: un panel que responde en tres segundos a «¿va bien?», con histórico de 30 días, y alertas que avisan antes de que el problema afecte a los clientes.
3. Incidentes del año
Fecha Qué pasó Impacto Estado 17/08 El disco de copias se llenó y bloqueó el registro de la base de datos Ninguno: detectado antes Resuelto + alerta nueva 18/08 Borrado accidental de 40.218 reservas históricas Ninguno: recuperadas en 34 min Resuelto + 6 medidas 19/08 Errores durante 17 min por una operación de mantenimiento ~2.100 peticiones fallidas Resuelto + 7 medidas Ningún incidente causó pérdida de datos. Los tres se analizaron con el mismo método —qué pasó, por qué, qué se cambia— y generaron quince mejoras concretas, todas aplicadas o con fecha.
Quiero destacar el del 18 de agosto: se recuperaron 40.000 reservas sin perder nada, usando un procedimiento que habíamos ensayado tres días antes. Ese ensayo, que parecía burocracia, se pagó solo en 72 horas.
4. Lo que proponemos acordar
Un objetivo de servicio. Hasta ahora no teníamos ninguno, y sin él no se puede decidir cuánto invertir. Proponemos:
Concepto Propuesta Qué significa en la práctica Disponibilidad 99,8 % mensual Como máximo 1 h 26 min de fallo cada 30 días Datos recuperables 15 minutos En el peor caso se perderían 15 min de reservas Tiempo de recuperación 2 horas Desde un desastre total hasta el servicio restablecido Ventana de cambios Martes y miércoles por la mañana Nunca viernes por la tarde ni fin de semana Estamos por encima de ese objetivo hoy (99,94 %). Lo proponemos así a propósito: un objetivo que ya se cumple con holgura permite desplegar mejoras con tranquilidad, y uno demasiado exigente obligaría a frenar el desarrollo del producto.
Y una idea que quiero explicar porque cambia cómo decidimos. Ese 0,2 % de margen es un presupuesto: podemos «gastarlo» en cambios y mejoras. Si un mes lo consumimos por debajo de la mitad, seguimos desplegando con normalidad. Si nos acercamos al límite, se congelan las funciones nuevas y se dedica el esfuerzo a estabilizar. Convierte la discusión «¿vamos demasiado rápido?» en una consulta a un número. Este mes llevamos consumido el 31 %.
5. Lo que hace falta
Prioridad alta, decisión de dirección:
- Política de retención de datos. Guardamos el histórico de actividad de la aplicación sin plazo definido, y ya ocupa más que todas las reservas juntas. Además de ser un problema de espacio, conservar datos personales sin plazo determinado no es defendible ante el Reglamento de Protección de Datos. Necesito que se decida cuánto tiempo guardamos cada cosa; lo aplico en una semana. Recomiendo asesoría legal para fijar los plazos.
- Registro de actividades de tratamiento. Es un documento que el RGPD exige y que no tenemos. No es trabajo técnico, pero necesita mi aportación.
Prioridad media, inversión moderada:
- Segundo servidor de aplicación con reparto de carga. Es la recomendación que analicé en detalle: eliminaría las interrupciones por despliegue y por fallo de una máquina, que son la mayoría. Coste: dos máquinas y una semana de trabajo, más tres o cuatro semanas de desarrollo previo con Luis. Nos llevaría del 99,8 % actual a un margen mucho más holgado.
- Réplica de la base de datos. Justificada sobre todo porque permitiría ejecutar los informes pesados sin afectar a la web, y de paso actualizar sin cortar servicio.
Lo que NO recomiendo, y quiero dejarlo por escrito:
- Arquitectura de alta disponibilidad completa (cinco máquinas, cambio automático de servidor). Analizado con números: cuesta varias veces más que la opción 3 y aporta apenas 670 € adicionales de ahorro anual. Y añadiría una complejidad que con la plantilla actual no podemos vigilar bien — un sistema sofisticado mal mantenido es menos fiable que uno sencillo bien cuidado.
- Kubernetes, que es la tecnología de moda. Lo he montado y evaluado en el entorno de pruebas: para nuestro tamaño consumiría entre dos y cuatro semanas de trabajo al año solo en mantenerlo, para resolver problemas que no tenemos.
6. Riesgos que asumimos conscientemente
Prefiero que estén escritos a que se descubran el día malo:
Riesgo Consecuencia Por qué lo aceptamos Una sola máquina de aplicación Un fallo de hardware = servicio caído hasta 2 h Propuesta 3 lo resolvería Cambio manual de base de datos 5-15 min adicionales en ese caso Automatizarlo exige 5 máquinas y crearía un riesgo mayor Una sola persona con conocimiento operativo Vacaciones o baja = respuesta lenta Mitigado en parte: procedimientos escritos Dependencia de un proveedor Una caída suya nos afecta entera Habitual a nuestra escala El tercero es el que más me preocupa a medio plazo, y es la razón de que este año haya dedicado tanto esfuerzo a escribir procedimientos: hoy, alguien con conocimientos técnicos generales podría seguir la mayoría de ellos sin haber trabajado nunca en nuestro sistema. Hace un año, no.
7. Conclusión
El sistema está en una situación considerablemente mejor que hace un año, y —lo que más importa— está en una situación que puedo demostrar con datos en lugar de afirmar con confianza. Hemos pasado de «creo que va bien» a «99,94 %, aquí está el panel».
Lo que queda pendiente son sobre todo decisiones, no trabajo técnico: cuánto tiempo guardamos los datos, qué nivel de servicio nos comprometemos a dar, y si invertimos en el segundo servidor. Con esas tres decisiones tomadas, tenemos un sistema del que se puede depender.
Conclusión
El curso termina donde tenía que terminar: no en una herramienta más, sino en la capa que convierte un servidor que funciona en un servicio del que se puede depender. Tienes una checklist de cuarenta y dos filas con evidencia comprobable en cada una, y sabes exactamente cuáles faltan, que es una forma de saber más valiosa que creer que están todas. Tienes monitorización con histórico de treinta días, alertas que se pueden atender porque son pocas y son de síntoma, y un panel que responde en tres segundos a la única pregunta que importa.
Sobre todo tienes criterio operativo. Sabes que monitorizar y alertar no son lo mismo y que hay que hacer mucho de lo primero y muy poco de lo segundo. Sabes que una alerta con precisión del 2 % es peor que ninguna, porque entrena al equipo a ignorar el canal por donde también llegan las buenas. Sabes que un post-incidente que busca culpables garantiza que el siguiente se oculte, y que una acción correctiva del tipo «tener más cuidado» no es una acción sino un deseo. Y sabes que el presupuesto de error convierte «¿desplegamos?» en una consulta a un número, con la conclusión que sorprende: gastar solo el 5 % del presupuesto significa haber sido demasiado conservador.
Las tres deudas están cerradas. La copia externa ya no la puede borrar el servidor que respalda, porque un sistema que puede destruir sus copias no tiene copias. El authorized_keys2 resultó ser lo que suelen ser estas cosas: no una vulnerabilidad exótica, sino un fichero de un proveedor de hace año y medio que seguía dando acceso porque nadie lo había mirado — y lo que se ha corregido no es el fichero, es la ausencia de revisión periódica que permitió que estuviera ahí. Y el objetivo de disponibilidad existe por fin, con su número, su ventana y su presupuesto.
Y con esto se cierra el curso completo. Ocho módulos.
Empezaste en el Módulo 1 sin saber qué era un kernel, y montaste el sistema desde una imagen de instalación. En el 2 dejaste de temer la terminal: permisos, inodos, enlaces y man como primera parada. En el 3 el shell dejó de ser un intérprete de órdenes sueltas y pasó a ser un lenguaje: tuberías, find, awk, procesos, señales. En el 4 escribiste scripts que no se rompen —set -euo pipefail, trap, flock, idempotencia—, y desplegar.sh con su rollback atómico sigue en producción cincuenta lecciones después. El 5 fue la administración de verdad: usuarios, paquetes con pinning, LVM, systemd endurecido, journal persistente, y unas copias con RPO y RTO acordados en lugar de improvisados. El 6 puso la seguridad como disciplina y no como lista de trucos: modelo de amenazas, cortafuegos de lista blanca, AIDE con su base de datos fuera, secretos cifrados y aquel certificado TLS que esperó dos módulos. El 7 miró debajo del capó —arranque, strace, perf, eBPF, el kernel, KVM, contenedores, Ansible— y terminó con el análisis honesto de si hacía falta alta disponibilidad. Y el 8 aplicó todo a construcciones completas, de principio a fin.
Lo que se llevan de aquí no es una lista de comandos. Es un método: medir antes y después, porque un ajuste sin medición es superstición. Probar en pruebas antes de producción. --dry-run antes de actuar. Copia y diff -u antes de editar. Validar antes de recargar, sea nginx -t, promtool check, testparm -s o pg_hba_file_rules. Nunca cierres la puerta por la que estás entrando. Silencio si todo va bien. Y decir siempre qué protege y qué no protege una medida, que es lo que separa un informe honesto de un argumento de venta.
Y una idea que ha aparecido en cada módulo con distinta ropa: la respuesta profesional no siempre es que sí. No a la arquitectura completa de alta disponibilidad, no a Kubernetes, no a subir max_connections, no a copiar seis terabytes a la nube. Saber decir que no, con números y con alternativa, vale tanto como saber montarlo.
Sobre cómo seguir. Las certificaciones LPIC-1 y LPIC-2 o la RHCSA ordenan y acreditan lo que ya sabes; la RHCSA es práctica y exigente, y prepararla enseña de verdad. Las especializaciones naturales desde aquí son cuatro: ingeniería de fiabilidad, que es esta última lección llevada a escala; plataforma y nube, donde Kubernetes y la infraestructura como código son el día a día; ciberseguridad, prolongando el Módulo 6; y bases de datos, prolongando 08-02. Participa en la comunidad —listas, foros, un informe de error bien escrito, una corrección de documentación— porque explicar algo es la mejor forma de descubrir si lo entiendes. Y lee los cambios de las versiones del kernel y de tu distribución: es la manera más barata de no quedarse atrás.
Pero si solo puedes conservar una cosa, que sea esta: mantén el laboratorio. Esa VM de pruebas, o el servidor de medios de 08-03, o el clúster de k3s que decidiste no llevar a producción. Un sitio propio donde romper cosas a propósito, provocar los fallos en horario laboral en lugar de esperarlos, y probar lo que aún no sabes. Todo lo que has aprendido en este curso lo has aprendido haciéndolo, y esa es la única forma que hay. La diferencia entre quien lee sobre sistemas y quien los administra no es lo que ha estudiado: es lo que ha roto y ha vuelto a levantar. Enciende la máquina y sigue.
Curso de Linux: De Principiante a Administrador de Sistemas
Módulo 1: Introducción a Linux
- ¿Qué es Linux?
- Historia de Linux
- Distribuciones de Linux
- Instalando Linux
- Primer Contacto con el Sistema
- Estructura del Sistema de Archivos de Linux
Módulo 2: Comandos Básicos de Linux
- Introducción a la Línea de Comandos
- Obtener Ayuda y Documentación del Sistema
- Navegando el Sistema de Archivos
- Operaciones con Archivos y Directorios
- Visualización y Edición de Archivos
- Enlaces Duros y Simbólicos
- Permisos y Propiedad de Archivos
Módulo 3: Habilidades Avanzadas en la Línea de Comandos
- El Entorno del Shell: Variables, Alias e Historial
- Uso de Comodines y Expresiones Regulares
- Búsqueda de Archivos y Contenido: find, locate y grep
- Tuberías y Redirección
- Procesamiento de Texto: cut, sort, uniq, sed y awk
- Gestión de Procesos
- Programación de Tareas con Cron
- Comandos de Redes
Módulo 4: Scripting en Shell
- Introducción al Scripting en Shell
- Variables y Tipos de Datos
- Entrada, Salida y Argumentos de un Script
- Estructuras de Control
- Funciones y Librerías
- Depuración y Manejo de Errores
- Scripts de Producción: Buenas Prácticas
Módulo 5: Administración del Sistema
- Gestión de Usuarios y Grupos
- sudo y Permisos Especiales
- Gestión de Paquetes
- Gestión de Discos
- systemd y la Gestión de Servicios
- Registros del Sistema: journald y syslog
- Monitoreo del Sistema y Optimización del Rendimiento
- Respaldo y Restauración
Módulo 6: Redes y Seguridad
- Configuración de Redes
- SSH y Acceso Remoto
- Firewall y Seguridad Perimetral
- Sistemas de Detección de Intrusos
- Gestión de Secretos y Certificados TLS
- Asegurando Sistemas Linux
Módulo 7: Temas Avanzados
- El Proceso de Arranque y la Recuperación del Sistema
- Diagnóstico Avanzado: strace, perf y eBPF
- Optimización del Kernel de Linux
- Virtualización con Linux
- Contenedores de Linux y Docker
- Automatización con Ansible
- Alta Disponibilidad y Balanceo de Carga
