Día 05 - Crear archivos con permisos y propietario específico usando Ansible
Problema / Desafío
Crear un archivo vacío /tmp/code.txt en todos los servidores de aplicación usando Ansible. El archivo debe tener permisos 0644 y el propietario (user/group) debe corresponder al usuario específico de cada servidor: tony en app1, steve en app2 y banner en app3.
Conceptos clave
- Módulo
file: Gestiona archivos y directorios en hosts remotos. Permite crear, eliminar, modificar permisos y cambiar propietarios. Constate: touchcrea un archivo vacío si no existe, o actualiza su timestamp si ya existe (similar al comandotouchen Linux). state: touche idempotencia: A diferencia de otros states comoabsentodirectory,touchsiempre reportachangedporque actualiza el timestamp del archivo incluso si ya existe. Sin embargo, es seguro ejecutarlo múltiples veces: no elimina contenido existente ni causa errores si el archivo ya está presente. No es necesario verificar previamente si el archivo existe.- Variable
ansible_user: Es una variable de conexión que define con qué usuario Ansible se conecta al host remoto. Se define en el inventario por host. En este playbook la reutilizamos como valor paraownerygroup, lo que permite asignar el propietario correcto en cada servidor sin usar condicionales ni variables adicionales. - Módulo
stat: Obtiene información detallada de un archivo (permisos, propietario, tamaño, timestamps, etc.). Equivalente al comandostatde Linux. Los resultados se almacenan en una variable conregisterpara uso posterior. - Filtro
regex_replace: Transforma cadenas usando expresiones regulares. En este caso se usa para eliminar los ceros iniciales del modo octal (ej:0100644→644) y hacer la salida más legible.
Por qué usar ansible_user como owner/group
La variable ansible_user se define en el inventario para cada host como parte de la configuración de conexión:
Al usar owner: "{{ ansible_user }}" y group: "{{ ansible_user }}" en la tarea, Ansible resuelve la variable con el valor correspondiente a cada host durante la ejecución. Esto significa que:
- En
stapp01, el archivo queda con owner/grouptony - En
stapp02, consteve - En
stapp03, conbanner
Ventaja: No se necesitan condicionales (when), bloques host_vars, ni variables adicionales. Se reutiliza una variable que ya existe en el inventario, manteniendo el playbook simple y sin duplicación.
Alternativa sin ansible_user: Si los usuarios de conexión fueran diferentes a los propietarios deseados, se podría usar host_vars o una variable personalizada en el inventario:
Pasos
- Crear el archivo de inventario con los tres servidores y sus usuarios
- Crear el playbook con la tarea de creación del archivo, permisos y validación
- Ejecutar el playbook
- Verificar la salida del
debugpara confirmar owner, group y permisos
Comandos / Código
Inventory
[all]
stapp01 ansible_user=tony ansible_password=xxxx ansible_host=stapp01.stratos.xfusioncorp.com
stapp02 ansible_user=steve ansible_password=xxxx ansible_host=stapp02.stratos.xfusioncorp.com
stapp03 ansible_user=banner ansible_password=xxxx ansible_host=stapp03.stratos.xfusioncorp.com
Playbook
---
- name: create empty file
hosts: all
become: yes
vars:
file_dst: "/tmp/code.txt"
tasks:
- name: create file
file:
path: "{{ file_dst }}"
state: touch
mode: "0644"
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
- name: valid file creation
stat:
path: "{{ file_dst }}"
register: file_creation
- name: output stat
debug:
msg: "Owner : {{ file_creation.stat.pw_name }}, Group : {{ file_creation.stat.gr_name }}, Mode : {{ file_creation.stat.mode | string | regex_replace('^0+', '') }}"
Desglose del playbook:
hosts: all: Ejecuta en todos los hosts del inventario.become: yes: Escala privilegios para poder cambiar el owner/group del archivo.vars: Define la ruta del archivo como variable para evitar repetición.
Tareas:
- create file: Usa el módulo
fileconstate: touchpara crear el archivo vacío. Aplica permisos0644y asigna owner/group usandoansible_user. - valid file creation: Usa
statpara obtener los metadatos del archivo creado y los guarda enfile_creation. - output stat: Muestra owner, group y permisos del archivo usando
debug. El filtroregex_replacelimpia los ceros iniciales del modo.
Ejecución
Salida esperada (resumida):
PLAY [create empty file] ******************************************************
TASK [create file] ************************************************************
changed: [stapp01]
changed: [stapp02]
changed: [stapp03]
TASK [valid file creation] ****************************************************
ok: [stapp01]
ok: [stapp02]
ok: [stapp03]
TASK [output stat] ************************************************************
ok: [stapp01] => {
"msg": "Owner : tony, Group : tony, Mode : 644"
}
ok: [stapp02] => {
"msg": "Owner : steve, Group : steve, Mode : 644"
}
ok: [stapp03] => {
"msg": "Owner : banner, Group : banner, Mode : 644"
}
PLAY RECAP ********************************************************************
stapp01 : ok=3 changed=1 unreachable=0 failed=0 skipped=0
stapp02 : ok=3 changed=1 unreachable=0 failed=0 skipped=0
stapp03 : ok=3 changed=1 unreachable=0 failed=0 skipped=0
Tips y notas adicionales
No es necesario verificar si el archivo existe antes de usar touch
El módulo file con state: touch ya maneja ambos escenarios:
- Si el archivo no existe, lo crea.
- Si el archivo ya existe, actualiza su timestamp sin modificar el contenido.
Agregar una tarea previa con stat + when: not file.stat.exists es innecesario y agrega complejidad sin beneficio. El módulo file ya es seguro para ejecutar múltiples veces.
mode como string vs número
Siempre definir los permisos como string entre comillas ("0644"), no como número (0644). En YAML, un número que empieza con 0 se interpreta como octal, lo que puede causar resultados inesperados dependiendo del parser. Usar string garantiza que Ansible reciba el valor correcto.
become: yes es necesario para cambiar owner/group
Aunque el archivo se crea en /tmp (donde cualquier usuario puede escribir), cambiar el owner y group a un usuario diferente al que ejecuta la tarea requiere privilegios de root. Sin become: yes, la tarea fallaría con Permission denied al intentar hacer chown.
Cuidado al reutilizar variables ansible_* como datos del playbook
Las variables que empiezan con ansible_ son variables especiales de conexión. Ansible las usa internamente para establecer la conexión SSH. ansible_user define con qué usuario conectarse, ansible_password la contraseña, ansible_host la dirección del servidor, etc.
En este playbook reutilizamos ansible_user para un segundo propósito: asignar el propietario del archivo. Funciona porque el usuario de conexión y el propietario deseado son el mismo. Pero si en algún momento se cambia el usuario de conexión (por ejemplo, usar un usuario genérico deploy para todos los servidores), el owner del archivo también cambiaría, rompiendo el comportamiento esperado:
# Si cambias la conexión a un usuario genérico...
stapp01 ansible_user=deploy ansible_host=stapp01.stratos.xfusioncorp.com
# ...el archivo quedaría con owner "deploy" en vez de "tony"
owner: "{{ ansible_user }}" # resuelve a "deploy", no a "tony"
Por esta razón, cuando el propietario del archivo no depende directamente del usuario de conexión, es mejor usar una variable propia. A continuación se documentan las alternativas.
Alternativas para asignar valores únicos por servidor
1. Variables personalizadas en el inventory (lo más simple)
Se agregan variables propias directamente en la línea de cada host:
[all]
stapp01 ansible_user=tony ansible_password=xxxx ansible_host=stapp01.stratos.xfusioncorp.com file_owner=tony
stapp02 ansible_user=steve ansible_password=xxxx ansible_host=stapp02.stratos.xfusioncorp.com file_owner=steve
stapp03 ansible_user=banner ansible_password=xxxx ansible_host=stapp03.stratos.xfusioncorp.com file_owner=banner
Se pueden agregar cuantas variables se necesiten por host. La desventaja es que la línea se hace larga cuando hay muchas.
2. Diccionario en el playbook
Se define un diccionario en vars mapeando cada host a su valor:
inventory_hostname es una variable especial de Ansible que contiene el nombre del host actual según el inventario. Se usa como clave para buscar el valor en el diccionario. Útil cuando se quiere mantener toda la lógica en el playbook sin modificar el inventario.
3. Directorio host_vars/ (lo más organizado)
Se crea un archivo YAML por host en un directorio host_vars/:
project/
├── inventory
├── playbook.yml
└── host_vars/
├── stapp01.yml
├── stapp02.yml
└── stapp03.yml
Ansible carga estas variables automáticamente por host sin declararlas en el inventario ni en el playbook. Solo se usa {{ file_owner }} en la tarea. Es la mejor práctica cuando el proyecto crece y cada host tiene muchas variables propias.
Comparación de enfoques
| Enfoque | Dónde se define | Ventaja | Desventaja |
|---|---|---|---|
| Variable en inventory | Línea del host en el inventario | Simple, todo en un archivo | Líneas largas con muchas variables |
| Diccionario en playbook | Sección vars del playbook |
Lógica centralizada en el playbook | Se debe mantener sincronizado con el inventario |
host_vars/ |
Archivos separados por host | Organizado, escalable, carga automática | Más archivos que gestionar |
Reutilizar ansible_* |
Inventario (ya existe) | Sin variables extra | Acoplamiento entre conexión y lógica de negocio |
Troubleshooting
| Problema | Solución |
|---|---|
chown failed: failed to look up user |
El usuario definido en ansible_user no existe en el servidor remoto. Verificar que el usuario existe con id <usuario> |
Permission denied al crear el archivo |
Verificar que become: yes está definido en el playbook |
touch siempre reporta changed |
Es el comportamiento esperado de state: touch. No indica un problema, simplemente actualiza el timestamp del archivo |
regex_replace no muestra el modo correcto |
Verificar que se usa file_creation.stat.mode (string) y no file_creation.stat.mode como entero. El filtro string antes de regex_replace asegura la conversión |
Authentication failure |
Verificar usuario y contraseña en el inventario para ese host específico |