Automating VM Creation on VergeOS with Packer

TL;DR

I successfully automated Debian 13 VM creation on VergeOS using HashiCorp Packer. The key insight: use Debian cloud images (qcow2). This approach is fast, reliable, and avoids the complexities of ISO installation or template cloning. Build time: ~2 minutes. Code on GitHub.

The Goal

I wanted to automate VM creation on my VergeOS homelab using HashiCorp Packer. The goal was simple: run one command and get a fully provisioned Debian 13 VM ready to use.

Modern infrastructure relies on "Golden Images"β€”pre-baked, standardized VM images that are ready to deploy. Instead of manually installing an OS from an ISO every time, we want to script the creation of these images so they are reproducible and version-controlled.

Prerequisites

Before diving in, there's one critical requirement: VergeOS API v4. The Packer plugin uses API v4 endpoints (/api/v4/vms, /api/v4/files, etc.), so make sure your VergeOS installation supports this API version.

The Solution: Cloud Images

After exploring various methods, the clear winner is using Cloud Images.

Why Cloud Images?

Cloud images (like Debian's generic cloud image in qcow2 format) are pre-built, minimal OS images specifically designed for cloud and virtualization platforms. They are superior for automation because they:

  1. Boot Immediately: No installation wizard to click through.
  2. Cloud-Init Ready: They are designed to be configured on first boot via cloud-init.
  3. Automation Friendly: They leverage standard tools like cloud-init to set hostnames, users, and SSH keys.

VergeOS supports importing these images directly, and the Packer plugin works seamlessly with this workflow.

The Working Configuration

Here is the complete configuration to build a Debian 13 image.

File: build.pkr.hcl

packer {
  required_plugins {
    vergeio = {
      version = ">= 0.1.1"
      source  = "github.com/verge-io/vergeio"
    }
  }
}

variable "vergeio_password" {
  type      = string
  sensitive = true
}

variable "network_name" {
  type    = string
  default = "External"
}

# Discover network by name instead of hardcoding ID
data "vergeio-networks" "target_network" {
  vergeio_endpoint = "192.168.1.111"
  vergeio_username = "admin"
  vergeio_password = var.vergeio_password
  vergeio_insecure = true
  vergeio_port     = 443
  filter_name      = var.network_name
}

source "vergeio" "debian13_cloud" {
  # VergeOS Connection
  vergeio_endpoint = "192.168.1.111"
  vergeio_username = "admin"
  vergeio_password = var.vergeio_password
  vergeio_insecure = true
  vergeio_port     = 443
  
  # VM Configuration
  name        = "packer-debian13-${formatdate("YYYYMMDD-hhmmss", timestamp())}"
  description = "Debian 13 from cloud image"
  os_family   = "linux"
  cpu_cores   = 2
  ram         = 2048
  power_state = true   # Auto power-on after creation
  guest_agent = true   # Enable guest agent for IP discovery

  # Import from Debian 13 cloud image (qcow2)
  vm_disks {
    name           = "System Disk"
    disksize       = 20
    interface      = "virtio-scsi" # Standard for performance
    preferred_tier = 1
    media          = "import"
    media_source   = 72  # ID of: debian-13-generic-amd64.qcow2
  }

  # Network discovery via data source (better than hardcoding IDs)
  vm_nics {
    name             = "primary_nic"
    vnet             = data.vergeio-networks.target_network.networks[0].id
    interface        = "virtio"
    assign_ipaddress = true
    enabled          = true
  }

  # Cloud-init configuration
  cloud_init_data_source = "nocloud"
  cloud_init_files {
    name  = "user-data"
    files = ["cloud-init/cloud-user-data.yml"]
  }

  # Cloud-init meta-data
  cloud_init_files {
    name  = "meta-data"
    files = ["cloud-init/meta-data.yml"]
  }

  # SSH connectivity
  communicator = "ssh"
  ssh_username = "debian"  # Default user for Debian cloud images
  ssh_password = "packer123" # Set via cloud-init
  ssh_timeout  = "20m"

  # Timeouts and shutdown
  power_on_timeout = "5m"
  shutdown_command = "sudo shutdown -P now"
  shutdown_timeout = "5m"
}

build {
  sources = ["source.vergeio.debian13_cloud"]

  # Verify connectivity and system state
  provisioner "shell" {
    inline = [
      "echo 'SSH connection successful!'",
      "whoami",
      "hostname",
      "ip addr show"
    ]
  }

  # Install and configure guest agent
  provisioner "shell" {
    inline = [
      "echo 'Installing qemu-guest-agent...'",
      "sudo apt-get update",
      "sudo DEBIAN_FRONTEND=noninteractive apt-get install -y qemu-guest-agent",
      "sudo systemctl enable qemu-guest-agent",
      "echo 'VM provisioning completed!'"
    ]
  }
}

Cloud-Init Configuration

To make the cloud image accessible, we use a simple cloud-user-data.yml to set the password and enable SSH.

#cloud-config
hostname: debian13-packer

ssh_pwauth: true
users:
  - name: debian
    sudo: ALL=(ALL) NOPASSWD:ALL
    passwd: $6$rounds=4096$...  # hashed "packer123" password

package_update: true
package_upgrade: true

runcmd:
  - echo "debian:packer123" | chpasswd

File: cloud-init/meta-data.yml

instance-id: packer-debian-${uuidv4()}
local-hostname: debian13-packer

The meta-data.yml file provides instance-specific information that cloud-init uses to configure the VM's identity.

The Results

Running packer build automates the entire lifecycle:

$ packer build -var-file="variables.pkrvars.hcl" build.pkr.hcl

==> vergeio.debian13_cloud: VM created successfully...
==> vergeio.debian13_cloud: Creating disk 'System Disk'...
==> vergeio.debian13_cloud: Waiting for disk import...
==> vergeio.debian13_cloud: Powering on VM...
==> vergeio.debian13_cloud: Connected to SSH!
==> vergeio.debian13_cloud: Provisioning with shell script...
==> vergeio.debian13_cloud: Gracefully shutting down VM...
Build 'vergeio.debian13_cloud' finished after 1 minute 45 seconds.

Best Practices

Based on real-world usage, here are some key recommendations:

1. Use Network Data Sources

Instead of hardcoding network IDs:

# ❌ Hardcoded - breaks when moving between environments
vm_nics {
  vnet = 17
}

# βœ… Dynamic discovery - portable across environments
data "vergeio-networks" "target_network" {
  filter_name = "External"
}

vm_nics {
  vnet = data.vergeio-networks.target_network.networks[0].id
}

Benefits:

  • Configuration is portable between VergeOS environments
  • No need to manually look up network IDs
  • Self-documenting (network name is human-readable)

2. Always Use media = "import"

The plugin automatically waits for disk imports to complete, preventing "Cannot power on VM while drives importing" errors.

3. Leverage Cloud-Init Files

Store cloud-init configuration in separate files for better organization:

cloud_init_files {
  name  = "user-data"
  files = ["cloud-init/cloud-user-data.yml"]
}

This keeps your Packer template clean and makes cloud-init configs reusable.

4. Use Timestamps in VM Names

Avoid naming conflicts by adding timestamps to VM names:

name = "packer-debian13-${formatdate(\"YYYYMMDD-hhmmss\", timestamp())}"

Benefits:

  • No naming conflicts when running multiple builds
  • Easy to identify when an image was built
  • No need to manually delete old VMs between builds
  • Maintains build history for comparison

Example output: packer-debian13-20251211-133045

Conclusion

Automating VM creation on VergeOS with Packer is efficient and reliable when you leverage Cloud Images.

By skipping ISO installations and template cloning in favor of standardized qcow2 images, we get a fast, maintainable workflow that integrates perfectly with modern DevOps practices. The VergeOS Packer plugin handles the heavy lifting of importing the disk and creating the VM, while standard tools like cloud-init handle the Guest OS configuration.

It just works! πŸŽ‰

Resources

Read more