Problema
Los equipos que gestionan infraestructuras híbridas suelen mezclar hipervisores distintos (VMware, Proxmox, Nutanix, etc.) por motivos de coste, licencias o migraciones parciales. Cada plataforma expone su propia API y sus propias herramientas de automatización. Cuando los procesos de aprovisionamiento se fragmentan entre scripts SSH heredados, playbooks de Ansible disparados manualmente y módulos de Terraform aislados, aparecen varios síntomas: falta de trazabilidad, reintentos manuales, logs dispersos y dificultad para añadir pasos como registro DNS o notificaciones. El reto es crear un flujo único donde la definición de la VM sea declarativa, la configuración sea idempotente y los eventos de éxito o fallo se notifiquen de forma consistente.
Causa
- Proveedores de IaC no unificados – Terraform dispone de proveedores para cada hipervisor, pero en muchos proyectos se crean módulos aislados sin un “wrapper” común.
- Separación de etapas – Ansible se ejecuta después de crear la VM, pero la coordinación entre Terraform y Ansible depende de scripts ad‑hoc que no manejan errores ni retries.
- Ausencia de orquestador ligero – Herramientas como Jenkins o GitLab CI pueden encadenar jobs, pero a menudo se evita su uso por la sobrecarga de pipelines Groovy o YAML complejos.
- Logging y notificaciones dispersas – Cada herramienta escribe en su propio log; sin un punto central, la auditoría se vuelve costosa.
- Estado de la infraestructura desalineado – Cuando los scripts SSH crean recursos fuera del estado de Terraform, el “drift” impide una gestión fiable.
Solución
Adoptar un patrón de pipeline declarativo + orquestador de tareas que mantenga el flujo completo bajo control de versiones:
-
Terraform como capa de aprovisionamiento
- Define un módulo genérico que acepte parámetros (
hypervisor,cpu,ram,disk,network). - Usa los proveedores
vsphere,proxmoxynutanixdentro del mismo proyecto. - Exporta la IP y el hostname como outputs.
- Define un módulo genérico que acepte parámetros (
-
Ansible como capa de configuración
- Crea un playbook que reciba la IP mediante
--extra-varsy aplique roles comunes (cloud‑init, usuarios, paquetes). - Marca la ejecución como “failed” si cualquier tarea crítica falla; Ansible ya maneja idempotencia.
- Crea un playbook que reciba la IP mediante
-
Orquestador basado en GitHub Actions (o GitLab CI)
- Un workflow con tres jobs:
terraform-apply,ansible-provision,notify. - Cada job depende del anterior (
needs:) y usacontinue-on-error: falsepara abortar en fallos. - Configura retries con la directiva
retry:de Actions (max 3 intentos). - Centraliza logs en los artefactos del workflow y envía un mensaje a Slack mediante un webhook.
- Un workflow con tres jobs:
-
Gestión de estado
- Almacena el state de Terraform en un bucket S3 o en un backend de Terraform Cloud; evita drift y permite bloqueos de concurrencia.
- Usa
terraform refreshantes de cadaapplypara detectar cambios externos.
-
Registro DNS automático
- Añade un recurso
cloudflare_record(o el proveedor que corresponda) que tome la IP de salida de Terraform. - El mismo workflow actualiza el registro antes de lanzar Ansible.
- Añade un recurso
Arquitectura resumida
Git repo
├─ terraform/
│ └─ main.tf ← módulos multi‑hypervisor
├─ ansible/
│ └─ site.yml ← roles comunes
└─ .github/workflows/provision.yml
Cuándo aplicar esta solución
- Entornos mixtos donde al menos dos hipervisores diferentes están en producción.
- Equipos con CI/CD ya configurado (GitHub, GitLab, Azure DevOps) y que prefieren evitar pipelines Groovy.
- Necesidad de auditoría: logs centralizados y estado versionado son obligatorios.
- No es adecuado cuando la infraestructura es estática y solo se necesita un par de scripts puntuales; la sobrecarga de Terraform + CI puede ser innecesaria.
Código
# terraform/main.tf (fragmento)
terraform {
required_providers {
vsphere = { source = "hashicorp/vsphere" }
proxmox = { source = "Telmate/proxmox" }
nutanix = { source = "nutanix/nutanix" }
}
backend "s3" {
bucket = "tf-state-bucket"
key = "multi-hypervisor/terraform.tfstate"
region = "us-east-1"
}
}
variable "hypervisor" {
description = "Target hypervisor: vmware|proxmox|nutanix"
}
# Módulo genérico que selecciona el provider adecuado
module "vm" {
source = "./modules/vm"
hypervisor = var.hypervisor
name = var.vm_name
cpu = var.cpu
ram = var.ram
disk = var.disk
network = var.network
}
output "vm_ip" {
value = module.vm.ip_address
}
# ansible/site.yml (fragmento)
- hosts: all
become: true
vars:
ansible_user: root
roles:
- common
- cloud_init
# .github/workflows/provision.yml (fragmento)
name: Provision VM
on:
workflow_dispatch:
inputs:
hypervisor:
description: 'Target hypervisor'
required: true
default: 'proxmox'
vm_name:
description: 'Nombre de la VM'
required: true
jobs:
terraform-apply:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Terraform
uses: hashicorp/setup-terraform@v2
- name: Terraform Init & Apply
env:
TF_VAR_hypervisor: ${{ github.event.inputs.hypervisor }}
TF_VAR_vm_name: ${{ github.event.inputs.vm_name }}
run: |
terraform init
terraform apply -auto-approve
- name: Capture IP
id: ip
run: echo "ip=$(terraform output -raw vm_ip)" >> $GITHUB_OUTPUT
ansible-provision:
needs: terraform-apply
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Ansible
run: sudo apt-get update && sudo apt-get install -y ansible
- name: Run Playbook
env:
ANSIBLE_HOST_KEY_CHECKING: "False"
run: |
ansible-playbook ansible/site.yml -i "${{ steps.ip.outputs.ip }},"
notify:
needs: [terraform-apply, ansible-provision]
runs-on: ubuntu-latest
steps:
- name: Slack notification
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
run: |
STATUS="success"
if [ "${{ job.status }}" != "success" ]; then STATUS="failed"; fi
curl -X POST -H 'Content-type: application/json' \
--data "{\"text\":\"Provisioning ${{ github.event.inputs.vm_name }} on ${{ github.event.inputs.hypervisor }} $STATUS\"}" \
$SLACK_WEBHOOK
Verificación
- Ejecuta el workflow manualmente desde la UI de GitHub y observa que el job
terraform-applycrea la VM en el hipervisor seleccionado. - Revisa el output
vm_ipy confirma que la IP corresponde a la VM recién creada (puedes usarpingossh). - Verifica que el playbook instaló los paquetes esperados (
ssh,curl, etc.) mediante una conexión SSH a la IP. - Comprueba que el registro DNS se actualizó (consulta con
dig <hostname>). - Revisa el canal Slack; debería haber un mensaje indicando “success” o “failed” según el resultado.
Notas adicionales
- Retries en Ansible: si alguna tarea falla por problemas transitorios (por ejemplo, repositorios apt no disponibles), añade
retries: 3ydelay: 10en la tarea. - Bloqueo de estado: cuando varios ingenieros ejecutan el mismo workflow simultáneamente, el backend S3 con
dynamodb_tablepara bloqueo evita colisiones. - Variables sensibles: guarda credenciales de los proveedores y el webhook de Slack en los Secrets del repositorio; nunca las hardcodees.
- Extensibilidad: para añadir otro hipervisor (por ejemplo, oVirt) basta con crear un nuevo sub‑módulo y exponer los mismos outputs; el workflow no necesita cambios.
- Auditoría: habilita
terraform plancomo artefacto del job para que cada ejecución quede registrada en GitHub Actions.
Con este patrón, el proceso de aprovisionamiento pasa de ser una colección de scripts ad‑hoc a una cadena reproducible, versionada y observable, apta para cualquier combinación de hipervisores que una organización pueda necesitar.