Playbook de Ansible para Configuración de Servidores
Cómo escribir y ejecutar playbooks de Ansible para aprovisionar, configurar y administrar servidores con tareas idempotentes, roles e inventarios.
Descripción General
Ansible es automatización sin agentes — ningún demonio en el destino, solo SSH y un playbook YAML. Esa única propiedad es por la que ganó la guerra de la gestión de configuración: aprovisiona una máquina una vez (con Terraform o a mano), y deja que un playbook describa cómo debería quedar, idempotente y versionado.
Antes de Ansible, los sysadmins entraban a cada servidor para instalar paquetes y reiniciar servicios — lento, irrepetible, inauditable. Un playbook sustituye eso con una descripción declarativa que configura cientos de hosts en minutos y vive en git junto a la app que despliega — que es también cómo se complementa con el pipeline de CI/CD que lo ejecuta y las imágenes Docker que empaqueta.
Cuándo Usar
Usa Ansible cuando el mismo cambio tiene que ocurrir en más de una máquina. Aprovisionar una base (paquetes, usuarios, claves SSH) en toda una flota, empujar una actualización de app a todos los servidores web a la vez, mantener configs idénticas de Nginx/Postgres/Redis en cada nodo, o ejecutar un comando puntual en todas partes — reiniciar tras un parche de seguridad, por ejemplo. Y cuando el playbook necesita una contraseña, Ansible Vault la cifra en vez de commitearla en claro.
Si solo tienes un servidor y no prevés crecer, un script de shell es sinceramente más simple. El playbook empieza a compensar en el momento en que existe un segundo host.
Sáltalo cuando el trabajo no es configuración en absoluto: construir imágenes golden es cosa de Packer, las imágenes de contenedor inmutables son cosa de tu Dockerfile, y los comandos puntuales (ansible webservers -m ping) no necesitan archivo de playbook. Ansible también sufre donde el destino no acepta SSH — para eso está la API del proveedor cloud o una herramienta con agente.
Solución
Una configuración mínima que funciona son tres archivos: un inventario, el playbook y un archivo de configuración. Este instala Nginx, despliega una config y recarga el servicio solo cuando el archivo cambia de verdad:
# inventory.ini — los hosts que gestiona el playbook
[webservers]
web1.example.com
web2.example.com ansible_user=deployer
[webservers:vars]
ansible_port=22
# site.yml — el playbook en sí
- name: Configure web servers
hosts: webservers
become: yes
vars:
nginx_worker_processes: 4
tasks:
- name: Install Nginx
ansible.builtin.apt:
name: nginx
state: present
update_cache: yes
- name: Deploy nginx.conf
ansible.builtin.template:
src: templates/nginx.conf.j2
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: "0644"
notify: Reload nginx
- name: Ensure Nginx is running and enabled
ansible.builtin.service:
name: nginx
state: started
enabled: yes
handlers:
- name: Reload nginx
ansible.builtin.service:
name: nginx
state: reloaded
# ansible.cfg — configuración a nivel de proyecto
[defaults]
inventory = inventory.ini
retry_files_enabled = False
host_key_checking = False
Ejecútalo:
# Dry run — muestra qué cambiaría, sin cambiar nada
ansible-playbook site.yml --check --diff
# Ejecución real
ansible-playbook site.yml
# Solo contra un host
ansible-playbook site.yml --limit web1.example.com
Explicación
El play apunta al grupo webservers del inventario, y cada tarea invoca un módulo con un estado final declarado — state: present dice “asegura que esto esté instalado”, no “ejecuta apt-get”. Eso es lo que hace que la segunda ejecución sea un no-op: Ansible comprueba si el sistema ya coincide antes de hacer nada. La tarea de template registra un notify al handler Reload nginx, que dispara una vez al final del play y solo si el archivo renderizado difirió — sin recargas inútiles en cada ejecución.
become: yes a nivel de play eleva a root en toda la ejecución; ajústalo a become por tarea cuando un playbook mezcle trabajo privilegiado y no privilegiado (la sección de Errores Comunes muestra cómo). Y el par --check --diff es el hábito que más importa: --check simula, --diff muestra los cambios reales de archivo, y juntos son la diferencia entre un despliegue confiado y una plegaria.
Dos piezas del modelo de Ansible conviene interiorizar antes de escalar más allá de un playbook. Los facts son los datos por host que Ansible recolecta al inicio de cada play (familia de OS, IPs, memoria) — ansible_os_family == "Debian" en un when: es un fact haciendo trabajo real. La precedencia de variables decide cuál de los ocho lugares donde puedes definir nginx_worker_processes gana de verdad: las vars del playbook ganan a las del inventario, group_vars gana a los defaults del host, y extra_vars (-e) gana a todo. Cuando un playbook “ignora” tu variable, no la está ignorando — algo con mayor precedencia la está sobrescribiendo.
Cuando un playbook crece más allá de una pantalla de tareas, sepáralo en roles: un directorio roles/nginx/ con tasks/, handlers/, templates/ y defaults/ que un play consume como roles: [nginx]. Es la unidad que comparte Ansible Galaxy, y también cómo un monorepo evita que diez playbooks redeclaren la misma configuración de servidor web. Empieza plano, refactoriza a rol la segunda vez que copies un bloque de tareas — ese orden es deliberado, porque la estructura de roles prematura es su propio tipo de desorden.
Errores Comunes
Los errores se repiten en todas partes. Ejecutar un playbook sin --check primero — el modo check muestra qué cambiaría antes de que nada lo haga, y en producción eso no es opcional. Usar shell cuando existe un módulo: los módulos son idempotentes y manejan los casos borde que tu one-liner no maneja. Olvidar become: yes y perseguir un error de permisos críptico durante una hora. Hardcodear IPs en el inventario — se pudren; usa DNS o un inventario dinámico que se mantenga actualizado. Y reiniciar Nginx en cada tarea en vez de notificar a un handler — los handlers disparan una vez, al final, cuando algo cambió de verdad.
- Saltarse el caché de facts. Recolectar facts es lento a escala de flota:
# ansible.cfg
[defaults]
gathering = smart
fact_caching = jsonfile
fact_caching_connection = ./.facts
fact_caching_timeout = 86400
- Ejecutar todo como root. Usa
becomeselectivo por tarea:
# Mal: todo corre como root
- hosts: all
become: yes
tasks:
- name: Clone repo as root
ansible.builtin.git:
repo: https://github.com/myorg/myapp.git
dest: /home/deployer/myapp
# Bien: become solo donde hace falta
- hosts: all
tasks:
- name: Install packages
ansible.builtin.apt:
name: nginx
state: present
become: yes
- name: Clone repo as deployer
ansible.builtin.git:
repo: https://github.com/myorg/myapp.git
dest: /home/deployer/myapp
become: yes
become_user: deployer
- No usar
--diffpara auditar. Ve exactamente qué cambia en cada ejecución:
ansible-playbook site.yml --diff
Lo que Funciona
Los playbooks deberían ser aburridos — ese es el punto. Haz cada tarea idempotente: la segunda ejecución no debería cambiar nada, lo que significa state: present por encima de comandos shell que reinstalan a ciegas. Extrae las tareas compartidas a roles — eso es lo que las hace compartibles vía Galaxy o un repo interno en vez de copiar-pegar. Todo vive en git — playbooks, inventarios, variables — así los cambios se revisan y se revierten. Ejecuta ansible-lint antes de publicar; atrapa los errores que cuestan una ventana de despliegue. Y cifra los secretos con Ansible Vault; una contraseña en claro commiteada es un incidente esperando ocurrir.
Tips de Rendimiento
- Ejecuta
strategy: freepara más velocidad. Los hosts dejan de esperarse entre sí:
- name: Fast parallel deployment
hosts: webservers
strategy: free
tasks:
- name: Deploy app
ansible.builtin.git:
repo: https://github.com/myorg/myapp.git
dest: /var/www/myapp
- Desactiva la recolección de facts cuando no se use. Ahorra 2-5 segundos por host:
- name: Simple task without facts
hosts: webservers
gather_facts: no
tasks:
- name: Restart service
ansible.builtin.service:
name: nginx
state: restarted
- Usa
asyncpara tareas largas. No bloquees el playbook:
- name: Run slow backup
ansible.builtin.shell: pg_dump mydb > /tmp/backup.sql
async: 300
poll: 5
Archivos complementarios: el repo companion de ansible-playbook tiene los tres archivos de arriba — inventory.ini, site.yml, ansible.cfg — listos para ejecutar.
Lectura Adicional
- Ansible Documentation — Playbooks — la guía oficial
- Red Hat — Ansible Best Practices — consejos de la fuente
Preguntas frecuentes
¿Qué ventaja tiene Ansible sobre los scripts de shell?
Los playbooks declaran el estado final en vez de programar los pasos — ejecútalo dos veces y la segunda no cambia nada, mientras un script de shell reinstala alegremente el mismo paquete o falla.
¿Cómo gestiono los secretos en Ansible?
Cífralos con Ansible Vault, o sácalos de un gestor de secretos externo (HashiCorp Vault, AWS Secrets Manager). La única regla que importa: nada de contraseñas en claro en git.
¿Qué es un inventario de Ansible?
La lista de hosts que Ansible gestiona, agrupados como tenga sentido — web, db, cache. Puede ser un archivo estático o un plugin dinámico que trae la flota viva directo del proveedor cloud.
¿Cómo uso un inventario dinámico con proveedores cloud?
Apunta ansible.cfg a un archivo de plugin en vez de un inventario estático — el plugin consulta la API del proveedor en tiempo de ejecución:
# ansible.cfg
[defaults]
inventory = aws_ec2.yml
host_key_checking = False
retry_files_enabled = False
# aws_ec2.yml
plugin: aws_ec2
regions:
- us-east-1
- us-west-2
keyed_groups:
- prefix: tag
key: tags
host_filters:
- tag:Name=production-*
compose:
ansible_host: public_ip_address
ansible_user: "'ubuntu'"
# Listar inventario dinámico
ansible-inventory --list
# Vista de grafo
ansible-inventory --graph
¿Cómo ejecuto el mismo playbook en varios entornos?
Mantén un solo playbook y separa el estado: un directorio environments/ con un inventario por entorno, y group_vars para los valores que difieren:
ansible-playbook site.yml -i environments/production/inventory.ini
ansible-playbook site.yml -i environments/staging/inventory.ini
environments/
├── staging/
│ ├── inventory.ini
│ └── group_vars/webservers.yml
└── production/
├── inventory.ini
└── group_vars/webservers.yml
¿Cómo ejecuto tareas condicionales o en bucle?
when: condiciona una tarea a un fact o variable; loop: la repite sobre una lista — los dos cubren la mayoría de lo que el if/for de shell hacía:
- name: Configure database servers
hosts: dbservers
become: yes
tasks:
- name: Install PostgreSQL on Debian
ansible.builtin.apt:
name: postgresql
state: present
when: ansible_os_family == "Debian"
- name: Install PostgreSQL on RHEL
ansible.builtin.yum:
name: postgresql-server
state: present
when: ansible_os_family == "RedHat"
- name: Create databases
ansible.builtin.postgresql_db:
name: "{{ item.name }}"
encoding: "{{ item.encoding | default('UTF8') }}"
owner: "{{ item.owner }}"
loop:
- { name: app_db, owner: appuser }
- { name: log_db, owner: loguser }
¿Cómo depuro un playbook que falla a mitad de ejecución?
Ejecútalo con -v (o -vvv para el torrente completo) y --start-at-task para saltar las partes que ya funcionaron:
ansible-playbook site.yml -v --start-at-task "Deploy app code"
Para una tarea que devuelve salida extraña, regístrala e imprímela — register: captura el resultado en una variable y debug la muestra:
- name: Probe service
ansible.builtin.uri:
url: http://localhost:8080/health
return_content: yes
register: health
failed_when: false
- ansible.builtin.debug:
var: health.status
¿Cómo ejecuto solo una parte del playbook con tags?
Etiqueta tareas, roles o plays enteros y filtra con --tags/--skip-tags:
tasks:
- name: Install packages
ansible.builtin.apt:
name: "{{ item }}"
state: present
loop: ["nginx", "postgresql"]
tags: packages
- name: Deploy app code
ansible.builtin.git:
repo: https://github.com/myorg/myapp.git
dest: /var/www/myapp
tags: deploy
ansible-playbook site.yml --tags deploy
ansible-playbook site.yml --skip-tags packages
Recursos Relacionados
Provisionar una VPC de AWS con Terraform
Cómo usar Terraform para provisionar una VPC de AWS lista para producción con subredes públicas y privadas, NAT gateways y security groups
RecipeFundamentos de Docker: Dockerfile, Compose y Comandos
Cómo empaquetar una aplicación en un contenedor, escribir un Dockerfile y ejecutar contenedores con Docker Compose.
RecipeGestionar Secretos de Aplicaciones de Forma Segura
Cómo almacenar, rotar e inyectar API keys, contraseñas de base de datos y certificados sin hardcodearlos en código fuente o archivos de entorno.
RecipeRotación de copias en Bash: retención e integridad
Automatiza la rotación de copias en Bash con archivos diarios, semanales y mensuales, retención, cifrado y verificación de integridad.
RecipeRotación y Compresión de Logs en Bash
Rota, comprime y purga logs de aplicaciones con configuraciones de logrotate y scripts bash personalizados —incluyendo zstd, copytruncate, chequeo de disco y envío a S3.
RecipeConfigurar Certificados SSL con Let's Encrypt
Guía paso a paso para obtener certificados TLS gratuitos de Let's Encrypt con Certbot — en Nginx, Apache, standalone, Docker y Kubernetes.