Skip to main content
RunBook Academy

Proxmox VEXX · CLI & AutomationConfiguration management

Ansible for Proxmox: from ad-hoc commands to full fleet management

Intermediate⏱ ~22 min🧪 Lab requiredansiblecommunity.proxmox

What you'll learn

  • Install and configure the community.proxmox Ansible collection
  • Manage PVE nodes and VMs with declarative Ansible playbooks
  • Use dynamic inventories to discover VMs automatically
  • Build idempotent roles that work at fleet scale

Prerequisites

Verified against Proxmox VE 9.2.4 · Proxmox Backup Server 4.2.5 · Ceph Squid / Tentacle · Debian 13 (Trixie) · Linux kernel 7.0 (PVE 9.2 default) · 2026-08-07

Not yet marked complete on this device.

Ansible for Proxmox: from ad-hoc commands to full fleet management

Ansible is the most common configuration management tool for operational teams. The community.proxmox collection provides modules that wrap the PVE API and PBS API into Ansible tasks. This lesson shows how to use them at fleet scale.

Installing the collection

# Install Ansible
apt install -y ansible python3-pip

# Install the community.proxmox collection
ansible-galaxy collection install community.proxmox

# Install the Proxmox API helper (for some modules)
pip install proxmoxer

Verify with a ping:

# inventory.ini
[pve]
pve-01.cluster.example.com
pve-02.cluster.example.com
pve-03.cluster.example.com

[pve:vars]
ansible_python_interpreter=/usr/bin/python3
ansible pve -i inventory.ini -m ping

One-off operations

# Get cluster status
ansible pve-01 -i inventory.ini -m community.proxmox.proxmox_cluster_info

# List VMs on each node
ansible pve -i inventory.ini -m community.proxmox.proxmox_vm_info \
  -a "node={\{ inventory_hostname \}}"

# Reboot a specific node
ansible pve-02 -i inventory.ini -m community.proxmox.proxmox_node \
  -a "node=pve-02 command=reboot"

Dynamic inventory

The community.proxmox collection ships with a dynamic inventory script:

# Get the script
wget https://raw.githubusercontent.com/ansible-collections/community.proxmox/main/scripts/inventory/proxmox.py -O proxmox-inventory.py
chmod +x proxmox-inventory.py

Configure it:

# proxmox.ini
[proxmox]
url=https://pve-01.cluster.example.com:8006/api2/json
token=ansible@pve!inventory=12345678-...
# Validate cert
validate_certs=True
# Filter VMs to a subset (optional)
# filter_groups=tag=production

Use it:

ansible all -i proxmox.py -m ping
# Lists every VM and container as an inventory host

The script creates groups based on VM tags, node, type (qemu vs lxc), and other attributes. You can target groups in playbooks:

# All production VMs
ansible 'group_production' -i proxmox.py -m ping

# All VMs on a specific node
ansible 'pve_01' -i proxmox.py -m ping

# All qemu VMs (not containers)
ansible 'type_qemu' -i proxmox.py -m ping

Playbook: cluster maintenance

A playbook that updates all PVE nodes in a rolling fashion:

# cluster-update.yml
---
- name: PVE cluster maintenance
  hosts: pve
  serial: 1   # One node at a time
  become: true

  vars:
    maintenance_window: "Sun 02:00-04:00"

  tasks:
    - name: Check if we\'re in maintenance window
      ansible.builtin.fail:
        msg: "Outside maintenance window"
      when:
        - ansible_date_time.weekday not in ['Sunday']
        - ansible_date_time.hour < 2 or ansible_date_time.hour >= 4
      ignore_errors: true
      register: in_window

    - name: Skip if outside window
      ansible.builtin.meta: end_play
      when: in_window is failed

    - name: Drain VMs from this node
      ansible.builtin.command:
        cmd: ha-manager group modify production-grp
             --nodes "pve-01:0,pve-02:100,pve-03:100"
      delegate_to: pve-01
      changed_when: false

    - name: Migrate VMs off this node
      community.proxmox.proxmox_vm_info:
        node: "&#123;\&#123; inventory_hostname \&#125;&#125;"
      register: vms

    - name: Migrate each VM
      community.proxmox.proxmox_kvm:
        api_host: pve-01
        api_user: ansible@pve
        api_token_id: automation
        api_token_secret: "&#123;\&#123; proxmox_token \&#125;&#125;"
        vmid: "&#123;&#123; item.vmid &#125;&#125;"
        state: started
        migrate: true
        migration_target: pve-02
      loop: "&#123;&#123; vms.proxmox_vms &#125;&#125;"
      loop_control:
        pause: 30
      when: vms.proxmox_vms | length > 0

    - name: Upgrade packages
      ansible.builtin.apt:
        upgrade: dist
        update_cache: yes

    - name: Reboot if needed
      ansible.builtin.command: /usr/bin/needs-restarting -r
      register: needs_restart
      failed_when: false
      changed_when: needs_restart.rc == 1

    - name: Reboot
      ansible.builtin.reboot:
        reboot_timeout: 600
      when: needs_restart.rc == 1

Playbook: VM lifecycle

# vm-lifecycle.yml
---
- name: Create production web tier
  hosts: localhost
  gather_facts: false
  vars:
    api_host: pve-01.cluster.example.com
    api_user: ansible@pve
    api_token_id: automation
    api_token_secret: "&#123;\&#123; vault_proxmox_token \&#125;&#125;"

  tasks:
    - name: Create 3 web VMs from template
      community.proxmox.proxmox_kvm:
        api_host: "&#123;\&#123; api_host \&#125;&#125;"
        api_user: "&#123;\&#123; api_user \&#125;&#125;"
        api_token_id: "&#123;\&#123; api_token_id \&#125;&#125;"
        api_token_secret: "&#123;\&#123; api_token_secret \&#125;&#125;"
        vmid: "&#123;&#123; 200 + item &#125;&#125;"
        name: "web-&#123;&#123; '%02d' | format(item) &#125;&#125;"
        node: "pve-01"
        clone: "debian-12-web-template"
        full: false
        storage: "local-zfs"
        cores: 2
        memory: 2048
        net:
          net0: "virtio,bridge=vmbr0,tag=100"
        ipconfig:
          ipconfig0: "ip=dhcp"
        state: present
      loop: [1, 2, 3]

    - name: Wait for VMs to be ready
      community.proxmox.proxmox_vm_info:
        api_host: "&#123;\&#123; api_host \&#125;&#125;"
        api_user: "&#123;\&#123; api_user \&#125;&#125;"
        api_token_id: "&#123;\&#123; api_token_id \&#125;&#125;"
        api_token_secret: "&#123;\&#123; api_token_secret \&#125;&#125;"
        vmid: "&#123;&#123; 200 + item &#125;&#125;"
        name: "web-&#123;&#123; '%02d' | format(item) &#125;&#125;"
      register: vm_info
      until: vm_info.proxmox_vms[0].status == "running"
      retries: 30
      delay: 10
      loop: [1, 2, 3]

    - name: Configure VMs with Ansible
      hosts: "web-*"
      become: true
      tasks:
        - name: Install nginx
          ansible.builtin.apt:
            name: nginx
            state: present

        - name: Deploy nginx config
          ansible.builtin.template:
            src: nginx.conf.j2
            dest: /etc/nginx/sites-available/default

        - name: Enable and start nginx
          ansible.builtin.service:
            name: nginx
            state: started
            enabled: yes

Roles for reuse

A role packages tasks, vars, defaults, and templates into a reusable unit. For Proxmox:

roles/
├── proxmox-cluster/
│   ├── tasks/main.yml       # Update, patch, configure PVE nodes
│   ├── handlers/main.yml    # Service restart handlers
│   ├── defaults/main.yml    # Defaults for storage, network names
│   └── vars/main.yml        # Cluster-specific variables
├── proxmox-vm/
│   ├── tasks/main.yml       # Create / migrate / destroy VMs
│   ├── defaults/main.yml
│   └── templates/cloud-init.yml.j2
└── proxmox-backup/
    ├── tasks/main.yml       # Configure backup jobs, verify schedules
    └── defaults/main.yml

Use them:

# site.yml
- hosts: pve
  roles:
    - proxmox-cluster

- hosts: localhost
  roles:
    - role: proxmox-vm
      vars:
        vmid: 100
        vm_name: "web-01"
        cores: 2
        memory: 2048

Idempotency

Ansible modules for Proxmox should be idempotent — running the same playbook twice should not create duplicates. The community.proxmox modules handle this:

- name: Ensure VM exists
  community.proxmox.proxmox_kvm:
    name: "web-01"
    node: "pve-01"
    state: present

# First run: creates the VM
# Second run: no change (idempotent)

For state-checking:

- name: Verify VM is running
  community.proxmox.proxmox_vm_info:
    name: "web-01"
  register: info

- name: Start VM if stopped
  community.proxmox.proxmox_kvm:
    name: "web-01"
    state: started
  when: info.proxmox_vms[0].status != "running"

Production considerations

  • Use Ansible Vault for secrets. API tokens are sensitive; never in plaintext.
  • Limit concurrent operations. serial: 1 for cluster-wide changes; throttle: 1 for tasks that touch many nodes.
  • Test in dry-run mode. Use check_mode: true for playbooks that modify resources.
  • Inventory versioning. Commit inventory to git so changes are traceable.
  • Separate control node. Run Ansible from a dedicated control host, not from one of the PVE nodes. The control host should have SSH access to all PVE nodes and VMs.

Common mistakes

  • Using the wrong auth method. The community.proxmox modules support both password and API token. API token is more secure.
  • Mixing command: and modules. Use modules for idempotency. command: runs every time.
  • No rollback plan. Before applying, snapshot or backup the affected VMs. ansible-playbook --check shows what would change without applying.
  • No timeouts. Long-running tasks without timeouts hang Ansible forever.

Key takeaways

  • community.proxmox wraps the PVE API in Ansible modules.
  • Use dynamic inventory for automatic VM discovery.
  • Use Vault for API tokens and other secrets.
  • Always use serial: 1 for cluster-wide changes.

Knowledge check

Knowledge check · 4 questions

  1. Q1. What is the standard Ansible collection for Proxmox?

  2. Q2. An idempotent Ansible module can be run multiple times safely.

  3. Q3. Which of these are good Ansible practices? (Select all that apply)

  4. Q4. Name the ansible.cfg setting that limits concurrent host execution.

Passing score: 75%. Answers are checked in this browser.