Packer: Automated VM Template Creation
Build VM templates automatically. Cloud-init, customization, and multi-platform support.
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!