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:
sudo apt update
sudo apt install ansible
ansible --version
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:
mkdir -p ~/src && cd ~/src
ansible-galaxy collection init miempresa.infraestructura
cd miempresa/infraestructura
- Collection miempresa.infraestructura was created successfully
Revisa la estructura generada:
ls
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:
nano galaxy.yml
namespace: miempresa
name: infraestructura
version: 1.0.0
readme: README.md
authors:
- Tu Nombre <[email protected]>
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:
nano meta/runtime.yml
---
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.
nano plugins/modules/kv_setting.py
#!/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,EXAMPLESyRETURNson YAML queansible-docmuestra y que Galaxy usa para generar la documentación.argument_specvalida los parámetros antes de ejecutar nada;required_ifexigevaluecuandostate=present.- El módulo calcula el contenido final y lo compara con el actual: si son iguales devuelve
changed=Falsesin tocar el archivo. Esa es la base de la idempotencia. - Con
supports_check_mode=Truey la comprobación demodule.check_mode,--checkinforma 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:
mkdir -p plugins/filter
nano plugins/filter/dns.py
# -*- 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:
ansible-galaxy role init --init-path roles motd
nano roles/motd/tasks/main.yml
---
- 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:
nano roles/motd/defaults/main.yml
---
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:
ansible-galaxy collection build
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:
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:
ansible-galaxy collection list miempresa.infraestructura
ansible-doc miempresa.infraestructura.kv_setting
# /home/your_user/.ansible/collections/ansible_collections
Collection Version
------------------------ -------
miempresa.infraestructura 1.0.0
Consejodurante 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 formaansible_collections/miempresa/infraestructuray añadir el directorio padre deansible_collectionsacollections_pathen tuansible.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:
ansible localhost -m miempresa.infraestructura.kv_setting \
-a "path=/tmp/demo.conf name=max_connections value=200" --check --diff
--- 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:
mkdir -p ~/proyecto && cd ~/proyecto
nano demo.yml
---
- 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:
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:
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:
- 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:
read -rs GALAXY_TOKEN
Pega el token y pulsa Intro. Después publica el paquete:
cd ~/src/miempresa/infraestructura
ansible-galaxy collection publish miempresa-infraestructura-1.0.0.tar.gz --api-key "$GALAXY_TOKEN"
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.
Notasi 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:
cd ~/proyecto
nano requirements.yml
---
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 ([email protected]: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:
nano ansible.cfg
[defaults]
collections_path = ./collections
Instala las dependencias en ese directorio:
ansible-galaxy collection install -r requirements.yml -p ./collections
Comprueba qué versiones se han instalado:
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.
