Home Linux & Systems Cybersecurity Cloud & DevOps Networks & Infrastructure SIEM & Monitoring DFIR & Threat Intel Development & Other All categories Projects About Tools

Dify + FastAPI: an AI Agent that Deploys Virtual Machines On Demand

Leer en espanol
Dify + FastAPI: an AI Agent that Deploys Virtual Machines On Demand

Table of contents

What We Are Going to Build

An AI agent that receives natural-language requests such as:

CODE
I need an Ubuntu 24.04 VM with 4GB of RAM, 2 CPUs and Docker installed

And automatically:

  1. Interprets the specification
  2. Calls a provisioning API
  3. Deploys the virtual machine
  4. Returns the IP and access credentials

Architecture

CODE
┌─────────────────────────────────────────────────────────────────┐
│                         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

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

Edit .env:

BASH
# Access port
EXPOSE_NGINX_PORT=3000

# Secret key
SECRET_KEY=$(openssl rand -hex 32)

Bring up Dify

BASH
docker compose up -d

Verify:

BASH
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

CODE
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
└── Dockerfile

models.py — Data schemas

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="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: str

provisioner.py — Deployment logic

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:
    """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

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 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

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

Dockerfile

DOCKERFILE
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):

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

Test it manually:

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
  }'

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:

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: 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 destroyed

Create the agent

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

System prompt

CODE
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

YAML
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"
CODE
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.15
CODE
User: 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 + VirtualBoxTerraform + AWS/Azure/GCP
Local FastAPIInternal API on K8s + authentication
Self-hosted DifyDify Enterprise or equivalent
Local VMsCloud instances or VMs on Proxmox/vSphere

Example: replace Vagrant with Terraform

The provisioner.py would change from:

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

To:

PYTHON
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:

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()

Full Docker Compose for the lab

To bring everything up together (Dify + provisioning API):

YAML
# 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-stopped

Note: 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:

  1. API authentication: Bearer token or mTLS between Dify and the API
  2. Limits: Max VMs per user, resource quotas
  3. Validation: Whitelisting of allowed OSes and resource ranges
  4. Audit log: Record who created/destroyed each VM
  5. Network segmentation: Provisioning API on an internal network, not exposed
PYTHON
# 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

References

Comments