Inicio Linux & Systems Cybersecurity Cloud & DevOps Networks & Infrastructure SIEM & Monitoring DFIR & Threat Intel Development & Other Todas las categorias Proyectos Sobre Herramientas

Dify + FastAPI: agente IA que despliega maquinas virtuales bajo demanda

Read in English
Dify + FastAPI: agente IA que despliega maquinas virtuales bajo demanda

Tabla de contenidos

Que vamos a construir

Un agente de IA que recibe peticiones en lenguaje natural como:

CODE
Necesito una VM Ubuntu 24.04 con 4GB de RAM, 2 CPUs y Docker instalado

Y automaticamente:

  1. Interpreta la especificacion
  2. Llama a una API de provisioning
  3. Despliega la maquina virtual
  4. Devuelve la IP y credenciales de acceso

Arquitectura

CODE
┌─────────────────────────────────────────────────────────────────┐
│                         Dify (Docker)                            │
│                                                                  │
│  ┌──────────────┐     ┌─────────────┐     ┌─────────────────┐  │
│  │   Usuario    │────▶│   Agente    │────▶│  Tool HTTP:     │  │
│  │  (chat/API)  │◀────│   (LLM)     │◀────│  provision_vm   │  │
│  └──────────────┘     └─────────────┘     └────────┬────────┘  │
│                                                      │           │
└──────────────────────────────────────────────────────┼───────────┘
                                                       │
                                                       ▼
┌──────────────────────────────────────────────────────────────────┐
│                  Provisioning API (FastAPI)                       │
│                                                                   │
│  POST /api/v1/vms                                                │
│  ├── Valida parametros                                           │
│  ├── Genera Vagrantfile                                          │
│  ├── Ejecuta vagrant up                                          │
│  └── Retorna IP + estado                                         │
│                                                                   │
│  GET  /api/v1/vms/{id}         → Estado de la VM                 │
│  DELETE /api/v1/vms/{id}       → Destruir VM                     │
└──────────────────────────────────────────────────────────────────┘
                       │
                       ▼
            ┌─────────────────────┐
            │  VirtualBox / libvirt │
            │  (VMs desplegadas)    │
            └─────────────────────┘

Nota para produccion: En un entorno empresarial, la capa de Vagrant se reemplaza por Terraform (AWS/Azure/GCP), Ansible AWX, la API de Proxmox/vSphere, o Kubernetes. La arquitectura Dify → API → Backend de infra es la misma.


Requisitos previos

  • Docker y Docker Compose
  • VirtualBox + Vagrant (para el lab)
  • Minimo 8 GB RAM (Dify + VMs)
  • Un modelo LLM (Ollama local o API key de OpenAI/Anthropic)

Paso 1: Instalar Dify con Docker

Clonar y configurar

BASH
git clone https://github.com/langgenius/dify.git
cd dify/docker
cp .env.example .env

Editar .env:

BASH
# Puerto de acceso
EXPOSE_NGINX_PORT=3000

# Clave secreta
SECRET_KEY=$(openssl rand -hex 32)

Levantar Dify

BASH
docker compose up -d

Verificar:

BASH
docker compose ps
# Todos los servicios deben estar "Up"

Accede a http://localhost:3000 y crea tu cuenta de admin.

Configurar modelo LLM

Ve a Settings > Model Providers:

  • Ollama (local): Base URL http://host.docker.internal:11434
  • OpenAI: API Key sk-xxxx

Modelo recomendado para el agente: llama3.1:8b (local) o gpt-4o-mini (API).


Paso 2: API de provisioning (FastAPI)

Esta es la pieza clave: una API REST que recibe parametros de una VM y la despliega.

Estructura del proyecto

CODE
provisioning-api/
├── app/
│   ├── __init__.py
│   ├── main.py           # FastAPI app
│   ├── models.py         # Pydantic schemas
│   ├── provisioner.py    # Logica Vagrant
│   └── templates/
│       └── Vagrantfile.j2  # Template Jinja2
├── vms/                  # Directorio de VMs desplegadas
├── requirements.txt
└── Dockerfile

models.py — Esquemas de datos

PYTHON
from pydantic import BaseModel, Field
from enum import Enum
from typing import Optional

class OSType(str, Enum):
    ubuntu_2404 = "ubuntu-24.04"
    ubuntu_2204 = "ubuntu-22.04"
    debian_12 = "debian-12"
    fedora_42 = "fedora-42"
    rocky_9 = "rocky-9"

class VMRequest(BaseModel):
    name: str = Field(..., description="Nombre de la VM", pattern=r'^[a-z0-9-]+$')
    os: OSType = Field(..., description="Sistema operativo")
    cpus: int = Field(default=2, ge=1, le=8, description="Numero de CPUs")
    memory_mb: int = Field(default=2048, ge=512, le=16384, description="RAM en MB")
    disk_gb: int = Field(default=20, ge=10, le=100, description="Disco en GB")
    install_docker: bool = Field(default=False, description="Instalar Docker")
    install_k8s: bool = Field(default=False, description="Instalar kubeadm/kubectl")
    ssh_public_key: Optional[str] = Field(default=None, description="Clave publica SSH")

class VMStatus(str, Enum):
    creating = "creating"
    running = "running"
    stopped = "stopped"
    error = "error"
    destroyed = "destroyed"

class VMResponse(BaseModel):
    id: str
    name: str
    status: VMStatus
    ip: Optional[str] = None
    ssh_user: str = "vagrant"
    ssh_port: int = 22
    os: str
    cpus: int
    memory_mb: int
    message: str

provisioner.py — Logica de despliegue

PYTHON
import os
import subprocess
import uuid
import json
from pathlib import Path
from jinja2 import Template

from .models import VMRequest, VMResponse, VMStatus

VMS_DIR = Path(__file__).parent.parent / "vms"
VMS_DIR.mkdir(exist_ok=True)

BOX_MAP = {
    "ubuntu-24.04": "bento/ubuntu-24.04",
    "ubuntu-22.04": "bento/ubuntu-22.04",
    "debian-12": "debian/bookworm64",
    "fedora-42": "bento/fedora-42",
    "rocky-9": "bento/rockylinux-9",
}

VAGRANTFILE_TEMPLATE = """
Vagrant.configure("2") do |config|
  config.vm.box = "{{ box }}"
  config.vm.hostname = "{{ name }}"
  config.vm.network "private_network", type: "dhcp"

  config.vm.provider "virtualbox" do |vb|
    vb.memory = "{{ memory_mb }}"
    vb.cpus = {{ cpus }}
    vb.name = "{{ name }}"
  end

  config.vm.provision "shell", inline: <<-SHELL
    set -e
    {% if install_docker %}
    curl -fsSL https://get.docker.com | sh
    usermod -aG docker vagrant
    {% endif %}
    {% if install_k8s %}
    curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.31/deb/Release.key | \
      gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
    echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] \
      https://pkgs.k8s.io/core:/stable:/v1.31/deb/ /' | \
      tee /etc/apt/sources.list.d/kubernetes.list
    apt-get update && apt-get install -y kubelet kubeadm kubectl
    {% endif %}
    {% if ssh_public_key %}
    echo "{{ ssh_public_key }}" >> /home/vagrant/.ssh/authorized_keys
    {% endif %}
  SHELL
end
"""


def create_vm(request: VMRequest) -> VMResponse:
    """Genera Vagrantfile y despliega la VM."""
    vm_id = str(uuid.uuid4())[:8]
    vm_dir = VMS_DIR / f"{request.name}-{vm_id}"
    vm_dir.mkdir(parents=True)

    # Generar Vagrantfile
    template = Template(VAGRANTFILE_TEMPLATE)
    vagrantfile = template.render(
        box=BOX_MAP[request.os.value],
        name=request.name,
        memory_mb=request.memory_mb,
        cpus=request.cpus,
        install_docker=request.install_docker,
        install_k8s=request.install_k8s,
        ssh_public_key=request.ssh_public_key or "",
    )

    (vm_dir / "Vagrantfile").write_text(vagrantfile)

    # Guardar metadata
    meta = {
        "id": vm_id,
        "name": request.name,
        "os": request.os.value,
        "cpus": request.cpus,
        "memory_mb": request.memory_mb,
        "status": "creating",
    }
    (vm_dir / "meta.json").write_text(json.dumps(meta))

    # Ejecutar vagrant up en background
    log_file = vm_dir / "vagrant.log"
    with open(log_file, "w") as log:
        subprocess.Popen(
            ["vagrant", "up"],
            cwd=vm_dir,
            stdout=log,
            stderr=subprocess.STDOUT,
        )

    return VMResponse(
        id=vm_id,
        name=request.name,
        status=VMStatus.creating,
        os=request.os.value,
        cpus=request.cpus,
        memory_mb=request.memory_mb,
        ssh_user="vagrant",
        message=f"VM '{request.name}' en proceso de creacion. Consulta GET /api/v1/vms/{vm_id} para el estado.",
    )


def get_vm_status(vm_id: str) -> VMResponse:
    """Consulta el estado de una VM."""
    # Buscar directorio de la VM
    for vm_dir in VMS_DIR.iterdir():
        if vm_dir.is_dir() and vm_id in vm_dir.name:
            meta = json.loads((vm_dir / "meta.json").read_text())

            # Verificar si vagrant termino
            result = subprocess.run(
                ["vagrant", "status", "--machine-readable"],
                cwd=vm_dir, capture_output=True, text=True
            )

            ip = None
            status = VMStatus.creating

            if "running" in result.stdout:
                status = VMStatus.running
                # Obtener IP
                ip_result = subprocess.run(
                    ["vagrant", "ssh", "-c",
                     "hostname -I | awk '{print $2}'"],
                    cwd=vm_dir, capture_output=True, text=True
                )
                ip = ip_result.stdout.strip() if ip_result.returncode == 0 else None
            elif "poweroff" in result.stdout:
                status = VMStatus.stopped
            elif "not_created" in result.stdout:
                status = VMStatus.error

            return VMResponse(
                id=vm_id, name=meta["name"], status=status,
                ip=ip, os=meta["os"], cpus=meta["cpus"],
                memory_mb=meta["memory_mb"], ssh_user="vagrant",
                message=f"VM en estado: {status.value}" + (f" | IP: {ip}" if ip else ""),
            )

    return VMResponse(
        id=vm_id, name="unknown", status=VMStatus.error,
        os="", cpus=0, memory_mb=0, ssh_user="",
        message=f"VM {vm_id} no encontrada",
    )


def destroy_vm(vm_id: str) -> VMResponse:
    """Destruye una VM."""
    for vm_dir in VMS_DIR.iterdir():
        if vm_dir.is_dir() and vm_id in vm_dir.name:
            subprocess.run(["vagrant", "destroy", "-f"], cwd=vm_dir)
            meta = json.loads((vm_dir / "meta.json").read_text())
            return VMResponse(
                id=vm_id, name=meta["name"], status=VMStatus.destroyed,
                os=meta["os"], cpus=meta["cpus"], memory_mb=meta["memory_mb"],
                ssh_user="vagrant", message="VM destruida correctamente",
            )

    return VMResponse(
        id=vm_id, name="unknown", status=VMStatus.error,
        os="", cpus=0, memory_mb=0, ssh_user="",
        message=f"VM {vm_id} no encontrada",
    )

main.py — Endpoints FastAPI

PYTHON
from fastapi import FastAPI, HTTPException
from .models import VMRequest, VMResponse
from .provisioner import create_vm, get_vm_status, destroy_vm

app = FastAPI(
    title="VM Provisioning API",
    description="API para desplegar maquinas virtuales bajo demanda",
    version="1.0.0",
)


@app.post("/api/v1/vms", response_model=VMResponse)
def provision_vm(request: VMRequest):
    """Crear y desplegar una nueva VM."""
    return create_vm(request)


@app.get("/api/v1/vms/{vm_id}", response_model=VMResponse)
def vm_status(vm_id: str):
    """Consultar estado de una VM."""
    return get_vm_status(vm_id)


@app.delete("/api/v1/vms/{vm_id}", response_model=VMResponse)
def delete_vm(vm_id: str):
    """Destruir una VM."""
    return destroy_vm(vm_id)


@app.get("/api/v1/vms/{vm_id}/logs")
def vm_logs(vm_id: str):
    """Ver logs de despliegue."""
    from pathlib import Path
    vms_dir = Path(__file__).parent.parent / "vms"
    for vm_dir in vms_dir.iterdir():
        if vm_dir.is_dir() and vm_id in vm_dir.name:
            log_file = vm_dir / "vagrant.log"
            if log_file.exists():
                return {"logs": log_file.read_text()[-5000:]}
    raise HTTPException(status_code=404, detail="VM no encontrada")

requirements.txt

CODE
fastapi==0.115.0
uvicorn==0.30.0
jinja2==3.1.4
pydantic==2.9.0

Dockerfile

DOCKERFILE
FROM python:3.12-slim

# Instalar Vagrant y VirtualBox CLI
RUN apt-get update && apt-get install -y --no-install-recommends \
    curl gnupg2 lsb-release && \
    curl -fsSL https://apt.releases.hashicorp.com/gpg | gpg --dearmor -o /usr/share/keyrings/hashicorp.gpg && \
    echo "deb [signed-by=/usr/share/keyrings/hashicorp.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" \
    > /etc/apt/sources.list.d/hashicorp.list && \
    apt-get update && apt-get install -y vagrant && \
    rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY app/ ./app/
RUN mkdir -p vms

EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

Levantar la API

Para el lab, es mas sencillo ejecutar directamente en el host (necesita acceso a VirtualBox):

BASH
cd provisioning-api
pip install -r requirements.txt
uvicorn app.main:app --host 0.0.0.0 --port 8000

Probar manualmente:

BASH
curl -X POST http://localhost:8000/api/v1/vms \
  -H "Content-Type: application/json" \
  -d '{
    "name": "test-vm",
    "os": "ubuntu-24.04",
    "cpus": 2,
    "memory_mb": 2048,
    "install_docker": true
  }'

Paso 3: Configurar el agente en Dify

Crear la Tool HTTP custom

En Dify, ve a Tools > Custom > Create Custom Tool y define el schema OpenAPI:

YAML
openapi: 3.0.0
info:
  title: VM Provisioning
  version: 1.0.0
servers:
  - url: http://host.docker.internal:8000

paths:
  /api/v1/vms:
    post:
      operationId: createVM
      summary: Crear y desplegar una maquina virtual
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, os]
              properties:
                name:
                  type: string
                  description: Nombre de la VM (solo minusculas, numeros y guiones)
                os:
                  type: string
                  enum: [ubuntu-24.04, ubuntu-22.04, debian-12, fedora-42, rocky-9]
                  description: Sistema operativo
                cpus:
                  type: integer
                  description: Numero de CPUs (1-8)
                  default: 2
                memory_mb:
                  type: integer
                  description: RAM en MB (512-16384)
                  default: 2048
                install_docker:
                  type: boolean
                  description: Instalar Docker en la VM
                  default: false
                install_k8s:
                  type: boolean
                  description: Instalar kubeadm/kubectl
                  default: false
      responses:
        '200':
          description: VM en proceso de creacion

  /api/v1/vms/{vm_id}:
    get:
      operationId: getVMStatus
      summary: Consultar estado de una VM
      parameters:
        - name: vm_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Estado actual de la VM
    delete:
      operationId: destroyVM
      summary: Destruir una VM
      parameters:
        - name: vm_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: VM destruida

Crear el agente

  1. Studio > Create App > Agent
  2. Nombre: "Infra Provisioner"
  3. Modelo: gpt-4o-mini o llama3.1:8b

System prompt

CODE
Eres un agente de infraestructura que despliega maquinas virtuales bajo demanda.

REGLAS:
1. Cuando el usuario pida una VM, extrae los parametros: nombre, SO, CPUs, RAM, software a instalar.
2. Si no especifica algo, usa estos valores por defecto:
   - OS: ubuntu-24.04
   - CPUs: 2
   - RAM: 2048 MB
   - Docker: no
3. El nombre de la VM debe ser solo minusculas, numeros y guiones. Generalo a partir de la peticion si no lo da el usuario.
4. SIEMPRE llama a la tool createVM para desplegar. Nunca simules la respuesta.
5. Tras crear la VM, informa al usuario del ID y dile que puede consultar el estado.
6. Si piden destruir una VM, usa destroyVM con el ID.
7. Si piden el estado, usa getVMStatus.

SISTEMAS OPERATIVOS DISPONIBLES:
- ubuntu-24.04
- ubuntu-22.04
- debian-12
- fedora-42
- rocky-9

Responde siempre en espanol y se conciso.

Asignar tools

En la seccion Tools del agente, anade la custom tool "VM Provisioning" que creaste antes.


Paso 4: Probarlo

Conversacion de ejemplo

CODE
Usuario: Necesito una maquina con Ubuntu, 4GB de RAM y Docker instalado para pruebas

Agente: Voy a crear una VM con estas especificaciones:
- Nombre: pruebas-docker
- SO: Ubuntu 24.04
- CPUs: 2
- RAM: 4096 MB
- Docker: si

[Llama a createVM]

✓ VM en proceso de creacion.
- ID: a3f2b1c8
- Estado: creating
- Usuario SSH: vagrant

La VM tardara 2-3 minutos en estar lista. Puedes preguntarme
por el estado con: "estado de la VM a3f2b1c8"
CODE
Usuario: ya esta lista la vm a3f2b1c8?

Agente: [Llama a getVMStatus]

✓ VM lista:
- Estado: running
- IP: 192.168.56.15
- SSH: vagrant@192.168.56.15

Para conectar: ssh vagrant@192.168.56.15
CODE
Usuario: destruye esa vm, ya no la necesito

Agente: [Llama a destroyVM]

✓ VM a3f2b1c8 destruida correctamente.

Adaptacion a produccion

El lab usa Vagrant + VirtualBox, pero la arquitectura es identica para entornos reales:

Lab (este post)Produccion
Vagrant + VirtualBoxTerraform + AWS/Azure/GCP
FastAPI localAPI interna en K8s + autenticacion
Dify self-hostedDify Enterprise o equivalente
VMs localesInstancias cloud o VMs en Proxmox/vSphere

Ejemplo: reemplazar Vagrant por Terraform

El provisioner.py cambiaria de:

PYTHON
subprocess.Popen(["vagrant", "up"], cwd=vm_dir)

A:

PYTHON
subprocess.Popen(["terraform", "apply", "-auto-approve"], cwd=vm_dir)

Con un template main.tf en lugar de Vagrantfile. El resto de la arquitectura (Dify → Tool HTTP → API) permanece igual.

Integracion con Ansible AWX

Otra opcion es que la API lance un Job Template en AWX:

PYTHON
import requests

def create_vm_awx(request: VMRequest):
    response = requests.post(
        "https://awx.empresa.local/api/v2/job_templates/42/launch/",
        headers={"Authorization": "Bearer xxx"},
        json={
            "extra_vars": {
                "vm_name": request.name,
                "vm_os": request.os.value,
                "vm_cpus": request.cpus,
                "vm_memory": request.memory_mb,
            }
        }
    )
    return response.json()

Docker Compose completo del lab

Para levantar todo junto (Dify + API de provisioning):

YAML
# docker-compose.lab.yml
# Ejecutar junto con el compose de Dify
services:
  provisioning-api:
    build: ./provisioning-api
    ports:
      - "8000:8000"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ./provisioning-api/vms:/app/vms
      # Montar el socket de VirtualBox si es necesario
    environment:
      - VAGRANT_HOME=/app/.vagrant.d
    restart: unless-stopped

Nota: Para que el contenedor acceda a VirtualBox del host, es mas practico ejecutar la API directamente en el host con uvicorn. El Dockerfile es util si el backend es Terraform o llamadas API (que no necesitan acceso al hypervisor local).


Seguridad

En un despliegue real, anade estas capas:

  1. Autenticacion en la API: Bearer token o mTLS entre Dify y la API
  2. Limites: Max VMs por usuario, cuotas de recursos
  3. Validacion: Whitelisting de SO y rangos de recursos permitidos
  4. Audit log: Registrar quien creo/destruyo cada VM
  5. Network segmentation: API de provisioning en red interna, no expuesta
PYTHON
# Ejemplo: middleware de autenticacion
from fastapi import Header, HTTPException

API_TOKEN = os.environ["PROVISIONING_API_TOKEN"]

@app.post("/api/v1/vms")
def provision_vm(request: VMRequest, authorization: str = Header(...)):
    if authorization != f"Bearer {API_TOKEN}":
        raise HTTPException(status_code=401, detail="Unauthorized")
    return create_vm(request)

Conclusion

Este patron — agente LLM → API HTTP → backend de infraestructura — es la base de las plataformas de self-service IT modernas. Dify facilita la parte del agente (interpretacion de lenguaje natural, gestion de herramientas, historial de conversacion), mientras que tu controlas completamente el backend de ejecucion.

Casos de uso reales en empresas:

  • Self-service DevOps: Desarrolladores piden entornos via chat
  • Onboarding automatizado: Nuevos empleados reciben sus VMs configuradas
  • Incident response: "Levanta una VM forense con Volatility instalado"
  • Lab provisioning: Entornos de formacion bajo demanda

Referencias

Comentarios