--- title: "Cómo crear y usar colecciones de Ansible con módulos propios" description: "Crea una colección de Ansible con un módulo, un filtro y un rol, constrúyela e instálala, publícala en Galaxy y gestiona dependencias con requirements.yml." category: "Automatización Avanzada" language: es source: https://cubepath.com/es/docs/automatización-avanzada/desarrollo-y-uso-de-colecciones-ansible updated: 2026-09-25 --- # Cómo crear y usar colecciones de Ansible con módulos propios Las colecciones son el formato de distribución de contenido de Ansible: agrupan módulos, plugins, roles y playbooks bajo un nombre `namespace.coleccion` con versionado semántico, y se instalan con `ansible-galaxy` igual que un paquete. En este tutorial crearás en Ubuntu 24.04 una colección `miempresa.infraestructura` con un módulo propio idempotente, un filtro Jinja2 y un rol, la construirás, la instalarás y la usarás desde un playbook. Después verás cómo publicarla en Ansible Galaxy y cómo declarar las colecciones de un proyecto en `requirements.yml`. ## Requisitos previos - Una máquina con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath, con un usuario no root con privilegios `sudo`. Hará de nodo de control de Ansible. - Conocimientos básicos de playbooks de Ansible y de Python. - Para el paso 8 (opcional): una cuenta en galaxy.ansible.com. ## Paso 1: Instalar Ansible El paquete `ansible` de Ubuntu 24.04 incluye `ansible-core` y un conjunto amplio de colecciones de la comunidad: ```bash sudo apt update sudo apt install ansible ansible --version ``` ```output ansible [core 2.16.3] config file = None configured module search path = ['/home/your_user/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules'] ansible python module location = /usr/lib/python3/dist-packages/ansible ansible collection location = /home/your_user/.ansible/collections:/usr/share/ansible/collections ... ``` La línea `ansible collection location` indica dónde busca Ansible las colecciones instaladas. Por defecto, `ansible-galaxy` instala las de tu usuario en `~/.ansible/collections`. ## Paso 2: Crear el esqueleto de la colección Una colección tiene un nombre de dos partes: el namespace (tu organización o usuario de Galaxy) y el nombre de la colección. Ambas deben estar en minúsculas, empezar por una letra y contener solo letras, números y guiones bajos. Crea el esqueleto en un directorio de trabajo: ```bash mkdir -p ~/src && cd ~/src ansible-galaxy collection init miempresa.infraestructura cd miempresa/infraestructura ``` ```output - Collection miempresa.infraestructura was created successfully ``` Revisa la estructura generada: ```bash ls ``` ```output README.md docs galaxy.yml meta plugins roles ``` Qué contiene cada parte: | Ruta | Contenido | |---|---| | `galaxy.yml` | Metadatos: nombre, versión, autores, licencia y dependencias | | `meta/runtime.yml` | Versión mínima de `ansible-core` y redirecciones de módulos | | `plugins/modules/` | Módulos propios | | `plugins/filter/` | Filtros Jinja2 | | `roles/` | Roles incluidos en la colección | ## Paso 3: Rellenar los metadatos El archivo `galaxy.yml` generado incluye todas las claves posibles con comentarios. Sustitúyelo por una versión con solo lo necesario: ```bash nano galaxy.yml ``` ```yaml namespace: miempresa name: infraestructura version: 1.0.0 readme: README.md authors: - Tu Nombre description: Módulos, filtros y roles de infraestructura de Mi Empresa license: - GPL-3.0-or-later tags: - linux - infrastructure dependencies: ansible.posix: ">=1.5.0" repository: https://github.com/miempresa/ansible-collection-infraestructura build_ignore: - "*.tar.gz" ``` La clave `dependencies` declara otras colecciones necesarias: `ansible-galaxy` las instala automáticamente al instalar esta. `build_ignore` evita que los paquetes construidos anteriormente se incluyan dentro del siguiente. Declara también la versión mínima de `ansible-core` en `meta/runtime.yml`: ```bash nano meta/runtime.yml ``` ```yaml --- requires_ansible: ">=2.15.0" ``` ## Paso 4: Escribir un módulo propio Un módulo es un programa Python que recibe parámetros, comprueba el estado actual del sistema, lo cambia solo si hace falta y devuelve JSON con `changed`. Como ejemplo crearás `kv_setting`, que gestiona una línea `clave=valor` en un archivo de configuración, con soporte de modo `--check` y `--diff`. ```bash nano plugins/modules/kv_setting.py ``` ```python #!/usr/bin/python # -*- coding: utf-8 -*- # GNU General Public License v3.0+ (see https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import annotations DOCUMENTATION = r""" --- module: kv_setting short_description: Gestiona una línea clave=valor en un archivo version_added: "1.0.0" description: - Crea, actualiza o elimina una línea C(nombre=valor) en un archivo de texto. - Solo modifica el archivo si el contenido cambia. options: path: description: Ruta del archivo. Se crea si no existe y O(state=present). type: path required: true name: description: Nombre de la clave. type: str required: true value: description: Valor de la clave. Obligatorio si O(state=present). type: str state: description: Si la clave debe existir o no. type: str choices: [present, absent] default: present author: - Tu Nombre (@tu_usuario) """ EXAMPLES = r""" - name: Fijar el número máximo de conexiones miempresa.infraestructura.kv_setting: path: /etc/miapp/app.conf name: max_connections value: "200" - name: Eliminar una clave obsoleta miempresa.infraestructura.kv_setting: path: /etc/miapp/app.conf name: legacy_mode state: absent """ RETURN = r""" path: description: Ruta del archivo gestionado. returned: always type: str sample: /etc/miapp/app.conf """ import os from ansible.module_utils.basic import AnsibleModule def read_lines(path): if not os.path.exists(path): return [] with open(path, encoding="utf-8") as f: return f.read().splitlines() def build_lines(lines, name, value, state): prefix = name + "=" result = [] found = False for line in lines: if line.startswith(prefix): if state == "present" and not found: result.append(prefix + value) found = True continue result.append(line) if state == "present" and not found: result.append(prefix + value) return result def main(): module = AnsibleModule( argument_spec=dict( path=dict(type="path", required=True), name=dict(type="str", required=True), value=dict(type="str"), state=dict(type="str", default="present", choices=["present", "absent"]), ), required_if=[("state", "present", ("value",))], supports_check_mode=True, ) path = module.params["path"] state = module.params["state"] if state == "absent" and not os.path.exists(path): module.exit_json(changed=False, path=path) before = read_lines(path) after = build_lines(before, module.params["name"], module.params["value"], state) changed = before != after result = dict(changed=changed, path=path) if module._diff: result["diff"] = dict( before="\n".join(before) + "\n" if before else "", after="\n".join(after) + "\n" if after else "", before_header=path, after_header=path, ) if changed and not module.check_mode: try: with open(path, "w", encoding="utf-8") as f: f.write("\n".join(after) + "\n" if after else "") except OSError as e: module.fail_json(msg="No se pudo escribir %s: %s" % (path, e), **result) module.exit_json(**result) if __name__ == "__main__": main() ``` Las claves de este módulo: - `DOCUMENTATION`, `EXAMPLES` y `RETURN` son YAML que `ansible-doc` muestra y que Galaxy usa para generar la documentación. - `argument_spec` valida los parámetros antes de ejecutar nada; `required_if` exige `value` cuando `state=present`. - El módulo calcula el contenido final y lo compara con el actual: si son iguales devuelve `changed=False` sin tocar el archivo. Esa es la base de la idempotencia. - Con `supports_check_mode=True` y la comprobación de `module.check_mode`, `--check` informa de lo que cambiaría sin escribir. ## Paso 5: Añadir un filtro y un rol Los filtros Jinja2 van en `plugins/filter/`. Crea uno que convierte un texto cualquiera en un nombre válido para DNS: ```bash mkdir -p plugins/filter nano plugins/filter/dns.py ``` ```python # -*- coding: utf-8 -*- # GNU General Public License v3.0+ (see https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import annotations import re def to_dns_label(value): """Convierte un texto en una etiqueta DNS válida: minúsculas, letras, números y guiones.""" label = re.sub(r"[^a-z0-9-]+", "-", str(value).lower()) label = re.sub(r"-{2,}", "-", label).strip("-") return label[:63] class FilterModule: def filters(self): return {"to_dns_label": to_dns_label} ``` Añade también un rol. Dentro de una colección, los nombres de rol solo pueden tener minúsculas, números y guiones bajos: ```bash ansible-galaxy role init --init-path roles motd nano roles/motd/tasks/main.yml ``` ```yaml --- - name: Publicar el mensaje de bienvenida ansible.builtin.copy: content: "{{ motd_texto }}\n" dest: /etc/motd owner: root group: root mode: "0644" ``` Define el valor por defecto de la variable: ```bash nano roles/motd/defaults/main.yml ``` ```yaml --- motd_texto: "Servidor gestionado con Ansible. Los cambios manuales se sobrescribirán." ``` ## Paso 6: Construir e instalar la colección `ansible-galaxy collection build` empaqueta la colección en un archivo `.tar.gz` con un manifiesto y las sumas de todos los archivos. Desde la raíz de la colección: ```bash ansible-galaxy collection build ``` ```output Created collection for miempresa.infraestructura at /home/your_user/src/miempresa/infraestructura/miempresa-infraestructura-1.0.0.tar.gz ``` Instálala para tu usuario. Como declara `ansible.posix` como dependencia, `ansible-galaxy` la resolverá: si ya tienes una versión compatible (el paquete `ansible` de Ubuntu la incluye), no descargará nada: ```bash ansible-galaxy collection install miempresa-infraestructura-1.0.0.tar.gz ``` Comprueba que Ansible la encuentra y que la documentación del módulo es correcta: ```bash ansible-galaxy collection list miempresa.infraestructura ansible-doc miempresa.infraestructura.kv_setting ``` ```output # /home/your_user/.ansible/collections/ansible_collections Collection Version ------------------------ ------- miempresa.infraestructura 1.0.0 ``` > Consejo: durante el desarrollo, cada cambio exige volver a construir e instalar con `--force`. Para evitarlo, puedes crear la colección directamente dentro de una ruta con la forma `ansible_collections/miempresa/infraestructura` y añadir el directorio padre de `ansible_collections` a `collections_path` en tu `ansible.cfg`. ## Paso 7: Usar la colección en un playbook Dentro de un playbook, los módulos, filtros y roles de una colección se llaman por su nombre completo (FQCN, `namespace.coleccion.elemento`). Es la forma recomendada: deja claro de dónde viene cada cosa y evita colisiones de nombres. Prueba primero el módulo con un comando ad hoc, en modo `--check` y con `--diff`: ```bash ansible localhost -m miempresa.infraestructura.kv_setting \ -a "path=/tmp/demo.conf name=max_connections value=200" --check --diff ``` ```output --- before: /tmp/demo.conf +++ after: /tmp/demo.conf @@ -0,0 +1 @@ +max_connections=200 localhost | CHANGED => { "changed": true, "path": "/tmp/demo.conf" } ``` El archivo no se ha creado porque es una simulación. Crea ahora un playbook de prueba: ```bash mkdir -p ~/proyecto && cd ~/proyecto nano demo.yml ``` ```yaml --- - name: Probar la colección miempresa.infraestructura hosts: localhost connection: local gather_facts: false tasks: - name: Fijar max_connections miempresa.infraestructura.kv_setting: path: /tmp/demo.conf name: max_connections value: "200" - name: Mostrar un nombre DNS derivado ansible.builtin.debug: msg: "{{ 'Servidor Web Producción 01' | miempresa.infraestructura.to_dns_label }}" ``` Ejecútalo dos veces: ```bash ansible-playbook demo.yml ansible-playbook demo.yml ``` En la primera ejecución la tarea del módulo aparece como `changed`; en la segunda, como `ok`, porque el valor ya es el correcto. La tarea `debug` muestra el filtro en acción: ```output TASK [Mostrar un nombre DNS derivado] ****************************************** ok: [localhost] => { "msg": "servidor-web-producci-n-01" } ``` El filtro sustituye los caracteres no válidos en DNS, como la `ó`, por guiones. Los roles de la colección se usan también por su nombre completo, por ejemplo en un play contra tus servidores: ```yaml - name: Configurar el mensaje de bienvenida hosts: webservers become: true roles: - role: miempresa.infraestructura.motd ``` ## Paso 8: Publicar la colección en Ansible Galaxy Para publicar en Galaxy, el namespace de `galaxy.yml` debe existir en Galaxy y pertenecer a tu cuenta. Al iniciar sesión en galaxy.ansible.com con GitHub se crea un namespace con tu usuario (con guiones convertidos en guiones bajos); para otro nombre, como el de tu organización, solicítalo desde la propia web. Obtén un token de API en galaxy.ansible.com, en la sección **Collections > API token**, y guárdalo en una variable de entorno para no dejarlo en el historial dentro de la línea de comandos: ```bash read -rs GALAXY_TOKEN ``` Pega el token y pulsa Intro. Después publica el paquete: ```bash cd ~/src/miempresa/infraestructura ansible-galaxy collection publish miempresa-infraestructura-1.0.0.tar.gz --api-key "$GALAXY_TOKEN" ``` ```output Publishing collection artifact '/home/your_user/src/miempresa/infraestructura/miempresa-infraestructura-1.0.0.tar.gz' to default https://galaxy.ansible.com/api/ Collection has been published to the Galaxy server default https://galaxy.ansible.com/api/ Waiting until Galaxy import task https://galaxy.ansible.com/api/... has completed Collection has been successfully published and imported to the Galaxy server default https://galaxy.ansible.com/api/ ``` Una versión publicada no se puede sobrescribir. Para cualquier cambio, incrementa `version` en `galaxy.yml` siguiendo versionado semántico (`1.0.1` para correcciones, `1.1.0` para funciones nuevas, `2.0.0` para cambios incompatibles), vuelve a construir y publica. > Nota: si la colección es interna, no la publiques en Galaxy. Puedes instalarla directamente desde un repositorio Git privado, como se muestra en el paso siguiente. ## Paso 9: Declarar dependencias con requirements.yml En un proyecto de Ansible, las colecciones necesarias se declaran en `requirements.yml` para que cualquier persona, o un pipeline de CI, pueda instalar exactamente lo mismo. En el directorio del proyecto: ```bash cd ~/proyecto nano requirements.yml ``` ```yaml --- collections: - name: community.general version: ">=9.0.0,<10.0.0" - name: ansible.posix version: ">=1.5.0" - name: https://github.com/miempresa/ansible-collection-infraestructura.git type: git version: v1.0.0 ``` La última entrada instala la colección desde un repositorio Git, en la etiqueta `v1.0.0`. El repositorio debe contener `galaxy.yml` en su raíz. Para repositorios privados, usa una URL SSH (`git@github.com:miempresa/ansible-collection-infraestructura.git`) con una clave con acceso. Para que el proyecto use sus propias copias de las colecciones, independientes de las del sistema, crea un `ansible.cfg`: ```bash nano ansible.cfg ``` ```ini [defaults] collections_path = ./collections ``` Instala las dependencias en ese directorio: ```bash ansible-galaxy collection install -r requirements.yml -p ./collections ``` Comprueba qué versiones se han instalado: ```bash ansible-galaxy collection list -p ./collections ``` Añade `collections/` a tu `.gitignore`: el archivo que se versiona es `requirements.yml`, no las colecciones descargadas. Para actualizar a la última versión que cumpla los rangos, añade `--upgrade` al comando de instalación. ## Solución de problemas **`couldn't resolve module/action 'miempresa.infraestructura.kv_setting'`.** Ansible no encuentra la colección en ninguna ruta de `collections_path`. Comprueba con `ansible-galaxy collection list` dónde está instalada y con `ansible-config dump --only-changed` si un `ansible.cfg` del proyecto cambia las rutas. **Los cambios en el módulo no se aplican.** Estás ejecutando la copia instalada, no la de `~/src`. Vuelve a construir e instalar con `ansible-galaxy collection install miempresa-infraestructura-1.0.0.tar.gz --force`. **`ansible-galaxy collection publish` falla con un error de permisos sobre el namespace.** El namespace de `galaxy.yml` no existe en Galaxy o tu cuenta no es propietaria. Revísalo en la sección **My Namespaces** de galaxy.ansible.com. **La instalación desde Git falla con `galaxy.yml not found`.** El repositorio no tiene `galaxy.yml` en la raíz. Si la colección está en un subdirectorio, indícalo en la URL con `#/ruta/`, por ejemplo `https://github.com/miempresa/repo.git#/colecciones/infraestructura/`. ## Conclusión Has creado una colección de Ansible con un módulo idempotente con soporte de `--check` y `--diff`, un filtro Jinja2 y un rol; la has construido, instalado y usado con nombres completos, y has visto cómo publicarla en Galaxy y fijar las dependencias de un proyecto con `requirements.yml`. Como siguientes pasos puedes añadir pruebas con `ansible-test sanity` y `ansible-test units`, probar los roles de la colección con Molecule, o automatizar la publicación en Galaxy desde CI al crear una etiqueta de versión.