1. Introducción
La automatización de infraestructura ha pasado de ser un lujo a una necesidad. Gestionar servidores manualmente con SSH no escala: es lento, propenso a errores y no deja trazabilidad. Herramientas como Puppet, Chef y Salt resuelven esto, pero Ansible se ha convertido en la opción preferida por su simplicidad y curva de aprendizaje reducida.
A diferencia de Puppet (Ruby DSL) o Chef (Ruby puro), Ansible usa YAML, legible incluso para quienes no escriben código diariamente. Salt requiere agente en los nodos gestionados; Ansible es agentless y funciona puramente sobre SSH. Esto reduce drásticamente la fricción de adopción.
En esta guía recorreremos desde la instalación hasta casos prácticos reales, como si estuvieras trabajando codo a codo con un ingeniero senior de infraestructura.
2. Arquitectura de Ansible
Entender la arquitectura es clave para diseñar automatizaciones correctas. Ansible sigue un modelo push-based sin agentes:
- Control node: Máquina donde se instala Ansible y desde donde se ejecutan los comandos. Puede ser tu laptop, un servidor CI/CD o un bastión.
- Managed nodes: Servidores objetivo. Solo necesitan Python 3 y acceso SSH. No instalan ningún agente.
- Inventory: Lista de nodos gestionados. Archivos estáticos (INI/YAML) o dinámicos (cloud providers, CMDB).
- Modules: Unidades de trabajo. Ansible incluye cientos:
copy,service,apt,yum,template, etc. - Playbooks: Archivos YAML que orquestan módulos en secuencia, con condicionales, bucles y handlers.
+----------------+ SSH (22/tcp) +----------------+
| | ---------------------> | |
| Control Node | | Managed Node |
| (Ansible) | <--------------------- | (Python 3) |
| | stdout/stderr | |
+----------------+ +----------------+
Ventajas del modelo agentless:
- ✅ Sin puertos extra que abrir (solo SSH)
- ✅ Sin procesos en segundo plano en los nodos
- ✅ Actualizar Ansible no requiere tocar los nodos gestionados
- ✅ Ideal para entornos efímeros (contenedores, autoscaling)
3. Instalación y Configuración Inicial
Instalación en Ubuntu/Debian
sudo apt update
sudo apt install -y ansible
ansible --version
Instalación en RHEL/CentOS/Rocky
sudo dnf install -y epel-release
sudo dnf install -y ansible
ansible --version
Configuración: ansible.cfg
Ansible busca configuración en este orden: ANSIBLE_CONFIG (variable de entorno) > ./ansible.cfg > ~/.ansible.cfg > /etc/ansible/ansible.cfg. Crea un archivo en tu proyecto:
[defaults]
inventory = ./inventory/hosts
host_key_checking = False
forks = 20
timeout = 30
stdout_callback = yaml
callback_whitelist = profile_tasks
[ssh_connection]
pipelining = True
control_path = /tmp/ansible-%%h-%%p-%%r
pipelining = True reduce drásticamente el número de conexiones SSH. Obligatorio en entornos con muchos nodos.
Inventario estático
# inventory/hosts
[webservers]
web01 ansible_host=192.168.1.10 ansible_user=deploy
web02 ansible_host=192.168.1.11 ansible_user=deploy
[dbservers]
db01 ansible_host=192.168.1.20 ansible_user=deploy
[monitoring]
monitor ansible_host=192.168.1.30
[production:children]
webservers
dbservers
Inventario dinámico con AWS EC2
# inventory/aws_ec2.yml
plugin: amazon.aws.aws_ec2
regions:
- us-east-1
- us-west-2
filters:
tag:Environment: production
hostnames:
- dns-name
- private-ip
keyed_groups:
- key: tags.Role
prefix: role
compose:
ansible_user: "'ec2-user'"
Se ejecuta con ansible-inventory -i inventory/aws_ec2.yml --graph. Los inventarios dinámicos se integran con cualquier proveedor que tenga API: AWS, GCP, Azure, VMware, OpenStack.
4. Comandos Ad-Hoc
Los comandos ad-hoc ejecutan tareas sin playbook. Ideales para diagnósticos rápidos y operaciones puntuales.
Ping de conectividad
ansible all -i inventory/hosts -m ping
Ejecutar comandos remotos
ansible webservers -m shell -a "uptime && free -h && df -h"
Copiar archivos
ansible webservers -m copy -a "src=./nginx.conf dest=/etc/nginx/nginx.conf mode=0644"
Gestionar servicios
ansible dbservers -m service -a "name=postgresql state=restarted enabled=yes"
Gestionar usuarios
ansible all -m user -a "name=jdoe state=present groups=sudo shell=/bin/bash create_home=yes"
Instalar paquetes
ansible webservers -m apt -a "name=nginx state=latest update_cache=yes" -b
Usa -b (become) para escalar privilegios. Combínalo con --become-user= si necesitas un usuario específico.
5. Playbooks
Los playbooks son el corazón de Ansible. Definen el estado deseado del sistema en YAML puro.
Sintaxis básica
---
- name: Configurar servidor web
hosts: webservers
become: yes
vars:
app_port: 3000
app_env: production
tasks:
- name: Instalar Nginx
apt:
name: nginx
state: latest
update_cache: yes
- name: Crear directorio de la aplicación
file:
path: /var/www/miapp
state: directory
owner: www-data
group: www-data
mode: '0755'
- name: Desplegar configuración de Nginx
template:
src: nginx.conf.j2
dest: /etc/nginx/sites-available/miapp
notify: reload nginx
Handlers
Los handlers solo se ejecutan si una tarea notifica el cambio, evitando reinicios innecesarios:
handlers:
- name: reload nginx
service:
name: nginx
state: reloaded
Variables y Facts
Ansible recopila facts del sistema automáticamente (IP, SO, memoria, discos). Puedes definir variables en varios niveles:
# group_vars/webservers.yml
---
nginx_port: 443
ssl_enabled: true
upstream_servers:
- 10.0.1.10
- 10.0.1.11
# host_vars/web01.yml
---
ansible_host: 192.168.1.10
app_version: 2.5.1
Usa facts en tus templates:
# templates/index.html.j2
<html>
<body>
<h1>Servidor: {{ ansible_hostname }}</h1>
<p>IP: {{ ansible_default_ipv4.address }}</p>
<p>SO: {{ ansible_distribution }} {{ ansible_distribution_version }}</p>
</body>
</html>
Condicionales
- name: Configurar firewall en Ubuntu
ufw:
rule: allow
port: "{{ item }}"
loop:
- 80
- 443
when: ansible_os_family == "Debian"
- name: Configurar firewall en RHEL
firewalld:
service: "{{ item }}"
permanent: yes
state: enabled
loop:
- http
- https
when: ansible_os_family == "RedHat"
Bucles
- name: Crear usuarios del sistema
user:
name: "{{ item.username }}"
groups: "{{ item.groups | default('users') }}"
shell: "{{ item.shell | default('/bin/bash') }}"
state: present
loop:
- { username: 'alice', groups: 'sudo' }
- { username: 'bob', groups: 'docker' }
- { username: 'charlie' }
6. Roles y Estructura de Proyectos
Un rol encapsula tareas, handlers, templates, variables y defaults en una estructura reutilizable. Es el equivalente a un "módulo" en Puppet o una "cookbook" en Chef.
Estructura estándar
roles/
nginx/
defaults/ # Variables por defecto (baja prioridad)
main.yml
vars/ # Variables sobreescribibles
main.yml
tasks/ # Tareas principales
main.yml
handlers/ # Handlers del rol
main.yml
templates/ # Templates Jinja2
nginx.conf.j2
files/ # Archivos estáticos
index.html
meta/ # Dependencias del rol
main.yml
Crear un rol con ansible-galaxy
ansible-galaxy init roles/nginx
ansible-galaxy init roles/postgresql
ansible-galaxy init roles/monitoring
Playbook consumiendo roles
---
- name: Desplegar servidor web completo
hosts: webservers
become: yes
roles:
- role: common
- role: nginx
vars:
nginx_port: 443
ssl_cert: /etc/ssl/certs/server.crt
- role: monitoring
when: enable_monitoring | default(false)
Ansible Galaxy
Galaxy es el repositorio público de roles. En lugar de escribir un rol desde cero, busca si ya existe uno mantenido por la comunidad:
ansible-galaxy install geerlingguy.nginx
ansible-galaxy install geerlingguy.postgresql
ansible-galaxy install geerlingguy.firewall
Define dependencias en requirements.yml:
# requirements.yml
---
roles:
- name: geerlingguy.nginx
version: 3.1.4
- name: geerlingguy.postgresql
version: 3.5.0
- name: geerlingguy.certbot
version: 5.0.0
ansible-galaxy install -r requirements.yml
7. Caso Práctico: Despliegue de LAMP Stack
Construyamos un playbook completo que despliega Apache, MySQL y PHP con hardening básico.
Estructura del proyecto
lamp-deployment/
ansible.cfg
inventory/
hosts
group_vars/
webservers.yml
dbservers.yml
requirements.yml
site.yml
roles/
common/
apache/
mysql/
php/
site.yml
---
- name: Configuración base de todos los servidores
hosts: all
become: yes
roles:
- common
- name: Desplegar servidores web Apache + PHP
hosts: webservers
become: yes
roles:
- apache
- php
- name: Configurar base de datos MySQL
hosts: dbservers
become: yes
roles:
- mysql
Rol common: hardening base
# roles/common/tasks/main.yml
---
- name: Actualizar todos los paquetes
apt:
update_cache: yes
upgrade: dist
cache_valid_time: 3600
- name: Instalar herramientas base
apt:
name:
- htop
- curl
- wget
- git
- ufw
- fail2ban
- unattended-upgrades
state: present
- name: Configurar unattended-upgrades
copy:
src: 50unattended-upgrades
dest: /etc/apt/apt.conf.d/50unattended-upgrades
mode: '0644'
- name: Configurar firewall
ufw:
rule: "{{ item.rule }}"
port: "{{ item.port | default(omit) }}"
proto: "{{ item.proto | default('tcp') }}"
loop:
- { rule: 'allow', port: '22' }
when: ansible_os_family == "Debian"
Rol apache
# roles/apache/tasks/main.yml
---
- name: Instalar Apache
apt:
name: apache2
state: latest
- name: Habilitar módulos de seguridad
apache2_module:
name: "{{ item }}"
state: present
loop:
- headers
- rewrite
- ssl
- security
- name: Deshabilitar módulos innecesarios
apache2_module:
name: "{{ item }}"
state: absent
loop:
- autoindex
- status
- info
notify: restart apache
- name: Configurar cabeceras de seguridad
template:
src: security-headers.conf.j2
dest: /etc/apache2/conf-available/security-headers.conf
notify: restart apache
- name: Remover banner del servidor
lineinfile:
path: /etc/apache2/conf-enabled/security.conf
regexp: '^ServerTokens'
line: 'ServerTokens Prod'
notify: restart apache
# roles/apache/handlers/main.yml
---
- name: restart apache
service:
name: apache2
state: restarted
# roles/apache/templates/security-headers.conf.j2
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "DENY"
Header always set X-XSS-Protection "1; mode=block"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()"
Rol mysql
# roles/mysql/tasks/main.yml
---
- name: Instalar MySQL
apt:
name:
- mysql-server
- python3-mysqldb
state: latest
- name: Ejecutar mysql_secure_installation
mysql_user:
name: root
host: "{{ item }}"
password: "{{ mysql_root_password }}"
check_implicit_admin: yes
login_unix_socket: /var/run/mysqld/mysqld.sock
loop:
- 127.0.0.1
- ::1
- localhost
- name: Eliminar usuario anónimo
mysql_user:
name: ''
host_all: yes
state: absent
- name: Eliminar base de datos de prueba
mysql_db:
name: test
state: absent
- name: Crear base de datos para la aplicación
mysql_db:
name: "{{ app_db_name }}"
state: present
- name: Crear usuario de aplicación
mysql_user:
name: "{{ app_db_user }}"
password: "{{ app_db_password }}"
priv: "{{ app_db_name }}.*:ALL"
host: '%'
state: present
Rol php
# roles/php/tasks/main.yml
---
- name: Instalar PHP y extensiones
apt:
name:
- php
- php-mysql
- php-curl
- php-gd
- php-mbstring
- php-xml
- php-xmlrpc
- libapache2-mod-php
state: latest
notify: restart apache
- name: Configurar php.ini para producción
template:
src: php.ini.j2
dest: /etc/php/{{ php_version }}/apache2/php.ini
notify: restart apache
# roles/php/templates/php.ini.j2
expose_php = Off
display_errors = Off
log_errors = On
error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT
max_execution_time = 30
memory_limit = 256M
post_max_size = 20M
upload_max_filesize = 20M
date.timezone = America/Bogota
Variables del proyecto
# group_vars/all.yml
---
mysql_root_password: "{{ vault_mysql_root_password }}"
app_db_name: miapp
app_db_user: miapp_user
app_db_password: "{{ vault_app_db_password }}"
php_version: 8.3
Ejecutar el despliegue completo
ansible-playbook -i inventory/hosts site.yml --ask-vault-pass
8. Ansible Vault
Nunca, bajo ninguna circunstancia, subas contraseñas o claves privadas a tu repositorio. Ansible Vault cifra archivos completos o variables individuales.
Cifrar un archivo entero
ansible-vault encrypt group_vars/all/vault.yml
ansible-vault view group_vars/all/vault.yml
ansible-vault edit group_vars/all/vault.yml
ansible-vault decrypt group_vars/all/vault.yml
Cifrar una variable individual
ansible-vault encrypt_string 'Str0ng!P4ss' --name 'mysql_root_password'
Salida:
mysql_root_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
61333931363438373766373036313334643939343861303866646662356162
65366339376335306265333464323539356166633138386435653661376239
366566383261310a3666386132623066663138373662333935353739376362
36376361633938303630346633306664316262623438633638633730663463
6137363131353232620a663434313261623163343132633437623965393763
6437633239616430376238
Integración con CI/CD
Para entornos CI/CD, usa un archivo de contraseñas en lugar de pedirla interactivamente:
# .vault_pass (NUNCA subir al repo)
my_secret_vault_password
ansible-playbook site.yml --vault-password-file .vault_pass
En GitHub Actions, almacena la contraseña como secret y pásala con echo "${{ secrets.VAULT_PASS }}" > .vault_pass.
Múltiples vaults por entorno
group_vars/
production/
vault.yml # Cifrado con vault-pass-prod
vars.yml
staging/
vault.yml # Cifrado con vault-pass-staging
vars.yml
# ansible.cfg
[defaults]
vault_identity_list = production@.vault-pass-prod, staging@.vault-pass-staging
9. Integración con CI/CD
Ansible brilla cuando se integra en pipelines automatizados. Aquí no solo desplegamos configuraciones, sino que validamos, probamos y liberamos con trazabilidad.
GitHub Actions
# .github/workflows/deploy.yml
name: Deploy Infrastructure
on:
push:
branches: [main]
paths:
- 'ansible/**'
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Instalar Ansible
run: |
sudo apt update
sudo apt install -y ansible
- name: Instalar roles de Galaxy
run: ansible-galaxy install -r ansible/requirements.yml
working-directory: ./ansible
- name: Crear vault password
run: echo "${{ secrets.VAULT_PASS }}" > .vault_pass
- name: Check syntax
run: ansible-playbook --syntax-check -i inventory/hosts site.yml
working-directory: ./ansible
- name: Dry-run (check mode)
run: ansible-playbook -i inventory/hosts site.yml --check --diff
working-directory: ./ansible
- name: Ejecutar despliegue
run: ansible-playbook -i inventory/hosts site.yml --vault-password-file .vault_pass
working-directory: ./ansible
Jenkins Pipeline
// Jenkinsfile
pipeline {
agent any
environment {
ANSIBLE_HOST_KEY_CHECKING = 'False'
VAULT_PASS = credentials('vault-password')
}
stages {
stage('Checkout') {
steps { checkout scm }
}
stage('Install Ansible') {
steps {
sh 'sudo apt install -y ansible'
sh 'ansible-galaxy install -r requirements.yml'
}
}
stage('Syntax Check') {
steps {
sh 'ansible-playbook --syntax-check -i inventory/hosts site.yml'
}
}
stage('Deploy') {
steps {
sh 'echo "$VAULT_PASS" > .vault_pass'
sh 'ansible-playbook -i inventory/hosts site.yml --vault-password-file .vault_pass'
}
}
}
}
AWX / Red Hat Ansible Automation Platform
AWX es la versión open-source del Automation Platform de Red Hat. Proporciona:
- Interfaz web para ejecutar playbooks
- Gestión de credenciales centralizada
- Programación de ejecuciones (jobs schedules)
- RBAC (control de acceso basado en roles)
- Integración con Git, LDAP, logging centralizado
# Despliegue rápido de AWX con Docker Compose
git clone https://github.com/ansible/awx.git
cd awx/docker-compose
ansible-playbook -i inventory install.yml
10. Buenas Prácticas
Idempotencia
Un playbook es idempotente cuando ejecutarlo N veces produce el mismo resultado. Diseña cada tarea para que verifique el estado antes de actuar:
# MAL: siempre reinicia
- name: Reiniciar Nginx
service:
name: nginx
state: restarted
# BIEN: solo recarga si hay cambios
- name: Recargar Nginx
service:
name: nginx
state: reloaded
Testing con Molecule
Molecule es el framework de testing para roles Ansible. Aísla el rol en contenedores o VMs y ejecuta pruebas automatizadas:
pip install molecule molecule-plugins[docker]
# Iniciar proyecto de testing
molecule init scenario --driver-name docker
# molecule/default/molecule.yml
---
dependency:
name: galaxy
driver:
name: docker
platforms:
- name: instance
image: geerlingguy/docker-ubuntu2404-ansible:latest
pre_build_image: true
provisioner:
name: ansible
verifier:
name: ansible
# Ejecutar pruebas
molecule create # Crea contenedores
molecule converge # Ejecuta el rol
molecule verify # Verifica el estado
molecule destroy # Limpia contenedores
molecule test # Todo en uno
Version Control
- Mantén ansible/ en el mismo repositorio del proyecto o en uno dedicado
- Usa .gitignore para excluir vault passwords, archivos .retry, inventarios con IPs reales
- Versiona los roles con semver y etiquetas en Git
- Revisa los playbooks en pull requests antes de aplicar
Tagging
---
- name: Configuración completa
hosts: all
tasks:
- name: Instalar paquetes base
apt:
name: "{{ item }}"
loop:
- curl
- git
tags:
- packages
- base
- name: Configurar SSH
template:
src: sshd_config.j2
dest: /etc/ssh/sshd_config
tags:
- ssh
- security
- name: Configurar firewall
ufw:
rule: allow
port: 22
tags:
- firewall
- security
# Ejecutar solo tareas específicas
ansible-playbook site.yml --tags security
ansible-playbook site.yml --tags packages --skip-tags security
Checklist de código limpio
| Práctica | Descripción |
|---|---|
| ✅ Nombres descriptivos | Instalar dependencias de Python vs Task 1 |
| ✅ Usar módulos, no shell/command | Prefiere copy sobre shell: cp |
| ✅ Variables con defaults | Siempre define valores por defecto en defaults/main.yml |
| ✅ Secrets en Vault | Nunca texto plano en vars |
| ✅ Idempotencia | Verifica estado, no asumas |
| ✅ Testing | Molecule + linting (ansible-lint) |
| ✅ Documentación | Cada rol con README describiendo variables y uso |
Conclusión
Ansible es mucho más que "SSH con esteroides". Es una plataforma de automatización que, bien utilizada, permite gestionar desde un puñado de servidores hasta flotas completas en la nube con consistencia, trazabilidad y seguridad.
La clave está en empezar simple: inventario, algunos ad-hoc, un playbook pequeño. Luego roles, vault, CI/CD y testing. No intentes abarcar todo el ecosistema el primer día. Cada capa que agregues resolverá problemas reales de tu día a día como ingeniero de infraestructura.
El código de esta guía está listo para adaptar a tu entorno. La automatización no es el destino, es el camino.