Que vamos a construir
Un agente de IA que recibe peticiones en lenguaje natural como:
Necesito una VM Ubuntu 24.04 con 4GB de RAM, 2 CPUs y Docker instaladoY automaticamente:
- Interpreta la especificacion
- Llama a una API de provisioning
- Despliega la maquina virtual
- Devuelve la IP y credenciales de acceso
Arquitectura
┌─────────────────────────────────────────────────────────────────┐
│ 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
git clone https://github.com/langgenius/dify.git
cd dify/docker
cp .env.example .envEditar .env:
# Puerto de acceso
EXPOSE_NGINX_PORT=3000
# Clave secreta
SECRET_KEY=$(openssl rand -hex 32)Levantar Dify
docker compose up -dVerificar:
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
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
└── Dockerfilemodels.py — Esquemas de datos
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: strprovisioner.py — Logica de despliegue
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
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
fastapi==0.115.0
uvicorn==0.30.0
jinja2==3.1.4
pydantic==2.9.0Dockerfile
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):
cd provisioning-api
pip install -r requirements.txt
uvicorn app.main:app --host 0.0.0.0 --port 8000Probar manualmente:
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:
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 destruidaCrear el agente
- Studio > Create App > Agent
- Nombre: "Infra Provisioner"
- Modelo:
gpt-4o-miniollama3.1:8b
System prompt
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
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"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.15Usuario: 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 + VirtualBox | Terraform + AWS/Azure/GCP |
| FastAPI local | API interna en K8s + autenticacion |
| Dify self-hosted | Dify Enterprise o equivalente |
| VMs locales | Instancias cloud o VMs en Proxmox/vSphere |
Ejemplo: reemplazar Vagrant por Terraform
El provisioner.py cambiaria de:
subprocess.Popen(["vagrant", "up"], cwd=vm_dir)A:
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:
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):
# 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-stoppedNota: 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:
- Autenticacion en la API: Bearer token o mTLS entre Dify y la API
- Limites: Max VMs por usuario, cuotas de recursos
- Validacion: Whitelisting de SO y rangos de recursos permitidos
- Audit log: Registrar quien creo/destruyo cada VM
- Network segmentation: API de provisioning en red interna, no expuesta
# 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
Comentarios