Ansible resuelve un problema concreto: tienes diez, cincuenta, doscientos servidores y necesitas garantizar que todos tienen exactamente el mismo estado. No “más o menos el mismo”, sino el mismo. La alternativa —un documento con pasos manuales, o un script bash que se fue parcheando durante tres años— funciona hasta que no funciona, y cuando falla no sabes en qué servidor ni desde qué ejecución.
La arquitectura de Ansible es deliberadamente minimalista: el nodo de control (tu máquina, un servidor CI, lo que sea) conecta por SSH a los hosts gestionados y ejecuta Python ahí. No hay daemons, no hay certificados TLS que rotar, no hay agentes que mantener actualizados. Los prerrequisitos en el destino son Python 3 y una cuenta con privilegios suficientes. Eso es todo.
El elemento central es el playbook: un archivo YAML que describe el estado deseado del sistema en términos de tareas (tasks), cada una invocando un módulo —apt, copy, systemd, user, template— con los parámetros necesarios. Ansible traduce cada tarea a código Python, lo copia al host remoto via SSH, lo ejecuta, y devuelve el resultado. El inventario (inventory) es la lista de hosts sobre los que opera ese playbook, agrupados como necesites.
El principio que hace todo esto sostenible es la idempotencia: ejecutar el mismo playbook dos veces, diez veces, produce exactamente el mismo estado final. Si el paquete ya está instalado en la versión correcta, Ansible no lo reinstala. Si el archivo ya tiene el contenido correcto, no lo sobreescribe. Cada módulo comprueba el estado actual antes de actuar. Esto importa en producción porque puedes ejecutar un playbook como verificación, como remediación, o como parte de un pipeline de CI sin miedo a efectos secundarios acumulativos.
Si tienes un script que instala nginx, configura un virtualhost y arranca el servicio, y lo ejecutas dos veces, puede que falle la segunda porque el usuario ya existe, o que arranque el servicio dos veces, o que añada una línea de configuración por duplicado. Con Ansible, la segunda ejecución simplemente confirma que el estado es el correcto y termina sin cambios. Cuando algo sí cambia —una tarea reporta changed en lugar de ok— es información: ese host estaba desviado del estado deseado.
Lo que rompes si lo haces mal: tareas no idempotentes (usar el módulo command o shell sin creates o when adecuados), inventarios desactualizados con hosts que ya no existen, o variables sin validar en templates Jinja2 que generan configuraciones silenciosamente incorrectas.
# Instalación en el nodo de control (Debian Bookworm)
apt install ansible python3-paramiko
# Estructura del proyecto — evita /etc/ansible para proyectos reales;
# un directorio propio por proyecto es más mantenible
mkdir -p ~/infra-web/{roles/nginx/{tasks,templates,handlers},group_vars}
cd ~/infra-web
# ── inventory.ini ────────────────────────────────────────────────────────
cat > inventory.ini << 'EOF'
[web]
web01.ejemplo.com ansible_user=admin
web02.ejemplo.com ansible_user=admin
[db]
db01.ejemplo.com ansible_user=admin
# Variables aplicables a todos los hosts del grupo web
[web:vars]
http_port=80
EOF
# ── group_vars/all.yml — variables globales ──────────────────────────────
cat > group_vars/all.yml << 'EOF'
ntp_server: ntp.ejemplo.com
admin_email: ops@ejemplo.com
EOF
# ── roles/nginx/tasks/main.yml ───────────────────────────────────────────
cat > roles/nginx/tasks/main.yml << 'EOF'
---
- name: Instalar nginx
ansible.builtin.apt:
name: nginx
state: present # "present" es idempotente; "latest" forzaría upgrade siempre
update_cache: true
cache_valid_time: 3600 # no ejecuta apt-get update si la caché tiene menos de 1h
- name: Desplegar configuración del virtualhost
ansible.builtin.template:
src: vhost.conf.j2
dest: /etc/nginx/sites-available/web.conf
owner: root
group: root
mode: '0644'
notify: Recargar nginx # solo dispara el handler si esta tarea reporta "changed"
- name: Activar el virtualhost
ansible.builtin.file:
src: /etc/nginx/sites-available/web.conf
dest: /etc/nginx/sites-enabled/web.conf
state: link
- name: Garantizar que nginx está habilitado y corriendo
ansible.builtin.systemd:
name: nginx
enabled: true
state: started
EOF
# ── roles/nginx/handlers/main.yml ───────────────────────────────────────
# Los handlers se ejecutan al final del play, una sola vez,
# aunque múltiples tareas los hayan notificado
cat > roles/nginx/handlers/main.yml << 'EOF'
---
- name: Recargar nginx
ansible.builtin.systemd:
name: nginx
state: reloaded
EOF
# ── roles/nginx/templates/vhost.conf.j2 ─────────────────────────────────
# Jinja2: {{ variable }}, {% if %}, {% for %} — Ansible resuelve esto
# contra las variables del host antes de copiar el archivo
cat > roles/nginx/templates/vhost.conf.j2 << 'EOF'
server {
listen {{ http_port }};
server_name {{ inventory_hostname }};
access_log /var/log/nginx/{{ inventory_hostname }}-access.log;
error_log /var/log/nginx/{{ inventory_hostname }}-error.log;
location / {
root /var/www/html;
index index.html;
}
}
EOF
# ── site.yml — playbook principal ────────────────────────────────────────
cat > site.yml << 'EOF'
---
- name: Configurar servidores web
hosts: web
become: true # equivale a sudo; requiere que ansible_user tenga privilegios
roles:
- nginx
EOF
# ── Verificar conectividad antes de ejecutar ─────────────────────────────
# El módulo ping no es ICMP: comprueba SSH + Python en el destino
ansible -i inventory.ini web -m ping
# ── Ejecución en modo dry-run (check) ────────────────────────────────────
# No aplica cambios; muestra qué cambiaría. Útil antes de tocar producción.
ansible-playbook -i inventory.ini site.yml --check --diff
# ── Ejecución real ───────────────────────────────────────────────────────
ansible-playbook -i inventory.ini site.yml
# ── Ejecución limitada a un host específico ──────────────────────────────
ansible-playbook -i inventory.ini site.yml --limit web01.ejemplo.com
# ── Ver hechos (facts) de un host: hardware, OS, IPs, etc. ───────────────
# ansible_facts se puede usar en cualquier tarea o template
ansible -i inventory.ini web01.ejemplo.com -m setup | grep ansible_os_family
Qué está pasando aquí exactamente
El módulo apt con state: present y cache_valid_time: 3600 hace dos cosas inteligentes a la vez: garantiza que la caché de APT no sea más antigua de una hora (evita reinstalar paquetes innecesariamente pero tampoco trabaja con índices de hace tres semanas), y no reinstala nginx si ya está presente. state: latest haría un upgrade en cada ejecución, que puede ser lo que quieres en un entorno de desarrollo y exactamente lo que no quieres en producción un martes por la tarde.
El módulo template es donde Jinja2 entra en juego. Cuando Ansible procesa vhost.conf.j2, sustituye {{ http_port }} por el valor definido en [web:vars] del inventario, e {{ inventory_hostname }} por el nombre del host que se está configurando en ese momento. El mismo template genera una configuración personalizada para web01 y otra para web02 en la misma ejecución. Si el archivo resultante es idéntico al que ya existe en el destino, la tarea reporta ok y el handler Recargar nginx no se dispara. Si hay diferencia, la tarea reporta changed y el handler se encola para ejecutarse al final del play —una sola vez, aunque cinco tareas lo hayan notificado.
El handler usando systemd con state: reloaded en lugar de restarted es una decisión deliberada: nginx puede recargar su configuración sin cortar conexiones activas. restarted cerraría todas las conexiones en curso. Para un cambio de configuración de virtualhost, reloaded es siempre la opción correcta.
become: true en el playbook significa que Ansible ejecutará las tareas con sudo. El usuario definido en ansible_user necesita estar en sudoers (con o sin contraseña, según tu política). Si necesitas contraseña sudo, añade --ask-become-pass al comando ansible-playbook.
El flujo --check --diff antes de la ejecución real es lo más cercano que vas a tener a una preview de cambios en producción. --check simula la ejecución sin aplicar nada; --diff muestra las diferencias línea a línea en archivos gestionados por template o copy. No es infalible —algunos módulos no soportan check mode correctamente, especialmente los que ejecutan comandos— pero cubre el 90% de los casos y te salva de sorpresas.
La separación entre inventory.ini, group_vars/, roles/ y site.yml no es arbitraria: es la estructura que escala. Cuando tienes veinte roles y variables específicas por entorno (group_vars/production/, group_vars/staging/), mantener todo en un único playbook monolítico se vuelve inmanejable rápidamente. Los roles también son reutilizables: el rol nginx de este proyecto puede compartirse entre proyectos distintos o publicarse en Ansible Galaxy.
N° 105