Día 92 - Managing Jinja2 Templates Using Ansible (módulo template + roles + inventory_hostname)
Problema / Desafío
Un miembro del equipo Nautilus está desarrollando un role para instalar y configurar httpd. Falta agregar un template Jinja2 para index.html y la task que lo despliega. El inventario ~/ansible/inventory ya existe. La tarea:
a. Actualizar ~/ansible/playbook.yml para correr el role httpd en App Server 1 (stapp01)
b. Crear un template index.html.j2 en /home/thor/ansible/role/httpd/templates/ con la línea:
inventory_hostname
c. Agregar una task en /home/thor/ansible/role/httpd/tasks/main.yml que copie el template a /var/www/html/index.html con permisos 0777
d. El owner (user/group) de /var/www/html/index.html debe ser el sudo user respectivo del server (ej. tony en stapp01)
Restricción de validación: se corre con ansible-playbook -i inventory playbook.yml — sin argumentos extra. El become ya está en el playbook.
Día clave: es el primer role del journal y el primer uso del módulo
template(renderiza Jinja2). Reúne todo lo de Ansible visto hasta ahora dentro de la estructura formal de un role.
Conceptos clave
Qué es un role — Ansible organizado por convención
Hasta ahora los playbooks eran un archivo plano con tasks. Un role es la forma estándar de empaquetar y reutilizar automatización: directorios con nombres fijos que Ansible carga automáticamente.
role/httpd/
├── tasks/ → main.yml: las tareas del role (punto de entrada)
├── templates/ → archivos .j2 (Jinja2); el módulo template busca aquí por defecto
├── files/ → archivos estáticos; el módulo copy busca aquí por defecto
├── handlers/ → handlers (notify)
├── defaults/ → variables por defecto (prioridad más baja)
├── vars/ → variables del role (prioridad alta)
├── meta/ → dependencias del role + metadata Galaxy
├── tests/ → playbook de prueba del role
└── README.md → documentación del role
La magia es la convención sobre configuración: no se le dice a Ansible dónde buscar el template — al usar template: src: index.html.j2, Ansible automáticamente lo busca en templates/ del role. Lo mismo copy con files/, los handlers en handlers/main.yml, etc.
En este lab el directorio se llama
role/(singular) en vez del convencionalroles/(plural). Funciona porque el playbook referencia la ruta explícitarole/httpd; Ansible respeta el path dado. La convención de Galaxy esroles/, pero el path se puede sobreescribir.
El módulo template — copy que renderiza Jinja2
- name: Copy index from template
ansible.builtin.template:
src: index.html.j2 # ← buscado en templates/ del role
dest: /var/www/html/index.html
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
mode: '0777'
| Módulo | Qué hace con el contenido |
|---|---|
copy |
Transfiere el archivo tal cual (texto literal) |
template |
Renderiza Jinja2 ({{ }}, {% %}) antes de transferir |
Si se usara copy con index.html.j2, el archivo destino contendría literalmente {{ inventory_hostname }}. template evalúa esa expresión y escribe stapp01. Por eso el lab pide template, no copy.
Jinja2 — el motor de templates
Jinja2 es el motor de templates de Python (Django, Flask) que Ansible usa para interpolar variables y lógica:
This file was created using Ansible on {{ inventory_hostname }}
{% if ansible_distribution == "CentOS" %}Running on CentOS{% endif %}
{% for user in app_users %}{{ user }}{% endfor %}
| Sintaxis | Función |
|---|---|
{{ var }} |
Interpolar el valor de una variable |
{% ... %} |
Lógica de control: if, for, set |
{# ... #} |
Comentario (no se renderiza) |
\| filter |
Filtros: {{ name \| upper }}, {{ x \| default(5) }} |
★ inventory_hostname vs ansible_hostname — la distinción que pide el lab
El requisito dice explícitamente: usar inventory_hostname. Hay varias variables que parecen "el nombre del host" pero no son lo mismo:
| Variable | De dónde sale | Necesita facts | Valor en stapp01 |
|---|---|---|---|
inventory_hostname |
El nombre tal como está escrito en el inventario | No | stapp01 |
ansible_hostname |
Fact gathered — el hostname corto real de la máquina |
Sí | stapp01 |
ansible_fqdn |
Fact — el FQDN completo | Sí | stapp01.… |
inventory_hostname_short |
inventory_hostname cortado en el primer . |
No | stapp01 |
inventory_hostnamees una magic variable (Día 85): Ansible siempre la conoce, singather_facts. Refleja la identidad lógica del host en el inventario.ansible_hostnamees un fact: requiere elGathering Facts, y refleja lo que la máquina cree que es su nombre (hostname).
En los labs de KodeKloud ambos coinciden (stapp01), así que un template con ansible_hostname produce la misma salida y "pasa". Pero conceptualmente son distintos: si la máquina tuviera un hostname configurado diferente al nombre del inventario, divergirían. El requisito pide inventory_hostname → se usa ese, que además es más robusto (no depende de facts ni del hostname real del SO).
Patrón recurrente (Días 65, 67, 90): traducir el requisito literal al campo correcto. "Use
inventory_hostname" es una instrucción exacta, no una sugerencia genérica de "pon el nombre del host".
ansible_user para el owner — el sudo user por host
El requisito (d) pide que el owner sea el "sudo user respectivo" (tony en stapp01, steve en stapp02, …). Ese valor vive en el inventario como ansible_user (la variable de conexión, Día 85):
Como cada host define su propio ansible_user en el inventario, esto da el owner correcto por host sin when: ni hardcodear — el mismo patrón "owner por host vía magic var" del Día 85.
★ Templating perezoso (lazy) — cuándo se resuelven los {{ }}
Clave conceptual: Jinja2 no se evalúa al cargar el playbook, sino por host, justo antes de ejecutar la task. El flujo real de Ansible:
- Carga/parseo YAML: Ansible lee el archivo como YAML puro. Las
{{ }}son todavía texto, no se tocan. - Compilación: se arma el plan de tareas. Sigue sin resolver variables (lazy).
- Ejecución por host: cuando la task corre contra
stapp01, Ansible toma el contexto de variables de ese host y recién ahí evalúa los{{ }}con Jinja2. - Dispatch del módulo:
templaterecibe los valores ya resueltos (owner=tony, contenido constapp01, etc.).
Consecuencia práctica: el mismo template y la misma task producen contenido distinto por host, porque el contexto de variables cambia. Por eso un solo index.html.j2 sirve para toda la flota.
El playbook — apuntar el role a App Server 1
El playbook original tenía hosts: vacío. Hay que apuntarlo a stapp01:
---
- hosts: stapp01 # ← App Server 1 (estaba vacío)
become: yes
become_user: root
roles:
- role/httpd # ← path explícito al role
become: yes + become_user: root para que el role pueda instalar paquetes y escribir en /var/www/html.
Pasos
- Login al jump host como
thor;cd ~/ansible - Inspeccionar el role:
ls -lhart role/httpd/ycat role/httpd/tasks/main.yml - Editar
playbook.yml→hosts: stapp01 - Crear
role/httpd/templates/index.html.j2con la línea +{{ inventory_hostname }} - Agregar la task
templateenrole/httpd/tasks/main.yml - Correr
ansible-playbook -i inventory playbook.yml - Validar contenido + owner + permisos en
stapp01
Comandos / Código
1. El playbook
2. El template Jinja2
{# ~/ansible/role/httpd/templates/index.html.j2 #}
This file was created using Ansible on {{ inventory_hostname }}
Una sola línea.
{{ inventory_hostname }}se resuelve astapp01al ejecutar contra ese host.
3. La task dentro del role
# ~/ansible/role/httpd/tasks/main.yml
---
- name: install the latest version of HTTPD
yum:
name: httpd
state: latest
- name: Start service httpd
service:
name: httpd
state: started
- name: Copy index from template
template:
src: index.html.j2 # ← Ansible lo busca en templates/
dest: /var/www/html/index.html
owner: "{{ ansible_user }}" # ← tony en stapp01 (sudo user del host)
group: "{{ ansible_user }}"
mode: '0777'
4. Ejecutar
Output real del lab:
PLAY [stapp01] *************************************************
TASK [Gathering Facts] *****************************************
ok: [stapp01]
TASK [role/httpd : install the latest version of HTTPD] ********
changed: [stapp01]
TASK [role/httpd : Start service httpd] ************************
changed: [stapp01]
TASK [role/httpd : Copy index from template] *******************
changed: [stapp01]
PLAY RECAP *****************************************************
stapp01 : ok=4 changed=3 unreachable=0 failed=0
Las tasks aparecen prefijadas con
role/httpd :— así Ansible indica que vienen de un role.
5. Verificación
ansible stapp01 -i inventory -b -m shell \
-a "cat /var/www/html/index.html; ls -l /var/www/html/index.html"
This file was created using Ansible on stapp01
-rwxrwxrwx 1 tony tony 47 ... /var/www/html/index.html
stapp01 (de inventory_hostname), owner tony tony (de ansible_user), -rwxrwxrwx = 0777. Los tres requisitos cumplidos.
Variantes (referencia)
Correr el role en todos los hosts (no solo stapp01)
Cada host renderizaría su propio index.html con su inventory_hostname y su ansible_user — sin cambiar el template ni la task. Ese es el poder del templating lazy.
Sintaxis alternativa de roles (con parámetros)
Template con lógica Jinja2
This file was created using Ansible on {{ inventory_hostname }}
{% if ansible_default_ipv4 is defined %}
Server IP: {{ ansible_default_ipv4.address }}
{% endif %}
Generated for: {{ ansible_user }}
validate — chequear sintaxis antes de escribir (configs críticas)
- name: desplegar httpd.conf validándolo
ansible.builtin.template:
src: httpd.conf.j2
dest: /etc/httpd/conf/httpd.conf
validate: 'httpd -t -f %s' # %s = archivo temporal; si falla, no lo instala
Troubleshooting
| Problema | Causa | Solución |
|---|---|---|
El archivo destino contiene {{ inventory_hostname }} literal |
Se usó copy en vez de template |
Usar el módulo template para que renderice Jinja2 |
Could not find or access 'index.html.j2' |
El template no está en templates/ del role, o el nombre no coincide |
Crear el .j2 en role/httpd/templates/; src es relativo a esa carpeta |
| El nombre del server sale mal o vacío | Se usó ansible_hostname (fact) sin gather_facts, o variable equivocada |
Usar inventory_hostname (magic var, no necesita facts) como pide el lab |
Owner queda como root en vez de tony |
No se usó ansible_user, o se hardcodeó |
owner: "{{ ansible_user }}" — toma el sudo user por host |
hosts: vacío → no corre en ningún host |
El playbook original tenía hosts: sin valor |
Poner hosts: stapp01 |
| Permisos no son 0777 | mode mal escrito o sin comillas (gotcha octal) |
mode: '0777' con comillas |
the role 'httpd' was not found |
Path del role mal referenciado | Usar el path que existe: - role/httpd (no solo httpd si no está en roles/) |
Funciona con --become pero no sin args |
La validación corre sin --become |
become: yes debe estar en el playbook |
Conexión con días anteriores
- Día 85 (magic variables):
inventory_hostnameyansible_userson magic vars; hoy se usan dentro de un template para renderizar contenido por host. Mismo patrón "el dato del host vive en variables, no hardcodeado". - Días 88/91 (
blockinfile/lineinfile): gestionaban fragmentos de un archivo existente. Hoytemplategenera el archivo completo desde una plantilla con variables — el nivel más alto de gestión de contenido. - Días 87/89 (
yum/service): las dos primeras tasks del role son exactamente ese stack; hoy se ven empaquetadas dentro de un role reutilizable. - Patrón "traducir requisito literal" (Días 65, 67, 90): el lab pide
inventory_hostnameespecíficamente — usaransible_hostname"funciona" en el lab pero no es lo que se pidió.