Día 82 - Ansible Inventory for App Server Testing (formato INI + variables de conexión)
Problema / Desafío
Después de 13 días de CI/CD con Jenkins (Días 68-81), el journal retoma Ansible desde el Día 5. El lab arranca con el concepto más fundamental: el inventario — el catálogo de hosts donde los playbooks van a ejecutarse.
Requirements:
- Crear inventario tipo INI en
/home/thor/playbook/inventory(jump host) - Incluir App Server 3 (
stapp03) con las variables necesarias para conexión - El hostname del inventario debe matchear el nombre del wiki (
stapp03) - La validación corre:
ansible-playbook -i inventory playbook.yml— el inventario debe funcionar sin args extra
Datos del host destino:
| Campo | Valor |
|---|---|
| Hostname / wiki name | stapp03 |
| IP | 172.16.238.12 |
| SSH user | banner |
| SSH password | BigGr33n |
| Método de conexión | SSH |
Conceptos clave
Qué es un inventario Ansible
El inventario es el catálogo de hosts donde Ansible va a ejecutar playbooks. Para cada host define:
- Cómo se llama (alias usado en playbooks)
- Cómo conectarse (IP, user, autenticación, puerto, método)
- A qué grupos pertenece (para targeting selectivo)
Es el equivalente Ansible del kubeconfig de K8s (lista de clusters + auth) o del ~/.ssh/config de SSH (lista de hosts + cómo conectarse a cada uno).
Tipos de archivo para escribir inventarios en Ansible
Ansible soporta cinco formatos de inventario, agrupados en dos categorías:
Categoría 1 — Inventarios estáticos (archivos)
| Formato | Extensión típica | Cuándo conviene |
|---|---|---|
| INI | inventory, hosts, *.ini |
Inventarios chicos (≤ 20 hosts), estructura simple, lectura rápida |
| YAML | inventory.yml, *.yaml |
Inventarios grandes, jerarquías de grupos, variables complejas (listas, dicts) |
| JSON | *.json |
Output de scripts/herramientas que generan inventario; menos común a mano |
| TOML | *.toml (Ansible 2.8+) |
Alternativa estructurada — uso marginal, casi no se ve en producción |
Categoría 2 — Inventarios dinámicos (scripts)
| Tipo | Cuándo usar |
|---|---|
| Dynamic inventory plugin | Inventario generado en runtime desde una fuente externa (AWS, GCP, Azure, K8s) |
| Inventory script | Script ejecutable (Python, Bash, etc.) que devuelve JSON con la estructura esperada |
Comparación lado a lado — el mismo host en cada formato
El inventario del lab traducido a cada formato:
INI (lo del lab)
[stapp03]
stapp03 ansible_host=stapp03 ansible_user=banner ansible_password=BigGr33n ansible_connection=ssh
YAML equivalente
# inventory.yml
all:
children:
stapp03:
hosts:
stapp03:
ansible_host: stapp03
ansible_user: banner
ansible_password: BigGr33n
ansible_connection: ssh
Estructura YAML jerárquica: all → children → grupo → hosts → host → variables.
JSON equivalente
{
"stapp03": {
"hosts": ["stapp03"],
"vars": {}
},
"_meta": {
"hostvars": {
"stapp03": {
"ansible_host": "stapp03",
"ansible_user": "banner",
"ansible_password": "BigGr33n",
"ansible_connection": "ssh"
}
}
}
}
JSON sigue el formato canónico que Ansible espera de un script dinámico. La sección _meta.hostvars es la convención para variables por host. Raramente se escribe a mano — sale de tooling automatizado.
TOML equivalente
[stapp03]
[stapp03.hosts.stapp03]
ansible_host = "stapp03"
ansible_user = "banner"
ansible_password = "BigGr33n"
ansible_connection = "ssh"
TOML es una opción válida pero poco usada en el ecosistema Ansible. La doc oficial lo menciona pero la mayoría de tooling de Ansible asume INI o YAML.
Inventarios dinámicos — el caso "real" en cloud
En producción cloud, los inventarios estáticos no escalan: las instancias EC2/GCE aparecen y desaparecen constantemente. Dynamic inventory plugins consultan el cloud provider en runtime.
Ejemplo: AWS EC2 dynamic inventory
inventory_aws_ec2.yml:
plugin: amazon.aws.aws_ec2
regions:
- us-east-1
filters:
tag:Environment: production
instance-state-name: running
keyed_groups:
- key: tags.Role
prefix: role
- key: placement.availability_zone
prefix: az
hostnames:
- tag:Name
- dns-name
Al correr ansible-inventory -i inventory_aws_ec2.yml --list, Ansible llama a la API de AWS, lista las instancias filtradas, y devuelve un inventario JSON dinámico generado en runtime. Cada vez que se ejecuta, el inventario refleja el estado actual del cloud.
Plugins comunes:
| Plugin | Para qué |
|---|---|
amazon.aws.aws_ec2 |
Instancias EC2 de AWS |
google.cloud.gcp_compute |
VMs de GCP |
azure.azcollection.azure_rm |
VMs de Azure |
kubernetes.core.k8s |
Pods de un cluster K8s |
community.docker.docker_machines |
Containers de Docker |
theforeman.foreman.foreman |
Servers gestionados por Foreman/Satellite |
community.vmware.vmware_vm_inventory |
VMs de vSphere/vCenter |
Inventory script (formato custom)
Un script ejecutable que respeta el contrato:
- Acepta
--list→ devuelve JSON con toda la estructura - Acepta
--host <hostname>→ devuelve JSON con las variables de ese host
Ejemplo trivial en Bash:
#!/bin/bash
# /usr/local/bin/my_inventory.sh
case "$1" in
--list)
cat <<EOF
{
"stapp03": { "hosts": ["stapp03"] },
"_meta": { "hostvars": { "stapp03": { "ansible_host": "stapp03", "ansible_user": "banner" } } }
}
EOF
;;
--host)
echo '{}'
;;
esac
chmod +x /usr/local/bin/my_inventory.sh
ansible-playbook -i /usr/local/bin/my_inventory.sh playbook.yml
Ansible detecta que el archivo es ejecutable y lo trata como inventory script en lugar de archivo estático.
Cuándo usar cada tipo — guía rápida
| Escenario | Tipo recomendado |
|---|---|
| Lab, demo, ≤ 10 hosts fijos | INI |
| Producción on-prem, ≤ 50 hosts, estructura compleja | YAML |
| Producción cloud (AWS/GCP/Azure) | Dynamic inventory plugin |
| Inventario generado por otra herramienta (Terraform, CMDB) | Inventory script o JSON output |
| Multi-source (parte estático + parte dinámico) | Directorio con múltiples archivos — Ansible los une todos |
Múltiples inventarios — directorio en lugar de archivo
Una propiedad útil: -i puede apuntar a un directorio que contiene varios archivos de inventario. Ansible los lee todos y los une:
inventories/
├── ini/
│ └── hosts # INI con hosts on-prem
├── yaml/
│ └── databases.yml # YAML con grupo dbservers
└── dynamic/
└── aws_ec2.yml # Dynamic plugin para EC2
Esto une los tres inventarios — útil para entornos híbridos (on-prem + cloud).
Para el lab — INI es lo correcto
El lab pide INI explícitamente y es el formato más simple para un único host. Los otros formatos son referencia conceptual para días futuros y producción real.
Variables de conexión — las más usadas
| Variable | Significado |
|---|---|
ansible_host |
IP o hostname real (puede diferir del alias del inventario) |
ansible_user |
Usuario remoto para SSH |
ansible_password |
Password SSH (alias moderno de ansible_ssh_pass) |
ansible_ssh_pass |
Password SSH (alias legacy — todavía funciona) |
ansible_ssh_private_key_file |
Path a la clave privada SSH |
ansible_port |
Puerto SSH (default 22) |
ansible_connection |
Método: ssh (default), local, docker, winrm, kubectl |
ansible_python_interpreter |
Path al python del host remoto (útil si no es /usr/bin/python3) |
ansible_become |
true para usar sudo |
ansible_become_user |
Usuario al que escalar privilegios (default root) |
ansible_become_password |
Password para sudo (si no NOPASSWD) |
El alias del inventario (
stapp03en[stapp03]) puede ser cualquier nombre.ansible_hostes la IP/hostname real al que Ansible se conecta. Esto permite tener alias amigables (ej.web-prod-01) que apuntan a IPs cambiantes.
Anatomía del archivo INI — sintaxis completa
# Comentarios con # o ;
# Host simple sin grupo (va al grupo implícito "ungrouped")
hostA ansible_host=10.0.0.1
# Grupo de hosts
[webservers]
web1 ansible_host=10.0.0.10
web2 ansible_host=10.0.0.11
web3 ansible_host=10.0.0.12
# Otro grupo
[dbservers]
db1 ansible_host=10.0.1.10
db2 ansible_host=10.0.1.11
# Variables aplicables a TODOS los hosts del grupo
[webservers:vars]
ansible_user=deploy
ansible_port=22
http_port=8080
# Grupo de grupos (composición)
[stratos:children]
webservers
dbservers
# Variables aplicables al grupo compuesto
[stratos:vars]
datacenter=stratos
environment=production
Tres secciones especiales:
| Sección | Función |
|---|---|
[group_name] |
Hosts que pertenecen al grupo |
[group_name:vars] |
Variables que aplican a todos los hosts del grupo |
[group_name:children] |
Grupos que pertenecen a este grupo padre (composición jerárquica) |
Grupos especiales pre-existentes
Ansible crea automáticamente dos grupos:
| Grupo | Hosts incluidos |
|---|---|
all |
Todos los hosts del inventario |
ungrouped |
Hosts que no están en ningún grupo definido por usuario |
Esto permite targeting genérico:
ansible_password vs SSH keys — la práctica de seguridad
El password plano en el inventario es anti-pattern de producción. Las tres alternativas en orden de preferencia:
Opción 1 — SSH keys (lo más usado en producción)
[stapp03]
stapp03 ansible_host=172.16.238.12 ansible_user=banner \
ansible_ssh_private_key_file=~/.ssh/banner_key
Sin password en el archivo. La key privada vive en disco, protegida por permisos del filesystem (chmod 600).
Opción 2 — ansible-vault para encriptar el password
# Crear archivo de vault con el password
ansible-vault encrypt_string 'BigGr33n' --name 'ansible_password'
# Output: ansible_password: !vault | $ANSIBLE_VAULT;1.1;AES256\n<encrypted>
# Pegar en group_vars/stapp03/vault.yml
# Correr playbook con --ask-vault-pass o --vault-password-file
ansible-playbook -i inventory playbook.yml --ask-vault-pass
El password está cifrado at-rest pero descifrable con la master password del vault.
Opción 3 — Lookup desde secret manager externo
ansible_password: "{{ lookup('community.hashi_vault.vault_kv2_get', 'secret/data/stapp03')['data']['password'] }}"
El secret nunca toca disco — se lee en runtime desde Vault/AWS Secrets Manager/etc.
Para este lab: password plano es lo que pide el lab y funciona. Pero para producción real, SSH keys es la base y ansible-vault para los secretos restantes.
Validar el inventario antes de correr playbooks
Tres comandos útiles para validar:
# 1. Listar todos los hosts y su estructura de grupos
ansible-inventory -i inventory --list
# 2. Listar solo los hosts (formato más simple)
ansible-inventory -i inventory --graph
# 3. Hacer ping a todos los hosts (test de conectividad)
ansible all -i inventory -m ping
El ansible -m ping no solo testea ICMP — testea que Ansible puede ejecutar Python en el host remoto, que es el requisito mínimo para que cualquier playbook funcione. Es el test de salud canónico.
Por qué el playbook se ejecuta con -i inventory (path relativo)
El comando de validación del lab:
Esto solo funciona si:
- El archivo
inventoryestá en el directorio de trabajo (o relativo a él) playbook.ymltambién está en el mismo directorio- El usuario tiene permisos de lectura sobre los dos
Por eso el lab pide específicamente /home/thor/playbook/inventory — porque el playbook va a estar en /home/thor/playbook/ y el comando se va a ejecutar desde ahí.
Pasos
- Conectarse al jump host como
thor - Verificar que existe el directorio
/home/thor/playbook/ - Crear el archivo
/home/thor/playbook/inventorycon el formato INI - Definir el host
stapp03con sus variables de conexión - Validar con
ansible-inventory -i /home/thor/playbook/inventory --list - Validar conectividad con
ansible all -i /home/thor/playbook/inventory -m ping
Comandos / Código
1. Conectarse al jump host
2. Crear el archivo inventory
cat > /home/thor/playbook/inventory <<'EOF'
[stapp03]
stapp03 ansible_host=stapp03 ansible_user=banner ansible_password=BigGr33n ansible_connection=ssh
EOF
Dos detalles a notar en esta versión:
| Decisión | Por qué |
|---|---|
ansible_host=stapp03 (no IP) |
Usa resolución DNS interna del lab. Si la IP cambia, el inventario sigue funcionando |
ansible_password (no ansible_ssh_pass) |
Alias moderno (canónico desde Ansible 2.x), evita warning de deprecation |
El nombre del grupo
[stapp03]es opcional acá — el host podría ir directamente al grupoungroupedsi se omite el header. Convención: nombrar el grupo para poder targetear "todos los app servers" más adelante.
Versión con IP literal (alternativa)
Si el DNS interno no es confiable o se quiere fijar la IP explícitamente:
[stapp03]
stapp03 ansible_host=172.16.238.12 ansible_user=banner ansible_password=BigGr33n ansible_connection=ssh
Trade-off: la versión con DNS es más portable (sobrevive cambios de IP), la versión con IP es más determinística (no depende de resolución DNS). Para labs como este, cualquiera funciona; para producción, suele preferirse DNS interno con un service discovery confiable.
3. Versión equivalente sin ansible_connection (default es ssh)
ansible_connection=ssh es el default — solo se especifica cuando el método es distinto.
4. Validar la estructura del inventario
Output esperado:
{
"_meta": {
"hostvars": {
"stapp03": {
"ansible_connection": "ssh",
"ansible_host": "172.16.238.12",
"ansible_ssh_pass": "BigGr33n",
"ansible_user": "banner"
}
}
},
"all": {
"children": [
"stapp03",
"ungrouped"
]
},
"stapp03": {
"hosts": [
"stapp03"
]
}
}
Detalles a notar:
| Bloque | Significado |
|---|---|
_meta.hostvars.stapp03.<var> |
Variables del host (lo que aparece después del nombre en el INI) |
all.children |
Grupos hijos del grupo all — siempre incluye ungrouped |
stapp03.hosts |
Hosts del grupo stapp03 (solo uno en este caso) |
5. Validar conectividad — el "smoke test" del inventario
Output esperado:
stapp03 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": false,
"ping": "pong"
}
"ping": "pong"confirma que: 1. La resolución DNS / IP funcionó 2. El SSH se autenticó correctamente 3. Python existe en el host remoto y Ansible puede ejecutar módulos
6. Variantes del inventario (referencia, no aplica al lab estricto)
Variante con grupo más descriptivo
[app_servers]
stapp03 ansible_host=172.16.238.12 ansible_user=banner ansible_ssh_pass=BigGr33n
[app_servers:vars]
http_port=8080
Permite agregar más app servers después sin duplicar variables comunes.
Variante con SSH keys (producción)
[app_servers]
stapp03 ansible_host=172.16.238.12 ansible_user=banner
[app_servers:vars]
ansible_ssh_private_key_file=~/.ssh/banner_key
Sin password en el archivo — la key vive aparte con chmod 600.
Variante YAML equivalente
# inventory.yml
all:
children:
app_servers:
hosts:
stapp03:
ansible_host: 172.16.238.12
ansible_user: banner
ansible_ssh_pass: BigGr33n
vars:
http_port: 8080
Anatomía — orden de precedencia de variables
Ansible busca variables en este orden (de menor a mayor prioridad):
1. role defaults
2. inventory file (este lab) / inventory script
3. inventory group_vars/all
4. inventory group_vars/<group_name>
5. inventory host_vars/<host>
6. playbook group_vars / host_vars
7. host facts (gathered)
8. play vars
9. task vars
10. extra vars (-e en la línea de comando) ← gana sobre todo
Para el lab, todas las variables están en el inventory file (nivel 2) — el más bajo. Si más adelante se agregan group_vars/app_servers.yml, esas variables sobreescriben.
Inventario INI vs hosts file de Linux
| Aspecto | /etc/hosts del SO |
Inventario Ansible |
|---|---|---|
| Para qué sirve | Resolución DNS local (hostname → IP) | Catálogo de hosts + metadata de conexión + grupos |
| Contenido por host | Solo IP + alias | IP, user, password/key, port, vars custom |
| Estructura | Plano | Jerarquía de grupos con composición |
| Quién lo usa | Cualquier proceso del SO via libnss | Solo Ansible (otros tools tienen sus propios formatos) |
El paralelo conceptual existe: ambos son "lo primero que se consulta para resolver un host". Ansible inventory es la versión enriquecida con metadata operacional.
Conexión con días anteriores
- Día 1-5 (Ansible journal original): los primeros pasos con Ansible. Hoy retoma esa sección después de 77 días — el inventario es el primer prerequisito para todo lo que sigue.
- Día 7 (passwordless SSH): la alternativa al password en el inventario. En producción, el inventario apunta a una key privada, no a un password literal.
- Día 8 (Install Ansible con pip3): el setup de Ansible. Hoy se asume que está instalado en el jump host.
- Día 75 (Slave Nodes de Jenkins): paralelo conceptual — Jenkins inventory = lista de nodes con sus labels y credentials; Ansible inventory = lista de hosts con sus variables. Mismo problema, distinto stack.
- Día 71 (Jenkins job con SSH a server): en Jenkins el SSH se configura via Credentials Manager +
agent { label }. En Ansible se configura viaansible_user+ansible_passworden el inventory. La diferencia: Ansible es declarativo (todo en el archivo), Jenkins distribuye entre UI/credentials/labels.
Reflexión: el inventario como contrato de Ansible
Troubleshooting
| Problema | Causa | Solución |
|---|---|---|
ansible-playbook -i inventory ... no encuentra el inventario |
Path relativo y el comando se corre desde otro directorio | cd /home/thor/playbook antes, o usar path absoluto en -i |
ansible all -m ping devuelve unreachable |
(a) IP/hostname incorrecta; (b) puerto SSH bloqueado; (c) credenciales mal | Probar SSH manual: sshpass -p BigGr33n ssh banner@172.16.238.12 hostname |
Failed to connect to the host via ssh: Permission denied (publickey,password) |
Password incorrecto o SSH del host destino tiene PasswordAuth disabled | Verificar /etc/ssh/sshd_config del destino: PasswordAuthentication yes |
sshpass: Failed to run command: No such file or directory |
Falta sshpass en el control node — Ansible lo requiere para usar ansible_password |
sudo yum install -y sshpass o sudo apt install -y sshpass |
| El playbook funciona desde un user pero no desde otro | Ansible busca ~/.ansible/, config local — el otro user no tiene los archivos |
Usar -i con path absoluto, o setear ANSIBLE_CONFIG=/path/to/ansible.cfg |
[WARNING]: provided hosts list is empty |
El inventario está vacío, mal formateado, o el grupo target no existe | ansible-inventory --list para validar la estructura |
| Variables del inventory no aplican | Typo en el nombre de la variable (ej. ansible_users en lugar de ansible_user) |
Validar nombres exactos contra la doc oficial |
ansible_ssh_pass da warning de deprecation |
Es alias deprecated desde 2.x — sigue funcionando pero conviene migrar | Reemplazar por ansible_password |
Playbook funciona en stapp03 pero al agregar otro host falla |
Las variables son por-host; cada host necesita sus propias credenciales | Mover a [group:vars] lo común; mantener por-host solo lo específico |
Host key verification failed |
Primera conexión al host — known_hosts no tiene la host key | ansible all -m ping con host_key_checking=False (en ansible.cfg) o aceptar manualmente |
Deep dive — referencia transversal de inventories
Para una referencia completa de inventarios fuera del scope de este lab específico (formatos, dynamic plugins, vault, precedencia de variables, troubleshooting), ver:
tools/ansible/inventories.md— guía de referencia transversal
Esa guía amplía los temas tocados acá con casos productivos (AWS EC2 plugin, inventario por directorio, ansible-vault para secrets), troubleshooting general, y el orden completo de precedencia de variables.