Molecule es la herramienta del proyecto Ansible para probar roles de forma automática: crea instancias temporales, aplica el rol, comprueba que una segunda ejecución no cambia nada (idempotencia), ejecuta verificaciones y destruye el entorno. En este tutorial instalarás Molecule en Ubuntu 24.04, crearás un rol sencillo que instala Nginx y lo probarás en contenedores Docker con systemd para Ubuntu 24.04, Debian 12 y Rocky Linux 9. Al final ejecutarás las mismas pruebas en GitHub Actions en cada push.

Requisitos previos

  • Una máquina con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath, con un usuario no root con privilegios sudo.
  • Docker Engine instalado desde el repositorio oficial de Docker, y tu usuario en el grupo docker para usarlo sin sudo.
  • Unos 5 GB libres en disco para las imágenes de prueba.
  • Conocimientos básicos de roles de Ansible.

Comprueba que Docker funciona sin sudo:

docker run --rm hello-world

Si recibes permission denied while trying to connect to the Docker daemon socket, ejecuta sudo usermod -aG docker $USER, cierra la sesión y vuelve a entrar.

Paso 1: Instalar Molecule en un entorno virtual

Ubuntu 24.04 no permite instalar paquetes con pip en el Python del sistema (PEP 668), y además conviene fijar las versiones de las herramientas de pruebas por proyecto. Crea un entorno virtual:

sudo apt update
sudo apt install python3-venv
python3 -m venv ~/.venvs/molecule
source ~/.venvs/molecule/bin/activate

Instala Ansible, Molecule, el driver de Docker (incluido en molecule-plugins) y ansible-lint:

pip install --upgrade pip
pip install ansible-core molecule "molecule-plugins[docker]" ansible-lint

El driver de Docker usa módulos de la colección community.docker, que no viene con ansible-core. Instálala:

ansible-galaxy collection install community.docker

Comprueba las versiones:

molecule --version

La salida muestra la versión de Molecule, la de Ansible y la lista de drivers disponibles. Debe aparecer una línea con docker y from molecule_plugins, que confirma que el driver está instalado. Si Molecule avisa de que falta alguna colección requerida por el driver (por ejemplo ansible.posix), instálala con ansible-galaxy collection install igual que la anterior.

Paso 2: Crear un rol de ejemplo

Crea un directorio de trabajo y un rol con la estructura estándar. El nombre del directorio es el nombre del rol, así que usa solo minúsculas y guiones bajos:

mkdir -p ~/ansible-roles && cd ~/ansible-roles
ansible-galaxy role init nginx_basico
cd nginx_basico

Define las tareas del rol:

nano tasks/main.yml
---
- name: Instalar Nginx
  ansible.builtin.package:
    name: nginx
    state: present

- name: Publicar una página de inicio
  ansible.builtin.copy:
    content: "{{ nginx_basico_mensaje }}\n"
    dest: "{{ nginx_basico_docroot }}/index.html"
    owner: root
    group: root
    mode: "0644"

- name: Arrancar y habilitar Nginx
  ansible.builtin.service:
    name: nginx
    state: started
    enabled: true

Las rutas por defecto de la página cambian entre familias de distribuciones. Define los valores por defecto:

nano defaults/main.yml
---
nginx_basico_mensaje: "Hola desde Molecule"
nginx_basico_docroot: "{{ '/var/www/html' if ansible_facts['os_family'] == 'Debian' else '/usr/share/nginx/html' }}"

Por último, ajusta los metadatos para que ansible-lint no se queje de los valores de ejemplo. Abre meta/main.yml:

nano meta/main.yml

Sustituye su contenido por:

---
galaxy_info:
  role_name: nginx_basico
  namespace: miempresa
  author: Tu Nombre
  description: Instala Nginx y publica una página de inicio
  license: MIT
  min_ansible_version: "2.15"
  platforms:
    - name: Ubuntu
      versions:
        - noble
    - name: Debian
      versions:
        - bookworm
    - name: EL
      versions:
        - "9"
dependencies: []

Paso 3: Crear el escenario de Molecule

Un escenario es un conjunto de plataformas y playbooks de prueba. El escenario default vive en molecule/default/. Créalo a mano, así sabes exactamente qué contiene cada archivo:

mkdir -p molecule/default
nano molecule/default/molecule.yml
---
driver:
  name: docker

platforms:
  - name: instancia
    image: "geerlingguy/docker-${MOLECULE_DISTRO:-ubuntu2404}-ansible:latest"
    pre_build_image: true
    privileged: true
    cgroupns_mode: host
    volumes:
      - /sys/fs/cgroup:/sys/fs/cgroup:rw
    command: ""

provisioner:
  name: ansible

verifier:
  name: ansible

Por qué esta configuración:

  • Las imágenes geerlingguy/docker-*-ansible son imágenes mantenidas para probar roles: incluyen Python y systemd como proceso principal, algo que las imágenes oficiales de Ubuntu o Debian no traen. Sin systemd, la tarea que arranca Nginx fallaría.
  • privileged, cgroupns_mode: host y el volumen de /sys/fs/cgroup son necesarios para que systemd funcione dentro del contenedor con cgroups v2, el modo por defecto en Ubuntu 24.04.
  • command: "" respeta el comando de la imagen (arrancar systemd) en lugar de sustituirlo.
  • La variable MOLECULE_DISTRO permite elegir la distribución al lanzar las pruebas; si no se define, se usa ubuntu2404.

Crea el playbook que aplica el rol, converge.yml:

nano molecule/default/converge.yml
---
- name: Converge
  hosts: all
  become: true
  pre_tasks:
    - name: Actualizar la caché de apt
      ansible.builtin.apt:
        update_cache: true
        cache_valid_time: 3600
      when: ansible_facts['os_family'] == 'Debian'
  roles:
    - role: nginx_basico

Molecule añade el directorio padre del proyecto a la ruta de roles, por eso basta con el nombre nginx_basico.

Paso 4: Escribir las verificaciones

La fase verify comprueba el resultado final con un playbook de Ansible. Crea verify.yml:

nano molecule/default/verify.yml
---
- name: Verify
  hosts: all
  become: true
  gather_facts: true
  tasks:
    - name: Recoger la lista de paquetes
      ansible.builtin.package_facts:

    - name: Comprobar que Nginx está instalado
      ansible.builtin.assert:
        that: "'nginx' in ansible_facts.packages"

    - name: Recoger el estado de los servicios
      ansible.builtin.service_facts:

    - name: Comprobar que Nginx está activo y habilitado
      ansible.builtin.assert:
        that:
          - ansible_facts.services['nginx.service'].state == 'running'
          - ansible_facts.services['nginx.service'].status == 'enabled'

    - name: Pedir la página de inicio
      ansible.builtin.uri:
        url: http://localhost/
        return_content: true
      register: pagina

    - name: Comprobar el contenido de la página
      ansible.builtin.assert:
        that: "'Hola desde Molecule' in pagina.content"

Cada assert falla con un mensaje claro si no se cumple, y la fase entera falla si falla cualquiera de ellos.

Paso 5: Ejecutar las fases durante el desarrollo

Mientras desarrollas no hace falta destruir y recrear el contenedor en cada cambio. Crea la instancia y aplica el rol:

molecule converge

La primera vez descarga la imagen, lo que tarda un par de minutos. Al final verás el resumen del playbook:

PLAY RECAP *********************************************************************
instancia                  : ok=6    changed=3    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Ejecuta las verificaciones:

molecule verify
PLAY RECAP *********************************************************************
instancia                  : ok=7    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

INFO     Verifier completed successfully.

Comprueba la idempotencia, es decir, que una segunda ejecución del rol no produce cambios:

molecule idempotence
INFO     Idempotence completed successfully.

Si necesitas investigar algo dentro del contenedor, abre una shell en él:

molecule login

Cuando termines, destruye la instancia:

molecule destroy

Paso 6: Ejecutar la secuencia completa en varias distribuciones

molecule test ejecuta la secuencia completa: destruir restos anteriores, crear, converger, comprobar idempotencia, verificar y destruir. Lánzalo con la distribución por defecto:

molecule test

Para probar en otras distribuciones, cambia la variable MOLECULE_DISTRO:

MOLECULE_DISTRO=debian12 molecule test
MOLECULE_DISTRO=rockylinux9 molecule test

En Rocky Linux 9 la tarea apt se omite gracias a la condición when, y el rol usa /usr/share/nginx/html como raíz. Si las tres ejecuciones terminan sin errores, el rol funciona en las tres familias.

Ejecuta también ansible-lint desde la raíz del rol. Molecule ya no lo lanza por sí mismo, así que es un paso independiente:

ansible-lint
Passed: 0 failure(s), 0 warning(s) on 10 files. Last profile that met the validation criteria was 'production'.

El número de archivos puede variar. Lo importante es 0 failure(s).

Paso 7: Ejecutar las pruebas en GitHub Actions

Para que las pruebas corran en cada push y pull request, crea un workflow. Los runners ubuntu-latest de GitHub traen Docker instalado:

mkdir -p .github/workflows
nano .github/workflows/molecule.yml
---
name: Molecule

on:
  push:
    branches: [main]
  pull_request:

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install ansible-core ansible-lint
      - run: ansible-lint

  molecule:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        distro: [ubuntu2404, debian12, rockylinux9]
    steps:
      - uses: actions/checkout@v4
        with:
          path: nginx_basico
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - name: Instalar dependencias
        run: |
          pip install ansible-core molecule "molecule-plugins[docker]"
          ansible-galaxy collection install community.docker
      - name: Ejecutar Molecule
        run: molecule test
        working-directory: nginx_basico
        env:
          MOLECULE_DISTRO: ${{ matrix.distro }}
          PY_COLORS: "1"
          ANSIBLE_FORCE_COLOR: "1"

El checkout se hace en un subdirectorio llamado nginx_basico para que el nombre del directorio coincida con el del rol, igual que en local. La matriz lanza un job por distribución en paralelo, y fail-fast: false evita que un fallo en una cancele las demás.

Sube el rol a un repositorio de GitHub y abre la pestaña Actions: deberías ver un job de lint y tres de Molecule, uno por distribución.

Solución de problemas

Failed to connect to bus o la tarea del servicio falla dentro del contenedor. systemd no está arrancando como proceso principal. Comprueba que molecule.yml incluye privileged: true, cgroupns_mode: host, el volumen de /sys/fs/cgroup y command: "".

the role 'nginx_basico' was not found. El directorio del rol no se llama igual que el rol, o ejecutas Molecule desde otro directorio. Lanza los comandos desde la raíz del rol, donde está la carpeta molecule/.

couldn't resolve module/action 'community.docker.docker_container'. Falta la colección community.docker en el entorno donde se ejecuta Molecule. Instálala con ansible-galaxy collection install community.docker.

La fase de idempotencia falla. Molecule muestra qué tareas han cambiado en la segunda ejecución. Suele deberse a tareas command o shell sin creates, removes o changed_when, o a plantillas que incluyen datos variables como fechas. Corrige la tarea para que solo informe de cambios cuando realmente modifica algo.

Conclusión

Tienes un rol de Ansible probado automáticamente en tres distribuciones: Molecule crea contenedores con systemd, aplica el rol, comprueba que es idempotente y verifica el resultado, y GitHub Actions repite las pruebas en cada cambio. Como siguientes pasos puedes añadir escenarios adicionales en molecule/ para probar combinaciones de variables distintas, cubrir más casos en verify.yml, o empaquetar el rol dentro de una colección de Ansible para distribuirlo.