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

  1. 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.
  2. 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.
  3. 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.
  4. Logging y notificaciones dispersas – Cada herramienta escribe en su propio log; sin un punto central, la auditoría se vuelve costosa.
  5. 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:

  1. Terraform como capa de aprovisionamiento

    • Define un módulo genérico que acepte parámetros (hypervisor, cpu, ram, disk, network).
    • Usa los proveedores vsphere, proxmox y nutanix dentro del mismo proyecto.
    • Exporta la IP y el hostname como outputs.
  2. Ansible como capa de configuración

    • Crea un playbook que reciba la IP mediante --extra-vars y aplique roles comunes (cloud‑init, usuarios, paquetes).
    • Marca la ejecución como “failed” si cualquier tarea crítica falla; Ansible ya maneja idempotencia.
  3. 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 usa continue-on-error: false para 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.
  4. 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 refresh antes de cada apply para detectar cambios externos.
  5. 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.

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

  1. Ejecuta el workflow manualmente desde la UI de GitHub y observa que el job terraform-apply crea la VM en el hipervisor seleccionado.
  2. Revisa el output vm_ip y confirma que la IP corresponde a la VM recién creada (puedes usar ping o ssh).
  3. Verifica que el playbook instaló los paquetes esperados (ssh, curl, etc.) mediante una conexión SSH a la IP.
  4. Comprueba que el registro DNS se actualizó (consulta con dig <hostname>).
  5. 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: 3 y delay: 10 en la tarea.
  • Bloqueo de estado: cuando varios ingenieros ejecutan el mismo workflow simultáneamente, el backend S3 con dynamodb_table para 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 plan como 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.