Featured image of post VM template in Proxmox

VM template in Proxmox

Building a Proxmox VM template with Packer and Cloud-Init, then deploying VMs from this template with Terraform

Introduction

In my homelab, I recently worked on a project that let me create virtual machine (VM) templates on Proxmox using a combination of tools such as Packer, Cloud-Init and Terraform. The goal was to create VMs in an automated and repeatable way, while keeping manual steps to a minimum.

Context

Managing VMs and their configuration by hand can quickly become tedious, especially when deploying several instances with almost identical configurations every time. So I needed a solution to standardise this whole process and have a ready-to-use VM template to serve as the base for all my VMs.

In the rest of this article, I explain how I reached this goal with different tools, in order to streamline deployments in my homelab.

Components

  • Proxmox: provides the infrastructure to run and manage virtual machines.
  • Packer: automates the installation and configuration of the OS to produce a standardised image or template.
  • Terraform: defines, provisions and manages infrastructure through declarative code.
  • Cloud-Init: configures machines dynamically at first boot.

Packer

Packer is a community tool for creating identical machine images for several platforms from a single source configuration. To do this, Packer uses what are called builders, Packer components able to create a virtual machine template or image on a specific platform.

HashiCorp Packer logo

Packer logo

In my case, I simply use Packer on my Proxmox environment through its dedicated builder.

1
2
3
4
5
6
7
8
9
#packer.pkr.hcl
packer {
  required_plugins {
    name = {
      version = "1.2.2"
      source  = "github.com/hashicorp/Proxmox"
    }
  }
}

Template configuration

This builder can either clone a virtual machine to turn it into a template (Proxmox-clone) or create a virtual machine template from a source ISO (Proxmox-iso). To meet my initial need, I use the Proxmox-iso builder. For the configuration, I used the builder’s documentation.

Example packer.pkr.hcl file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#packer.pkr.hcl
source "Proxmox-iso" "debian12" {
    # Proxmox server
    Proxmox_url              = "proxmox_api_url"
    node                     = "pve"
    username                 = "<Proxmox_token_id>"
    token                    = "<Proxmox_token>"
    insecure_skip_tls_verify = true

    # VM Info
    template_name        = "pckr-debian-12"
    vm_name              = "pckr-debian-12"
    template_description = "Debian 12.9.0, generated on ${timestamp()}"
    tags                 = "template;debian12"
    vm_id                = 1101
    pool                 = "TEMPLATE"
    os                   = "l26"
    machine              = "q35"

    # Boot
    boot_iso {
        type         = "scsi"
        iso_file     = "local:iso/debian-12.9.0-amd64-netinst.iso"
        unmount      = true
        iso_checksum = "none"
    }
    boot_command   = ["<esc><wait>auto console-keymaps-at/keymap=fr console-setup/ask_detect=false debconf/frontend=noninteractive fb=false url=http://{{ .HTTPIP }}:{{ .HTTPPort }}/preseed.cfg<enter>"]
    boot_wait  	   = "10s"
    http_directory = "http"

    # Disks
    disks {
        type         = "scsi"
        storage_pool = "local-lvm"
        disk_size    = "30G"
        format       = "raw"
    }

    # Networks
    network_adapters {
        bridge = "vmbr0"
        model  = "virtio"
    }

    # Spec
    bios               = "seabios"
    qemu_agent         = true
    scsi_controller    = "virtio-scsi-pci"
    cpu_type           = "x86-64-v2-AES"
    sockets            = 1
    cores              = 1
    memory             = 1024
    ballooning_minimum = 1024

    # Communication
    communicator           = "ssh"
    ssh_handshake_attempts = 5
    ssh_pty                = true
    ssh_timeout            = "10m"
    ssh_username           = <username>
    ssh_password           = <password>

    # Cloud-init
    cloud_init              = "true"
    cloud_init_storage_pool = "local-lvm"
}

In this packer.pkr.hcl file, I give the builder the information about the target machine and its specifications for Proxmox, as well as the boot information (path of the ISO file, boot command, etc.).

Preseed file

The boot section gives the virtual machine, the future template, the path of the preseed.cfg file, served by a web server that Packer starts during the process. This file answers the questions asked during the OS installation, without having to type the answers by hand while the installation runs, which fully automates the installation. An example of this file is available in the Debian documentation.

Building the template

Once the configuration is ready, build the template with the Packer CLI using the commands below:

1
2
3
4
5
6
7
8
# Initialise the repository to fetch the dependencies
packer init packer.pkr.hcl

# Validate the syntax and configuration
packer validate packer.pkr.hcl

# Build the image, replacing the template if it already exists
packer build -on-error=ask -force packer.pkr.hcl

The template is now present in Proxmox with all the characteristics I set in the packer.pkr.hcl file, and it is ready to be used for future virtual machine deployments.

Template 1101 pckr-debian-12 in Proxmox with the debian12 and template tags, 1 vCPU, 1 GiB of memory and a 30 GiB disk

Template in Proxmox

Now on to the next step: dynamic configuration with Cloud-Init!

Cloud-init

Cloud-Init is an open source tool that automates the initial configuration of virtual machines at their first boot. Using configuration files (user-data and meta-data), Cloud-Init can perform various tasks such as creating users, injecting SSH keys, configuring the network or installing packages. This tool works with many cloud platforms and virtualisation environments, which makes it an agnostic way to customise instances dynamically without manual intervention.

Cloud-Init logo

Cloud-Init logo

Example user-data file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#user-data.yml
#cloud-config
ssh_pwauth: false
disable_root: true
users:
  - name: user
    gecos: user
    groups: users, sudo
    shell: /bin/bash
    ssh_authorized_keys:
        - ssh-ed25519 AAAA.......
    sudo: ALL=(ALL) NOPASSWD:ALL
    lock_passwd: true
package_update: true
package_upgrade: true

In my case, I chose to use cloud-init to:

  • Disable SSH password authentication as well as the root account.
  • Create a user with an authorised SSH key, passwordless sudo access and a locked password.
  • Automatically update and upgrade the packages.

Once applied at the machine’s first boot, the machine is ready to use, and also ready to be provisioned with a tool such as Ansible. That was my goal, since I wanted to run my Ansible playbooks over an SSH key connection once the virtual machine was deployed, without copying my key by hand or having to log in with a password first.

Terraform

To deploy my virtual machine from my template and the cloud-init file, I chose to use Terraform. Terraform is an infrastructure as code tool, meaning a tool that lets you define your infrastructure through declarative configuration files in HCL (HashiCorp Configuration Language). To do this, Terraform relies on providers that handle communication with the underlying infrastructure. This method lets you manage infrastructure like code, which makes versioning and reproducible deployments easier.

These plugins expose resource types (create/modify resources) and data sources (read existing information) that let Terraform manage the infrastructure behind the plugin. Providers use the vendor’s SDK or API to carry out all the actions described in the Terraform configuration.

HashiCorp Terraform logo

Terraform logo

For my homelab, I chose the bpg provider, a provider built by a community member that manages the whole Proxmox environment (resource pools, users, VMs, ISOs).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#providers.tf
provider "proxmox" {
  endpoint  = var.endpoint
  api_token = var.api_token
  insecure  = true
  ssh {
    agent       = false
    username    = "root"
    private_key = var.private_key
  }
}

To deploy the virtual machine from the template, with cloud-init for provisioning, I used the proxmox_virtual_environment_vm resource, which creates and manages a virtual machine with all the characteristics you pass to it.

Example Terraform resource for a virtual machine in Proxmox:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#virtual_machine.tf
resource "proxmox_virtual_environment_vm" "example_vm" {
    name                = "example"
    description         = "example VM"
    tags                = ["debian12", "terraform"]
    node_name           = "pve"
    vm_id               = 101
    pool_id             = "example"
    reboot_after_update = false
  
    agent {
      enabled = true
    }
  
    cpu {
      cores   = 2
      sockets = 1
      type    = "x86-64-v2-AES"
    }
  
    memory {
      dedicated = 4096
      floating  = 4096
    }

  initialization {
    user_data_file_id = proxmox_virtual_environment_file.user_data_cloud_config.id
    dns {
      servers = ["9.9.9.9", "1.1.1.1"]
    }
    ip_config {
      ipv4 {
        address = "192.168.1.101/24"
        gateway = "192.168.1.1"
      }
    }
  }

  clone {
    vm_id        = 1101
    datastore_id = "local-lvm"
    full         = true
    node_name    = "pve"
  }
}

In my case, the parameters I care about are:

  • initialization: the cloud-init configuration, where I set the DNS configuration and the IP address of my machine, and give the ID of my cloud-init file, also managed with Terraform through the proxmox_virtual_environment_file resource
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
initialization {
  user_data_file_id = proxmox_virtual_environment_file.user_data_cloud_config.id
  dns {
    servers = ["9.9.9.9", "1.1.1.1"]
  }
  ip_config {
    ipv4 {
      address = "192.168.1.101/24"
      gateway = "192.168.1.1"
    }
  }
}
  • clone: the clone configuration, where I give the information about my template in Proxmox.
1
2
3
4
5
6
clone {
  vm_id        = 1101
  datastore_id = "local-lvm"
  full         = true
  node_name    = "pve"
}

I can then run my Infrastructure as Code project with the new resource added, and Terraform clones the template and gives it the cloud-init configuration to provision the virtual machine.

The result is a VM with a standard configuration that I defined to match my needs, ready to be provisioned with a tool such as Ansible.

Sequence diagram

Sequence diagram: Packer creates the template from the ISO in Proxmox, then Terraform clones the template and cloud-init provisions the VM

Sequence diagram

Conclusion

Setting up an automated process to create and deploy virtual machines on Proxmox with Packer, Cloud-Init and Terraform has made my homelab more efficient, reproducible and flexible.

This workflow standardises the environments, reduces the risk of manual errors and makes provisioning the machines with Ansible or other automation tools easier. With this solution, I can now quickly deploy ready-to-use virtual machines, while adapting their configuration to my needs.

Built with Hugo
Theme Stack designed by Jimmy