Día 85 - Create Files on App Servers using Ansible (módulo file + variable mágica ansible_user)
Problema / Desafío
El equipo DevOps de Nautilus prueba módulos de Ansible para crear archivos en hosts remotos. El reto tiene un detalle elegante: el owner del archivo debe ser distinto en cada server — y resulta que ese owner coincide exactamente con el user de conexión de cada host.
Requirements:
- Crear inventario
~/playbook/inventorycon todos los app servers - Crear
~/playbook/playbook.ymlque cree un archivo en blanco/opt/nfsdata.txten todos los servers - Permisos del archivo:
0777 - Owner user/group del archivo:
tonyen stapp01,steveen stapp02,banneren stapp03
| Host | User conexión | Owner esperado del archivo |
|---|---|---|
stapp01 |
tony |
tony |
stapp02 |
steve |
steve |
stapp03 |
banner |
banner |
Cambio de path: este lab usa
~/playbook/(no~/ansible/como los Días 83-84). Es solo otra ruta — el concepto no cambia.
Continúa del Día 83 (módulo file + state: touch) y del Día 84 (hosts: all a la flota), agregando el truco de las variables de conexión por host.
Conceptos clave
El insight del lab: owner == user de conexión
El requirement pide owner tony/steve/banner por server. La tentación es escribir tres tasks o un when: por host:
# ❌ Verboso y frágil — repite la lógica por host
tasks:
- name: file en stapp01
file: { path: /opt/nfsdata.txt, state: touch, owner: tony, group: tony }
when: inventory_hostname == "stapp01"
- name: file en stapp02
file: { path: /opt/nfsdata.txt, state: touch, owner: steve, group: steve }
when: inventory_hostname == "stapp02"
# ...
Pero el owner pedido es exactamente el user con el que Ansible se conecta a cada host (ansible_user). Eso permite un solo task que se adapta solo:
# ✅ Un solo task — la variable resuelve el valor correcto por host
- name: create blank file
file:
path: /opt/nfsdata.txt
state: touch
mode: '0777'
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
Variables mágicas (magic variables) de Ansible
Ansible expone variables especiales en cada host durante la ejecución. No hay que definirlas — están siempre disponibles:
| Variable | Contenido |
|---|---|
{{ ansible_user }} |
El user de conexión definido en el inventario (tony, steve, ...) |
{{ inventory_hostname }} |
El alias del host en el inventario (stapp01, stapp02, ...) |
{{ inventory_hostname_short }} |
El alias sin dominio (stapp01 de stapp01.example.com) |
{{ ansible_host }} |
IP/hostname real de conexión (si difiere del alias) |
{{ groups }} |
Diccionario con todos los grupos y sus hosts |
{{ group_names }} |
Lista de grupos a los que pertenece el host actual |
{{ hostvars }} |
Diccionario con las variables de todos los hosts |
{{ play_hosts }} / {{ ansible_play_hosts }} |
Hosts activos en el play actual |
Clave conceptual: estas variables se evalúan por host. Cuando el play corre sobre stapp01, {{ ansible_user }} vale tony; sobre stapp02 vale steve. El mismo task produce resultados distintos en cada nodo — sin condicionales.
ansible_uservs facts:ansible_userviene del inventario (variable de conexión), está disponible desde el arranque. Losansible_facts.*(Día 83) vienen delGathering Factsy requieren conectar al host primero. Por esoansible_userfunciona aunque se hagagather_facts: false.
El módulo file con permisos y owner
Mismo módulo del Día 83, ahora usando más parámetros:
- name: create blank file
file:
path: /opt/nfsdata.txt
state: touch # ← Crea el archivo vacío
mode: '0777' # ← Permisos
owner: "{{ ansible_user }}" # ← chown al user
group: "{{ ansible_user }}" # ← chgrp al grupo
| Parámetro | Función |
|---|---|
path: |
Path del archivo (obligatorio) |
state: |
touch para crear vacío (ver Día 83 para todos los states) |
mode: |
Permisos — string entre comillas ('0777') |
owner: |
Dueño del archivo (chown) — requiere become si no es el user actual |
group: |
Grupo del archivo (chgrp) |
Por qué mode va entre comillas — el gotcha del octal
mode: '0777' con comillas, no mode: 0777. Es uno de los errores más comunes en Ansible:
- En YAML,
0777sin comillas se interpreta como número. Según la versión, puede leerse como octal (511 decimal) o decimal (777), produciendo permisos impredecibles. '0777'como string Ansible lo parsea siempre como octal correctamente →rwxrwxrwx.
mode: '0777' # ✅ rwxrwxrwx — owner, group, others: todo
mode: 0777 # ⚠️ ambiguo — puede dar permisos incorrectos
mode: 777 # ⚠️ se lee como decimal → octal 1411 → permisos basura
Regla: siempre comillas en mode. Vale para file, copy, template.
0777 — qué significa world-writable
0777 = rwxrwxrwx → lectura + escritura + ejecución para owner, grupo y todos los demás.
| Dígito | Aplica a | 7 = rwx |
|---|---|---|
| 1° | owner | read + write + execute |
| 2° | group | read + write + execute |
| 3° | others | read + write + execute |
En el output del lab se ve reflejado: -rwxrwxrwx 1 tony tony 0 ... nfsdata.txt. Para el lab está bien, pero 0777 en producción es un riesgo de seguridad — cualquier user del sistema puede modificar o borrar el archivo.
become: true — necesario igual que el Día 84
El destino es /opt/ (de root). Sin become, el user no puede crear ni hacer chown ahí:
Además, hacer chown a otro user siempre requiere privilegios root, aunque el directorio fuera escribible.
Pasos
- Login al jump host como
thor - Crear
~/playbook/inventorycon los 3 app servers y sus credenciales - Validar conectividad:
ansible -i ~/playbook/inventory all -m ping - Crear
~/playbook/playbook.ymlconhosts: all,become: truey el módulofile - Usar
{{ ansible_user }}enowner/grouppara resolver el dueño por host - Correr
ansible-playbook -i inventory playbook.yml - Validar en cada host:
ls -lhart /opt/nfsdata.txt
Comandos / Código
1. Crear el inventario
cat > ~/playbook/inventory <<'EOF'
stapp01 ansible_user=tony ansible_ssh_pass=Ir0nM@n ansible_ssh_common_args='-o StrictHostKeyChecking=no'
stapp02 ansible_user=steve ansible_ssh_pass=Am3ric@ ansible_ssh_common_args='-o StrictHostKeyChecking=no'
stapp03 ansible_user=banner ansible_ssh_pass=BigGr33n ansible_ssh_common_args='-o StrictHostKeyChecking=no'
EOF
2. Validar conectividad
Output esperado (los 3 responden pong):
stapp01 | SUCCESS => { ... "ping": "pong" }
stapp02 | SUCCESS => { ... "ping": "pong" }
stapp03 | SUCCESS => { ... "ping": "pong" }
3. Crear el playbook
# ~/playbook/playbook.yml
- hosts: all
become: true
tasks:
- name: create blank file
file:
path: /opt/nfsdata.txt
state: touch
mode: '0777'
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
4. Ejecutar el playbook
Output real del lab:
PLAY [all] *********************************************************************
TASK [Gathering Facts] *********************************************************
ok: [stapp03]
ok: [stapp02]
ok: [stapp01]
TASK [create blank file] *******************************************************
changed: [stapp02]
changed: [stapp03]
changed: [stapp01]
PLAY RECAP *********************************************************************
stapp01 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
stapp02 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
stapp03 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
changed=1y no0: a diferencia del Día 84 (módulocopy, idempotente),state: touchsiempre marcachangedporque actualiza elmtimeen cada corrida. Ver Día 83 para el detalle.
5. Validar en el host destino
Lo que confirma cada parte:
-rwxrwxrwx→ permisos0777aplicados ✅tony tony→ owner y group correctos (resuelto por{{ ansible_user }}) ✅0→ archivo vacío (blank file) ✅
Validar los 3 de una vez sin SSH manual:
ansible -i ~/playbook/inventory all -m shell -a "ls -l /opt/nfsdata.txt"
# stapp01: -rwxrwxrwx 1 tony tony ...
# stapp02: -rwxrwxrwx 1 steve steve ...
# stapp03: -rwxrwxrwx 1 banner banner ...
Cada host muestra su propio owner — la prueba de que {{ ansible_user }} se evaluó por host.
Variantes (referencia)
Variante explícita con when: por host (lo que evitamos)
Funciona, pero es lo que {{ ansible_user }} reemplaza con elegancia:
- hosts: all
become: true
vars:
owners:
stapp01: tony
stapp02: steve
stapp03: banner
tasks:
- name: create blank file
file:
path: /opt/nfsdata.txt
state: touch
mode: '0777'
owner: "{{ owners[inventory_hostname] }}"
group: "{{ owners[inventory_hostname] }}"
Útil cuando el owner no coincide con el user de conexión. Aquí sí coincide, así que {{ ansible_user }} es más simple.
Variante con host_vars
Definir el owner como variable por host en archivos host_vars/:
# host_vars/stapp01.yml → file_owner: tony
# host_vars/stapp02.yml → file_owner: steve
# host_vars/stapp03.yml → file_owner: banner
- name: create blank file
file:
path: /opt/nfsdata.txt
state: touch
mode: '0777'
owner: "{{ file_owner }}"
group: "{{ file_owner }}"
Troubleshooting
| Problema | Causa | Solución |
|---|---|---|
Permisos salen mal (ej. 0511 en vez de 0777) |
mode: 0777 sin comillas — YAML lo interpreta como número |
Usar siempre mode: '0777' como string |
chown failed: failed to look up user X |
El user no existe en ese host remoto | Verificar que el owner existe; con {{ ansible_user }} está garantizado (es quien conecta) |
Permission denied al crear o hacer chown |
Falta become: true — chown siempre requiere root |
Agregar become: true al play |
{{ ansible_user }} queda literal o da undefined |
El inventario no define ansible_user para ese host |
Asegurar ansible_user=... en cada línea del inventario |
Owner igual en los 3 hosts (todos tony) |
Se hardcodeó el owner en vez de usar la variable | Usar owner: "{{ ansible_user }}" para que resuelva por host |
changed=1 en cada re-run |
state: touch actualiza mtime siempre — comportamiento documentado |
Normal; para idempotencia estricta ver Día 83 (state: file o creates:) |
unreachable en un host |
Credenciales incorrectas de ese host | ansible all -m ping -i inventory para aislar cuál falla |
Conexión con días anteriores
- Día 83 (Ansible Playbook): introdujo el módulo
file+state: touch+ Gathering Facts + PLAY RECAP. Hoy reusatouchagregandomode/owner/group. - Día 84 (copy a app servers): introdujo
hosts: all+become: truea la flota de 3 servers. Hoy mismo target, distinto módulo. - Contraste de idempotencia 84↔85: el
copydel Día 84 era idempotente (changed=0en re-run); eltouchde hoy siempre marcachanged. Mismo PLAY RECAP, lectura distinta. - Día 78 (Conditional Pipeline en Jenkins): el paralelo conceptual — ahí se parametrizaba el comportamiento con
params; aquí con variables de inventario. Ambos evitan hardcodear valores por entorno.