Un rol de Ansible agrupa tareas, plantillas, handlers y variables con una estructura fija, de modo que puedes reutilizarlo en varios playbooks y proyectos. Ansible Galaxy es el repositorio público de roles y colecciones, y ansible-galaxy la herramienta para crearlos e instalarlos. En este tutorial crearás desde cero un rol que despliega Nginx con un sitio configurable, aplicarás las prácticas que hacen que un rol sea mantenible, instalarás dependencias de Galaxy con versiones fijadas y probarás el rol con ansible-lint y Molecule en Ubuntu 24.04.
Requisitos previos
Para seguir esta guía necesitas:
- Una máquina de control con Ubuntu 24.04 LTS y un usuario no root con privilegios
sudo. Puede ser tu portátil o un VPS de CubePath. - Docker Engine instalado en la máquina de control, para las pruebas con Molecule (consulta la guía de instalación de Docker en Ubuntu 24.04) y tu usuario en el grupo
docker. - Opcional: un servidor Ubuntu 24.04 de destino con acceso SSH por clave, para aplicar el rol de verdad.
- Conocimientos básicos de playbooks e inventarios de Ansible.
Paso 1: Instalar Ansible, ansible-lint y Molecule
Instalar las herramientas con pipx deja cada una en su propio entorno virtual, sin tocar el Python del sistema, y permite actualizarlas por separado:
sudo apt update
sudo apt install pipx
pipx ensurepath
Cierra la sesión y vuelve a entrar para que ~/.local/bin quede en el PATH. Después instala las tres herramientas:
pipx install --include-deps ansible
pipx install ansible-lint
pipx install molecule
pipx inject molecule "molecule-plugins[docker]"
molecule-plugins[docker] añade el driver que crea contenedores Docker como máquinas de prueba. Comprueba las versiones:
ansible --version | head -1
ansible-lint --version
molecule --version
Cada comando debe imprimir su versión. En la salida de molecule --version debe aparecer una línea con el driver docker procedente de molecule_plugins; si no aparece, repite el pipx inject.
Paso 2: Preparar el proyecto
Crea un directorio para el proyecto con un ansible.cfg que le diga a Ansible dónde están los roles y colecciones propios del proyecto:
mkdir -p ~/infra/roles && cd ~/infra
nano ansible.cfg
[defaults]
roles_path = ./roles
collections_path = ./collections
inventory = ./inventory.ini
Guardar roles y colecciones dentro del proyecto, en lugar de en ~/.ansible, hace que cada proyecto tenga sus propias versiones y que el resultado sea reproducible en otra máquina o en CI.
Paso 3: Crear el esqueleto del rol
Genera la estructura estándar con ansible-galaxy:
ansible-galaxy role init --init-path roles nginx_site
- Role nginx_site was created successfully
Revisa lo que se ha creado:
tree roles/nginx_site
roles/nginx_site
├── README.md
├── defaults
│ └── main.yml
├── files
├── handlers
│ └── main.yml
├── meta
│ └── main.yml
├── tasks
│ └── main.yml
├── templates
├── tests
│ ├── inventory
│ └── test.yml
└── vars
└── main.yml
(Si no tienes tree, instálalo con sudo apt install tree.) Cada directorio tiene un propósito:
| Directorio | Contenido |
|---|---|
defaults/ | Variables con valores por defecto que el usuario del rol puede sobrescribir. Es la "API" del rol. |
vars/ | Variables internas con prioridad alta, que no deberían cambiarse desde fuera. |
tasks/ | Las tareas; main.yml es el punto de entrada. |
handlers/ | Acciones que se ejecutan solo si una tarea notifica un cambio, como recargar un servicio. |
templates/ | Plantillas Jinja2 (.j2). |
files/ | Archivos que se copian tal cual. |
meta/ | Metadatos para Galaxy, dependencias y validación de argumentos. |
Borra lo que no vayas a usar: rm -r roles/nginx_site/files roles/nginx_site/vars roles/nginx_site/tests. Menos archivos vacíos hacen el rol más fácil de leer, y las pruebas las harás con Molecule.
Paso 4: Definir las variables del rol
Las variables por defecto son la interfaz pública del rol. Prefija todas con el nombre del rol para evitar colisiones con variables de otros roles:
nano roles/nginx_site/defaults/main.yml
---
nginx_site_name: example
nginx_site_server_name: example.com
nginx_site_root: "/var/www/{{ nginx_site_name }}"
nginx_site_listen_port: 80
nginx_site_index_content: "Hola desde {{ nginx_site_server_name }}"
Declara además qué acepta el rol en meta/argument_specs.yml. Ansible valida estas especificaciones antes de ejecutar el rol y falla con un mensaje claro si una variable tiene un tipo incorrecto o un valor no permitido:
nano roles/nginx_site/meta/argument_specs.yml
---
argument_specs:
main:
short_description: Instala Nginx y publica un sitio estático.
options:
nginx_site_name:
type: str
description: Nombre del sitio, usado para el archivo de configuración y el directorio raíz.
nginx_site_server_name:
type: str
description: Valor de la directiva server_name.
nginx_site_root:
type: path
description: Directorio raíz del sitio.
nginx_site_listen_port:
type: int
description: Puerto en el que escucha el sitio.
nginx_site_index_content:
type: str
description: Contenido de la página index.html inicial.
Paso 5: Escribir las tareas, la plantilla y el handler
Escribe las tareas usando nombres de módulo completos (FQCN, como ansible.builtin.apt), que evitan ambigüedades si una colección instala un módulo con el mismo nombre corto:
nano roles/nginx_site/tasks/main.yml
---
- name: Install Nginx
ansible.builtin.apt:
name: nginx
state: present
update_cache: true
cache_valid_time: 3600
- name: Create site root directory
ansible.builtin.file:
path: "{{ nginx_site_root }}"
state: directory
owner: root
group: root
mode: "0755"
- name: Deploy index page
ansible.builtin.copy:
content: "{{ nginx_site_index_content }}\n"
dest: "{{ nginx_site_root }}/index.html"
owner: root
group: root
mode: "0644"
- name: Deploy site configuration
ansible.builtin.template:
src: site.conf.j2
dest: "/etc/nginx/sites-available/{{ nginx_site_name }}.conf"
owner: root
group: root
mode: "0644"
notify: Reload nginx
- name: Enable site
ansible.builtin.file:
src: "/etc/nginx/sites-available/{{ nginx_site_name }}.conf"
dest: "/etc/nginx/sites-enabled/{{ nginx_site_name }}.conf"
state: link
notify: Reload nginx
- name: Ensure Nginx is running and enabled
ansible.builtin.service:
name: nginx
state: started
enabled: true
Crea la plantilla de configuración del sitio:
nano roles/nginx_site/templates/site.conf.j2
# {{ ansible_managed }}
server {
listen {{ nginx_site_listen_port }};
server_name {{ nginx_site_server_name }};
root {{ nginx_site_root }};
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
Y los handlers, que solo se ejecutan cuando una tarea notifica un cambio. El primero comprueba la sintaxis completa de Nginx y el segundo recarga el servicio; como se ejecutan en el orden en que están definidos, una configuración rota detiene el play antes de la recarga y Nginx sigue sirviendo la configuración anterior:
nano roles/nginx_site/handlers/main.yml
---
- name: Check nginx configuration
ansible.builtin.command: nginx -t
changed_when: false
listen: Reload nginx
- name: Apply nginx reload
ansible.builtin.service:
name: nginx
state: reloaded
listen: Reload nginx
Las tareas son idempotentes: cada una describe un estado final, no una acción, así que ejecutar el rol dos veces seguidas no debe producir cambios la segunda vez. Evita ansible.builtin.shell y command cuando existe un módulo que hace lo mismo; si no hay alternativa, añade creates: o changed_when: para que la tarea siga siendo idempotente.
Paso 6: Completar los metadatos para Galaxy
meta/main.yml describe el rol para Galaxy y declara sus dependencias de otros roles. Sustitúyelo por una versión reducida con datos reales:
nano roles/nginx_site/meta/main.yml
---
galaxy_info:
role_name: nginx_site
namespace: your_namespace
author: your_name
description: Instala Nginx y publica un sitio estático configurable.
license: MIT
min_ansible_version: "2.15"
platforms:
- name: Ubuntu
versions:
- noble
galaxy_tags:
- nginx
- web
dependencies: []
Sustituye your_namespace y your_name. Mantén dependencies vacío siempre que puedas: las dependencias entre roles se ejecutan de forma implícita y dificultan seguir qué ocurre. Es más claro listar los roles en orden en el playbook.
Documenta en README.md qué hace el rol, sus variables y un ejemplo de uso. Es lo primero que leerá quien lo reutilice.
Paso 7: Instalar dependencias de Galaxy con requirements.yml
Los proyectos reales combinan roles propios con roles y colecciones de Galaxy. Decláralos en un requirements.yml con versiones fijadas, para que una actualización del autor no cambie tu infraestructura sin que lo decidas:
nano requirements.yml
---
collections:
- name: community.general
version: ">=10.0.0,<11.0.0"
- name: ansible.posix
version: ">=1.5.0"
- name: community.docker
version: ">=3.10.2"
roles:
- name: geerlingguy.ntp
Para los roles, fija una versión concreta con version: usando una etiqueta del repositorio del rol; puedes consultar las disponibles en su página de Galaxy o GitHub. Instala todo:
ansible-galaxy install -r requirements.yml
Comprueba qué se ha instalado y dónde:
ansible-galaxy collection list -p ./collections
ansible-galaxy role list
# /home/your_user/infra/collections/ansible_collections
Collection Version
----------------- -------
ansible.posix 2.1.0
community.docker 4.7.0
community.general 10.7.3
...
# /home/your_user/infra/roles
- geerlingguy.ntp, 3.0.0
- nginx_site, (unknown version)
Añade collections/ y los roles de Galaxy a .gitignore y versiona solo requirements.yml: cualquiera que clone el proyecto ejecuta el mismo ansible-galaxy install -r y obtiene las mismas versiones.
Paso 8: Revisar el rol con ansible-lint
ansible-lint detecta errores y malas prácticas: módulos sin FQCN, tareas sin nombre, permisos sin comillas, uso de shell evitable y más. Ejecútalo desde la raíz del proyecto:
ansible-lint roles/nginx_site
Passed: 0 failure(s), 0 warning(s) on 7 files. Last profile that met the validation criteria was 'production'.
Si aparece algún aviso, la salida indica el archivo, la línea y el identificador de la regla. Corrige el código en lugar de silenciar la regla, salvo que tengas una razón concreta.
Paso 9: Probar el rol con Molecule
Molecule crea una máquina de prueba, aplica el rol, comprueba el resultado, vuelve a aplicarlo para verificar la idempotencia y destruye la máquina. El driver de Docker necesita las colecciones community.docker y ansible.posix en la ruta por defecto de Ansible, ya que Molecule no lee el ansible.cfg del proyecto:
ansible-galaxy collection install community.docker ansible.posix
Un escenario de Molecule es un directorio dentro del rol con tres archivos. Créalo:
cd ~/infra/roles/nginx_site
mkdir -p molecule/default
Define el escenario con una imagen de Ubuntu 24.04 preparada para Ansible:
nano molecule/default/molecule.yml
---
driver:
name: docker
platforms:
- name: ubuntu2404
image: geerlingguy/docker-ubuntu2404-ansible
pre_build_image: true
command: ""
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:rw
cgroupns_mode: host
privileged: true
provisioner:
name: ansible
verifier:
name: ansible
La imagen ejecuta systemd, por eso necesita privileged y el montaje de cgroups: sin ellos no se puede arrancar Nginx como servicio. Úsala solo en máquinas de prueba.
El playbook converge.yml aplica el rol. Molecule añade el directorio padre a la ruta de roles, así que el rol se referencia por el nombre de su directorio:
nano molecule/default/converge.yml
---
- name: Converge
hosts: all
become: true
roles:
- role: nginx_site
vars:
nginx_site_name: molecule
nginx_site_server_name: localhost
Y en verify.yml comprueba que el sitio responde:
nano molecule/default/verify.yml
---
- name: Verify
hosts: all
gather_facts: false
tasks:
- name: Request the site
ansible.builtin.uri:
url: http://localhost/
return_content: true
register: site
- name: Check page content
ansible.builtin.assert:
that:
- "'Hola desde localhost' in site.content"
Ejecuta el ciclo completo:
molecule test
Molecule crea el contenedor, aplica el rol, lo aplica una segunda vez (prueba de idempotencia), ejecuta la verificación y lo destruye. La prueba es correcta si el comando termina con código de salida 0 y en la fase verify las dos tareas aparecen como ok:
TASK [Check page content] ******************************************************
ok: [ubuntu2404] => {
"changed": false,
"msg": "All assertions passed"
}
Si la idempotencia falla, Molecule lista las tareas que devolvieron changed en la segunda ejecución. Mientras desarrollas, molecule converge aplica el rol sin destruir el contenedor y molecule login te abre una shell dentro para investigar.
Paso 10: Usar el rol en un playbook
Con el rol probado, aplícalo a tus servidores. Vuelve a la raíz del proyecto y crea el inventario:
cd ~/infra
nano inventory.ini
[web]
web1 ansible_host=your_server_ip ansible_user=your_user
Crea el playbook:
nano site.yml
---
- name: Configure web servers
hosts: web
become: true
roles:
- role: nginx_site
vars:
nginx_site_name: miweb
nginx_site_server_name: your_domain
Revisa primero qué cambiaría sin aplicar nada, y después aplícalo:
ansible-playbook site.yml --check --diff
ansible-playbook site.yml --ask-become-pass
PLAY RECAP *********************************************************************
web1 : ok=9 changed=6 unreachable=0 failed=0 skipped=0
Una segunda ejecución debe mostrar changed=0.
Paso 11: Compartir el rol
Para reutilizar el rol en otros proyectos, publícalo en un repositorio Git propio, crea etiquetas semánticas (v1.0.0, v1.1.0...) y consúmelo desde requirements.yml:
roles:
- name: nginx_site
src: https://github.com/your_org/ansible-role-nginx_site.git
scm: git
version: v1.0.0
Si vas a publicar varios roles, módulos o plugins relacionados, empaquétalos como una colección (ansible-galaxy collection init your_namespace.web), que es el formato actual de distribución en Galaxy y permite versionarlos juntos.
Conclusión
Has creado un rol de Ansible con variables prefijadas y validadas, tareas idempotentes con módulos FQCN y un handler, has fijado las dependencias de Galaxy en requirements.yml y has probado el rol con ansible-lint y Molecule antes de aplicarlo. Como siguientes pasos, ejecuta ansible-lint y molecule test en tu pipeline de CI para cada cambio, añade más plataformas al escenario de Molecule si el rol debe soportar Debian, y agrupa tus roles en una colección cuando empiecen a crecer.
