Automation intermediate ⏱ 45 minutes

Packer: Automated VM Template Creation

Build VM templates automatically. Cloud-init, customization, and multi-platform support.

By homelabbing.io • 2025-12-17
#packer#templates#vm#automation#cloud-init

Packer automates the creation of machine images and VM templates, eliminating manual template building. This guide covers comprehensive Packer usage for Proxmox, including Ubuntu, Rocky Linux, and Windows templates with cloud-init, security hardening, and CI/CD integration for production-ready homelab automation.

Understanding Packer

What is Packer?

Packer is HashiCorp’s open-source tool for creating identical machine images for multiple platforms from a single source configuration.

Key Benefits:

  • Automated: Build templates without manual clicking
  • Reproducible: Same configuration produces identical images
  • Version Controlled: Track template changes in Git
  • Multi-Platform: Build for Proxmox, VMware, AWS, Azure, etc.
  • Fast: Parallel builds across platforms
  • Testable: Validate before deploying

Packer vs Manual Templates

Manual Template Creation:

  • Time-consuming (30-60 minutes per template)
  • Error-prone (missed steps, inconsistencies)
  • Difficult to reproduce
  • No version control
  • Hard to test changes

Packer Automation:

  • Builds in 10-20 minutes
  • Consistent and repeatable
  • Version controlled with Git
  • Easy to test and validate
  • Update with a single command

Part 1: Installation and Setup

Prerequisites

# Update system
sudo apt update && sudo apt upgrade -y

# Install dependencies
sudo apt install -y wget unzip git curl mkisofs

Install Packer

Method 1: Official HashiCorp Repository (Recommended)

# Add HashiCorp GPG key
wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg

# Add repository
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list

# Install Packer
sudo apt update && sudo apt install packer

# Verify installation
packer version

Method 2: Binary Installation

# Download latest version
cd /tmp
wget https://releases.hashicorp.com/packer/1.10.0/packer_1.10.0_linux_amd64.zip

# Extract and install
unzip packer_1.10.0_linux_amd64.zip
sudo mv packer /usr/local/bin/

# Verify
packer version

Enable Autocomplete

# Bash
packer -autocomplete-install
source ~/.bashrc

# Zsh
packer -autocomplete-install
source ~/.zshrc

Install Packer Plugins

# Initialize plugins (in your project directory)
packer init .

# Or install specific plugins
packer plugins install github.com/hashicorp/proxmox

Part 2: Proxmox Configuration

Proxmox API User Setup

# SSH into Proxmox host
ssh root@YOUR-PROXMOX-IP

# Create Packer user
pveum user add packer@pve --password YOUR-SECURE-PASSWORD

# Create Packer role
pveum role add PackerRole -privs "VM.Allocate 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.Monitor VM.Audit VM.PowerMgmt Datastore.AllocateSpace Datastore.Audit Sys.Audit Sys.Modify SDN.Use"

# Assign role to user
pveum aclmod / -user packer@pve -role PackerRole

# Verify
pveum user list

Upload ISO Files

# Download Ubuntu Server ISO
cd /var/lib/vz/template/iso
wget https://releases.ubuntu.com/22.04/ubuntu-22.04.3-live-server-amd64.iso

# Download Rocky Linux ISO
wget https://download.rockylinux.org/pub/rocky/9/isos/x86_64/Rocky-9.3-x86_64-minimal.iso

# Download Windows Server (from Microsoft)
# Upload via Proxmox web UI or wget

# Verify
ls -lh /var/lib/vz/template/iso/

Project Structure

# Create Packer project directory
mkdir -p ~/packer-templates
cd ~/packer-templates

# Recommended structure:
mkdir -p {ubuntu,rocky,windows,scripts,http}

# Directory structure:
# packer-templates/
# ├── ubuntu/
# │   ├── ubuntu-22.04.pkr.hcl       # Main Packer config
# │   ├── variables.pkr.hcl          # Variables
# │   ├── http/
# │   │   └── user-data              # Cloud-init config
# │   └── scripts/
# │       ├── setup.sh               # Post-install script
# │       └── cleanup.sh             # Cleanup script
# ├── rocky/
# │   └── ...
# ├── windows/
# │   └── ...
# └── common/
#     ├── scripts/
#     └── files/

Part 3: Ubuntu Template with Cloud-Init

ubuntu/variables.pkr.hcl

variable "proxmox_url" {
  type    = string
  default = "https://YOUR-PROXMOX-IP:8006/api2/json"
}

variable "proxmox_username" {
  type    = string
  default = "packer@pve"
}

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

variable "proxmox_node" {
  type    = string
  default = "pve"
}

variable "iso_file" {
  type    = string
  default = "local:iso/ubuntu-22.04.3-live-server-amd64.iso"
}

variable "iso_checksum" {
  type    = string
  default = "sha256:a4acfda10b18da50e2ec50ccaf860d7f20b389df8765611142305c0e911d16fd"
}

variable "vm_name" {
  type    = string
  default = "ubuntu-22.04-template"
}

variable "vm_id" {
  type    = number
  default = 9000
}

variable "cores" {
  type    = number
  default = 2
}

variable "memory" {
  type    = number
  default = 2048
}

variable "disk_size" {
  type    = string
  default = "32G"
}

variable "storage_pool" {
  type    = string
  default = "local-lvm"
}

variable "ssh_username" {
  type    = string
  default = "ubuntu"
}

variable "ssh_password" {
  type      = string
  sensitive = true
  default   = "ubuntu"
}

ubuntu/ubuntu-22.04.pkr.hcl

packer {
  required_version = ">= 1.10.0"

  required_plugins {
    proxmox = {
      version = ">= 1.1.8"
      source  = "github.com/hashicorp/proxmox"
    }
  }
}

source "proxmox-iso" "ubuntu-22-04" {
  # Proxmox connection
  proxmox_url              = var.proxmox_url
  username                 = var.proxmox_username
  password                 = var.proxmox_password
  insecure_skip_tls_verify = true
  node                     = var.proxmox_node

  # VM settings
  vm_id                = var.vm_id
  vm_name              = var.vm_name
  template_description = "Ubuntu 22.04 LTS template built with Packer on ${timestamp()}"

  # ISO configuration
  iso_file     = var.iso_file
  iso_checksum = var.iso_checksum
  unmount_iso  = true

  # VM hardware
  cores     = var.cores
  memory    = var.memory
  sockets   = 1
  cpu_type  = "host"
  os        = "l26"  # Linux kernel 2.6+
  bios      = "ovmf"

  # EFI configuration
  efi_config {
    efi_storage_pool  = var.storage_pool
    efi_type          = "4m"
    pre_enrolled_keys = true
  }

  # Disks
  scsi_controller = "virtio-scsi-single"
  disks {
    disk_size         = var.disk_size
    storage_pool      = var.storage_pool
    type              = "scsi"
    discard           = true
    io_thread         = true
    ssd               = true
  }

  # Network
  network_adapters {
    model    = "virtio"
    bridge   = "vmbr0"
    firewall = false
  }

  # Cloud-init
  cloud_init              = true
  cloud_init_storage_pool = var.storage_pool

  # Boot configuration
  boot_wait    = "5s"
  boot_command = [
    "<esc><wait>",
    "e<wait>",
    "<down><down><down><end>",
    "<bs><bs><bs><bs><wait>",
    "autoinstall ds=nocloud-net\;s=http://{{ .HTTPIP }}:{{ .HTTPPort }}/ ---<wait>",
    "<f10><wait>"
  ]

  # HTTP server for autoinstall
  http_directory = "ubuntu/http"
  http_port_min  = 8802
  http_port_max  = 8802

  # SSH configuration
  ssh_username         = var.ssh_username
  ssh_password         = var.ssh_password
  ssh_timeout          = "20m"
  ssh_handshake_attempts = 50

  # QEMU agent
  qemu_agent = true

  # Optimization
  machine = "q35"
}

build {
  sources = ["source.proxmox-iso.ubuntu-22-04"]

  # Wait for cloud-init to complete
  provisioner "shell" {
    inline = [
      "while [ ! -f /var/lib/cloud/instance/boot-finished ]; do echo 'Waiting for cloud-init...'; sleep 1; done"
    ]
  }

  # Update system
  provisioner "shell" {
    inline = [
      "sudo apt-get update",
      "sudo apt-get upgrade -y",
      "sudo apt-get dist-upgrade -y"
    ]
  }

  # Install essential packages
  provisioner "shell" {
    inline = [
      "sudo apt-get install -y qemu-guest-agent cloud-init cloud-utils-growpart",
      "sudo apt-get install -y vim curl wget git htop net-tools",
      "sudo systemctl enable qemu-guest-agent",
      "sudo systemctl start qemu-guest-agent"
    ]
  }

  # Run custom setup script
  provisioner "shell" {
    scripts = [
      "ubuntu/scripts/setup.sh",
      "ubuntu/scripts/cleanup.sh"
    ]
  }

  # Final cleanup
  provisioner "shell" {
    inline = [
      "sudo apt-get autoremove -y",
      "sudo apt-get clean",
      "sudo cloud-init clean",
      "sudo rm -rf /tmp/*",
      "sudo rm -rf /var/tmp/*",
      "sudo truncate -s 0 /etc/machine-id",
      "sudo rm /var/lib/dbus/machine-id",
      "sudo ln -s /etc/machine-id /var/lib/dbus/machine-id",
      "sudo sync"
    ]
  }
}

ubuntu/http/user-data

#cloud-config
autoinstall:
  version: 1
  locale: en_US.UTF-8
  keyboard:
    layout: us
  network:
    network:
      version: 2
      ethernets:
        ens18:
          dhcp4: true
  storage:
    layout:
      name: direct
    config:
      - type: disk
        id: disk0
        match:
          size: largest
      - type: partition
        id: boot-partition
        device: disk0
        size: 1G
        flag: boot
      - type: partition
        id: root-partition
        device: disk0
        size: -1
  identity:
    hostname: ubuntu-template
    username: ubuntu
    password: "$6$rounds=4096$saltsaltsal$YhQjJwxb7XN3P6cP5V.p/l2YZJq3J1gNPl6c8bV5X7M0GRqNf1x/g4sY3Y1Kx5M1cP8H6Z2X3Y4Z5A6B7C8D9E"
    # Password: ubuntu (change in production!)
  ssh:
    install-server: true
    allow-pw: true
  packages:
    - qemu-guest-agent
    - cloud-init
    - vim
    - curl
    - wget
  user-data:
    disable_root: false
    timezone: UTC
  late-commands:
    - echo 'ubuntu ALL=(ALL) NOPASSWD:ALL' > /target/etc/sudoers.d/ubuntu
    - chmod 440 /target/etc/sudoers.d/ubuntu
    - curtin in-target --target=/target -- systemctl enable qemu-guest-agent

ubuntu/http/meta-data

instance-id: ubuntu-template
local-hostname: ubuntu-template

ubuntu/scripts/setup.sh

#!/bin/bash
set -e

echo "=== Running setup script ==="

# Update system
echo "Updating system packages..."
apt-get update
apt-get upgrade -y

# Install useful packages
echo "Installing additional packages..."
apt-get install -y \
  software-properties-common \
  apt-transport-https \
  ca-certificates \
  gnupg \
  lsb-release \
  python3-pip \
  build-essential \
  dkms

# Configure cloud-init
echo "Configuring cloud-init..."
cat > /etc/cloud/cloud.cfg.d/99-packer.cfg <<EOF
datasource_list: [ ConfigDrive, NoCloud, None ]
datasource:
  NoCloud:
    seedfrom: /dev/sr0
EOF

# Disable swap (optional, for Kubernetes compatibility)
# swapoff -a
# sed -i '/swap/d' /etc/fstab

# Security hardening
echo "Applying security hardening..."

# Disable IPv6 if not needed
cat >> /etc/sysctl.conf <<EOF
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1
net.ipv6.conf.lo.disable_ipv6 = 1
EOF

# Configure automatic security updates
apt-get install -y unattended-upgrades
cat > /etc/apt/apt.conf.d/50unattended-upgrades <<EOF
Unattended-Upgrade::Allowed-Origins {
    "\${distro_id}:\${distro_codename}-security";
};
Unattended-Upgrade::Automatic-Reboot "false";
EOF

# SSH hardening
sed -i 's/#PermitRootLogin yes/PermitRootLogin no/' /etc/ssh/sshd_config
sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config
sed -i 's/X11Forwarding yes/X11Forwarding no/' /etc/ssh/sshd_config

# Configure timezone
timedatectl set-timezone UTC

# Enable and configure firewall (UFW)
apt-get install -y ufw
ufw --force enable
ufw default deny incoming
ufw default allow outgoing
ufw allow ssh

echo "=== Setup script completed ==="

ubuntu/scripts/cleanup.sh

#!/bin/bash
set -e

echo "=== Running cleanup script ==="

# Clean apt cache
apt-get autoremove -y
apt-get autoclean
apt-get clean

# Clean logs
find /var/log -type f -exec truncate -s 0 {} \;

# Clean temporary files
rm -rf /tmp/*
rm -rf /var/tmp/*

# Clean SSH keys (will be regenerated by cloud-init)
rm -f /etc/ssh/ssh_host_*

# Clean machine ID (will be regenerated)
truncate -s 0 /etc/machine-id
rm -f /var/lib/dbus/machine-id
ln -s /etc/machine-id /var/lib/dbus/machine-id

# Clean cloud-init
cloud-init clean --logs --seed

# Clean shell history
history -c
cat /dev/null > ~/.bash_history

# Zero out free space (optional, makes image smaller)
# WARNING: This takes time
# dd if=/dev/zero of=/EMPTY bs=1M || true
# rm -f /EMPTY

# Sync filesystem
sync

echo "=== Cleanup script completed ==="

Part 4: Building Templates

Build Ubuntu Template

# Navigate to project directory
cd ~/packer-templates

# Validate configuration
packer validate -var-file=ubuntu/variables.pkr.hcl ubuntu/

# Format code
packer fmt -recursive .

# Build template
packer build -var-file=ubuntu/variables.pkr.hcl ubuntu/

# Build with specific variables
packer build \
  -var 'proxmox_password=your-password' \
  -var 'vm_name=ubuntu-22.04-custom' \
  ubuntu/

# Build with variable file
packer build -var-file=secrets.pkrvars.hcl ubuntu/

Monitor Build Progress

# Packer will output progress:
# ==> proxmox-iso: Creating VM...
# ==> proxmox-iso: Starting VM...
# ==> proxmox-iso: Waiting for SSH to become available...
# ==> proxmox-iso: Connected to SSH!
# ==> proxmox-iso: Provisioning with shell script...
# ==> proxmox-iso: Stopping VM...
# ==> proxmox-iso: Converting VM to template...

# Watch Proxmox task log
ssh root@proxmox-host
tail -f /var/log/pve/tasks/active

Part 5: Rocky Linux Template

rocky/rocky-9.pkr.hcl

source "proxmox-iso" "rocky-9" {
  proxmox_url              = var.proxmox_url
  username                 = var.proxmox_username
  password                 = var.proxmox_password
  insecure_skip_tls_verify = true
  node                     = var.proxmox_node

  vm_id   = 9001
  vm_name = "rocky-9-template"

  iso_file     = "local:iso/Rocky-9.3-x86_64-minimal.iso"
  iso_checksum = "sha256:f4c5a89fef4f5e7e7f9bc9bb6f5c8c5d8f5e7e7e7e7e7e7e7e7e7e7e7e7e7e7e"
  unmount_iso  = true

  cores    = 2
  memory   = 2048
  sockets  = 1
  cpu_type = "host"
  os       = "l26"
  bios     = "ovmf"

  efi_config {
    efi_storage_pool  = var.storage_pool
    efi_type          = "4m"
    pre_enrolled_keys = true
  }

  scsi_controller = "virtio-scsi-single"
  disks {
    disk_size    = "32G"
    storage_pool = var.storage_pool
    type         = "scsi"
    discard      = true
  }

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

  cloud_init              = true
  cloud_init_storage_pool = var.storage_pool

  boot_wait = "10s"
  boot_command = [
    "<tab> text inst.ks=http://{{ .HTTPIP }}:{{ .HTTPPort }}/ks.cfg<enter><wait>"
  ]

  http_directory = "rocky/http"

  ssh_username = "root"
  ssh_password = "rocky"
  ssh_timeout  = "20m"

  qemu_agent = true
}

build {
  sources = ["source.proxmox-iso.rocky-9"]

  provisioner "shell" {
    inline = [
      "dnf update -y",
      "dnf install -y qemu-guest-agent cloud-init cloud-utils-growpart",
      "dnf install -y vim curl wget git htop",
      "systemctl enable qemu-guest-agent",
      "systemctl start qemu-guest-agent"
    ]
  }

  provisioner "shell" {
    scripts = ["rocky/scripts/cleanup.sh"]
  }
}

rocky/http/ks.cfg (Kickstart File)

#version=RHEL9
# System authorization information
auth --enableshadow --passalgo=sha512

# Use network installation
url --url="http://download.rockylinux.org/pub/rocky/9/BaseOS/x86_64/os/"

# Keyboard layouts
keyboard --vckeymap=us --xlayouts='us'

# System language
lang en_US.UTF-8

# Network information
network --bootproto=dhcp --device=ens18 --onboot=yes --ipv6=auto --activate
network --hostname=rocky-template

# Root password (changeme)
rootpw --plaintext rocky

# SELinux configuration
selinux --enforcing

# System timezone
timezone UTC --isUtc

# System bootloader configuration
bootloader --location=mbr --boot-drive=sda

# Partition clearing information
clearpart --all --initlabel

# Disk partitioning
autopart --type=lvm

# Reboot after installation
reboot

%packages --ignoremissing
@^minimal-environment
qemu-guest-agent
cloud-init
cloud-utils-growpart
vim
curl
wget
git
-iwl*firmware
-ivh*firmware
%end

%post --log=/root/ks-post.log
# Enable qemu-guest-agent
systemctl enable qemu-guest-agent

# Configure cloud-init
systemctl enable cloud-init

# Update system
dnf update -y

# Clean up
dnf clean all
%end

Part 6: Windows Server Template

windows/windows-server-2022.pkr.hcl

source "proxmox-iso" "windows-server-2022" {
  proxmox_url              = var.proxmox_url
  username                 = var.proxmox_username
  password                 = var.proxmox_password
  insecure_skip_tls_verify = true
  node                     = var.proxmox_node

  vm_id   = 9002
  vm_name = "windows-server-2022-template"

  iso_file     = "local:iso/windows-server-2022.iso"
  iso_checksum = "sha256:..."
  unmount_iso  = true

  # Additional drivers ISO (VirtIO)
  additional_iso_files {
    iso_file     = "local:iso/virtio-win-0.1.240.iso"
    unmount      = true
    cd_label     = "VIRTIO"
  }

  cores    = 4
  memory   = 4096
  sockets  = 1
  cpu_type = "host"
  os       = "win11"
  bios     = "ovmf"

  efi_config {
    efi_storage_pool  = var.storage_pool
    efi_type          = "4m"
    pre_enrolled_keys = true
  }

  scsi_controller = "virtio-scsi-single"
  disks {
    disk_size    = "60G"
    storage_pool = var.storage_pool
    type         = "scsi"
    cache_mode   = "writethrough"
  }

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

  # Windows-specific settings
  communicator   = "winrm"
  winrm_username = "Administrator"
  winrm_password = "P@ssw0rd123!"
  winrm_timeout  = "4h"
  winrm_use_ssl  = false
  winrm_insecure = true

  # Autounattend.xml for automated installation
  cd_files = [
    "windows/scripts/Autounattend.xml",
    "windows/scripts/setup.ps1"
  ]
  cd_label = "SETUP"

  qemu_agent = true
}

build {
  sources = ["source.proxmox-iso.windows-server-2022"]

  # Wait for Windows to be ready
  provisioner "windows-restart" {
    restart_timeout = "30m"
  }

  # Install updates
  provisioner "powershell" {
    scripts = [
      "windows/scripts/install-updates.ps1",
      "windows/scripts/install-qemu-agent.ps1"
    ]
  }

  # Sysprep and cleanup
  provisioner "powershell" {
    scripts = ["windows/scripts/sysprep.ps1"]
  }
}

windows/scripts/Autounattend.xml

<?xml version="1.0" encoding="utf-8"?>
<unattend xmlns="urn:schemas-microsoft-com:unattend">
    <settings pass="windowsPE">
        <component name="Microsoft-Windows-International-Core-WinPE" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
            <SetupUILanguage>
                <UILanguage>en-US</UILanguage>
            </SetupUILanguage>
            <InputLocale>en-US</InputLocale>
            <SystemLocale>en-US</SystemLocale>
            <UILanguage>en-US</UILanguage>
            <UserLocale>en-US</UserLocale>
        </component>
        <component name="Microsoft-Windows-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
            <DiskConfiguration>
                <Disk wcm:action="add">
                    <CreatePartitions>
                        <CreatePartition wcm:action="add">
                            <Order>1</Order>
                            <Type>EFI</Type>
                            <Size>100</Size>
                        </CreatePartition>
                        <CreatePartition wcm:action="add">
                            <Order>2</Order>
                            <Type>MSR</Type>
                            <Size>128</Size>
                        </CreatePartition>
                        <CreatePartition wcm:action="add">
                            <Order>3</Order>
                            <Type>Primary</Type>
                            <Extend>true</Extend>
                        </CreatePartition>
                    </CreatePartitions>
                    <DiskID>0</DiskID>
                    <WillWipeDisk>true</WillWipeDisk>
                </Disk>
            </DiskConfiguration>
            <UserData>
                <ProductKey>
                    <Key>YOUR-WINDOWS-KEY</Key>
                </ProductKey>
                <AcceptEula>true</AcceptEula>
            </UserData>
        </component>
    </settings>
    <settings pass="specialize">
        <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
            <ComputerName>WIN-TEMPLATE</ComputerName>
        </component>
    </settings>
    <settings pass="oobeSystem">
        <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
            <OOBE>
                <HideEULAPage>true</HideEULAPage>
                <ProtectYourPC>3</ProtectYourPC>
                <NetworkLocation>Work</NetworkLocation>
                <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
            </OOBE>
            <UserAccounts>
                <AdministratorPassword>
                    <Value>P@ssw0rd123!</Value>
                    <PlainText>true</PlainText>
                </AdministratorPassword>
            </UserAccounts>
            <AutoLogon>
                <Enabled>true</Enabled>
                <Username>Administrator</Username>
                <Password>
                    <Value>P@ssw0rd123!</Value>
                    <PlainText>true</PlainText>
                </Password>
                <LogonCount>1</LogonCount>
            </AutoLogon>
            <FirstLogonCommands>
                <SynchronousCommand wcm:action="add">
                    <Order>1</Order>
                    <CommandLine>powershell -ExecutionPolicy Bypass -File E:\setup.ps1</CommandLine>
                </SynchronousCommand>
            </FirstLogonCommands>
        </component>
    </settings>
</unattend>

Part 7: Advanced Features

Multi-Platform Builds

# Build for multiple platforms simultaneously
packer {
  required_plugins {
    proxmox = {
      version = ">= 1.1.8"
      source  = "github.com/hashicorp/proxmox"
    }
    vmware = {
      version = ">= 1.0.0"
      source  = "github.com/hashicorp/vmware"
    }
  }
}

# Multiple sources
source "proxmox-iso" "ubuntu" {
  # Proxmox config
}

source "vmware-iso" "ubuntu" {
  # VMware config
}

# Build both
build {
  sources = [
    "source.proxmox-iso.ubuntu",
    "source.vmware-iso.ubuntu"
  ]

  # Shared provisioners
  provisioner "shell" {
    scripts = ["scripts/setup.sh"]
  }
}

Post-Processors for Image Export

build {
  sources = ["source.proxmox-iso.ubuntu-22-04"]

  # ... provisioners ...

  # Create OVA for distribution
  post-processor "shell-local" {
    inline = [
      "qm exportvm ${source.vm_id} /tmp/ubuntu-22.04.ova --format=ova"
    ]
  }

  # Upload to S3
  post-processor "shell-local" {
    inline = [
      "aws s3 cp /tmp/ubuntu-22.04.ova s3://templates/ubuntu-22.04-${timestamp()}.ova"
    ]
  }

  # Create manifest
  post-processor "manifest" {
    output     = "manifest.json"
    strip_path = true
  }
}

Template Versioning

locals {
  timestamp = regex_replace(timestamp(), "[- TZ:]", "")
  version   = "1.0.${local.timestamp}"
}

source "proxmox-iso" "ubuntu" {
  vm_name              = "ubuntu-22.04-v${local.version}"
  template_description = "Ubuntu 22.04 LTS - Version ${local.version}"
  # ... rest of config ...
}

Conditional Provisioning

variable "install_docker" {
  type    = bool
  default = false
}

build {
  sources = ["source.proxmox-iso.ubuntu-22-04"]

  # Only run if variable is true
  provisioner "shell" {
    only = var.install_docker ? ["proxmox-iso.ubuntu-22-04"] : []
    scripts = ["scripts/install-docker.sh"]
  }
}

Part 8: CI/CD Integration

GitHub Actions Workflow

Create .github/workflows/packer.yml:

name: Build VM Templates

on:
  push:
    branches: [ main ]
    paths:
      - 'ubuntu/**'
      - 'rocky/**'
  schedule:
    - cron: '0 2 * * 0'  # Weekly on Sunday at 2 AM
  workflow_dispatch:

jobs:
  build-ubuntu:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout code
      uses: actions/checkout@v3

    - name: Setup Packer
      uses: hashicorp/setup-packer@main
      with:
        version: latest

    - name: Initialize Packer
      run: packer init ubuntu/

    - name: Validate Packer template
      run: packer validate ubuntu/

    - name: Build Ubuntu template
      run: |
        packer build \
          -var "proxmox_password=${{ secrets.PROXMOX_PASSWORD }}" \
          -var "proxmox_url=${{ secrets.PROXMOX_URL }}" \
          ubuntu/

    - name: Notify on success
      if: success()
      run: |
        curl -X POST ${{ secrets.SLACK_WEBHOOK }} \
          -H 'Content-Type: application/json' \
          -d '{"text":"Ubuntu template build completed successfully"}'

    - name: Notify on failure
      if: failure()
      run: |
        curl -X POST ${{ secrets.SLACK_WEBHOOK }} \
          -H 'Content-Type: application/json' \
          -d '{"text":"Ubuntu template build FAILED"}'

GitLab CI/CD Pipeline

Create .gitlab-ci.yml:

stages:
  - validate
  - build
  - test

variables:
  PACKER_VERSION: "1.10.0"

before_script:
  - apt-get update
  - apt-get install -y wget unzip
  - wget https://releases.hashicorp.com/packer/${PACKER_VERSION}/packer_${PACKER_VERSION}_linux_amd64.zip
  - unzip packer_${PACKER_VERSION}_linux_amd64.zip
  - mv packer /usr/local/bin/
  - packer version

validate:
  stage: validate
  script:
    - packer validate ubuntu/
    - packer validate rocky/

build-ubuntu:
  stage: build
  script:
    - packer build -var "proxmox_password=${PROXMOX_PASSWORD}" ubuntu/
  only:
    - main
  when: manual

build-rocky:
  stage: build
  script:
    - packer build -var "proxmox_password=${PROXMOX_PASSWORD}" rocky/
  only:
    - main
  when: manual

Jenkins Pipeline

Create Jenkinsfile:

pipeline {
    agent any

    parameters {
        choice(name: 'TEMPLATE', choices: ['ubuntu', 'rocky', 'all'], description: 'Which template to build')
        booleanParam(name: 'FORCE_BUILD', defaultValue: false, description: 'Force rebuild even if template exists')
    }

    environment {
        PROXMOX_PASSWORD = credentials('proxmox-password')
        PROXMOX_URL = 'https://192.168.1.10:8006/api2/json'
    }

    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }

        stage('Validate') {
            steps {
                script {
                    if (params.TEMPLATE == 'ubuntu' || params.TEMPLATE == 'all') {
                        sh 'packer validate ubuntu/'
                    }
                    if (params.TEMPLATE == 'rocky' || params.TEMPLATE == 'all') {
                        sh 'packer validate rocky/'
                    }
                }
            }
        }

        stage('Build Ubuntu') {
            when {
                expression { params.TEMPLATE == 'ubuntu' || params.TEMPLATE == 'all' }
            }
            steps {
                sh '''
                    packer build \
                        -var "proxmox_password=${PROXMOX_PASSWORD}" \
                        -var "proxmox_url=${PROXMOX_URL}" \
                        ubuntu/
                '''
            }
        }

        stage('Build Rocky') {
            when {
                expression { params.TEMPLATE == 'rocky' || params.TEMPLATE == 'all' }
            }
            steps {
                sh '''
                    packer build \
                        -var "proxmox_password=${PROXMOX_PASSWORD}" \
                        -var "proxmox_url=${PROXMOX_URL}" \
                        rocky/
                '''
            }
        }

        stage('Test Template') {
            steps {
                sh 'python3 tests/test_template.py'
            }
        }
    }

    post {
        success {
            slackSend(color: 'good', message: "Template build succeeded: ${env.JOB_NAME} ${env.BUILD_NUMBER}")
        }
        failure {
            slackSend(color: 'danger', message: "Template build FAILED: ${env.JOB_NAME} ${env.BUILD_NUMBER}")
        }
    }
}

Part 9: Testing and Validation

InSpec Tests

Create tests/ubuntu_test.rb:

describe command('uname -r') do
  its('stdout') { should match(/^5./) }
end

describe service('qemu-guest-agent') do
  it { should be_installed }
  it { should be_enabled }
  it { should be_running }
end

describe service('cloud-init') do
  it { should be_installed }
  it { should be_enabled }
end

describe package('vim') do
  it { should be_installed }
end

describe file('/etc/cloud/cloud.cfg') do
  it { should exist }
end

describe command('cloud-init --version') do
  its('exit_status') { should eq 0 }
end

Automated Template Testing

Create tests/test_template.py:

#!/usr/bin/env python3
import proxmoxer
import time
import sys

def test_template(proxmox, template_name):
    """Test that template can be cloned and started"""

    # Clone template
    print(f"Cloning template {template_name}...")
    result = proxmox.nodes('pve').qemu.post(
        clone=9000,
        newid=9999,
        name='test-vm'
    )

    # Start VM
    print("Starting test VM...")
    proxmox.nodes('pve').qemu(9999).status.start.post()

    # Wait for VM to start
    time.sleep(30)

    # Check VM status
    status = proxmox.nodes('pve').qemu(9999).status.current.get()
    assert status['status'] == 'running', "VM is not running"

    # Check QEMU agent
    try:
        agent_info = proxmox.nodes('pve').qemu(9999).agent.get()
        print("QEMU agent is responding")
    except:
        print("WARNING: QEMU agent not responding")

    # Cleanup
    print("Cleaning up test VM...")
    proxmox.nodes('pve').qemu(9999).status.stop.post()
    time.sleep(10)
    proxmox.nodes('pve').qemu(9999).delete()

    print("Test passed!")
    return True

if __name__ == "__main__":
    proxmox = proxmoxer.ProxmoxAPI(
        'YOUR-PROXMOX-IP',
        user='root@pam',
        password='your-password',
        verify_ssl=False
    )

    test_template(proxmox, 'ubuntu-22.04-template')

Part 10: Best Practices

Security Hardening

# scripts/security-hardening.sh
#!/bin/bash

# Remove unnecessary packages
apt-get purge -y \
  telnet \
  rsh-server \
  rsh-client \
  xinetd

# Configure firewall
ufw --force enable
ufw default deny incoming
ufw default allow outgoing
ufw allow ssh

# Disable root login
sed -i 's/PermitRootLogin yes/PermitRootLogin no/' /etc/ssh/sshd_config

# Enable automatic security updates
apt-get install -y unattended-upgrades

# Configure fail2ban
apt-get install -y fail2ban
systemctl enable fail2ban

# Set strong password policy
apt-get install -y libpam-pwquality

Maintenance and Updates

# Automated weekly template rebuild
crontab -e

# Add:
0 2 * * 0 cd /home/user/packer-templates && packer build ubuntu/

Template Inventory

Create templates.json:

{
  "templates": [
    {
      "name": "ubuntu-22.04-template",
      "vm_id": 9000,
      "os": "Ubuntu 22.04 LTS",
      "created": "2025-12-17",
      "last_updated": "2025-12-17",
      "packages": ["qemu-guest-agent", "cloud-init", "docker"],
      "notes": "Production template with Docker"
    },
    {
      "name": "rocky-9-template",
      "vm_id": 9001,
      "os": "Rocky Linux 9",
      "created": "2025-12-17",
      "last_updated": "2025-12-17",
      "packages": ["qemu-guest-agent", "cloud-init"],
      "notes": "Minimal RHEL-compatible template"
    }
  ]
}

Part 11: Troubleshooting

Common Issues

Issue: Boot command not working

# Solution: Adjust boot_wait and verify boot_command
# Check console in Proxmox web UI
# Try increasing wait times
boot_wait = "10s"

Issue: SSH timeout

# Check network configuration
# Verify SSH is installed and running
# Check firewall rules
# Increase ssh_timeout
ssh_timeout = "30m"

Issue: Cloud-init not working

# Verify cloud-init is installed
# Check configuration in /etc/cloud/
# Review cloud-init logs
cloud-init status
journalctl -u cloud-init

Issue: Template not appearing

# Check Proxmox logs
tail -f /var/log/pve/tasks/active

# Verify template was created
qm list

# Check for errors in Packer log
packer build -debug ubuntu/

Conclusion

Packer revolutionizes VM template creation by automating what was once a tedious manual process. With Packer, you can build consistent, reproducible templates that serve as the foundation for your entire homelab infrastructure.

Key Takeaways:

  • Packer automates template creation across platforms
  • Templates are version-controlled and reproducible
  • Integration with CI/CD enables automatic updates
  • Cloud-init provides flexible post-deployment configuration
  • Security hardening should be built into templates
  • Regular template updates maintain security and functionality

Next Steps:

  • Integrate Packer with Terraform for complete IaC
  • Set up automated testing pipelines
  • Implement security scanning with tools like Trivy
  • Create specialized templates for specific workloads
  • Build multi-platform templates for disaster recovery
  • Implement template lifecycle management

Happy building!