Finally Got the Automated Ansible VM Deployment Working

Converting a Base Ubuntu Image

Before creating a VM template, we needed a clean Ubuntu base image. Proxmox works best with raw disk formats, so we had to convert a QCOW2 image to RAW:

qemu-img convert -O raw /var/lib/vz/template/iso/ubuntu.qcow2 /var/lib/vz/template/iso/ubuntu.raw

This ensures that Proxmox can efficiently use the image for cloning operations.


Creating a Proxmox VM Template

Next, we created a base VM that would act as our Cloud-Init template. This VM was manually configured to have essential settings such as CPU, memory, disk, and networking:

qm create 3000 --name auto-test --sockets 1 --cores 4 --numa 0 --memory 4096 \
    --net0 virtio,bridge=cormgt_out,macaddr=cc:44:8a:aa:1a:50 --scsihw virtio-scsi-pci --machine q35

qm set 3000 --scsi0 mcp-zfs1:vm-3000-disk-0
qm set 3000 --ide2 local-lvm:cloudinit
qm set 3000 --boot c --bootdisk scsi0
qm set 3000 --serial0 socket --vga serial0
qm set 3000 --ciuser admin --cipassword securepassword
qm set 3000 --ipconfig0 ip=10.0.1.65/24,gw=10.0.1.1
qm resize 3000 scsi0 +40G

Once the VM was fully configured, we converted it into a template:

qm template 3000

This template became the foundation for all future VM deployments.


Deploying New VMs with Ansible

Now that we had a working template (3000), we needed an automated way to deploy new instances based on it. This was accomplished using Ansible and Proxmox's API.

Obfuscated Ansible Inventory Entry for Proxmox

We configured Ansible to connect to our Proxmox host (obfuscated for security):

[proxmox]
proxmox-host ansible_host=xxx.xxx.xxx.xxx ansible_user=root ansible_ssh_private_key_file=~/.ssh/proxmox_id_rsa

This allowed Ansible to communicate securely with Proxmox to issue API commands.

Final Ansible Playbook

The following Ansible playbook performs the entire process:

  • Clones a VM from template
  • Waits for cloning to complete
  • Configures Cloud-Init
  • Assigns a static IP
  • Boots the VM
- name: Clone and Configure a New Proxmox VM
  hosts: proxmox
  gather_facts: no
  tasks:

    - name: Clone the VM from Template (3000)
      uri:
        url: "https://{{ inventory_hostname }}:8006/api2/json/nodes/mcp/qemu/3000/clone"
        method: POST
        headers:
          Authorization: "PVEAPIToken=root@pam!your-api-token-here"
        body_format: form-urlencoded
        body:
          newid: "2022"
          name: "mariadb-core-1"
          full: "1"
          target: "mcp"
          storage: "mcp-zfs1"
        validate_certs: no
      register: clone_response

    - name: Wait for VM to be fully created (Check Lock Status)
      uri:
        url: "https://{{ inventory_hostname }}:8006/api2/json/nodes/mcp/qemu/2022/status/current"
        method: GET
        headers:
          Authorization: "PVEAPIToken=root@pam!your-api-token-here"
        validate_certs: no
      register: vm_status
      until: "'lock' not in vm_status.json.data"
      retries: 20
      delay: 5

    - name: Attach Cloud-Init Drive (Required for User & SSH Injection)
      uri:
        url: "https://{{ inventory_hostname }}:8006/api2/json/nodes/mcp/qemu/2022/config"
        method: POST
        headers:
          Authorization: "PVEAPIToken=root@pam!your-api-token-here"
        body_format: form-urlencoded
        body:
          ide2: "local-lvm:cloudinit"
        validate_certs: no
      when: "'lock' not in vm_status.json.data"

    - name: Set IP Address and Boot Configuration
      uri:
        url: "https://{{ inventory_hostname }}:8006/api2/json/nodes/mcp/qemu/2022/config"
        method: POST
        headers:
          Authorization: "PVEAPIToken=root@pam!your-api-token-here"
        body_format: form-urlencoded
        body:
          ipconfig0: "ip=10.0.1.65/24,gw=10.0.1.1"
          boot: "c"
          bootdisk: "scsi0"
          machine: "q35"
        validate_certs: no
      when: "'lock' not in vm_status.json.data"

    - name: Start the New VM
      uri:
        url: "https://{{ inventory_hostname }}:8006/api2/json/nodes/mcp/qemu/2022/status/start"
        method: POST
        headers:
          Authorization: "PVEAPIToken=root@pam!your-api-token-here"
        validate_certs: no
      when: "'lock' not in vm_status.json.data"

Conclusion

This process successfully automated Proxmox VM deployment using Cloud-Init and Ansible, allowing for repeatable, scalable infrastructure provisioning. The final playbook does not install additional software—it simply ensures the VM boots correctly with predefined settings, static IP, and Cloud-Init configurations.

Next Steps

Now that VMs can be automatically created and started, the next logical step is to automate software installation via Ansible. This will allow for full stack provisioning—including database installations, application configurations, and monitoring setups.