Saltar al contenido principal

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?

Video pendiente de grabación

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 un git 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=0 cuando 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 validate es 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ónSolución
El comando crea un archivo predeciblecreates: /ruta/al/archivo
El comando es una consulta/lecturachanged_when: false
El output indica si hubo cambiochanged_when: 'texto' not in result.stdout
Necesitas resultado para decidirregister: + 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/shell idempotentes: creates, changed_when, register + when.
  • Módulos de aprovisionamiento: get_url con 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.