Ansible: playbook o role, cuál usar y cuándo
Aprenda cuándo basta un playbook plano y cuándo conviene un role: estructura de directorios, ansible-galaxy init, llamadas y precedencia de variables.
Ansible playbook frente a role: cuál es la diferencia
Un playbook de Ansible es el archivo que se ejecuta con ansible-playbook. Asocia un grupo de hosts con las tareas que debe realizar. Un role de Ansible es un directorio con una estructura fija que contiene tareas, plantillas, handlers y variables predeterminadas; un playbook lo invoca por nombre. La sintaxis de las tareas es idéntica en ambos casos, por lo que no se trata de qué se puede expresar. Se trata de la reutilización.
Empiece con un playbook plano. Un site.yml que contiene una lista de tasks: tiene la estructura adecuada para su primera automatización y seguirá siendo adecuada durante más tiempo del que la mayoría espera. Conviértalo en un role cuando el mismo bloque de tareas deba ejecutarse para un segundo grupo de hosts o cuando el archivo supere aproximadamente las 100 líneas y ya no pueda localizar una tarea desplazándose por él.
Si todavía no ha escrito ninguno, empiece con un primer playbook para un VPS individual y vuelva cuando empiece a crecer.
Cuándo un playbook plano es la opción correcta
Un playbook plano es adecuado cuando el trabajo se ejecuta una sola vez, en un único host o cuando nadie más va a leerlo. Aprovisionar un único servidor de aplicaciones o aplicar parches a un equipo antes de una ventana de mantenimiento no justifica crear un árbol de directorios. Un role añade siete directorios y una capa de indirección. Si el único elemento que lo utiliza es el playbook que está junto a él, esa indirección no aporta nada y obliga a saltar entre archivos cada vez que se quiere revisar lo que realmente se ejecuta.
El playbook plano deja de ser adecuado en un momento concreto, que es fácil de identificar. Se copia un bloque de tareas en un segundo playbook. Esa copia es la señal. A partir de entonces, cada corrección debe hacerse dos veces, y algún día sólo se hará una vez.
Qué contiene realmente un directorio de rol
roles/common/
defaults/main.yml
vars/main.yml
tasks/main.yml
handlers/main.yml
templates/99-hardening.conf.j2
files/
meta/main.ymltasks/main.ymles el punto de entrada. Ansible ejecuta este archivo cuando se llama al rol, y todos los demás directorios son opcionales.defaults/main.ymlcontiene las variables que se espera que el invocador sobrescriba. Es la fuente con menor prioridad en Ansible, por lo que casi cualquier otra fuente tiene precedencia.vars/main.ymlcontiene las variables que no se espera que el invocador sobrescriba. Tiene mayor prioridad que el inventario, lo que supone una decisión importante. Úselo rara vez.handlers/main.ymlcontiene las tareas activadas pornotify. Un handler se ejecuta al final del play y una sola vez, independientemente del número de tareas que lo hayan notificado.files/contiene los archivos que el módulocopycopia literalmente, ytemplates/contiene las plantillas Jinja2 que procesa el módulotemplate. Dentro de un rol, haga referencia a ambos mediante el nombre de archivo sin ruta, porque Ansible busca primero en los directorios propios del rol.meta/main.ymldeclara las dependencias del rol y los metadatos que lee Ansible Galaxy.
La estructura no es una preferencia de estilo. Ansible busca en estas rutas exactas, por lo que una plantilla que coloque en roles/common/template/ (singular) simplemente no se encuentra.
Crear el rol common con ansible-galaxy init
mkdir -p ~/infra/roles
cd ~/infra
ansible-galaxy init --init-path roles commonEsto escribe toda la estructura bajo roles/common, incluidos directorios que no usará y archivos main.yml que sólo contienen ---. Elimine los que deje vacíos. Un vars/main.yml vacío no causa problemas a Ansible, pero oculta qué archivos del rol son realmente importantes.
Ahora complete los archivos que realizan el trabajo. Empiece por los valores predeterminados, porque son la interfaz pública del rol.
# roles/common/defaults/main.yml
---
common_packages:
- ufw
- fail2ban
- unattended-upgrades
common_admin_group: admins
common_permit_root_login: "no"
common_password_authentication: "no"Entrecomille "no" y "yes". Ansible analiza YAML con PyYAML, que interpreta un no sin comillas como el booleano false, por lo que la línea de configuración generada se convierte en PermitRootLogin False y sshd la rechaza. Las comillas mantienen el valor como una cadena.
# roles/common/tasks/main.yml
---
- name: Install the base packages
ansible.builtin.apt:
name: "{{ common_packages }}"
state: present
update_cache: true
cache_valid_time: 3600
- name: Create the admin group
ansible.builtin.group:
name: "{{ common_admin_group }}"
state: present
- name: Install the sshd hardening drop-in
ansible.builtin.template:
src: 99-hardening.conf.j2
dest: /etc/ssh/sshd_config.d/99-hardening.conf
owner: root
group: root
mode: "0644"
validate: /usr/sbin/sshd -t -f %s
notify: Restart sshd# roles/common/handlers/main.yml
---
- name: Restart sshd
ansible.builtin.service:
name: ssh
state: restarted# roles/common/templates/99-hardening.conf.j2
# Managed by Ansible. Local edits are overwritten on the next run.
PermitRootLogin {{ common_permit_root_login }}
PasswordAuthentication {{ common_password_authentication }}En Debian y Ubuntu, la unidad de systemd se llama ssh. En los sistemas de la familia RHEL se llama sshd. Un handler que indique el nombre incorrecto falla sólo cuando algo cambia realmente la plantilla. Por eso el problema suele aparecer varias semanas después.
La línea validate es el elemento más útil de esa tarea. Ansible genera la plantilla en un archivo temporal, sustituye %s por la ruta de ese archivo y ejecuta el comando. El destino sólo se reemplaza si el comando termina con el código 0. Añada una directiva incorrecta a la plantilla y vuelva a ejecutar la tarea: falla con failed to validate, el /etc/ssh/sshd_config.d/99-hardening.conf real no se modifica y todavía conserva un servidor al que puede conectarse. Tenga en cuenta que la comprobación valida más que la sintaxis. Si sshd -t no puede leer las claves de host, termina con sshd: no hostkeys available -- exiting. y Ansible informa del mismo failed to validate. Por tanto, revise msg del módulo antes de atribuir el problema a la plantilla.
Cómo un playbook llama a un role
# site.yml
---
- name: Base configuration for every server
hosts: all
become: true
roles:
- common# inventory.ini
[local]
localhost ansible_connection=localansible-playbook -i inventory.ini site.ymlEl play debe terminar con failed=0 en el resumen de ejecución. Pase los parámetros en el punto de llamada con la forma expandida. Así, un role puede servir a dos grupos de hosts:
roles:
- role: common
common_admin_group: ops
common_permit_root_login: prohibit-passwordHay una regla de orden que sorprende a casi todo el mundo. Un play puede contener pre_tasks, roles, tasks y post_tasks, y Ansible los ejecuta en ese orden, independientemente del orden en que los haya escrito en el archivo. Coloque tasks: encima de roles: y los roles seguirán ejecutándose primero. Por tanto, si algo debe ocurrir antes de un role, debe incluirse en pre_tasks:, no al principio de tasks:.
- name: Ordering demonstration
hosts: local
gather_facts: false
pre_tasks:
- name: Runs first
ansible.builtin.debug:
msg: pre
roles:
- common
tasks:
- name: Runs after the role
ansible.builtin.debug:
msg: task
post_tasks:
- name: Runs last
ansible.builtin.debug:
msg: postPara llamar a un role desde una lista de tareas en lugar de usar la clave roles:, use import_role o include_role.
tasks:
- name: Static, read when the playbook is parsed
ansible.builtin.import_role:
name: common
- name: Dynamic, resolved when the task runs
ansible.builtin.include_role:
name: postgres
when: "'db' in group_names"import_role es estático. Ansible lee el role durante el análisis y sus tareas pasan a formar parte del play, por lo que ansible-playbook --list-tasks site.yml las muestra y una etiqueta en la importación se aplica a todas las tareas internas. include_role es dinámico. Ansible no lee nada hasta que se ejecuta la tarea, lo que permite obtener el nombre del role desde una variable o un bucle. La desventaja es que esas tareas no aparecen en --list-tasks ni en --start-at-task.
Aquí hay un problema frecuente. Un when: en una tarea include_role se evalúa antes de que defaults/main.yml del role incluido esté disponible. Si escribe when: common_packages | length > 0 en la inclusión, la ejecución se detiene con 'common_packages' is undefined, aunque esa variable esté definida en el mismo role que está incluyendo. La solución consiste en sacar el conmutador del role: colóquelo en group_vars/all.yml, donde estará disponible en todas partes, y reserve los valores predeterminados del role para los valores que el propio role consume.
Qué variable prevalece: defaults, group_vars, vars y extra vars
Ansible documenta más de veinte niveles de precedencia de variables. Cuatro resuelven casi todas las discusiones reales. Aquí aparecen de menor a mayor prioridad.
roles/<name>/defaults/main.ymlestá cerca de la parte inferior. Casi cualquier valor que establezca en otro lugar lo sobrescribe. Por eso es el lugar adecuado para los parámetros ajustables de un rol.group_vars/yhost_vars/están en la parte intermedia. Aquí deben estar las decisiones propias de su sitio. Además, sobrescriben limpiamente los valores predeterminados del rol.roles/<name>/vars/main.ymltiene más prioridad quehost_vars. Un valor que coloque aquí no puede sobrescribirse desde el inventario. Resérvelo para los valores que el rol necesita mantener coherentes internamente, como un nombre de paquete que debe coincidir con un nombre de servicio.- Un parámetro de rol pasado en el punto de llamada tiene más prioridad que
vars/main.yml, y-een la línea de comandos tiene más prioridad que todo lo demás, incluidos los parámetros del rol.
Puede observar este proceso en aproximadamente un minuto. Dé a un rol pequeño un valor predeterminado y una variable del rol. Después, establezca los mismos nombres en host_vars.
# roles/prec/defaults/main.yml
---
prec_tunable: from-defaults
prec_internal: from-defaults# roles/prec/vars/main.yml
---
prec_internal: from-rolevars# host_vars/localhost.yml
---
prec_tunable: from-hostvars
prec_internal: from-hostvars# roles/prec/tasks/main.yml
---
- name: Show which value survived
ansible.builtin.debug:
msg: "tunable={{ prec_tunable }} internal={{ prec_internal }}"ansible-playbook -i inventory.ini prec.yml
ansible-playbook -i inventory.ini prec.yml -e prec_internal=from-cliLa primera ejecución muestra tunable=from-hostvars internal=from-rolevars. El inventario prevaleció sobre el valor predeterminado del rol, pero perdió frente a la variable del rol. La segunda ejecución muestra internal=from-cli porque las variables adicionales tienen la máxima prioridad y ningún nivel inferior puede sobrescribirlas. Por eso -e es adecuado para una ejecución puntual, pero no para un script que conserve: sobrescribe silenciosamente todas las decisiones consideradas del repositorio.
La regla práctica es la siguiente: si quiere que un valor se pueda establecer, colóquelo en defaults/. Ponerlo en vars/ indica a todos los usuarios futuros del rol que el inventario no puede cambiarlo. En ocasiones eso es lo que quería. Normalmente es un accidente.
Demuestre que el rol es idempotente: ejecútelo dos veces
Una ejecución de Ansible fiable produce el mismo resultado la segunda vez e informa de que no cambió nada. Ejecute el playbook dos veces y revise el resumen.
ansible-playbook -i inventory.ini site.yml
ansible-playbook -i inventory.ini site.ymlEl segundo resumen debería ser similar a este:
PLAY RECAP *********************************************************************
localhost : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0changed=0 significa que cada módulo inspeccionó el estado actual y comprobó que el trabajo ya estaba hecho. changed=2 en una segunda ejecución significa que dos tareas no pueden distinguir la situación, por lo que seguirán reescribiendo archivos y reiniciando servicios indefinidamente. La causa habitual es command o shell, porque Ansible no puede saber qué hizo un comando arbitrario.
# traps.yml
---
- name: Command modules do not know what they changed
hosts: local
gather_facts: false
tasks:
- name: This appends a line on every run
ansible.builtin.shell: "echo run >> /tmp/grow.txt"
- name: This appends a line only once
ansible.builtin.shell: "echo run >> /tmp/guarded.txt"
args:
creates: /tmp/guarded.txtEjecute ese playbook dos veces y cuente las líneas que contienen wc -l /tmp/grow.txt /tmp/guarded.txt. /tmp/grow.txt contiene dos líneas y /tmp/guarded.txt contiene una. En la segunda ejecución, la tarea protegida no se ejecutó y su resultado incluye el mensaje skipped, since /tmp/guarded.txt exists, porque creates proporciona al módulo un producto visible que debe buscar primero. Cuando un comando no deja un producto de ese tipo, registre su salida y decida usted mismo con changed_when.
ansible-playbook --check --diff site.yml predice los cambios sin aplicarlos, y --diff muestra las líneas exactas que una plantilla reescribiría. Lea la salida teniendo en cuenta una excepción: las tareas shell y command se omiten en el modo de comprobación, por lo que un plan que parece limpio todavía puede ocultar trabajo.
Otra columna de ese resumen requiere la misma atención: un host al que Ansible no pudo conectarse se cuenta en unreachable, no en failed, y ninguna de sus tareas se ejecutó. Por tanto, decida de antemano si un host inalcanzable debe detener toda la ejecución antes de aplicar este rol a más de un par de máquinas.
Por qué Ansible indica que no se encontró el role
Ansible busca un directorio roles/ junto al archivo del playbook y, después, en roles_path. La búsqueda sigue al playbook, no al shell.
ERROR! the role 'common' was not found in /home/deploy/lonely/roles:/home/deploy/lonelyEse mensaje significa que site.yml y roles/ han dejado de coincidir. También muestra las rutas que intentó utilizar. Mantenga ambos en el mismo directorio. Ejecutar el comando desde un directorio superior no supone un problema, porque cuenta la ruta del playbook:
ansible-playbook -i infra/inventory.ini infra/site.ymlExiste una variante menos evidente del mismo problema. Ansible ignora un ansible.cfg del directorio actual cuando ese directorio permite escritura a cualquier usuario, porque cualquier usuario del sistema podría colocar allí una configuración y modificar el comportamiento de la ejecución.
[WARNING]: Ansible is being run in a world writable directory (/tmp/infra), ignoring it as an ansible.cfg source.En ese caso, la configuración roles_path y inventory desaparece de forma silenciosa, y la búsqueda del role falla por un motivo que no está relacionado con los roles. ansible --version muestra el config file que cargó realmente, y ansible-config dump --only-changed muestra cada ajuste que difiere de los valores predeterminados integrados. Compruebe ambos comandos cuando una ejecución se comporte como si no existiera su configuración.
Compartir roles: requirements.yml y una versión fijada
Un role escrito por otra persona se instala, no se copia. Declárelo una sola vez:
# requirements.yml
---
roles:
- name: postgres
src: https://github.com/example/ansible-role-postgres
scm: git
version: v1.4.0ansible-galaxy install -r requirements.yml -p galaxy_rolesDefina siempre version. Sin este parámetro, se usa la rama predeterminada que exista el día en que ejecute el comando. Por eso, un despliegue que funcionaba el mes pasado puede fallar sin ningún cambio en su propio repositorio. Indique en roles_path el directorio de descarga y mantenga ese directorio fuera de git:
# ansible.cfg
[defaults]
inventory = inventory.ini
roles_path = ./galaxy_rolesLos roles situados en roles/ junto al playbook también se encuentran, porque esa ruta siempre se busca además de roles_path. Así, sus propios roles permanecen versionados y revisados, mientras que los roles de terceros son descargas reproducibles fijadas a un tag.
Cuando los roles dejan de ser la solución
Un rol es una unidad reutilizable dentro de una ejecución de Ansible. No crea servidores ni registros DNS en el proveedor, y tratar de hacerlo convierte los playbooks en algo que nadie quiere mantener. Conviene leer cómo se divide el trabajo entre Ansible y Terraform antes de empezar. Un rol tampoco sustituye al diseño del inventario: cuando se administran más de unas pocas máquinas, cómo agrupar esos servidores y acceder a ellos importa más que la forma de organizar las tareas.
El refuerzo de seguridad que instala este rol common también requiere decisiones específicas. El archivo adicional anterior establece dos directivas y ninguna más. Por eso, lea qué ajustes de SSH merece la pena cambiar y cómo hacer que Ubuntu aplique las actualizaciones de seguridad automáticamente antes de decidir qué debe incluirse en el rol para cada host que administre.
FAQ
¿Cuándo debo convertir un playbook de Ansible en un role?
Cuando el mismo bloque de tareas tenga que ejecutarse en un segundo play o contra un segundo grupo de hosts. Copiar tareas entre playbooks es la señal, porque desde ese momento cada corrección debe aplicarse dos veces y, algún día, sólo se aplicará una vez. Un playbook único de aproximadamente menos de 100 líneas que siempre se dirija a un solo grupo no obtiene ninguna ventaja de un role, y los directorios adicionales dificultan su lectura.
¿Los roles se ejecutan antes que las tareas del mismo play?
Sí. Ansible ejecuta pre_tasks, después todo lo incluido en roles:, luego tasks: y, por último, post_tasks:. Ignora el orden en que aparezcan esas claves en el archivo. Escribir tasks: encima de roles: no hace que esas tareas se ejecuten primero. Si algo debe ocurrir antes que un role, colóquelo en pre_tasks:.
¿Por qué el valor de group_vars no sobrescribe el del role?
Compruebe si la variable está definida en vars/main.yml del role en lugar de defaults/main.yml. vars/ tiene mayor prioridad que group_vars y host_vars en el orden de precedencia de Ansible, por lo que el inventario no puede sobrescribirlo. Mueva la variable a defaults/main.yml, que está cerca del final del orden y es el lugar correcto para cualquier valor que el caller deba poder cambiar. Para confirmar que la causa es la precedencia y no un error tipográfico, ejecute una vez con -e name=value, que tiene prioridad sobre cualquier otra fuente.
¿Por qué Ansible indica que no encontró el role?
La búsqueda comienza junto al archivo del playbook, por lo que site.yml y roles/ deben estar en el mismo directorio. El error muestra las rutas que probó, como en the role 'common' was not found in /home/deploy/lonely/roles:/home/deploy/lonely. Ejecutar el playbook desde un directorio padre no supone ningún problema, porque la búsqueda sigue la ruta del playbook y no el directorio de trabajo del shell. Si depende de roles_path desde ansible.cfg, confirme que el archivo se cargó con ansible --version, ya que Ansible ignora un directorio de trabajo con permisos de escritura para cualquier usuario.
¿Necesito ansible-galaxy init para crear un role?
No. Un role sólo consta de directorios con los nombres esperados, por lo que mkdir -p roles/common/tasks más un tasks/main.yml ya forman un role operativo. ansible-galaxy init --init-path roles common evita escribirlos manualmente y proporciona la estructura completa, incluidos meta/main.yml y un README básico. Elimine los directorios que deje vacíos, porque un vars/main.yml vacío oculta qué archivos del role realizan realmente alguna función.