Día 84 - Copy Data to App Servers using Ansible (módulo copy + hosts: all + become)
Problema / Desafío
El equipo DevOps de Nautilus necesita copiar datos desde el jump host hacia todos los application servers de Stratos DC usando Ansible. Es el primer lab que apunta a toda la flota (no a un solo host como el Día 83) y que copia un archivo desde el control node.
Requirements:
- Crear
/home/thor/ansible/inventorycon todos los application servers como managed nodes - Crear
/home/thor/ansible/playbook.ymlque copie/usr/src/finance/index.htmla/opt/financeen todos los servers - La validación corre
ansible-playbook -i inventory playbook.yml— sin args extra
Datos de los hosts destino:
| Host | User | Password |
|---|---|---|
stapp01 |
tony |
Ir0nM@n |
stapp02 |
steve |
Am3ric@ |
stapp03 |
banner |
BigGr33n |
Continúa del Día 83 — mismo formato INI, pero ahora con 3 hosts y el módulo copy en lugar de file.
Conceptos clave
hosts: all — apuntar a toda la flota
En el Día 83 el play apuntaba a un host específico (hosts: stapp01). Hoy el target es todos los hosts del inventario:
all es un grupo implícito que Ansible crea automáticamente con cada host del inventario. No hay que declararlo. Otras formas de seleccionar hosts:
Patrón en hosts: |
A qué apunta |
|---|---|
all / * |
Todos los hosts del inventario |
stapp01 |
Un host específico (por alias) |
stapp01,stapp02 |
Lista explícita de hosts |
webservers |
Un grupo definido en el inventario |
webservers:&production |
Intersección — en webservers Y en production |
webservers:!stapp03 |
En webservers excepto stapp03 |
stapp0* |
Wildcard — todos los que empiezan con stapp0 |
Inventario multi-host con credenciales por línea
A diferencia del Día 83 (un solo host), aquí cada host tiene su propio user y password porque son cuentas distintas en cada server:
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'
Producción: las credenciales inline son un anti-pattern. Lo correcto es SSH keys (Día 7) o Ansible Vault para cifrar los secretos. Para el lab, inline es lo más directo.
Cuando las variables se repiten (como ansible_ssh_common_args), se pueden factorizar en un grupo:
[appservers]
stapp01 ansible_user=tony ansible_ssh_pass=Ir0nM@n
stapp02 ansible_user=steve ansible_ssh_pass=Am3ric@
stapp03 ansible_user=banner ansible_ssh_pass=BigGr33n
[appservers:vars]
ansible_ssh_common_args='-o StrictHostKeyChecking=no'
El módulo copy — del control node al remoto
El módulo copy transfiere un archivo desde el control node (jump host) hacia los managed nodes:
- name: copy file
ansible.builtin.copy:
src: /usr/src/finance/index.html # ← Path en el CONTROL NODE (jump host)
dest: /opt/finance # ← Path en el REMOTO (cada app server)
| Parámetro | Función |
|---|---|
src: |
Origen — archivo en el control node (o relativo a files/ del playbook) |
dest: |
Destino — path en el remoto (obligatorio) |
owner: |
Dueño del archivo destino (chown) |
group: |
Grupo del archivo destino (chgrp) |
mode: |
Permisos: '0644' o u=rw,g=r,o=r |
backup: |
true para crear un backup con timestamp si el destino ya existía |
force: |
true (default) sobreescribe; false solo copia si el destino no existe |
remote_src: |
true si src está en el remoto (copia local en el remoto, no transfiere) |
content: |
Contenido inline en vez de un archivo src (mutuamente excluyente con src) |
dest como directorio vs archivo — la regla del trailing slash
Detalle clave de copy (y template): el comportamiento de dest depende de si termina en / y de si el path ya existe como directorio:
dest |
Si es directorio existente | Si NO existe |
|---|---|---|
/opt/finance (sin /) |
copia como /opt/finance/index.html |
crea un archivo llamado /opt/finance |
/opt/finance/ (con /) |
copia como /opt/finance/index.html |
error — el directorio debe existir |
En este lab /opt/finance ya existe como directorio, así que dest: /opt/finance deja el archivo en /opt/finance/index.html. Si /opt/finance no existiera como directorio, Ansible crearía un archivo con ese nombre — un error silencioso común.
become: true — por qué hace falta aquí
/opt/ pertenece a root. Los users del inventario (tony, steve, banner) no pueden escribir ahí sin escalar privilegios:
Diferencia con el Día 83: ahí el destino era /tmp/ (world-writable), así que become no era necesario. Aquí el destino es /opt/finance → sí hace falta.
| Directiva | Default | Función |
|---|---|---|
become: |
false |
Activar escalada de privilegios |
become_user: |
root |
A qué usuario escalar |
become_method: |
sudo |
Método: sudo, su, doas, pbrun, ... |
copy SÍ es idempotente (a diferencia de touch)
Punto de contraste fuerte con el Día 83. El file con state: touch siempre marcaba changed=true. El módulo copy es idempotente de verdad:
TASK [copy file] ********************************************
ok: [stapp01] ← ok, NO changed
ok: [stapp02]
ok: [stapp03]
PLAY RECAP **************************************************
stapp01 : ok=2 changed=0 ...
Cómo lo logra: copy calcula el checksum (SHA1) del archivo origen y lo compara con el del destino:
- Hash distinto (o el destino no existe) → copia el archivo → reporta
changed - Hash idéntico → no hace nada → reporta
ok
Por eso un changed=0 en este lab significa que el archivo ya estaba con el mismo contenido (o se corrió el playbook dos veces). La primera corrida real sobre servers vacíos mostraría changed=3.
# Primera corrida (destino vacío):
stapp01 : ok=2 changed=1 ...
# Segunda corrida (mismo contenido):
stapp01 : ok=2 changed=0 ... ← idempotencia real
| Módulo (state) | ¿Idempotente? | Cómo decide si cambió |
|---|---|---|
file + state: touch |
No (siempre changed) | Actualiza mtime cada vez |
copy |
Sí | Compara checksum SHA1 origen vs destino |
template |
Sí | Compara el resultado renderizado vs destino |
Pasos
- Login al jump host como
thor cd /home/thor/ansible/- Crear el inventario con los 3 app servers y sus credenciales
- Validar conectividad con
ansible all -m ping -i inventory - Crear
playbook.ymlconhosts: all,become: truey el módulocopy - Correr
ansible-playbook -i inventory playbook.yml - Validar en cada host con
ls /opt/finance/
Comandos / Código
1. Crear el inventario
cat > /home/thor/ansible/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 a toda la flota
Output esperado (un bloque por host):
stapp01 | SUCCESS => { "ping": "pong" }
stapp02 | SUCCESS => { "ping": "pong" }
stapp03 | SUCCESS => { "ping": "pong" }
Si los 3 responden pong, los 3 sets de credenciales son correctos.
3. Crear el playbook
# /home/thor/ansible/playbook.yml
- hosts: all
become: true
tasks:
- name: copy file
ansible.builtin.copy:
src: /usr/src/finance/index.html
dest: /opt/finance
4. Ejecutar el playbook
Output real del lab:
PLAY [all] ******************************************************************
TASK [Gathering Facts] ******************************************************
ok: [stapp02]
ok: [stapp03]
ok: [stapp01]
TASK [copy file] ************************************************************
ok: [stapp02]
ok: [stapp03]
ok: [stapp01]
PLAY RECAP ******************************************************************
stapp01 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
stapp02 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
stapp03 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Nota sobre el orden: los hosts aparecen en orden variable (
stapp02antes questapp01) porque Ansible ejecuta en paralelo sobre los hosts (hasta 5 a la vez por default — elforks). El orden del output refleja cuál terminó primero, no el orden del inventario.
5. Validar en los hosts destino
# Vía Ansible (más rápido — un solo comando para los 3)
ansible all -i /home/thor/ansible/inventory -m shell -a "ls -l /opt/finance/"
# O manualmente por host
ssh tony@stapp01 'cat /opt/finance/index.html'
Análisis — diferencias clave con el Día 83
| Aspecto | Día 83 (file + touch) | Día 84 (copy) |
|---|---|---|
| Target | hosts: stapp01 (1 host) |
hosts: all (3 hosts) |
| Módulo | file (state: touch) |
copy |
| Qué hace | Crea archivo vacío | Transfiere archivo del control node |
| Destino | /tmp/ (world-writable) |
/opt/finance (de root) |
become |
No necesario | Sí necesario |
| Idempotencia | No (siempre changed) |
Sí (compara checksum) |
| Credenciales | 1 set | 3 sets (uno por host) |
Variantes del playbook (referencia)
Variante con permisos y dueño explícitos
- hosts: all
become: true
tasks:
- name: copy index.html con permisos
ansible.builtin.copy:
src: /usr/src/finance/index.html
dest: /opt/finance/index.html
owner: root
group: root
mode: '0644'
Variante con backup del archivo previo
- name: copy con backup
ansible.builtin.copy:
src: /usr/src/finance/index.html
dest: /opt/finance/
backup: true # ← Guarda /opt/finance/index.html.2026-06-15@...~ si ya existía
Variante con remote_src (copia dentro del propio remoto)
# src ya está en el remoto — Ansible NO transfiere desde el control node
- name: copiar dentro del remoto
ansible.builtin.copy:
src: /usr/src/finance/index.html
dest: /opt/finance/
remote_src: true
Reflexión: push de archivos con Ansible vs scp/rsync manual
Troubleshooting
| Problema | Causa | Solución |
|---|---|---|
Permission denied al escribir en /opt/finance |
Falta become: true — el user no es dueño de /opt |
Agregar become: true al play |
El archivo se copia como /opt/finance (archivo, no dentro del dir) |
/opt/finance no existía como directorio en el remoto |
Crear el dir antes (file: state=directory) o usar dest: /opt/finance/ con el dir ya creado |
Could not find or access '/usr/src/finance/index.html' |
El src no existe en el control node (jump host), no en el remoto |
Verificar ls /usr/src/finance/index.html en el jump host |
unreachable en uno de los 3 hosts |
Credenciales incorrectas para ese host específico | ansible all -m ping -i inventory para aislar cuál falla; revisar user/password de ese host |
changed=0 en la primera corrida y se sospecha que no copió |
El destino ya tenía el mismo contenido (checksum idéntico) — comportamiento normal | Validar con ansible all -m shell -a "cat /opt/finance/index.html" |
| Solo un host recibe el archivo | hosts: apunta a un host específico en vez de all |
Cambiar a hosts: all |
ssh: Host key verification failed |
known_hosts sin la host key | El StrictHostKeyChecking=no del inventario lo resuelve |
sudo: a password is required |
become necesita password de sudo y no se proveyó |
En los labs el user suele tener sudo NOPASSWD; si no, agregar --ask-become-pass (rompe la validación sin args) |