Automate Proxmox VE with Ansible Full VM Playbooks

Provision Proxmox VMs and LXC containers with Ansible using community.general and API tokens. Get repeatable, zero-touch VM deployments in under 90 seconds.

Proxmox Pulse Proxmox Pulse
9 min read
ansible automation infrastructure-as-code vm-management proxmox
Glowing server rack nodes connected by automated workflow streams in a dark data center.

Ansible turns a Proxmox node — or a full three-node cluster — into reproducible, version-controlled infrastructure. By the end of this guide you'll have working playbooks that provision VMs and LXC containers on Proxmox VE 9.x using the community.general Ansible collection, a node-level configuration play you can run on every new host, and a structure you can commit to git and replay after a disaster.

Key Takeaways

  • Collection to use: community.general 8.x ships proxmox_kvm and proxmox modules covering full VM and LXC lifecycle
  • Auth method: API tokens (not root password) are the correct approach — scoped, revocable, and logged in the Proxmox audit trail
  • Idempotency: Both modules are idempotent; re-running a playbook will not clone duplicate VMs
  • Cloud-Init VMs: Combine proxmox_kvm with a Cloud-Init template for zero-touch VM deployment in under 90 seconds on NVMe storage
  • Node config: A separate OS-level play handles repo configuration, user accounts, and sysctl tuning on every new host automatically

Why Ansible Beats Clicking Through the Proxmox UI

The Proxmox web UI is excellent for one-off tasks — but the moment you're standing up a third K3s node or rebuilding a host after drive failure, manual UI work becomes a liability. You miss a CPU setting, forget to enable the QEMU guest agent, or pick the wrong storage pool. Ansible makes that impossible by turning your infrastructure into a YAML file you commit to git.

The practical payoff: I rebuilt an entire three-node homelab from scratch in about 20 minutes after a botched ZFS experiment, running a single ansible-playbook site.yml. Every VM came up with the right CPU topology, the right network bridge, and Cloud-Init pre-populated with my SSH keys.

That said, Ansible for Proxmox is not Terraform. It doesn't maintain remote state, so if you delete a VM manually and re-run the playbook, Ansible will try to create it again. For homelab scale this is fine. For production, combining Ansible with Terraform's Proxmox provider gives you both declarative provisioning and state tracking.

Prerequisites: Modules, API Tokens, and Inventory Setup

Installing the community.general Collection

ansible-galaxy collection install community.general

You need community.general 8.0.0 or later for the proxmox_kvm and proxmox (LXC) modules. Check your installed version:

ansible-galaxy collection list | grep community.general

Install the Python dependency on the machine running Ansible — not the Proxmox host itself:

pip install proxmoxer requests

Creating a Proxmox API Token

Never store your root password in a playbook. Create a dedicated API token instead:

pveum user add ansible@pve --comment "Ansible automation"
pveum role add AnsibleRole --privs "Datastore.AllocateSpace,Datastore.Audit,Pool.Allocate,Sys.Audit,Sys.Console,Sys.Modify,VM.Allocate,VM.Audit,VM.Clone,VM.Config.CDROM,VM.Config.CPU,VM.Config.Cloudinit,VM.Config.Disk,VM.Config.HWType,VM.Config.Memory,VM.Config.Network,VM.Config.Options,VM.Migrate,VM.Monitor,VM.PowerMgmt,SDN.Use"
pveum aclmod / -user ansible@pve -role AnsibleRole
pveum user token add ansible@pve automation --privsep 0

That last command outputs the token secret — copy it now, you won't see it again. Store it in Ansible Vault immediately:

ansible-vault create group_vars/all/vault.yml
# vault.yml (encrypted at rest)
vault_proxmox_token_id: "ansible@pve!automation"
vault_proxmox_token_secret: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"

Inventory Structure

A minimal inventory for a single-node setup:

[proxmox]
pve01 ansible_host=192.168.1.10

[proxmox:vars]
ansible_user=root
ansible_ssh_private_key_file=~/.ssh/id_ed25519

For the API-based Proxmox modules, ansible_host is used as api_host. Ansible calls the Proxmox REST API from your workstation — it does not SSH into the node to run proxmox_kvm tasks. SSH is only used for OS-level configuration plays.

How to Provision VMs with the proxmox_kvm Module

This playbook creates a Ubuntu 24.04 VM from a Cloud-Init template. It assumes you have a Cloud-Init-ready template at VMID 9000 — setting up that base template is part of building a full private cloud on Proxmox.

# playbooks/create_vm.yml
---
- name: Provision Ubuntu VM on Proxmox
  hosts: localhost
  gather_facts: false
  vars:
    api_host: "192.168.1.10"
    api_token_id: "{{ vault_proxmox_token_id }}"
    api_token_secret: "{{ vault_proxmox_token_secret }}"
    node: "pve01"
    template_vmid: 9000
    new_vmid: 101
    vm_name: "ubuntu-worker-01"
    vm_memory: 4096
    vm_cores: 2
    storage: "local-lvm"
    ipconfig: "ip=192.168.1.101/24,gw=192.168.1.1"
    ssh_keys: "ssh-ed25519 AAAAC3Nz your@key"

  tasks:
    - name: Clone VM from Cloud-Init template
      community.general.proxmox_kvm:
        api_host: "{{ api_host }}"
        api_token_id: "{{ api_token_id }}"
        api_token_secret: "{{ api_token_secret }}"
        node: "{{ node }}"
        name: "{{ vm_name }}"
        vmid: "{{ new_vmid }}"
        clone: "{{ template_vmid }}"
        full: true
        storage: "{{ storage }}"
        timeout: 300
        state: present

    - name: Configure VM hardware and Cloud-Init
      community.general.proxmox_kvm:
        api_host: "{{ api_host }}"
        api_token_id: "{{ api_token_id }}"
        api_token_secret: "{{ api_token_secret }}"
        node: "{{ node }}"
        vmid: "{{ new_vmid }}"
        memory: "{{ vm_memory }}"
        cores: "{{ vm_cores }}"
        ipconfig:
          ipconfig0: "{{ ipconfig }}"
        sshkeys: "{{ ssh_keys }}"
        ciuser: "ubuntu"
        update: true

    - name: Start VM
      community.general.proxmox_kvm:
        api_host: "{{ api_host }}"
        api_token_id: "{{ api_token_id }}"
        api_token_secret: "{{ api_token_secret }}"
        node: "{{ node }}"
        vmid: "{{ new_vmid }}"
        state: started

Run it:

ansible-playbook playbooks/create_vm.yml --vault-password-file ~/.vault_pass

On NVMe-to-NVMe (local-lvm to local-lvm on the same node), a full 32 GB clone completes in under 90 seconds. On HDD-backed storage, plan for 3-5 minutes and set timeout accordingly.

Deploying Multiple VMs with a Loop

The real payoff comes when you define your fleet in a variables file and loop over it:

# vars/vms.yml
vms:
  - name: k3s-master-01
    vmid: 101
    ip: "192.168.1.101"
    memory: 4096
    cores: 2
  - name: k3s-worker-01
    vmid: 102
    ip: "192.168.1.102"
    memory: 8192
    cores: 4
  - name: k3s-worker-02
    vmid: 103
    ip: "192.168.1.103"
    memory: 8192
    cores: 4
- name: Clone and configure all VMs
  community.general.proxmox_kvm:
    api_host: "{{ api_host }}"
    api_token_id: "{{ api_token_id }}"
    api_token_secret: "{{ api_token_secret }}"
    node: "{{ node }}"
    name: "{{ item.name }}"
    vmid: "{{ item.vmid }}"
    clone: "{{ template_vmid }}"
    full: true
    storage: "{{ storage }}"
    memory: "{{ item.memory }}"
    cores: "{{ item.cores }}"
    ipconfig:
      ipconfig0: "ip={{ item.ip }}/24,gw=192.168.1.1"
    sshkeys: "{{ ssh_keys }}"
    ciuser: ubuntu
    state: present
    timeout: 300
  loop: "{{ vms }}"

Three K3s nodes provisioned with one loop. Once they're up, the K3s Kubernetes cluster setup guide on Proxmox picks up exactly where this leaves off.

Provisioning LXC Containers with the proxmox Module

The community.general.proxmox module handles LXC lifecycle. The interface differs from proxmox_kvm — you specify a template from a storage pool rather than cloning a VMID.

First, download the template on the Proxmox node:

pveam update
pveam available | grep debian-12
pveam download local debian-12-standard_12.7-1_amd64.tar.zst

Then the Ansible tasks:

- name: Create Debian 12 LXC container
  community.general.proxmox:
    api_host: "{{ api_host }}"
    api_token_id: "{{ api_token_id }}"
    api_token_secret: "{{ api_token_secret }}"
    node: "{{ node }}"
    vmid: 200
    hostname: "monitoring-01"
    ostemplate: "local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst"
    storage: local-lvm
    disk: 8
    memory: 1024
    swap: 512
    cores: 2
    netif:
      net0: "name=eth0,bridge=vmbr0,ip=192.168.1.200/24,gw=192.168.1.1"
    password: "{{ vault_lxc_root_password }}"
    pubkey: "{{ ssh_keys }}"
    unprivileged: true
    features:
      - nesting=1
    state: present

- name: Start LXC container
  community.general.proxmox:
    api_host: "{{ api_host }}"
    api_token_id: "{{ api_token_id }}"
    api_token_secret: "{{ api_token_secret }}"
    node: "{{ node }}"
    vmid: 200
    state: started

Setting unprivileged: true and nesting=1 is the right default for containers that will run Docker — running Docker inside LXC containers on Proxmox covers the additional lxc.apparmor.profile and keyctl settings you'll apply after the container first starts.

Gotcha: If your Proxmox node uses a self-signed certificate, proxmoxer will refuse the API connection with a verification error. Install a proper TLS certificate or pass validate_certs: false in each module call for automation running entirely on a trusted internal network.

Automating Node-Level Configuration Over SSH

Ansible shines at the OS layer too. This play handles the common Proxmox post-install tasks every node needs:

# playbooks/configure_node.yml
---
- name: Configure Proxmox node base settings
  hosts: proxmox
  become: false

  tasks:
    - name: Disable enterprise repo
      ansible.builtin.copy:
        dest: /etc/apt/sources.list.d/pve-enterprise.list
        content: |
          # deb https://enterprise.proxmox.com/debian/pve bookworm pve-enterprise

    - name: Add no-subscription repo
      ansible.builtin.apt_repository:
        repo: "deb http://download.proxmox.com/debian/pve bookworm pve-no-subscription"
        state: present
        filename: pve-no-subscription

    - name: Update all packages
      ansible.builtin.apt:
        update_cache: true
        upgrade: dist

    - name: Set swappiness for VM host
      ansible.posix.sysctl:
        name: vm.swappiness
        value: "10"
        sysctl_file: /etc/sysctl.d/99-proxmox.conf
        reload: true

    - name: Install QEMU guest agent
      ansible.builtin.apt:
        name: qemu-guest-agent
        state: present

For SSH hardening, fail2ban, and Proxmox firewall rules, keep a separate harden_node.yml. Running it via Ansible means every new node gets an identical security baseline automatically — exactly the defense-in-depth approach that hardening Proxmox with firewall, fail2ban, and SSH config describes.

Structuring a site.yml That Ties Everything Together

Once you have individual playbooks, a top-level site.yml composes them in the correct order:

# site.yml
---
- import_playbook: playbooks/configure_node.yml
- import_playbook: playbooks/harden_node.yml
- import_playbook: playbooks/create_vms.yml
- import_playbook: playbooks/create_lxcs.yml

Run order matters. Configure and harden the node before creating workloads. If the node play reconfigures network bridges, a VM created before that step completes will start with no network interface.

# Dry run with diff output first
ansible-playbook site.yml --check --diff --vault-password-file ~/.vault_pass

# Apply
ansible-playbook site.yml --vault-password-file ~/.vault_pass

Common Pitfalls to Avoid

VMID conflicts: If a playbook targets a VMID already in use, proxmox_kvm fails with a confusing API error rather than a clear message. Check pvesh get /nodes/pve01/qemu before assigning VMIDs in automation, or reserve a dedicated range above 200 exclusively for Ansible-managed workloads.

Clone timeout on slow storage: The default timeout for proxmox_kvm is 30 seconds. A full clone to HDD-backed storage will time out and leave a partial VM. Set timeout: 300 as a minimum — even NVMe-to-NVMe can push past 90 seconds for a 100 GB disk.

SSH host key collisions: When a VM is rebuilt with the same IP, Ansible will refuse SSH because the host key changed. Add this to ansible.cfg:

[defaults]
host_key_checking = False

Or use the known_hosts module to explicitly clear stale entries before connecting.

API privilege scope: The role above is broad — intentionally so for a homelab. For production, the minimum set for VM and LXC management is: VM.Allocate, VM.Config.CPU, VM.Config.Memory, VM.Config.Disk, VM.Config.Network, VM.Config.Cloudinit, VM.PowerMgmt, Datastore.AllocateSpace, Datastore.Audit, SDN.Use.

Proxmox VE 9.1 and community.general 9.0: The proxmox_kvm module gained scsi_discard support and improved Cloud-Init disk handling in community.general 9.0. If you're on Proxmox VE 9.1 and see unexpected disk configuration behavior, upgrade the collection before debugging your playbook.

Conclusion

With these playbooks committed to git, standing up a new VM or LXC container on Proxmox takes one command and under two minutes — no UI clicks, no config drift, no forgotten settings. The logical next step is adding VLAN and bridge configuration to your node-level play (Proxmox's ifupdown2 config in /etc/network/interfaces maps cleanly to Ansible's template module), then tagging your VMs with Proxmox pools so you can filter by environment in the dashboard. From there, your infrastructure is a pull request.

Share
Proxmox Pulse

Written by

Proxmox Pulse

Sysadmin-driven guides for getting the most out of Proxmox VE in production and homelab environments.

Related Articles

View all →