Desarrollo y Uso de Colecciones Ansible
Las colecciones Ansible son el formato moderno de distribución de contenido reutilizable que agrupa roles, módulos, plugins y playbooks bajo un namespace con versionado semántico. Desde Ansible 2.10, las colecciones son la forma recomendada de distribuir contenido de automatización, permitiendo a los equipos compartir trabajo entre proyectos, publicar en Ansible Galaxy y gestionar dependencias de forma explícita con archivos de requirements.
Requisitos Previos
- Ansible 2.10+ o ansible-core 2.11+
- Python 3.8+ en el nodo de control
- Cuenta en Ansible Galaxy (para publicación)
ansible-galaxyCLI (incluido con Ansible)
# Verificar la versión de Ansible
ansible --version
# Instalar la herramienta de construcción de colecciones
pip install ansible-core
# Instalar el linter de Ansible para mantener la calidad
pip install ansible-lint
Estructura de una Colección
Una colección sigue una estructura de directorios estrictamente definida:
mi_namespace/
└── mi_coleccion/
├── README.md
├── MANIFEST.json # Generado automáticamente
├── galaxy.yml # Metadatos de la colección
├── requirements.txt # Dependencias Python
├── docs/ # Documentación
├── plugins/
│ ├── modules/ # Módulos personalizados
│ ├── filter/ # Filtros Jinja2
│ ├── lookup/ # Plugins de lookup
│ ├── callback/ # Plugins de callback
│ ├── inventory/ # Plugins de inventario
│ └── connection/ # Plugins de conexión
├── roles/ # Roles incluidos en la colección
│ └── mi_rol/
│ ├── tasks/
│ ├── defaults/
│ ├── handlers/
│ ├── templates/
│ ├── files/
│ └── meta/
├── playbooks/ # Playbooks reutilizables
└── tests/ # Tests de la colección
Crear una Colección desde Cero
# Crear el esqueleto de la colección
ansible-galaxy collection init miempresa.infraestructura
cd miempresa/infraestructura
# Ver la estructura creada
ls -la
# Editar los metadatos de la colección
cat > galaxy.yml << 'EOF'
namespace: miempresa
name: infraestructura
version: 1.0.0
readme: README.md
authors:
- Tu Nombre <[email protected]>
description: Colección de automatización de infraestructura para Mi Empresa
license:
- GPL-2.0-or-later
tags:
- linux
- networking
- database
- security
repository: https://github.com/miempresa/ansible-collection-infraestructura
documentation: https://github.com/miempresa/ansible-collection-infraestructura/wiki
homepage: https://www.miempresa.com
issues: https://github.com/miempresa/ansible-collection-infraestructura/issues
dependencies:
community.general: ">=6.0.0"
ansible.posix: ">=1.4.0"
EOF
# Agregar un rol a la colección
ansible-galaxy role init --init-path roles/ servidor_web
Desarrollo de Módulos Personalizados
Los módulos personalizados permiten interactuar con APIs propietarias o implementar lógica compleja.
# Crear un módulo personalizado para gestionar un servicio ficticio
mkdir -p plugins/modules
cat > plugins/modules/mi_api_resource.py << 'PYTHON'
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""Módulo Ansible para gestionar recursos de la API de Mi Empresa."""
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = r'''
---
module: mi_api_resource
short_description: Gestiona recursos en la API de Mi Empresa
version_added: "1.0.0"
description:
- Crea, actualiza y elimina recursos mediante la API REST de Mi Empresa.
- Soporta idempotencia: no hace cambios si el recurso ya tiene el estado deseado.
options:
name:
description: Nombre del recurso
required: true
type: str
state:
description: Estado deseado del recurso
choices: ['present', 'absent']
default: present
type: str
tipo:
description: Tipo de recurso
required: true
choices: ['servidor', 'red', 'base_datos']
type: str
api_url:
description: URL base de la API
required: false
default: https://api.miempresa.com
type: str
api_token:
description: Token de autenticación de la API
required: true
type: str
no_log: true # No mostrar en logs
author:
- Tu Nombre (@tu_usuario_github)
'''
EXAMPLES = r'''
- name: Crear un recurso de tipo servidor
miempresa.infraestructura.mi_api_resource:
name: web-server-01
state: present
tipo: servidor
api_token: "{{ vault_api_token }}"
- name: Eliminar un recurso
miempresa.infraestructura.mi_api_resource:
name: servidor-antiguo
state: absent
api_token: "{{ vault_api_token }}"
'''
RETURN = r'''
resource:
description: Datos del recurso gestionado
returned: when state is present
type: dict
sample:
id: "res-12345"
name: "web-server-01"
tipo: "servidor"
estado: "activo"
'''
from ansible.module_utils.basic import AnsibleModule
import json
# En un módulo real, aquí iría la lógica de la API
# Usando: from ansible.module_utils.urls import open_url
def obtener_recurso(module, api_url, api_token, nombre):
"""Obtener un recurso por nombre."""
# Simulación de llamada a la API
# En producción: usar open_url o requests
return None
def crear_recurso(module, api_url, api_token, nombre, tipo):
"""Crear un nuevo recurso."""
# Aquí iría la lógica real de la API
return {
"id": "res-nuevo",
"name": nombre,
"tipo": tipo,
"estado": "activo"
}
def eliminar_recurso(module, api_url, api_token, nombre):
"""Eliminar un recurso existente."""
pass
def run_module():
"""Función principal del módulo."""
# Definir los parámetros del módulo
module_args = dict(
name=dict(type='str', required=True),
state=dict(type='str', default='present', choices=['present', 'absent']),
tipo=dict(type='str', required=True, choices=['servidor', 'red', 'base_datos']),
api_url=dict(type='str', default='https://api.miempresa.com'),
api_token=dict(type='str', required=True, no_log=True),
)
# Inicializar el módulo
module = AnsibleModule(
argument_spec=module_args,
supports_check_mode=True # Soportar --check mode
)
nombre = module.params['name']
state = module.params['state']
tipo = module.params['tipo']
api_url = module.params['api_url']
api_token = module.params['api_token']
# Verificar el estado actual del recurso
recurso_actual = obtener_recurso(module, api_url, api_token, nombre)
result = dict(
changed=False,
resource=None
)
if state == 'present':
if recurso_actual is None:
# El recurso no existe, crear
if not module.check_mode:
result['resource'] = crear_recurso(module, api_url, api_token, nombre, tipo)
result['changed'] = True
else:
# El recurso ya existe, no hacer cambios
result['resource'] = recurso_actual
result['changed'] = False
elif state == 'absent':
if recurso_actual is not None:
# El recurso existe, eliminar
if not module.check_mode:
eliminar_recurso(module, api_url, api_token, nombre)
result['changed'] = True
# Retornar el resultado al engine de Ansible
module.exit_json(**result)
def main():
run_module()
if __name__ == '__main__':
main()
PYTHON
Desarrollo de Plugins
# Crear un filtro Jinja2 personalizado
mkdir -p plugins/filter
cat > plugins/filter/custom_filters.py << 'PYTHON'
"""Filtros Jinja2 personalizados para la colección de Mi Empresa."""
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import re
import hashlib
def to_dns_safe(value):
"""Convertir un string a un nombre DNS válido (sin caracteres especiales)."""
# Convertir a minúsculas y reemplazar caracteres no válidos
result = value.lower()
result = re.sub(r'[^a-z0-9-]', '-', result)
result = re.sub(r'-+', '-', result) # Eliminar guiones dobles
result = result.strip('-') # Eliminar guiones al inicio/final
return result
def generate_password(seed, length=24):
"""Generar una contraseña determinista a partir de un seed."""
chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%'
hash_val = hashlib.sha256(seed.encode()).hexdigest()
password = ''
for i in range(0, length * 2, 2):
idx = int(hash_val[i:i+2], 16) % len(chars)
password += chars[idx]
return password[:length]
def size_to_bytes(size_str):
"""Convertir un string de tamaño (como '2GB') a bytes."""
units = {
'B': 1,
'KB': 1024,
'MB': 1024 ** 2,
'GB': 1024 ** 3,
'TB': 1024 ** 4,
}
match = re.match(r'^(\d+(?:\.\d+)?)\s*([KMGT]?B)$', size_str.upper())
if match:
size = float(match.group(1))
unit = match.group(2)
return int(size * units.get(unit, 1))
raise ValueError(f"Formato de tamaño no válido: {size_str}")
class FilterModule(object):
"""Clase que registra los filtros personalizados en Ansible."""
def filters(self):
return {
'to_dns_safe': to_dns_safe,
'generate_password': generate_password,
'size_to_bytes': size_to_bytes,
}
PYTHON
# Ejemplo de uso en un playbook:
# - name: Ejemplo de filtros personalizados
# debug:
# msg: "{{ 'Mi Servidor Web!!' | miempresa.infraestructura.to_dns_safe }}"
# Resultado: "mi-servidor-web"
Publicación en Ansible Galaxy
# Construir el archivo de la colección (.tar.gz)
cd miempresa/infraestructura/
# Verificar que la colección pasa el lint
ansible-lint
# Construir el artefacto de la colección
ansible-galaxy collection build
# Esto genera: miempresa-infraestructura-1.0.0.tar.gz
# Verificar el contenido del artefacto
tar -tzf miempresa-infraestructura-1.0.0.tar.gz
# Publicar en Ansible Galaxy (requiere cuenta y token en galaxy.ansible.com)
ansible-galaxy collection publish miempresa-infraestructura-1.0.0.tar.gz \
--api-key tu-token-de-galaxy
# Para publicar en Automation Hub (Red Hat):
ansible-galaxy collection publish miempresa-infraestructura-1.0.0.tar.gz \
--server https://cloud.redhat.com/api/automation-hub/ \
--api-key tu-token-automation-hub
Gestión de Dependencias
# Archivo requirements.yml para declarar las colecciones necesarias en un proyecto
cat > requirements.yml << 'EOF'
---
collections:
# Colección de la empresa desde Galaxy
- name: miempresa.infraestructura
version: ">=1.0.0,<2.0.0"
# Colecciones de la comunidad
- name: community.general
version: ">=7.0.0"
- name: ansible.posix
version: ">=1.5.0"
# Colección desde un repositorio Git privado
- name: miempresa.privado
source: https://github.com/miempresa/ansible-collection-privado.git
type: git
version: main
# Colección local (para desarrollo)
- name: miempresa.dev
source: /home/usuario/proyectos/coleccion-dev/
type: dir
# Roles adicionales (si se necesitan)
roles:
- name: geerlingguy.docker
version: "7.0.0"
EOF
# Instalar las dependencias declaradas
ansible-galaxy collection install -r requirements.yml
# Instalar en un directorio específico (para proyectos)
ansible-galaxy collection install -r requirements.yml \
--collections-path ./collections/
# Actualizar todas las colecciones al último parche compatible
ansible-galaxy collection install -r requirements.yml --upgrade
# Ver las colecciones instaladas
ansible-galaxy collection list
Uso de Colecciones en Playbooks
# Usar módulos de una colección con el nombre completamente calificado (FQCN)
cat > playbook-ejemplo.yml << 'EOF'
---
- name: Usar la colección de infraestructura de Mi Empresa
hosts: all
# Declarar las colecciones a usar (evita tener que usar FQCN en cada tarea)
collections:
- miempresa.infraestructura
- community.general
vars:
api_token: "{{ vault_api_token }}"
tasks:
# Usar el módulo personalizado de la colección (con nombre corto por la declaración)
- name: Crear el recurso de servidor
mi_api_resource:
name: "web-server-{{ inventory_hostname }}"
state: present
tipo: servidor
api_token: "{{ api_token }}"
# O con FQCN (más explícito, funciona sin la declaración de collections)
- name: Crear recurso con FQCN
miempresa.infraestructura.mi_api_resource:
name: "test-resource"
state: present
tipo: red
api_token: "{{ api_token }}"
# Usar un filtro de la colección
- name: Mostrar nombre DNS seguro
debug:
msg: "{{ inventory_hostname | miempresa.infraestructura.to_dns_safe }}"
# Usar un rol de la colección
- name: Aplicar el rol servidor_web de la colección
include_role:
name: miempresa.infraestructura.servidor_web
vars:
puerto_http: 80
habilitar_ssl: true
EOF
ansible-playbook playbook-ejemplo.yml
Solución de Problemas
Error: module not found in collection:
# Verificar que la colección está instalada
ansible-galaxy collection list | grep miempresa
# Verificar la ruta donde se buscan las colecciones
ansible-config dump | grep COLLECTIONS_PATHS
# Reinstalar la colección
ansible-galaxy collection install miempresa.infraestructura --force
Conflictos de versión entre colecciones:
# Ver las versiones instaladas
ansible-galaxy collection list
# Ver las dependencias de una colección
ansible-galaxy collection install miempresa.infraestructura --dry-run
# Instalar versiones específicas para resolver conflictos
ansible-galaxy collection install community.general:7.5.0 --force
Módulo no funciona con --check mode:
# Verificar que el módulo declara supports_check_mode=True
grep -n "supports_check_mode" plugins/modules/mi_api_resource.py
# Y que maneja module.check_mode correctamente en la lógica
grep -n "check_mode" plugins/modules/mi_api_resource.py
Conclusión
Las colecciones Ansible representan una madurez significativa en la organización del código de automatización, permitiendo empaquetar, versionar y distribuir roles, módulos y plugins como una unidad coherente. Desarrollar módulos personalizados para APIs propietarias y publicarlos como colecciones privadas en Automation Hub o públicas en Galaxy facilita el reuso entre equipos y proyectos, reduciendo la duplicación y mejorando la mantenibilidad de la infraestructura como código en organizaciones que han adoptado Ansible a escala.


