SSD Nodes Learn Hosting plans →
Guías Matt ConnorPor Matt Connor · Actualizado 2026-08-23

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.

Verified Every command ran end-to-end on a fresh Ubuntu 24.04 server, July 30, 2026.

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.yml
  • tasks/main.yml es el punto de entrada. Ansible ejecuta este archivo cuando se llama al rol, y todos los demás directorios son opcionales.
  • defaults/main.yml contiene 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.yml contiene 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.yml contiene las tareas activadas por notify. 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ódulo copy copia literalmente, y templates/ contiene las plantillas Jinja2 que procesa el módulo template. 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.yml declara 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 common

Esto 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=local
ansible-playbook -i inventory.ini site.yml

El 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-password

Hay 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: post

Para 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.yml está 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/ y host_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.yml tiene más prioridad que host_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 -e en 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-cli

La 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.yml

El segundo resumen debería ser similar a este:

PLAY RECAP *********************************************************************
localhost   : ok=4  changed=0  unreachable=0  failed=0  skipped=0  rescued=0  ignored=0

changed=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.txt

Ejecute 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/lonely

Ese 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.yml

Existe 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.0
ansible-galaxy install -r requirements.yml -p galaxy_roles

Defina 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_roles

Los 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.

#ansible#roles#playbook#structure#automation