TL;DR — Resumen Rápido
Domina Ansible playbooks y roles para automatizar infraestructura. Cubre inventario, modulos, Jinja2, ansible-vault, manejo de errores y despliegue LAMP.
Gestionar infraestructura a escala sin automatizacion es una formula para la deriva de configuracion, pasos olvidados e incidentes a las 3 de la madrugada. Los playbooks y roles de Ansible te dan una forma estructurada y repetible de definir exactamente como debe verse cada servidor y aplicar ese estado de forma idempotente en toda tu flota. Esta guia cubre el flujo de trabajo completo de Ansible — desde la gestion de inventario y la arquitectura de playbooks hasta los roles, plantillas Jinja2, secretos con ansible-vault y manejo de errores — con un despliegue real de una pila LAMP como ejemplo final.
Requisitos Previos
- Ansible instalado en un nodo de control (Ubuntu 22.04+ o cualquier maquina Linux/macOS)
- Acceso SSH basado en clave a uno o mas hosts gestionados
- Python 3 en todos los hosts destino (preinstalado en la mayoria de distribuciones)
- Familiaridad basica con la sintaxis YAML y la linea de comandos de Linux
Arquitectura de Ansible: El Modelo Push
Ansible utiliza una arquitectura push sin agentes. Tu nodo de control empuja la configuracion a los nodos gestionados a traves de SSH. No hay ningun demonio que ejecutar, ni base de datos de estado, ni nada que instalar en los destinos mas alla de un interprete Python funcional.
La cadena de ejecucion es: Inventario → Playbook → Modulos.
- Ansible lee el inventario para determinar que hosts apuntar
- Analiza el playbook para construir una lista ordenada de tareas
- Para cada tarea, copia un pequeno modulo Python al host remoto via SSH, lo ejecuta, captura el resultado y elimina el modulo
- Los resultados (ok / changed / failed / skipped) se agregan y muestran en el terminal
Este modelo significa que cada ejecucion de playbook es independiente. Si un host no esta disponible, solo ese host falla — el resto continua.
Gestion de Inventario
Formato INI vs YAML
# inventory.ini
[webservers]
web1.example.com
web2.example.com ansible_port=2222
[dbservers]
db1.example.com
db2.example.com
[production:children]
webservers
dbservers
[all:vars]
ansible_user=deployer
ansible_ssh_private_key_file=~/.ssh/id_ed25519
# inventory.yml
all:
vars:
ansible_user: deployer
ansible_ssh_private_key_file: ~/.ssh/id_ed25519
children:
webservers:
hosts:
web1.example.com:
web2.example.com:
ansible_port: 2222
dbservers:
hosts:
db1.example.com:
db2.example.com:
group_vars y host_vars
Coloca las variables en directorios dedicados que Ansible carga automaticamente:
proyecto/
inventory.ini
group_vars/
all.yml
webservers.yml
dbservers.yml
host_vars/
web1.example.com.yml
site.yml
# group_vars/webservers.yml
http_port: 80
https_port: 443
document_root: /var/www/html
worker_processes: 4
Inventario dinamico con el plugin aws_ec2
# aws_ec2.yml
plugin: amazon.aws.aws_ec2
regions:
- us-east-1
filters:
instance-state-name: running
"tag:Environment": production
keyed_groups:
- key: tags.Role
prefix: role
Estructura de un Playbook
Un playbook contiene uno o mas plays. Cada play mapea un conjunto de tareas a un grupo de hosts.
- name: Configurar servidores web
hosts: webservers
become: true
vars:
app_name: myapp
app_port: 8080
tasks:
- name: Instalar nginx
apt:
name: nginx
state: present
update_cache: true
- name: Desplegar config de nginx desde plantilla
template:
src: templates/nginx.conf.j2
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: "0644"
notify: Reiniciar nginx
- name: Asegurar que nginx esta activo y habilitado
service:
name: nginx
state: started
enabled: true
handlers:
- name: Reiniciar nginx
service:
name: nginx
state: restarted
Condicionales con when
- name: Instalar apache2 (solo Debian)
apt:
name: apache2
state: present
when: ansible_os_family == "Debian"
- name: Instalar httpd (solo Red Hat)
yum:
name: httpd
state: present
when: ansible_os_family == "RedHat"
Bucles con loop
- name: Instalar paquetes necesarios
apt:
name: "{{ item }}"
state: present
loop:
- nginx
- curl
- ufw
- fail2ban
- name: Crear directorios de la aplicacion
file:
path: "{{ item.path }}"
state: directory
owner: "{{ item.owner }}"
mode: "{{ item.mode }}"
loop:
- { path: /var/www/myapp, owner: www-data, mode: "0755" }
- { path: /var/log/myapp, owner: www-data, mode: "0750" }
- { path: /etc/myapp, owner: root, mode: "0755" }
Plantillas Jinja2
{# templates/nginx.conf.j2 #}
user www-data;
worker_processes {{ worker_processes | default('auto') }};
events {
worker_connections {{ worker_connections | default(1024) }};
}
http {
server {
listen {{ http_port }};
server_name {{ server_name }};
root {{ document_root }};
{% if enable_ssl | default(false) %}
listen {{ https_port }} ssl;
ssl_certificate {{ ssl_cert_path }};
{% endif %}
}
}
Etiquetas para ejecucion selectiva
# Ejecutar solo tareas etiquetadas 'config'
ansible-playbook site.yml --tags config
# Omitir tareas etiquetadas 'install'
ansible-playbook site.yml --skip-tags install
Modulos Esenciales
| Modulo | Proposito | Parametros clave |
|---|---|---|
apt / yum | Gestion de paquetes | name, state, update_cache |
copy | Copiar archivos estaticos | src, dest, owner, mode |
template | Desplegar plantillas Jinja2 | src, dest, owner, mode |
service | Gestionar servicios del sistema | name, state, enabled |
user | Gestionar cuentas de usuario | name, groups, shell, state |
file | Crear dirs, enlaces simbolicos | path, state, owner, mode |
lineinfile | Editar lineas en un archivo | path, regexp, line, state |
command | Ejecutar un comando | argv, creates |
shell | Comandos de shell con pipes | cmd, chdir |
git | Clonar o actualizar un repo | repo, dest, version |
docker_container | Gestionar contenedores Docker | name, image, state, ports |
debug | Imprimir valores de variables | msg, var |
Estructura de los Roles
Los roles son el mecanismo principal para organizar y compartir automatizacion de Ansible. Crea uno con:
ansible-galaxy init roles/webserver
Esto crea el directorio completo:
roles/webserver/
tasks/
main.yml
handlers/
main.yml
templates/
nginx.conf.j2
files/
index.html
vars/
main.yml
defaults/
main.yml
meta/
main.yml
defaults vs vars
defaults/main.yml: Baja precedencia. Los llamadores pueden sobreescribir estos valores facilmente.vars/main.yml: Alta precedencia. Estos sobreescriben la mayoria de otras fuentes de variables.
# roles/webserver/defaults/main.yml
http_port: 80
https_port: 443
worker_processes: auto
document_root: /var/www/html
enable_ssl: false
# roles/webserver/tasks/main.yml
- name: Instalar nginx
apt:
name: nginx
state: present
update_cache: true
- name: Crear directorio raiz del documento
file:
path: "{{ document_root }}"
state: directory
owner: www-data
group: www-data
mode: "0755"
- name: Desplegar configuracion de nginx
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: Reiniciar nginx
Usar roles en un playbook
# site.yml
- name: Configurar nivel web
hosts: webservers
become: true
roles:
- role: common
- role: security
- role: webserver
vars:
http_port: 80
enable_ssl: true
document_root: /var/www/myapp
Ansible Galaxy para Roles de la Comunidad
# Instalar un rol desde Galaxy
ansible-galaxy install geerlingguy.mysql
# Instalar desde un archivo de requisitos
ansible-galaxy install -r requirements.yml
# requirements.yml
roles:
- name: geerlingguy.nginx
version: "3.2.0"
- name: geerlingguy.mysql
version: "4.3.2"
collections:
- name: community.docker
version: ">=3.4.0"
Ansible Vault para Gestion de Secretos
Cifrar archivos y cadenas
# Crear un nuevo archivo cifrado
ansible-vault create group_vars/all/vault.yml
# Editar un archivo cifrado existente
ansible-vault edit group_vars/all/vault.yml
# Cifrar un archivo existente
ansible-vault encrypt secrets.yml
# Cifrar una cadena individual
ansible-vault encrypt_string 'MiContraseña123' --name 'db_password'
El patron estandar es mantener dos archivos de variables por grupo:
# group_vars/all/vars.yml (texto plano, en git)
db_host: db1.example.com
db_password: "{{ vault_db_password }}"
# group_vars/all/vault.yml (cifrado, en git)
vault_db_password: "SuperSecreto123!"
vault_api_key: "abcdef1234567890"
# Usar archivo de contrasena para evitar prompts interactivos
ansible-playbook site.yml --vault-password-file ~/.vault_pass
Manejo de Errores
ignore_errors y failed_when
- name: Verificar si la aplicacion esta instalada
command: which myapp
register: myapp_check
ignore_errors: true
changed_when: false
- name: Instalar aplicacion solo si falta
apt:
name: myapp
state: present
when: myapp_check.rc != 0
- name: Ejecutar script de migracion
command: /opt/myapp/migrate.sh
register: migration_result
failed_when: "'ERROR' in migration_result.stdout"
changed_when: "'Changes applied' in migration_result.stdout"
block / rescue / always
- name: Desplegar aplicacion con rollback
block:
- name: Detener servicio de la aplicacion
service:
name: myapp
state: stopped
- name: Desplegar nueva version
git:
repo: https://github.com/org/myapp.git
dest: /opt/myapp
version: "{{ app_version }}"
- name: Iniciar servicio de la aplicacion
service:
name: myapp
state: started
rescue:
- name: Revertir a la version anterior
git:
repo: https://github.com/org/myapp.git
dest: /opt/myapp
version: "{{ previous_version }}"
- name: Notificar al equipo del fallo
debug:
msg: "Despliegue de {{ app_version }} fallido. Revertido a {{ previous_version }}."
always:
- name: Registrar intento de despliegue
shell: echo "{{ app_version }} intentado en $(date)" >> /var/log/deployments.log
changed_when: false
Buenas Practicas de Idempotencia
- Prefiere modulos sobre
shell/command: los modulos verifican el estado antes de actuar - Usa
createsconcommand: salta el comando si el archivo ya existe - Establece
changed_when: falseen comandos que solo leen estado - Usa
lineinfileen lugar deshell echo >>para modificar archivos de configuracion - Evita
apt: update_cache: trueen cada tarea: usacache_valid_time: 3600
Configuracion de ansible.cfg
[defaults]
inventory = ./inventory.ini
remote_user = deployer
private_key_file = ~/.ssh/id_ed25519
host_key_checking = False
retry_files_enabled = False
stdout_callback = yaml
roles_path = ./roles:~/.ansible/roles
forks = 10
[privilege_escalation]
become = True
become_method = sudo
become_user = root
Depuracion y Pruebas
# Simulacro -- muestra que cambiaria sin aplicar
ansible-playbook site.yml --check --diff
# Maxima verbosidad
ansible-playbook site.yml -vvv
# Validar sintaxis del playbook
ansible-playbook site.yml --syntax-check
- name: Mostrar variables resueltas
debug:
msg: "App: {{ app_name }}, Puerto: {{ app_port }}"
- name: Capturar e inspeccionar salida de comando
command: systemctl status nginx
register: nginx_status
ignore_errors: true
changed_when: false
- name: Imprimir salida de nginx
debug:
var: nginx_status.stdout_lines
Comparativa de Herramientas
| Caracteristica | Ansible | Terraform | Puppet | Chef | SaltStack |
|---|---|---|---|---|---|
| Uso principal | Gestion config + orquestacion | Aprovisionamiento infra | Gestion config | Gestion config | Config + ejecucion remota |
| Arquitectura | Sin agente (SSH) | Sin agente (API) | Con agente | Con agente | Con o sin agente |
| Lenguaje | YAML | HCL | Puppet DSL | Ruby | YAML / Python |
| Seguimiento estado | Sin estado | Archivo tfstate | PuppetDB | Chef server | Salt mine |
| Curva aprendizaje | Baja | Media | Alta | Alta | Media |
| Idempotente | Si | Si | Si | Si | Si |
Ansible y Terraform son complementarios: usa Terraform para crear recursos en la nube, luego Ansible para configurarlos.
Ejemplo Real: Despliegue de Pila LAMP
# lamp.yml
- name: Desplegar pila LAMP
hosts: webservers
become: true
vars:
php_version: "8.3"
mysql_root_password: "{{ vault_mysql_root_password }}"
mysql_db_name: myapp
mysql_db_user: appuser
mysql_db_password: "{{ vault_mysql_db_password }}"
tasks:
- name: Instalar Apache y paquetes PHP
apt:
name:
- apache2
- "libapache2-mod-php{{ php_version }}"
- "php{{ php_version }}"
- "php{{ php_version }}-mysql"
- "php{{ php_version }}-curl"
- "php{{ php_version }}-mbstring"
state: present
update_cache: true
- name: Instalar servidor MySQL
apt:
name:
- mysql-server
- python3-pymysql
state: present
- name: Iniciar y habilitar MySQL
service:
name: mysql
state: started
enabled: true
- name: Crear base de datos de la aplicacion
mysql_db:
login_user: root
login_password: "{{ mysql_root_password }}"
name: "{{ mysql_db_name }}"
state: present
- name: Habilitar mod_rewrite de Apache
apache2_module:
name: rewrite
state: present
notify: Reiniciar Apache
- name: Clonar aplicacion desde git
git:
repo: "{{ app_repo }}"
dest: /var/www/html
version: "{{ app_version }}"
force: true
- name: Establecer propiedad del directorio web
file:
path: /var/www/html
owner: www-data
group: www-data
recurse: true
- name: Asegurar que Apache esta iniciado y habilitado
service:
name: apache2
state: started
enabled: true
handlers:
- name: Reiniciar Apache
service:
name: apache2
state: restarted
ansible-playbook -i inventory.ini lamp.yml --vault-password-file ~/.vault_pass
Resumen
- La arquitectura push sin agentes de Ansible significa que solo necesitas acceso SSH para empezar a automatizar
- El inventario (INI, YAML o dinamico) dirige todo el apuntamiento a hosts;
group_varsyhost_varsmantienen las variables organizadas - Los playbooks encadenan plays, tareas, handlers y plantillas Jinja2 en un flujo de trabajo declarativo y legible
- Los roles empaquetan tareas, handlers, plantillas y defaults en unidades reutilizables y compartibles
- ansible-vault cifra secretos en reposo; combina archivos vault con archivos de variables en texto plano para mantener limpio el historial de git
block/rescue/alwaysproporciona manejo estructurado de errores y logica de rollback para despliegues en produccion- Siempre valida con
--check --diffy-vvvantes de ejecutar en produccion
Articulos Relacionados
- Ansible para Principiantes: Automatiza la Configuracion de Servidores sin Agente
- Ansible Vault: Cifra y Gestiona Secretos de Forma Segura
- Solucion de Problemas en Playbooks de Ansible
- Semaphore UI: Interfaz Web de Ansible para Gestion de Playbooks
- Introduccion a Terraform: Infraestructura como Codigo