What We Are Going to Build
An AI agent that receives natural-language requests such as:
I need an Ubuntu 24.04 VM with 4GB of RAM, 2 CPUs and Docker installedAnd automatically:
- Interprets the specification
- Calls a provisioning API
- Deploys the virtual machine
- Returns the IP and access credentials
Architecture
┌─────────────────────────────────────────────────────────────────┐
│ Dify (Docker) │
│ │
│ ┌──────────────┐ ┌─────────────┐ ┌─────────────────┐ │
│ │ User │────▶│ Agent │────▶│ Tool HTTP: │ │
│ │ (chat/API) │◀────│ (LLM) │◀────│ provision_vm │ │
│ └──────────────┘ └─────────────┘ └────────┬────────┘ │
│ │ │
└──────────────────────────────────────────────────────┼───────────┘
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ Provisioning API (FastAPI) │
│ │
│ POST /api/v1/vms │
│ ├── Validate parameters │
│ ├── Generate Vagrantfile │
│ ├── Run vagrant up │
│ └── Return IP + status │
│ │
│ GET /api/v1/vms/{id} → VM status │
│ DELETE /api/v1/vms/{id} → Destroy VM │
└──────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────┐
│ VirtualBox / libvirt │
│ (Deployed VMs) │
└─────────────────────┘Note for production: In an enterprise environment, the Vagrant layer is replaced by Terraform (AWS/Azure/GCP), Ansible AWX, the Proxmox/vSphere API, or Kubernetes. The Dify → API → infra backend architecture is the same.
Prerequisites
- Docker and Docker Compose
- VirtualBox + Vagrant (for the lab)
- Minimum 8 GB RAM (Dify + VMs)
- An LLM model (local Ollama or an OpenAI/Anthropic API key)
Step 1: Install Dify with Docker
Clone and configure
git clone https://github.com/langgenius/dify.git
cd dify/docker
cp .env.example .envEdit .env:
# Access port
EXPOSE_NGINX_PORT=3000
# Secret key
SECRET_KEY=$(openssl rand -hex 32)Bring up Dify
docker compose up -dVerify:
docker compose ps
# All services must be "Up"Go to http://localhost:3000 and create your admin account.
Configure the LLM model
Go to Settings > Model Providers:
- Ollama (local): Base URL
http://host.docker.internal:11434 - OpenAI: API Key
sk-xxxx
Recommended model for the agent: llama3.1:8b (local) or gpt-4o-mini (API).
Step 2: Provisioning API (FastAPI)
This is the key piece: a REST API that receives VM parameters and deploys it.
Project structure
provisioning-api/
├── app/
│ ├── __init__.py
│ ├── main.py # FastAPI app
│ ├── models.py # Pydantic schemas
│ ├── provisioner.py # Vagrant logic
│ └── templates/
│ └── Vagrantfile.j2 # Jinja2 template
├── vms/ # Directory of deployed VMs
├── requirements.txt
└── Dockerfilemodels.py — Data schemas
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="VM name", pattern=r'^[a-z0-9-]+$')
os: OSType = Field(..., description="Operating system")
cpus: int = Field(default=2, ge=1, le=8, description="Number of CPUs")
memory_mb: int = Field(default=2048, ge=512, le=16384, description="RAM in MB")
disk_gb: int = Field(default=20, ge=10, le=100, description="Disk in GB")
install_docker: bool = Field(default=False, description="Install Docker")
install_k8s: bool = Field(default=False, description="Install kubeadm/kubectl")
ssh_public_key: Optional[str] = Field(default=None, description="SSH public key")
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 — Deployment logic
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:
"""Generate a Vagrantfile and deploy the VM."""
vm_id = str(uuid.uuid4())[:8]
vm_dir = VMS_DIR / f"{request.name}-{vm_id}"
vm_dir.mkdir(parents=True)
# Generate 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)
# Save 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))
# Run vagrant up in the 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}' is being created. Query GET /api/v1/vms/{vm_id} for its status.",
)
def get_vm_status(vm_id: str) -> VMResponse:
"""Query the status of a VM."""
# Find the VM directory
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())
# Check whether vagrant finished
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
# Get the 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 in state: {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} not found",
)
def destroy_vm(vm_id: str) -> VMResponse:
"""Destroy a 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 destroyed successfully",
)
return VMResponse(
id=vm_id, name="unknown", status=VMStatus.error,
os="", cpus=0, memory_mb=0, ssh_user="",
message=f"VM {vm_id} not found",
)main.py — FastAPI endpoints
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 to deploy virtual machines on demand",
version="1.0.0",
)
@app.post("/api/v1/vms", response_model=VMResponse)
def provision_vm(request: VMRequest):
"""Create and deploy a new VM."""
return create_vm(request)
@app.get("/api/v1/vms/{vm_id}", response_model=VMResponse)
def vm_status(vm_id: str):
"""Query the status of a VM."""
return get_vm_status(vm_id)
@app.delete("/api/v1/vms/{vm_id}", response_model=VMResponse)
def delete_vm(vm_id: str):
"""Destroy a VM."""
return destroy_vm(vm_id)
@app.get("/api/v1/vms/{vm_id}/logs")
def vm_logs(vm_id: str):
"""View deployment logs."""
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 not found")requirements.txt
fastapi==0.115.0
uvicorn==0.30.0
jinja2==3.1.4
pydantic==2.9.0Dockerfile
FROM python:3.12-slim
# Install Vagrant and the 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"]Bring up the API
For the lab, it is simpler to run it directly on the host (it needs access to VirtualBox):
cd provisioning-api
pip install -r requirements.txt
uvicorn app.main:app --host 0.0.0.0 --port 8000Test it manually:
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
}'Step 3: Configure the agent in Dify
Create the custom HTTP Tool
In Dify, go to Tools > Custom > Create Custom Tool and define the OpenAPI schema:
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: Create and deploy a virtual machine
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [name, os]
properties:
name:
type: string
description: VM name (lowercase letters, numbers and hyphens only)
os:
type: string
enum: [ubuntu-24.04, ubuntu-22.04, debian-12, fedora-42, rocky-9]
description: Operating system
cpus:
type: integer
description: Number of CPUs (1-8)
default: 2
memory_mb:
type: integer
description: RAM in MB (512-16384)
default: 2048
install_docker:
type: boolean
description: Install Docker on the VM
default: false
install_k8s:
type: boolean
description: Install kubeadm/kubectl
default: false
responses:
'200':
description: VM is being created
/api/v1/vms/{vm_id}:
get:
operationId: getVMStatus
summary: Query the status of a VM
parameters:
- name: vm_id
in: path
required: true
schema:
type: string
responses:
'200':
description: Current VM status
delete:
operationId: destroyVM
summary: Destroy a VM
parameters:
- name: vm_id
in: path
required: true
schema:
type: string
responses:
'200':
description: VM destroyedCreate the agent
- Studio > Create App > Agent
- Name: "Infra Provisioner"
- Model:
gpt-4o-miniorllama3.1:8b
System prompt
You are an infrastructure agent that deploys virtual machines on demand.
RULES:
1. When the user asks for a VM, extract the parameters: name, OS, CPUs, RAM, software to install.
2. If something is not specified, use these default values:
- OS: ubuntu-24.04
- CPUs: 2
- RAM: 2048 MB
- Docker: no
3. The VM name must be lowercase letters, numbers and hyphens only. Generate it from the request if the user does not provide one.
4. ALWAYS call the createVM tool to deploy. Never fake the response.
5. After creating the VM, tell the user the ID and let them know they can query its status.
6. If they ask to destroy a VM, use destroyVM with the ID.
7. If they ask for the status, use getVMStatus.
AVAILABLE OPERATING SYSTEMS:
- ubuntu-24.04
- ubuntu-22.04
- debian-12
- fedora-42
- rocky-9
Always respond in English and be concise.Assign tools
In the agent's Tools section, add the custom "VM Provisioning" tool you created earlier.
Step 4: Test it
Example conversation
User: I need a machine with Ubuntu, 4GB of RAM and Docker installed for testing
Agent: I'm going to create a VM with these specifications:
- Name: docker-testing
- OS: Ubuntu 24.04
- CPUs: 2
- RAM: 4096 MB
- Docker: yes
[Calls createVM]
✓ VM is being created.
- ID: a3f2b1c8
- Status: creating
- SSH user: vagrant
The VM will take 2-3 minutes to be ready. You can ask me
for its status with: "status of VM a3f2b1c8"User: is VM a3f2b1c8 ready yet?
Agent: [Calls getVMStatus]
✓ VM ready:
- Status: running
- IP: 192.168.56.15
- SSH: vagrant@192.168.56.15
To connect: ssh vagrant@192.168.56.15User: destroy that VM, I don't need it anymore
Agent: [Calls destroyVM]
✓ VM a3f2b1c8 destroyed successfully.Adapting to production
The lab uses Vagrant + VirtualBox, but the architecture is identical for real environments:
| Lab (this post) | Production |
|---|---|
| Vagrant + VirtualBox | Terraform + AWS/Azure/GCP |
| Local FastAPI | Internal API on K8s + authentication |
| Self-hosted Dify | Dify Enterprise or equivalent |
| Local VMs | Cloud instances or VMs on Proxmox/vSphere |
Example: replace Vagrant with Terraform
The provisioner.py would change from:
subprocess.Popen(["vagrant", "up"], cwd=vm_dir)To:
subprocess.Popen(["terraform", "apply", "-auto-approve"], cwd=vm_dir)With a main.tf template instead of a Vagrantfile. The rest of the architecture (Dify → HTTP Tool → API) stays the same.
Integration with Ansible AWX
Another option is for the API to launch a Job Template in 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()Full Docker Compose for the lab
To bring everything up together (Dify + provisioning API):
# docker-compose.lab.yml
# Run alongside the Dify compose
services:
provisioning-api:
build: ./provisioning-api
ports:
- "8000:8000"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./provisioning-api/vms:/app/vms
# Mount the VirtualBox socket if needed
environment:
- VAGRANT_HOME=/app/.vagrant.d
restart: unless-stoppedNote: For the container to access the host's VirtualBox, it is more practical to run the API directly on the host with
uvicorn. The Dockerfile is useful if the backend is Terraform or API calls (which do not need access to the local hypervisor).
Security
In a real deployment, add these layers:
- API authentication: Bearer token or mTLS between Dify and the API
- Limits: Max VMs per user, resource quotas
- Validation: Whitelisting of allowed OSes and resource ranges
- Audit log: Record who created/destroyed each VM
- Network segmentation: Provisioning API on an internal network, not exposed
# Example: authentication middleware
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
This pattern — LLM agent → HTTP API → infrastructure backend — is the foundation of modern self-service IT platforms. Dify handles the agent side (natural-language interpretation, tool management, conversation history), while you retain full control over the execution backend.
Real-world use cases in companies:
- Self-service DevOps: Developers request environments via chat
- Automated onboarding: New employees receive their VMs pre-configured
- Incident response: "Spin up a forensic VM with Volatility installed"
- Lab provisioning: On-demand training environments
Comments