Ansible: playbook o role, cuándo usar cada uno
Aprenda cuándo basta un playbook plano y cuándo conviene un role: estructura, ansible-galaxy init, llamadas al role 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. Asigna un grupo de hosts al trabajo que deben 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 tasks: es la estructura adecuada para su primera automatización, y sigue siéndolo durante más tiempo del que la mayoría espera. Conviértalo en un role cuando el mismo bloque de tareas tenga que ejecutarse para un segundo grupo de hosts, o cuando el archivo supere aproximadamente las 100 líneas y ya no pueda encontrar una tarea desplazándose por él.
Si todavía no ha escrito uno, empiece con un primer playbook para un único VPS y vuelva cuando empiece a crecer.
Cuándo es correcta una playbook plana
Una playbook plana es adecuada cuando el trabajo se ejecuta una sola vez, en un solo host o cuando nadie más la va a leer. 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 la única persona que la invoca es la playbook que está junto a ella, esa indirección no aporta nada y obliga a saltar de un archivo a otro cada vez que se quiere leer lo que realmente se ejecuta.
La playbook plana deja de ser adecuada en un momento concreto, que es fácil de identificar. Se copia un bloque de tareas en una segunda 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 llamador 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 llamador sobrescriba. Tiene más 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, una sola vez, sin importar cuántas tareas lo hayan notificado.files/contiene los archivos que el módulocopycopia sin modificaciones, ytemplates/contiene las plantillas Jinja2 que procesa el módulotemplate. Dentro de un rol, haga referencia a ambos sólo por 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 nunca se encuentra.
Crear el rol común con ansible-galaxy init
mkdir -p ~/infra/roles
cd ~/infra
ansible-galaxy init --init-path roles commonEso escribe todo el esqueleto en 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 necesarios.
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"Ponga entre comillas "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, y en los sistemas de la familia RHEL se llama sshd. Un handler que especifique la unidad incorrecta falla sólo cuando algo cambia realmente la plantilla, por lo que normalmente el problema aparece semanas después.
La línea validate es lo 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 código 0. Añada una directiva no válida a la plantilla y vuelva a ejecutar la tarea: esta falla con failed to validate, el /etc/ssh/sshd_config.d/99-hardening.conf real permanece intacto y todavía puede iniciar sesión en el servidor. 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, lea msg del módulo antes de culpar a la plantilla.
Cómo un playbook llama a un rol
# 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. Pase los parámetros en el punto de llamada con la forma expandida. Así, un rol puede servir para 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: antes de roles: y los roles seguirán ejecutándose primero. Por tanto, si algo debe ocurrir antes de un rol, debe estar 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 rol desde una lista de tareas en lugar de usar la clave roles:, utilice 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 rol 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. No se lee nada hasta que se ejecuta la tarea, lo que permite obtener el nombre del rol desde una variable o un bucle. La desventaja es que esas tareas no son visibles para --list-tasks ni para --start-at-task.
Aquí hay una trampa. Un when: en una tarea include_role se evalúa antes de que defaults/main.yml del rol 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 rol que está incluyendo. La solución es sacar el indicador del rol: colóquelo en group_vars/all.yml, donde estará disponible en todas partes, y deje los valores predeterminados del rol para los valores que consuma el propio rol.
Qué variable prevalece: valores predeterminados, group_vars, vars o variables adicionales
Ansible documenta más de veinte niveles de precedencia de variables. Cuatro de ellos resuelven casi cualquier discusión real. Estos son, de menor a mayor prioridad.
roles/<name>/defaults/main.ymlse encuentra cerca del nivel inferior. Casi cualquier valor definido en otro lugar lo reemplaza. Por eso es el lugar adecuado para los parámetros ajustables de un role.group_vars/yhost_vars/se encuentran en los niveles intermedios. Aquí deben estar los valores específicos de su sitio. Además, reemplazan limpiamente los valores predeterminados del role.roles/<name>/vars/main.ymltiene mayor prioridad quehost_vars. Un valor definido aquí no puede reemplazarse desde el inventory. Resérvelo para los valores que el role necesita mantener coherentes internamente, como un nombre de paquete que debe coincidir con el nombre de un servicio.- Un parámetro de role pasado en el punto de invocación tiene prioridad sobre
vars/main.yml, y-een la línea de comandos tiene prioridad sobre todo lo demás, incluidos los parámetros del role.
Puede observar este proceso en aproximadamente un minuto. Cree un role pequeño con un valor predeterminado y una variable del role. Después, defina 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 inventory tuvo prioridad sobre el valor predeterminado del role, pero perdió frente a la variable del role. La segunda ejecución muestra internal=from-cli, porque las variables adicionales están en el nivel más alto y ningún nivel inferior puede reemplazarlas. Por eso -e es adecuado para una ejecución puntual, pero no para un script que conserva: tiene prioridad silenciosamente sobre todas las decisiones consideradas en su repositorio.
La regla práctica es la siguiente: si quiere que un valor pueda modificarse, colóquelo en defaults/. Colocarlo en vars/ indica a todos los usuarios futuros del role que el inventory no puede cambiarlo. A veces eso es lo que necesita. 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 indica que no se modificó 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 tener este aspecto:
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 el estado actual, 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 resultado visible que puede buscar primero. Cuando un comando no deja un resultado de ese tipo, registre su salida y tome la decisión 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. Interprete la salida teniendo en cuenta una excepción: las tareas shell y command se omiten en modo de comprobación, por lo que un plan que parece limpio aún puede ocultar trabajo.
Por qué Ansible indica que no se encontró el rol
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 desde el que se ejecuta.
ERROR! the role 'common' was not found in /home/deploy/lonely/roles:/home/deploy/lonelyEse mensaje significa que site.yml y roles/ ya no coinciden. También muestra las rutas que probó. Mantenga ambos en el mismo directorio. Ejecutar el comando desde un directorio padre no supone ningún problema, porque lo que cuenta es 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 en el directorio actual cuando ese directorio tiene permisos de escritura para todos, porque cualquier usuario del equipo podría colocar allí una configuración y cambiar 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, los ajustes roles_path y inventory quedan ausentes sin ningún aviso, y la búsqueda del rol 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 todos los ajustes que difieren de los valores predeterminados integrados. Compruebe ambos siempre que 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_rolesEstablezca siempre version. Sin esta opción, se usa lo que contenga la rama predeterminada el día en que ejecute el comando. Por eso, una implementación que funcionó el mes pasado puede fallar sin ningún cambio en su propio repositorio. Indique roles_path como directorio de descarga y excluya ese directorio de git:
# ansible.cfg
[defaults]
inventory = inventory.ini
roles_path = ./galaxy_rolesLos roles de roles/ situados 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 una etiqueta.
Cuando los roles dejan de ser la respuesta
Un role es una unidad de reutilización dentro de una ejecución de Ansible. No crea servidores ni registros DNS en su proveedor. Intentar que lo haga es la forma de convertir los playbooks en algo que nadie quiere mantener. Conviene leer cómo se divide el trabajo entre Ansible y Terraform antes de empezar. Un role tampoco sustituye al diseño del inventario: cuando se supera un puñado de máquinas, cómo agrupa y accede a esos servidores importa más que la forma de organizar las tareas.
El hardening que instala este role common también requiere decisiones específicas. El archivo drop-in anterior establece dos directivas y ninguna más. Por tanto, 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 incluir en el role para cada host que administre.
FAQ
¿Cuándo debo convertir un playbook de Ansible en un rol?
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. Un playbook único de unas 100 líneas como máximo que siempre se dirija a un solo grupo no obtiene ninguna ventaja de un rol, 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 finalmente post_tasks:, e ignora el orden en que esas claves aparecen en el archivo. Escribir tasks: encima de roles: no hace que esas tareas se ejecuten primero. Si algo debe ocurrir antes que un rol, colóquelo en pre_tasks:.
¿Por qué mi valor de group_vars no sobrescribe el del rol?
Compruebe si la variable está definida en vars/main.yml del rol 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 sobrescribirla. Mueva la variable a defaults/main.yml, que está cerca del final del orden y es el lugar correcto para cualquier valor que el usuario del rol 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 se encontró el rol?
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 de 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 rol?
No. Un rol sólo consiste en directorios con los nombres esperados, por lo que mkdir -p roles/common/tasks más un tasks/main.yml ya constituye un rol funcional. ansible-galaxy init --init-path roles common evita escribir parte de la estructura y proporciona el esqueleto completo, incluidos meta/main.yml y un README inicial. Elimine los directorios que deje vacíos, porque un vars/main.yml vacío oculta qué archivos del rol realizan realmente alguna función.