Idempotencia avanzada y módulos de producción 🔬
En el módulo anterior aprendiste los módulos esenciales y el concepto de idempotencia. Ahora toca profundizar: ¿qué pasa cuando los módulos no son idempotentes? ¿Cómo generamos configuraciones dinámicas? ¿Cómo cirugía un archivo de config sin reemplazarlo entero?
Idempotencia en la práctica: --check y --diff
Antes de tocar nada en producción, necesitas responder a la pregunta: ¿qué va a cambiar exactamente?
ansible-playbook site.yml --check --diff
--check: modo dry-run, no modifica nada en el sistema.--diff: muestra los cambios línea a línea, como ungit diff.
Cuando combinas ambos, Ansible te dice exactamente qué modificaría sin tocar nada. Esto es tu red de seguridad antes de cada deploy.
La regla de oro de la idempotencia
Un playbook bien escrito debe terminar con
changed=0cuando lo ejecutas dos veces seguidas. Si el segundo run reporta cambios, tienes un problema de idempotencia.
# Primera ejecución: configura el sistema
ansible-playbook site.yml
# → changed=5, failed=0
# Segunda ejecución sin tocar nada: todo debe estar ok
ansible-playbook site.yml
# → changed=0, failed=0
Si la segunda ejecución sigue reportando changed, hay una task que no respeta el estado deseado.
ansible-doc: tu documentación offline
No necesitas internet para consultar qué hace cada módulo. Ansible incluye documentación completa en local:
# Buscar módulos relacionados con "template"
ansible-doc -l | grep template
# Ver documentación completa
ansible-doc ansible.builtin.template
# Ver solo los parámetros (resumen rápido)
ansible-doc -s ansible.builtin.template
# Ver los ejemplos de uso
ansible-doc ansible.builtin.template | grep -A 30 EXAMPLES
Úsalo antes de buscar en Google. Suele ser más preciso y funciona sin conexión.
template: de archivos estáticos a configuraciones dinámicas
En el módulo anterior usamos copy para subir archivos. copy es estático: lo que sube es exactamente lo que hay en tu PC. template es su hermano mayor: procesa el archivo con Jinja2 antes de subirlo, permitiendo usar variables, condicionales y loops dentro del propio archivo de configuración.
La analogía: la plantilla de Word
Imagina una plantilla de carta formal donde cambias el nombre del destinatario, la fecha y el motivo. El cuerpo es siempre el mismo, pero los datos son dinámicos. Eso es exactamente lo que hace template con tus configs.
Sintaxis básica
- name: Generar configuración de Nginx
template:
src: templates/nginx.conf.j2 # Plantilla local (extensión .j2 por convención)
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: '0644'
notify: Recargar nginx
Sintaxis Jinja2 en templates
Hay tres constructores que usarás el 90% del tiempo:
{# Esto es un comentario Jinja2 — no aparece en el archivo final #}
{# 1. Variables: doble llave #}
server_name {{ domain_name }};
listen {{ http_port | default(80) }};
{# 2. Condicionales: bloques if #}
{% if enable_ssl %}
listen 443 ssl;
ssl_certificate /etc/ssl/{{ domain_name }}.crt;
{% endif %}
{# 3. Loops: iterar sobre listas #}
upstream backend {
{% for server in backend_servers %}
server {{ server }}:8080;
{% endfor %}
}
Ejemplo completo: nginx.conf.j2
user nginx;
worker_processes auto;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
server {
listen {{ http_port | default(80) }};
server_name _;
location / {
root /var/www/html;
index index.html;
}
}
}
Y el playbook que lo usa:
- name: Play 2 - Configurar servidor nginx
hosts: target1
become: true # Es para ejecutar con sudo
vars:
app_state: present
username: nginx
http_port: 80 # Impotante la variable para el template
tasks:
- name: Actualizar índices de paquetes
apt:
update_cache: yes
cache_valid_time: 3600
- name: Instalar nginx
apt:
name: nginx
state: "{{ app_state }}"
- name: Crear usuario nginx
user:
name: nginx
shell: /usr/bin/nologin
system: yes
- name: Subir la configuración de nginx
template:
src: ./nginx.conf.j2
dest: /etc/nginx/nginx.conf
owner: nginx
group: nginx
mode: '0644'
backup: yes
validate: 'nginx -t -c %s'
notify: Recargar nginx
- name: Arrancar servicio nginx
service:
name: nginx
state: "{{ 'started' if app_state == 'present' else 'stopped' }}"
enabled: "{{ app_state == 'present' }}"
handlers:
- name: Recargar nginx
service:
name: nginx
state: reloaded
El parámetro
validatees clave en producción: si la configuración generada tiene errores de sintaxis, Ansible la rechaza antes de sobrescribir la buena. Nginx nunca ve una config rota.
También que sepáis que dentro de un template Jinja2 puedes usar cualquier variable de Ansible, incluyendo facts del sistema (ansible_facts['distribution']) o variables definidas en el playbook (http_port).
Además, de las siguientes operadores, busquedas, funciones...:
- Filtros:
| default(),| upper,| lower,| length, etc. - Operadores lógicos:
and,or,not. - Condicionales inline:
{{ 'started' if app_state == 'present' else 'stopped' }}. - Loops:
{% for item in list %} ... {% endfor %}. - Inclusión de otros templates:
{% include 'otro_template.j2' %}.
lineinfile y blockinfile: cirugía de archivos
A veces no quieres reemplazar un archivo entero, solo modificar una línea específica o añadir un bloque. Para eso están estos dos módulos.
lineinfile: gestionar una línea
Asegura que una línea concreta existe (o no) en un archivo. Usa regexp para encontrar la línea existente y line para reemplazarla.
# Deshabilitar login de root por SSH
- name: Configurar SSH - deshabilitar root login
lineinfile:
path: /etc/ssh/sshd_config
regexp: '^PermitRootLogin' # Regex para encontrar la línea (aunque esté comentada)
line: 'PermitRootLogin no' # Valor final deseado
state: present
notify: Reiniciar SSH
# Ajustar límite de descriptores de archivo
- name: Configurar límite de archivos abiertos
lineinfile:
path: /etc/security/limits.conf
line: '* soft nofile 65536'
insertafter: EOF # Si no existe, añadir al final
blockinfile: gestionar un bloque de líneas
Cuando necesitas insertar o actualizar varias líneas a la vez. Ansible añade marcadores automáticos para saber dónde está el bloque y poder actualizarlo de forma idempotente.
- name: Añadir configuración de swap a sysctl
blockinfile:
path: /etc/sysctl.conf
marker: "# {mark} ANSIBLE MANAGED BLOCK - swap"
block: |
vm.swappiness=10
vm.vfs_cache_pressure=50
notify: Aplicar sysctl
El bloque resultante en el archivo queda así:
# BEGIN ANSIBLE MANAGED BLOCK - swap
vm.swappiness=10
vm.vfs_cache_pressure=50
# END ANSIBLE MANAGED BLOCK - swap
Si vuelves a ejecutar el playbook, Ansible encuentra los marcadores, compara el contenido y solo actúa si el bloque es diferente. Idempotente.
command y shell: domar lo no idempotente
Los módulos command y shell ejecutan comandos arbitrarios y no son idempotentes por defecto. Ansible no sabe si la segunda ejecución del comando produce cambios o no. Por eso hay que enseñárselo.
El problema
# ❌ Esto se ejecuta SIEMPRE y siempre reporta changed
- name: Compilar aplicación
command: make install
args:
chdir: /opt/miapp
Solución 1: creates / removes
Le dices a Ansible: "ejecuta este comando solo si este archivo NO existe" (creates) o "solo si SÍ existe" (removes).
# ✅ Solo compila si el binario no existe todavía
- name: Compilar aplicación
command: make install
args:
chdir: /opt/miapp
creates: /usr/local/bin/miapp # Skip si este archivo ya existe
Solución 2: register + changed_when
Captura la salida del comando y define tú mismo cuándo se considera un cambio.
# Obtener estado actual
- name: Verificar si la base de datos está inicializada
command: mysql -e "SHOW DATABASES LIKE 'app';"
register: db_check
changed_when: false # Este comando NUNCA provoca changed (es solo una consulta)
failed_when: db_check.rc != 0 and 'Access denied' not in db_check.stderr
# Actuar según el resultado
- name: Inicializar base de datos si no existe
command: /opt/app/bin/init-db.sh
when: "'app' not in db_check.stdout"
Solución 3: changed_when con condición en la salida
Algunos comandos te dicen si hicieron cambios en su output:
- name: Aplicar migraciones de base de datos
command: python manage.py migrate
register: migrate_result
changed_when: "'No migrations to apply' not in migrate_result.stdout"
Tabla resumen
| Situación | Solución |
|---|---|
| El comando crea un archivo predecible | creates: /ruta/al/archivo |
| El comando es una consulta/lectura | changed_when: false |
| El output indica si hubo cambio | changed_when: 'texto' not in result.stdout |
| Necesitas resultado para decidir | register: + when: en task siguiente |
Módulos de aprovisionamiento
get_url: descargar archivos
- name: Descargar binario de Prometheus
get_url:
url: "https://github.com/prometheus/prometheus/releases/download/v2.45.0/prometheus-2.45.0.linux-amd64.tar.gz"
dest: /tmp/prometheus.tar.gz
checksum: "sha256:1c7f489a3cc919c1ed0df2ae673a280309dc4a3eaa6ee3411e7d1f4bdec4d4c5"
mode: '0644'
El parámetro checksum verifica la integridad del archivo. Si el hash no coincide, la task falla. Si el archivo ya existe con el hash correcto, Ansible no lo descarga de nuevo. Idempotente.
unarchive: descomprimir archivos
# Descomprimir un archivo local
- name: Descomprimir Prometheus
unarchive:
src: /tmp/prometheus.tar.gz
dest: /opt/
remote_src: yes # El archivo está en el servidor remoto, no en tu PC
creates: /opt/prometheus # Skip si el directorio ya existe
git: clonar y actualizar repositorios
- name: Clonar repositorio de la aplicación
git:
repo: https://github.com/miorg/miapp.git
dest: /opt/miapp
version: "v2.1.0" # Branch, tag o commit hash (evita 'main' en producción)
update: yes # Pull si ya existe el repo
force: no # No sobreescribir cambios locales
Ejemplo completo de aprovisionamiento:
- name: Play - Aprovisionamiento
hosts: target1
become: true
tasks:
- name: Descargar binario de Prometheus
get_url:
url: "https://github.com/prometheus/prometheus/releases/download/v2.45.0/prometheus-2.45.0.linux-amd64.tar.gz"
dest: /tmp/prometheus.tar.gz
checksum: "sha256:1c7f489a3cc919c1ed0df2ae673a280309dc4a3eaa6ee3411e7d1f4bdec4d4c5"
mode: '0644'
- name: Descomprimir los ficheros
unarchive:
src: /tmp/prometheus.tar.gz
dest: /opt/
remote_src: yes # El archivo está en el servidor remoto, no en tu PC
creates: /opt/prometheus # Skip si el directorio ya existe
- name: Clonar repositorio de la aplicación
git:
repo: https://github.com/prometheus/prometheus.git
dest: /opt/miapp
version: "v2.1.0" # Branch, tag o commit hash (evita 'main' en producción)
update: yes # Pull si ya existe el repo
force: no
📝 Resumen del capítulo
--check --diff: Simula cambios y muestra diffs antes de aplicar. Tu red de seguridad.ansible-doc: Documentación offline de cualquier módulo. Más rápido que Google.template+ Jinja2: Configuraciones dinámicas con variables, condicionales y loops.lineinfile/blockinfile: Cirugía de archivos sin reemplazarlos enteros.command/shellidempotentes:creates,changed_when,register+when.- Módulos de aprovisionamiento:
get_urlcon checksum,unarchive,git.
Próximo paso: Variables, facts y plantillas más potentes para hacer tus playbooks verdaderamente dinámicos y reutilizables en cualquier entorno.