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-galaxy CLI (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.