Producción no es un servidor más rápido: principios de operación real

Un entorno de desarrollo tolera que el proceso caiga, que la configuración esté a medias, que alguien ejecute rm -rf en un directorio equivocado y se reconstruya todo en diez minutos. Producción no tolera nada de eso. La diferencia no es de escala: es de contrato.

Cuando tienes un SLA (Service Level Agreement) firmado con un cliente, ese documento dice que si el servicio cae más de X horas al año, hay consecuencias económicas o legales reales. El SLO (Service Level Objective) es el objetivo interno que estableces más estricto que el SLA precisamente para tener margen antes de incumplirlo. Si el SLA promete 99.9% de disponibilidad (8.7 horas de caída permitidas al año), tu SLO interno debería apuntar a 99.95% para que los incidentes reales no te coman el presupuesto de downtime de golpe. Esto no es burocracia: es el único mecanismo que impide que un incidente de tres horas en enero te deje sin margen para el resto del año.

La razón por la que estos contratos son posibles es que los sistemas de producción operan con una disciplina que los entornos de desarrollo ignoran deliberadamente. Esa disciplina tiene tres pilares que se refuerzan mutuamente: control de versiones de la infraestructura, gestión de cambios y observabilidad obligatoria.

El primero es el más frecuentemente ignorado hasta que duele: si la configuración de /etc/nginx/nginx.conf no está en un repositorio git, esa configuración no existe en ningún sentido operativo útil. Cuando el servidor muere a las 2 de la mañana y necesitas reconstruirlo, “existía en el servidor” no te sirve de nada. El principio es absoluto: /etc/ bajo git, Ansible playbooks en repositorio, Dockerfiles versionados, los scripts de cron bajo control de versiones. Infrastructure as Code (IaC) no es una moda; es la consecuencia lógica de que el estado del sistema sea reproducible en lugar de acumulado a golpe de comandos manuales sin documentar.

La gestión de cambios resuelve la pregunta “¿qué pasó?” cuando algo falla. Si no documentaste qué cambiaste, por qué lo cambiaste y cómo revertirlo antes de ejecutar el cambio, el postmortem va a ser arqueología. Las ventanas de mantenimiento no existen para molestar a los equipos de desarrollo: existen porque en producción un cambio que falla a las 14:00 de un martes laborable tiene un impacto radicalmente diferente que el mismo cambio fallando a las 02:00 del domingo. Los runbooks para incidencias son el documento que alguien que nunca ha visto el sistema puede ejecutar bajo presión a las 3 de la mañana. Si requieren conocimiento tribal, no son runbooks, son esperanza.

El tercer pilar, la observabilidad, es un requisito no negociable porque sin métricas no puedes saber si estás dentro del SLO. El capacity planning que se hace antes de que el sistema falle se basa en tendencias observadas; el que se hace después es gestión de crisis.

#!/bin/bash
# Ejemplo completo: inicializar /etc bajo git, aplicar una
# configuración de nginx mediante Ansible, y dejar trazabilidad
# del cambio lista para un runbook.
# Se asume: Debian Bookworm, Ansible instalado, git instalado.

set -euo pipefail

# ── 1. Control de versiones de /etc ─────────────────────────────────────
# etckeeper mantiene /etc bajo git automáticamente, incluyendo
# permisos y propietarios (que git normal no preserva).
apt-get install -y etckeeper

# etckeeper inicializa el repositorio en /etc y hace el primer commit.
# A partir de aquí, cualquier instalación de paquete genera un commit
# automático. Los cambios manuales requieren commit explícito.
etckeeper init
etckeeper commit "Estado inicial del sistema"

# ── 2. Repositorio de infraestructura como código ───────────────────────
INFRA_REPO="/opt/infra"
mkdir -p "${INFRA_REPO}"
cd "${INFRA_REPO}"
git init

# Estructura mínima de un repositorio IaC operativo
mkdir -p playbooks roles/nginx/{tasks,templates,handlers} runbooks

# Plantilla de nginx con variable de Ansible — no un fichero estático
# editado a mano en el servidor, sino el origen de verdad.
cat > roles/nginx/templates/nginx.conf.j2 << 'EOF'
# Generado por Ansible — no editar manualmente
# Repositorio: {{ ansible_repo_url }}
# Commit: {{ ansible_commit_sha }}

worker_processes auto;
events { worker_connections 1024; }

http {
    server {
        listen 443 ssl;
        server_name {{ server_name }};
        ssl_certificate     /etc/ssl/certs/{{ server_name }}.crt;
        ssl_certificate_key /etc/ssl/private/{{ server_name }}.key;

        location / {
            proxy_pass http://127.0.0.1:8080;
            proxy_set_header Host $host;
        }
    }
}
EOF

# Handler: solo recarga nginx si la configuración cambió realmente.
# Recargar sin cambios es ruido operativo; reiniciar innecesariamente
# rompe conexiones activas.
cat > roles/nginx/handlers/main.yml << 'EOF'
---
- name: reload nginx
  ansible.builtin.systemd:
    name: nginx
    state: reloaded
  # reloaded, no restarted: nginx recarga la config sin cerrar workers
EOF

cat > roles/nginx/tasks/main.yml << 'EOF'
---
- name: Instalar nginx
  ansible.builtin.apt:
    name: nginx
    state: present
    update_cache: false   # el cache se actualiza en el playbook padre

- name: Desplegar configuración desde plantilla
  ansible.builtin.template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
    owner: root
    group: root
    mode: "0644"
    validate: "/usr/sbin/nginx -t -c %s"  # falla el task antes de copiar si la config es inválida
  notify: reload nginx

- name: Verificar que nginx está activo y habilitado
  ansible.builtin.systemd:
    name: nginx
    state: started
    enabled: true
EOF

# Playbook principal con variables que documentan el cambio
cat > playbooks/deploy-nginx.yml << 'EOF'
---
# change_id: CHG-2024-0047
# motivo: Migración a TLS 1.3, eliminación de cipher suites débiles
# revertir: git checkout HEAD~1 roles/nginx && ansible-playbook playbooks/deploy-nginx.yml
# ventana: 2024-03-15 02:00-04:00 UTC
# aprobado_por: ops-team@empresa.com

- hosts: webservers
  become: true
  gather_facts: true

  pre_tasks:
    - name: Capturar estado de nginx antes del cambio
      ansible.builtin.command: systemctl is-active nginx
      register: nginx_estado_previo
      failed_when: false  # no abortar si nginx está caído; eso es información

    - name: Registrar timestamp de inicio del cambio
      ansible.builtin.set_fact:
        cambio_inicio: "{{ ansible_date_time.iso8601 }}"

  vars:
    server_name: "app.empresa.com"
    ansible_repo_url: "https://git.empresa.com/infra"
    # ansible_commit_sha se inyecta desde CI/CD; valor de fallback para ejecución manual
    ansible_commit_sha: "{{ lookup('env', 'GIT_COMMIT') | default('manual-' + ansible_date_time.epoch) }}"

  roles:
    - nginx

  post_tasks:
    - name: Verificar que nginx responde tras el cambio
      ansible.builtin.uri:
        url: "https://{{ server_name }}/health"
        validate_certs: true
        status_code: 200
      register: health_check
      # Si este task falla, Ansible reporta el error. El siguiente paso
      # es ejecutar el procedimiento de rollback del runbook.

    - name: Registrar resultado en el log de cambios
      ansible.builtin.lineinfile:
        path: /var/log/change-management.log
        line: >-
          {{ cambio_inicio }} | CHG-2024-0047 | nginx | estado_previo={{ nginx_estado_previo.stdout }}
          | health_post={{ health_check.status }} | commit={{ ansible_commit_sha }}
        create: true
        mode: "0640"
EOF

# ── 3. Runbook mínimo para este servicio ────────────────────────────────
cat > runbooks/nginx-rollback.md << 'EOF'
# Runbook: Rollback de nginx

## Cuándo usar esto
Cuando el health check de /health devuelve != 200 tras un deploy,
o cuando nginx no está en estado `active (running)`.

## Pasos (ejecutar en orden, no saltarse ninguno)

1. Verificar estado actual:
   systemctl status nginx
   journalctl -u nginx --since "10 minutes ago" --no-pager

2. Revertir configuración al commit anterior:
   cd /opt/infra
   git log --oneline -5          # identificar el commit bueno
   git checkout <commit-hash> roles/nginx
   ansible-playbook playbooks/deploy-nginx.yml

3. Si Ansible no está disponible, rollback manual de etckeeper:
   etckeeper vcs log /etc/nginx/nginx.conf   # ver historial
   etckeeper vcs checkout HEAD~1 -- nginx/nginx.conf
   nginx -t && systemctl reload nginx

4. Confirmar resolución:
   curl -I https://app.empresa.com/health
   echo "$(date -u --iso-8601=seconds) | rollback completado por $(whoami)" \
     >> /var/log/change-management.log

5. Abrir postmortem en el tracker (obligatorio, aunque el impacto fuera mínimo).
EOF

# Commit inicial del repositorio de infraestructura
git add .
git commit -m "CHG-2024-0047: configuración nginx con TLS, plantilla Ansible, runbook de rollback"

echo "Repositorio de infraestructura inicializado en ${INFRA_REPO}"
echo "Estado de /etc bajo control de versiones con etckeeper"

El script anterior está construido alrededor de decisiones que merecen explicarse porque cada una resuelve un problema real de producción.

etckeeper no es simplemente “git para /etc”. El problema de hacer git init /etc directamente es que git no almacena permisos de ficheros más allá del bit de ejecución, ni propietarios, ni ACLs. etckeeper resuelve exactamente eso, y además se engancha con apt para hacer commits automáticos antes y después de cualquier instalación de paquetes. Cuando alguien te pregunta “¿qué cambió en el sistema en las últimas dos semanas?”, git log /etc tiene la respuesta.

La directiva validate: "/usr/sbin/nginx -t -c %s" en el task de Ansible es el mecanismo que evita que una configuración sintácticamente inválida llegue a sustituir la configuración en producción. Ansible escribe el fichero en una ubicación temporal, ejecuta el validador sobre ese fichero temporal, y solo si el validador retorna 0 mueve el fichero al destino. Sin esto, un error tipográfico en la plantilla Jinja2 deja nginx caído sin configuración válida.

El uso de reloaded en lugar de restarted en el handler es una diferencia crítica bajo carga. systemctl restart nginx cierra los workers activos, lo que interrumpe las conexiones HTTP/1.1 persistentes y los streams HTTP/2 en curso. systemctl reload nginx envía SIGUSR1 al proceso maestro, que lanza nuevos workers con la configuración actualizada y deja que los workers antiguos terminen sus conexiones en curso antes de salir. En un servidor con tráfico real, la diferencia es visible en las métricas de error rate.

El campo ansible_commit_sha inyectado en la configuración generada por la plantilla cierra el círculo de trazabilidad: el fichero desplegado en el servidor lleva embebida la referencia al commit exacto que lo produjo. Cuando lees el fichero en producción puedes ir directamente al repositorio y ver el diff, el autor y el mensaje del commit que explica por qué existe esa configuración.

El log en /var/log/change-management.log puede parecer redundante si tienes un sistema de ticketing, pero es local al servidor y sobrevive a la caída del sistema de ticketing. Durante un incidente activo, no quieres depender de una herramienta externa para responder “¿qué cambió en este servidor en las últimas 4 horas?”.

El runbook incluye deliberadamente la rama de emergencia que no usa Ansible (paso 3). En un incidente real, el sistema de automatización puede estar inaccesible, las credenciales SSH pueden haber expirado, o el propio repositorio puede estar inalcanzable si el incidente afecta la red. Un runbook que solo funciona cuando todo lo demás funciona no es un runbook de incidencias.

106

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio