--- title: "Cómo detectar configuration drift con Ansible y Terraform" description: "Detecta cambios manuales en servidores Ubuntu con Ansible en modo check, terraform plan y AIDE, programa la comprobación con systemd y corrige la desviación." category: "Automatización e IaC" language: es source: https://cubepath.com/es/docs/automatización-e-iac/deteccion-de-desviacion-de-configuracion-del-servidor updated: 2026-09-25 --- # Cómo detectar configuration drift con Ansible y Terraform La desviación de configuración (*configuration drift*) aparece cuando el estado real de un servidor deja de coincidir con el que define tu código: alguien edita un archivo por SSH para apagar un incendio, un paquete se actualiza fuera del pipeline o un recurso se modifica desde el panel del proveedor. En este tutorial detectarás esas desviaciones en tres niveles: la configuración del sistema operativo con Ansible en modo check, la infraestructura con `terraform plan`, y los archivos críticos con AIDE. Después programarás la comprobación con un temporizador de systemd y verás cómo corregir lo que encuentre. ## Requisitos previos - Un nodo de control con Ubuntu 24.04 LTS desde el que ejecutarás Ansible (puede ser tu equipo o un VPS de CubePath). - Uno o más servidores gestionados con Ubuntu 24.04, accesibles por SSH con clave desde el nodo de control, con un usuario con `sudo`. - Para la parte de Terraform, un proyecto de Terraform ya desplegado con su estado accesible (local o en un backend remoto). - Conocimientos básicos de Ansible y Terraform. ## Paso 1: Instalar Ansible y definir el inventario En el nodo de control, instala Ansible desde los repositorios de Ubuntu y `jq`, que usarás más adelante para leer los resultados: ```bash sudo apt update sudo apt install ansible jq ``` Crea un directorio para el proyecto y el inventario con tus servidores: ```bash sudo mkdir -p /opt/drift sudo chown "$USER": /opt/drift nano /opt/drift/inventory.ini ``` ```ini [web] web1 ansible_host=203.0.113.10 web2 ansible_host=203.0.113.11 [all:vars] ansible_user=your_user ``` Sustituye las IP por las de tus servidores y `your_user` por el usuario remoto. Comprueba que Ansible llega a todos ellos: ```bash cd /opt/drift ansible -i inventory.ini all -m ping ``` ```output web1 | SUCCESS => { "changed": false, "ping": "pong" } web2 | SUCCESS => { "changed": false, "ping": "pong" } ``` ## Paso 2: Describir el estado deseado en un playbook Ansible solo puede detectar desviaciones en lo que gestiona. Este playbook de ejemplo define tres cosas habituales: paquetes que deben estar instalados, una configuración de SSH endurecida y un servicio que debe estar activo. Adáptalo a lo que realmente gestiones. ```bash nano /opt/drift/site.yml ``` ```yaml - name: Estado base de los servidores hosts: all become: true tasks: - name: Paquetes base instalados ansible.builtin.apt: name: - chrony - unattended-upgrades state: present - name: Configuración de SSH endurecida ansible.builtin.copy: dest: /etc/ssh/sshd_config.d/50-hardening.conf owner: root group: root mode: "0644" content: | PermitRootLogin no PasswordAuthentication no MaxAuthTries 3 notify: Reload ssh - name: chrony activo y habilitado ansible.builtin.service: name: chrony state: started enabled: true handlers: - name: Reload ssh ansible.builtin.service: name: ssh state: reloaded ``` > Warning: `PasswordAuthentication no` desactiva el acceso por contraseña. Confirma que entras a los servidores con clave SSH antes de aplicarlo. Aplica el playbook una vez para que los servidores partan del estado deseado: ```bash ansible-playbook -i inventory.ini site.yml ``` Vuelve a ejecutarlo. Un playbook idempotente no debe cambiar nada en la segunda pasada: ```output PLAY RECAP ********************************************************************* web1 : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 web2 : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 ``` Esta propiedad es la base de todo lo que sigue: si una ejecución en modo check informa de `changed` mayor que cero, el servidor se ha desviado. ## Paso 3: Detectar desviaciones con --check y --diff Simula un cambio manual en `web1`, como haría alguien que entra por SSH para depurar un problema: ```bash ssh -t your_user@203.0.113.10 "echo 'MaxAuthTries 10' | sudo tee /etc/ssh/sshd_config.d/50-hardening.conf" ``` Desde el nodo de control, ejecuta el playbook en modo check. `--check` no modifica nada en los servidores y `--diff` muestra la diferencia exacta entre lo que hay y lo que debería haber: ```bash ansible-playbook -i inventory.ini site.yml --check --diff ``` ```output TASK [Configuración de SSH endurecida] ***************************************** --- before: /etc/ssh/sshd_config.d/50-hardening.conf +++ after: /etc/ssh/sshd_config.d/50-hardening.conf @@ -1 +1,3 @@ -MaxAuthTries 10 +PermitRootLogin no +PasswordAuthentication no +MaxAuthTries 3 changed: [web1] ok: [web2] PLAY RECAP ********************************************************************* web1 : ok=4 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 web2 : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 ``` La salida identifica el servidor (`web1`), el archivo y el contenido que alguien ha cambiado. Para limitar la comprobación a un grupo o a un servidor concreto, añade `--limit web1`. > Note: Algunos módulos no pueden predecir su resultado en modo check (por ejemplo, `command` y `shell` se omiten). Si tu playbook depende de ellos, marca esas tareas con `check_mode: false` solo cuando sean de lectura, o sustitúyelas por módulos específicos. ## Paso 4: Programar la comprobación con systemd Revisar la salida a mano no escala. El callback `json` de Ansible devuelve el resultado en un formato que puedes procesar con `jq`: el objeto `stats` contiene, por servidor, cuántas tareas habrían cambiado. Crea un script que ejecute la comprobación y termine con error si hay desviaciones: ```bash sudo nano /usr/local/bin/drift-check ``` ```bash #!/usr/bin/env bash set -euo pipefail cd /opt/drift rc=0 report=$(ANSIBLE_STDOUT_CALLBACK=json ansible-playbook -i inventory.ini site.yml --check) || rc=$? # rc 2 = alguna tarea falló, rc 4 = algún servidor inalcanzable if [[ -z "$report" ]] || (( rc != 0 && rc != 2 && rc != 4 )); then echo "ansible-playbook terminó con código ${rc}" >&2 exit 2 fi unreachable=$(jq -r '.stats | to_entries[] | select(.value.unreachable > 0 or .value.failures > 0) | .key' <<<"$report") drifted=$(jq -r '.stats | to_entries[] | select(.value.changed > 0) | "\(.key) (\(.value.changed) cambios)"' <<<"$report") if [[ -n "$unreachable" ]]; then echo "Servidores con errores o inalcanzables: ${unreachable//$'\n'/, }" >&2 fi if [[ -n "$drifted" ]]; then echo "Desviación detectada: ${drifted//$'\n'/, }" exit 1 fi if [[ -n "$unreachable" ]]; then exit 2 fi echo "Sin desviaciones" ``` Hazlo ejecutable y pruébalo. Como `web1` sigue modificado, debe informar de la desviación y salir con código 1: ```bash sudo chmod 755 /usr/local/bin/drift-check drift-check; echo "exit=$?" ``` ```output Desviación detectada: web1 (1 cambios) exit=1 ``` Crea un servicio de systemd que ejecute el script con tu usuario, que es el que tiene las claves SSH: ```bash sudo nano /etc/systemd/system/drift-check.service ``` ```ini [Unit] Description=Detección de configuration drift con Ansible Wants=network-online.target After=network-online.target [Service] Type=oneshot User=your_user ExecStart=/usr/local/bin/drift-check ``` Y el temporizador que lo lanza cada hora: ```bash sudo nano /etc/systemd/system/drift-check.timer ``` ```ini [Unit] Description=Comprobación horaria de configuration drift [Timer] OnCalendar=hourly RandomizedDelaySec=5m Persistent=true [Install] WantedBy=timers.target ``` Activa el temporizador y lanza una ejecución inmediata para comprobarlo: ```bash sudo systemctl daemon-reload sudo systemctl enable --now drift-check.timer sudo systemctl start drift-check.service journalctl -u drift-check.service -n 5 --no-pager ``` ```output Sep 25 10:00:14 control drift-check[4121]: Desviación detectada: web1 (1 cambios) Sep 25 10:00:14 control systemd[1]: drift-check.service: Main process exited, code=exited, status=1/FAILURE Sep 25 10:00:14 control systemd[1]: drift-check.service: Failed with result 'exit-code'. ``` Una desviación deja el servicio en estado `failed`, así que cualquier sistema de monitorización que vigile unidades fallidas (`systemctl --failed`, node_exporter con el collector de systemd, etc.) la detectará sin configuración adicional. ## Paso 5: Detectar desviaciones en la infraestructura con Terraform Ansible cubre lo que hay dentro del servidor. Los cambios en la infraestructura en sí (una regla de firewall añadida desde el panel, un disco redimensionado a mano) se detectan con Terraform. Desde el directorio de tu proyecto, `-refresh-only` compara el estado guardado con la realidad sin proponer cambios: ```bash terraform plan -refresh-only ``` ```output Note: Objects have changed outside of Terraform Terraform detected the following changes made outside of Terraform since the last "terraform apply" which may have affected this plan: ``` Debajo aparece cada recurso modificado o borrado fuera de Terraform. Para automatizarlo, usa `-detailed-exitcode`, que cambia el código de salida de `terraform plan`: | Código | Significado | |---|---| | `0` | La infraestructura coincide con el código | | `1` | Error al ejecutar el plan | | `2` | Hay diferencias entre el código y la realidad | ```bash terraform plan -detailed-exitcode -input=false -lock=false > /dev/null; echo "exit=$?" ``` ```output exit=2 ``` `-lock=false` evita que la comprobación periódica bloquee el estado mientras alguien aplica cambios. Puedes añadir esta llamada al mismo script del paso anterior o ejecutarla como job programado en tu CI, que ya tiene las credenciales del proveedor. ## Paso 6: Vigilar archivos críticos con AIDE Ansible y Terraform solo detectan cambios en lo que declaras. AIDE (Advanced Intrusion Detection Environment) cubre el resto: guarda hashes y permisos de los archivos del sistema y avisa de cualquier modificación, esté o no gestionada. Instálalo en cada servidor gestionado: ```bash sudo apt install aide ``` Si apt te pide configurar Postfix para el envío de correo, elige **Sin configuración** si no vas a enviar los informes por email. Genera la base de datos inicial. Tarda unos minutos porque recorre todo el sistema de archivos: ```bash sudo aideinit sudo cp /var/lib/aide/aide.db.new /var/lib/aide/aide.db ``` Provoca un cambio y ejecuta una comprobación: ```bash echo "# prueba" | sudo tee -a /etc/hosts sudo aide --check --config /etc/aide/aide.conf ``` ```output AIDE found differences between database and filesystem!! Summary: Total number of entries: 160358 Added entries: 0 Removed entries: 0 Changed entries: 1 --------------------------------------------------- Changed entries: --------------------------------------------------- f ... .C... : /etc/hosts ``` El paquete de Ubuntu ya programa una comprobación diaria; verifica cómo lo hace en tu sistema con `systemctl list-timers | grep -i aide` o `ls /etc/cron.daily/`. Cuando un cambio sea legítimo (tras aplicar tu playbook, por ejemplo), actualiza la base de datos para que deje de aparecer: ```bash sudo aide --update --config /etc/aide/aide.conf sudo cp /var/lib/aide/aide.db.new /var/lib/aide/aide.db ``` Elimina la línea de prueba de `/etc/hosts` antes de actualizar la base de datos. ## Paso 7: Corregir la desviación Detectar solo es la mitad del trabajo. Ante cada desviación hay dos decisiones posibles: - **El cambio manual era un error o ya no hace falta**: vuelve al estado declarado. Con Ansible, ejecuta el playbook sin `--check` limitado al servidor afectado. Con Terraform, `terraform apply` revierte el recurso a lo que dice el código. - **El cambio manual era necesario**: llévalo al código (el playbook, el módulo de Terraform), haz commit y aplica. En Terraform, si el cambio es solo en el estado (un recurso que ya no existe, atributos actualizados), `terraform apply -refresh-only` registra la realidad en el estado sin tocar nada. Corrige la desviación de `web1`: ```bash cd /opt/drift ansible-playbook -i inventory.ini site.yml --limit web1 drift-check ``` ```output Sin desviaciones ``` La regla que evita que el drift vuelva a aparecer es organizativa más que técnica: cualquier cambio en producción pasa por el repositorio, y los accesos por SSH para depurar terminan con el cambio llevado al código o revertido. ## Solución de problemas - **Una tarea aparece siempre como `changed` en modo check**: el playbook no es idempotente (típico de `command`, `shell` o plantillas con marcas de tiempo). Añade `creates`, `changed_when` o sustitúyela por un módulo específico; si no, generará falsas alarmas cada hora. - **`drift-check` funciona a mano pero falla desde systemd**: el servicio corre como `your_user` sin tu entorno de shell. Comprueba que la clave SSH está en `~/.ssh` de ese usuario y que los servidores están en su `known_hosts`. - **`terraform plan` muestra diferencias que nadie ha provocado**: algunos proveedores normalizan atributos (mayúsculas, orden de listas). Revisa el diff y, si el atributo lo gestiona otro sistema, usa `lifecycle { ignore_changes = [...] }` en ese recurso. - **AIDE informa de cientos de cambios tras cada `apt upgrade`**: es esperado. Actualiza la base de datos después de cada actualización planificada. ## Conclusión Tienes detección de configuration drift en tres capas: Ansible en modo check para la configuración del sistema, `terraform plan -detailed-exitcode` para la infraestructura y AIDE para cambios en archivos no gestionados, con una comprobación horaria que deja rastro en el journal. Como siguientes pasos, puedes enviar el resultado de `drift-check` a tu canal de alertas con `OnFailure=` en la unidad de systemd, ejecutar la comprobación de Terraform como job programado en tu CI y ampliar el playbook para cubrir usuarios, reglas de UFW y parámetros de `sysctl`.