Post

Building Ubuntu 24.04 LTS Templates with Packer and HashiCorp Vault

Step-by-step guide to creating automated Ubuntu 24.04 LTS VM templates for Proxmox using Packer with HashiCorp Vault for secure credential management

Building Ubuntu 24.04 LTS Templates with Packer and HashiCorp Vault

This comprehensive guide shows you how to build production-ready Ubuntu 24.04 LTS (Noble Numbat) VM templates for Proxmox using HashiCorp Packer with Vault integration. Perfect for engineers implementing Infrastructure as Code in their homelab or enterprise environment.

The complete code for this guide is available in this repo https://github.com/tzalistar/packer-templates.

The packages I’m installing on these templates make sense in my case and I use them as an example. You can modify the templates and add packages that make more sense in your work load.

This guide is part of a series. Check out the companion guide for Building Debian Templates.

Prerequisites

Before starting, ensure you have:

Required Infrastructure

  • Proxmox VE cluster (version 7.x or 8.x):
    • One or more nodes with adequate resources
    • Storage pool for VM disks (LVM, Ceph, NFS, etc.)
    • Storage pool for ISO files
    • Configured network bridge
  • HashiCorp Vault instance:
    • Running and unsealed
    • KV v2 secrets engine enabled
    • Valid authentication token

Required Software

  • Packer (≥ 1.9.0) - Download
  • Vault CLI - Configured with environment variables:
    1
    2
    
    export VAULT_ADDR="https://vault.example.com:8200"
    export VAULT_TOKEN="your-vault-token"
    

Network Requirements

  • Static IP address for Packer’s HTTP server (must be reachable from Proxmox VMs)
  • Internet connectivity for ISO downloads and package installation
  • No firewall blocking port 8100 (or your chosen HTTP port)

Overview

This automation creates an enterprise-ready Ubuntu 24.04 LTS template featuring:

  • Cloud-init native configuration using autoinstall
  • Dual user setup (admin + automation) with SSH keys
  • Custom LVM partitioning with dedicated /var/log partition
  • QEMU guest agent for seamless Proxmox integration
  • Serial console support for web-based console access
  • UEFI boot with modern EFI configuration
  • Subscription manager (ATIX) for Foreman/Katello

Architecture Overview

The build workflow:

  1. Packer retrieves credentials from HashiCorp Vault
  2. Downloads Ubuntu 24.04 LTS ISO to Proxmox
  3. Creates a temporary VM and boots the ISO
  4. Serves autoinstall config via HTTP
  5. Ubuntu’s autoinstall performs unattended installation
  6. Packer provisions additional software and configuration
  7. VM is cleaned and converted to a reusable template

Step 1: Configure HashiCorp Vault

1.1 Store Secrets in Vault

Ubuntu templates use the same Vault structure as Debian. Store your credentials securely:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Set Vault environment
export VAULT_ADDR="https://vault.example.com:8200"
export VAULT_TOKEN="hvs.XXXXXXXXXXXXXXXXXX"

# Create secrets (run once)
vault kv put kv/proxmox \
  api_url="https://proxmox-server.example.com:8006/api2/json" \
  api_token_id="packer@pve!packer-build" \
  api_token_secret="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" \
  default_user="admin" \
  default_user_ssh_key="ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC... admin@workstation" \
  default_user_ssh_pass="AdminSecurePass123!" \
  default_user_password_hash='$6$rounds=656000$SALT$HASH...' \
  ansible_user="automation" \
  ansible_user_ssh_key="ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQD... automation@server" \
  ansible_user_ssh_pass="AutomationPass456!" \
  ansible_user_password_hash='$6$rounds=656000$SALT$HASH...'

Autoinstall vs Preseed: Ubuntu 24.04 uses cloud-init’s autoinstall instead of Debian’s preseed. This provides better cloud-native integration and more flexible configuration options.

1.2 Generate Password Hashes

Ubuntu’s cloud-init expects SHA-512 hashed passwords:

1
2
3
4
5
# Install mkpasswd (part of whois package on Debian/Ubuntu)
sudo apt-get install whois

# Generate hash
mkpasswd -m sha-512 'YourSecurePassword'

Output example:

1
$6$rounds=656000$SaltString$HashValue...

Use this hash for *_password_hash fields in Vault.

Step 2: Project Structure

2.1 Create Directory Layout

1
2
mkdir -p ubuntu-noble/packer/http
cd ubuntu-noble/packer

Your structure should look like:

1
2
3
4
5
6
7
8
9
ubuntu-noble/
├── packer/
│   ├── ubuntu-template.pkr.hcl      # Main Packer config
│   ├── variables.pkr.hcl             # Variable definitions
│   └── http/
│       ├── user-data.pkrtpl.hcl      # Cloud-init autoinstall template
│       ├── meta-data                 # Cloud-init metadata (empty)
│       └── vendor-data               # Cloud-init vendor data (empty)
└── README.md

2.2 Variable Configuration File

Create variables.pkr.hcl:

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
67
68
69
70
71
72
73
74
75
76
# Variables for Ubuntu 24.04 LTS base template

variable "proxmox_node" {
  type        = string
  description = "Proxmox node name (e.g., pve-node01)"
  default     = "pve-node01"
}

variable "vm_id" {
  type        = number
  description = "VM template ID (null = auto-assign)"
  default     = null
}

variable "vm_name" {
  type        = string
  description = "Template name"
  default     = "ubuntu-noble-base"
}

# ISO Configuration
variable "iso_url" {
  type        = string
  description = "Ubuntu 24.04.3 LTS ISO URL"
  default     = "https://releases.ubuntu.com/noble/ubuntu-24.04.3-live-server-amd64.iso"
}

variable "iso_checksum" {
  type        = string
  description = "ISO checksum"
  default     = "sha256:c3514bf0056180d09376462a7a1b4f213c1d6e8ea67fae5c25099c6fd3d8274b"
}

variable "iso_storage_pool" {
  type        = string
  description = "Proxmox storage for ISO files"
  default     = "local"
}

variable "storage_pool" {
  type        = string
  description = "Storage pool for VM disks"
  default     = "local-lvm"
}

# VM Hardware
variable "cpu_cores" {
  type        = number
  description = "Number of CPU cores"
  default     = 2
}

variable "memory" {
  type        = number
  description = "Memory in MB"
  default     = 2048
}

variable "disk_size" {
  type        = string
  description = "Disk size (min 35G for custom partitioning)"
  default     = "35G"
}

# Network
variable "vlan_tag" {
  type        = number
  description = "VLAN tag (0 = no VLAN)"
  default     = 0
}

variable "bridge" {
  type        = string
  description = "Network bridge"
  default     = "vmbr0"
}

Step 3: Main Packer Configuration

The main template ubuntu-template.pkr.hcl follows a similar structure to Debian but uses Ubuntu’s autoinstall system.

3.1 Boot Command for Ubuntu Autoinstall

1
2
3
4
5
6
7
8
  boot_wait = "5s"
  boot_command = [
    "<wait><esc><wait>",
    "e<wait>",
    "<down><down><down><end>",
    " autoinstall ds=nocloud-net\;s=http://{{ .HTTPIP }}:{{ .HTTPPort }}/",
    "<f10>"
  ]

Understanding the boot sequence:

  1. <wait><esc><wait>: Interrupt GRUB boot menu
  2. e<wait>: Enter edit mode for boot entry
  3. <down><down><down><end>: Navigate to kernel command line
  4. autoinstall ds=nocloud-net...:
    • autoinstall: Enable Ubuntu’s automated installer
    • ds=nocloud-net: Use NoCloud data source (HTTP)
    • s=http://...: URL to fetch cloud-init config
  5. <f10>: Boot with modified parameters

Key Difference: Ubuntu’s autoinstall is simpler than Debian’s preseed - it’s just adding two boot parameters instead of a long kernel command line.

Step 4: Cloud-Init Autoinstall Configuration

Create http/user-data.pkrtpl.hcl:

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
#cloud-config
autoinstall:
  version: 1

  # Locale and keyboard
  locale: en_US.UTF-8
  keyboard:
    layout: us

  # Network - use DHCP during install
  network:
    network:
      version: 2
      ethernets:
        any:
          match:
            name: "en*"
          dhcp4: true

  # Storage configuration with custom LVM
  storage:
    layout:
      name: lvm
    config:
      - type: disk
        id: disk0
        ptable: gpt
        wipe: superblock
        preserve: false
        grub_device: true

      # EFI partition
      - type: partition
        id: partition-efi
        device: disk0
        size: 512M
        flag: boot
        wipe: superblock
        preserve: false

      - type: format
        id: format-efi
        volume: partition-efi
        fstype: fat32
        preserve: false

      - type: mount
        id: mount-efi
        device: format-efi
        path: /boot/efi

      # Boot partition
      - type: partition
        id: partition-boot
        device: disk0
        size: 1G
        wipe: superblock
        preserve: false

      - type: format
        id: format-boot
        volume: partition-boot
        fstype: ext4
        preserve: false

      - type: mount
        id: mount-boot
        device: format-boot
        path: /boot

      # LVM partitions (root, /var/log, /tmp, /var)
      # ... (similar structure for other partitions)

  # Identity - temporary during install
  identity:
    hostname: ubuntu-template
    username: ${default_user}
    password: ${default_user_password}

  # SSH configuration
  ssh:
    install-server: true
    allow-pw: true

  # Package selection
  packages:
    - qemu-guest-agent
    - cloud-init
    - net-tools
    - vim
    - curl
    - wget

  # Late commands (run after install)
  late-commands:
    # Create automation user
    - curtin in-target --target=/target -- useradd -m -s /bin/bash ${ansible_user}
    - curtin in-target --target=/target -- usermod -aG sudo ${ansible_user}
    # Setup SSH and sudo access
    # ... (user configuration continues)

Declarative Storage: Unlike Debian’s recipe-based partitioning, Ubuntu uses a declarative YAML config. This makes it easier to read and modify partition layouts.

4.1 Create Meta-Data File

Create empty http/meta-data:

1
2
# Empty meta-data file
# Required by cloud-init NoCloud datasource

Step 5: Build and Use

The build process is identical to Debian:

1
2
3
4
5
6
7
8
9
10
11
# Initialize
packer init .

# Validate
packer validate .

# Build
packer build \
  -var="proxmox_node=pve-node01" \
  -var="bridge=vmbr0" \
  .

Comparison: Ubuntu vs Debian Templates

FeatureUbuntu 24.04Debian 13
InstallerAutoinstall (cloud-init)Preseed
Config FormatYAMLPreseed text
Boot CommandSimpler (GRUB edit)More complex (GRUB cmdline)
LVM ConfigDeclarative YAMLRecipe format
Cloud-initNative supportRequires installation
Package EcosystemPPAs availableDebian repos only
Release Cycle6 months / 2 years LTSStable + Testing tracks

When to Choose Ubuntu: Use Ubuntu for better hardware support, PPAs, and commercial support. Choose Debian for stability-first approach and predictable release cycles.

Troubleshooting

Issue: HTTP server unreachable

Symptoms: Installation hangs at “Configuring autoinstall”

Solution:

1
2
3
4
5
6
7
8
9
10
# Verify HTTP server accessibility
curl http://YOUR_IP:8100/user-data

# Test from Proxmox node
ssh root@proxmox-node
curl http://YOUR_IP:8100/user-data

# Check firewall
sudo ufw status
sudo ufw allow 8100/tcp  # If using UFW

Issue: Autoinstall fails with partitioning error

Symptoms: “Storage configuration failed”

Solution:

  • Ensure disk size is at least 35GB
  • Check Proxmox storage has available space
  • Verify storage pool supports your disk format

Conclusion

You’ve successfully built an automated, reproducible Ubuntu 24.04 LTS template using modern IaC practices. This setup provides:

Security: Credentials managed by Vault ✓ Repeatability: Build identical templates on-demand ✓ Flexibility: Customize via variables ✓ Cloud-native: Full cloud-init integration ✓ Production-ready: Custom partitioning and hardening

Complete File Reference

All files for this guide are available at: https://github.com/tzalistar/packer-templates


Questions or improvements? Reach out or submit a pull request!

This post is licensed under CC BY 4.0 by the author.