Infrastructure-as-Code repository for building standardized virtual machine images and systemd-capable Docker images using Packer, Docker, and Ansible.
This repository contains Packer templates, Dockerfiles, and Ansible playbooks for creating consistent, secure golden images for Princeton University Library infrastructure. Currently supports:
- VM Images: Ubuntu 22.04 LTS and Rocky Linux 9.4
- Docker Images: systemd-capable containers published to GHCR
# Install Devbox from https://www.jetbox.io/devbox
# Then simply run:
devbox shellThis will automatically install all required tools:
- Packer
- Ansible
- Python
- AWS CLI v2
- Google Cloud SDK
- QEMU
- Docker
- Git
- Just
If not using Devbox, (you know what you're doing 😉) manually install:
- Packer >= 1.12.0
- Ansible >= 2.9
- Python >= 3.8
- Docker >= 20.10
- QEMU (for local testing)
# Clone the repository
git clone https://github.com/pulibrary/vm-builds.git
cd vm-builds
# Enter the Devbox environment
devbox shell
# (One-time) Initialize packer plugins for all templates
just init-allDownload Ubuntu Cloud Image ISO and place it at build/linux/ubuntu/isos and/or Rocky Generic Cloud Base Image (uses Rocky-9-GenericCloud-Base.latest.x86_64.qcow2 format) and place it at build/linux/rocky/isos Note the checksums for any you plan to use, which will be needed in the steps below.
# Ubuntu QEMU (requires the cloud-image checksum)
# Example checksum shown; replace with the current release checksum
just build-ubuntu-qemu 'sha256:b119a978dcb66194761674c23a860a75cdb7778e95e222b51d7a3386dfe3c920' true
# Rocky QEMU (also requires checksum)
just build-rocky-qemu 'sha256:<rocky_cloud_image_sha256_here>'
# Ubuntu AWS AMI
just build-ubuntu-aws
# Rocky AWS AMI
just build-rocky-aws
# Ubuntu GCP image (failing for now)
just build-ubuntu-gcp pul-gcdc zone=us-east1-b machine_type=e2-standard-2# Ubuntu 22.04 systemd + Ansible image
just build-ubuntu-docker # builds ghcr.io/pulibrary/vm-builds/ubuntu-22.04:dev
just push-ubuntu-docker # pushes ghcr.io/pulibrary/vm-builds/ubuntu-22.04:dev
# Rocky 9 systemd + Ansible image
just build-rocky-docker # builds ghcr.io/pulibrary/vm-builds/rocky-9:dev
just push-rocky-docker # pushes ghcr.io/pulibrary/vm-builds/rocky-9:devCopy .env.example to .env and fill in values. Get these from the Prancible Vault. The will be read by Packer:
BIGFIX_MASTHEAD_URLRAPID7_TOKENRAPID7_ATTRIBUTESFALCON_CID
set -a; source .env; set +a
just build-ubuntu-aws
# or
just build-rocky-qemu 'sha256:<rocky sha256>' true.
├── ansible/ # Ansible provisioning
│ ├── roles/
│ │ ├── base/ # OS updates and packages
│ │ ├── users/ # User and SSH key management
│ │ ├── configure/ # System configuration
│ │ ├── clean/ # Image cleanup
│ │ ├── mirrors/ # Repository mirror configuration
│ │ └── security_firstboot/ # First-boot security setup
│ ├── collections.yaml # Ansible Galaxy collections
│ └── linux-playbook.yml # Main playbook
├── builds/
│ └── linux/
│ ├── rocky/ # Rocky Linux Packer configs
│ │ ├── data/ # Cloud-init templates
│ │ ├── isos/ # Rocky base images
│ │ ├── linux-rocky-aws.pkr.hcl
│ │ └── linux-rocky-qemu-cloudimg.pkr.hcl
│ └── ubuntu/ # Ubuntu Packer configs
│ ├── data/ # Cloud-init templates
│ ├── isos/ # Ubuntu base images
│ ├── linux-ubuntu-aws.pkr.hcl
│ ├── linux-ubuntu-gcp.pkr.hcl
│ └── linux-ubuntu-qemu-cloudimg.pkr.hcl
├── docker/ # Docker image definitions
│ ├── rocky/
│ │ └── Dockerfile # Rocky 9 systemd + Ansible
│ └── ubuntu/
│ └── Dockerfile # Ubuntu 22.04 systemd + Ansible
├── justfile # Command shortcuts
└── devbox.json # Development environment
Note: Build outputs are stored in artifacts/ and manifests/ directories (git-ignored).
| OS | Version | Platform | Status |
|---|---|---|---|
| Ubuntu | 22.04 LTS | QEMU | Working |
| Ubuntu | 22.04 LTS | GCP | Template exists |
| Ubuntu | 22.04 LTS | AWS | Working |
| Rocky Linux | 9.4 | QEMU | Working |
| Rocky Linux | 9.4 | AWS | Working |
| OS | Version | Registry | Status |
|---|---|---|---|
| Ubuntu | 22.04 LTS | GHCR | Working |
| Rocky Linux | 9.4 | GHCR | Working |
# Ubuntu QEMU build
packer build builds/linux/ubuntu/linux-ubuntu-qemu-cloudimg.pkr.hcl
# Rocky Linux QEMU build
packer build builds/linux/rocky/linux-rocky-qemu-cloudimg.pkr.hcl# Requires: gcloud auth application-default login
packer build -var "gcp_project_id=pul-gcdc" \
builds/linux/ubuntu/linux-ubuntu-gcp.pkr.hcl- QEMU builds:
artifacts/qemu/[os-version]/.qcow2- QEMU image.vmdk- VMware disk.vhd- Hyper-V disk.ovf- Open Virtualization Format
In addition to VM images, this repo builds systemd-capable Docker images that double as Ansible control hosts. Images are published to GitHub Container Registry (GHCR) under:
ghcr.io/pulibrary/vm-builds/ubuntu-22.04:<tag>ghcr.io/pulibrary/vm-builds/rocky-9:<tag>
These images:
- Run
systemdas PID 1 (for testing services with units) - Include
pulsyswith passwordless sudo - Have Python + Ansible core installed, plus the collections in
ansible/collections.yaml
GHCR uses GitHub Packages, which currently requires a personal access token (classic) for CLI access.
You need:
read:packagesto pull imageswrite:packagesto push images (and optionallydelete:packagesif you want to delete packages)
Steps:
- Log in to GitHub and go to
Settings → Developer settings → Personal access tokens → Tokens (classic). - Click "Generate new token (classic)".
- Give it a descriptive name, e.g.
vm-builds-ghcr. - Set an expiration that matches your org policy.
- Under Scopes, select at least:
read:packageswrite:packages
- Click Generate token, then copy the token somewhere safe (you won't be able to see it again).
Export it as an environment variable:
export GHCR_PAT=ghp_your_token_hereNote: In GitHub Actions, you usually don't need a PAT; use GITHUB_TOKEN with packages: write instead.
From your shell:
echo "$GHCR_PAT" | docker login ghcr.io -u "<your-github-username>" --password-stdin
# e.g.:
# echo "$GHCR_PAT" | docker login ghcr.io -u "kayiwa" --password-stdinYou should see Login Succeeded.
Or, using just:
just ghcr-login(Uses GHCR_PAT / GITHUB_TOKEN / GH_TOKEN under the hood.)
From the repo root:
# Ubuntu 22.04 systemd + Ansible image
just build-ubuntu-docker # builds ghcr.io/pulibrary/vm-builds/ubuntu-22.04:dev
just push-ubuntu-docker # pushes ghcr.io/pulibrary/vm-builds/ubuntu-22.04:dev
# Rocky 9 systemd + Ansible image
just build-rocky-docker # builds ghcr.io/pulibrary/vm-builds/rocky-9:dev
just push-rocky-docker # pushes ghcr.io/pulibrary/vm-builds/rocky-9:devYou can override the tag (e.g., use a date or git SHA):
just build-ubuntu-docker 2025-11-13
just push-ubuntu-docker 2025-11-13Resulting tags:
ghcr.io/pulibrary/vm-builds/ubuntu-22.04:2025-11-13ghcr.io/pulibrary/vm-builds/rocky-9:2025-11-13
Pull:
docker pull ghcr.io/pulibrary/vm-builds/ubuntu-22.04:dev
docker pull ghcr.io/pulibrary/vm-builds/rocky-9:devTo run with systemd inside the container (requires --privileged and cgroup mounts):
# Ubuntu
docker run --privileged \
--cgroupns=host \
-v /sys/fs/cgroup:/sys/fs/cgroup:ro \
-v /run:/run \
-v /tmp:/tmp \
-d --name ubuntu-systemd \
ghcr.io/pulibrary/vm-builds/ubuntu-22.04:dev
# Rocky
docker run --privileged \
--cgroupns=host \
-v /sys/fs/cgroup:/sys/fs/cgroup:ro \
-v /run:/run \
-v /tmp:/tmp \
-d --name rocky-systemd \
ghcr.io/pulibrary/vm-builds/rocky-9:devInside the container you can then:
docker exec -it ubuntu-systemd bash
systemctl status
ansible-galaxy collection list- Updates all packages to latest
- Installs essential packages
- Configures cloud-init (when enabled)
- Creates
pulsysuser with sudo access - Pulls SSH keys from GitHub for:
- Operations staff
- Library development staff
- Ansible Tower Keys
- Manages build users (
packer,ubuntu)
- Enables SSH public key authentication
- Sets hostname to
lib-vm - Configures cloud-init datasources
- Regenerates SSH host keys on first boot
- Configures repository mirrors for faster package downloads
- Supports both Ubuntu and Rocky Linux mirror configurations
- Installs and configures security tools on first boot:
- BigFix endpoint management
- Rapid7 InsightAgent
- CrowdStrike Falcon sensor
- Configures firewalls (UFW for Ubuntu, firewalld for Rocky)
- Sets up fail2ban for SSH protection
- Hardens SSH configuration
- Removes temporary files and logs
- Cleans build artifacts
- Optionally removes build users
- Clears machine-id for uniqueness
The pulsys user is automatically created with SSH keys from:
# ansible/roles/users/defaults/main.yml
ops_github_keys:
- https://github.com/acozine.keys
- https://github.com/kayiwa.keys
# ... additional ops staff
library_github_keys:
- https://github.com/escowles.keys
- https://github.com/hackartisan.keys
# ... additional library staffTo add/remove users, update the lists in ansible/roles/users/defaults/main.yml.
Set cleanup_final_image=true to remove build users from the final image:
packer build -var "cleanup_final_image=true" [template]Protected users (root, pulsys) are never removed.
# Boot the image
qemu-system-x86_64 \
-m 2048 -smp 2 \
-accel tcg,thread=multi \
-drive file="artifacts/qemu/linux-ubuntu-22-04-lts-20250922-062113/linux-ubuntu-22-04-lts-20250922-062113.qcow2",if=virtio,format=qcow2,cache=writeback \
-netdev user,id=n1,hostfwd=tcp::2222-:22 \
-device virtio-net-pci,netdev=n1 \
-serial mon:stdio -display
# SSH into the VM (in another terminal)
ssh -p 2222 pulsys@localhostCommon Packer variables:
# User settings
build_username = "packer"
ansible_username = "packer"
build_key = "ssh-rsa ..." # SSH key for build user
ansible_key = "ssh-rsa ..." # SSH key for ansible user
# Image settings
disk_size = 30 # GB
vm_guest_os_cloudinit = true # Enable cloud-init
# Cleanup
cleanup_final_image = true # Remove build artifactsProblem: invalid checksum: encoding/hex: invalid byte
Solution: Download the ISO and update the checksum in the .pkr.hcl file
Problem: Packer can't connect to the VM
Solution: Check QEMU is working and increase ssh_timeout
Problem: Command not found errors
Solution: Use devbox shell or install missing tools manually
Problem: ubuntu or packer users still present
Solution: Set -var "cleanup_final_image=true" during build
Problem: Unable to authenticate with GHCR
Solution: Ensure your PAT has read:packages and write:packages scopes, and verify you're using the correct username
Problem: systemctl commands fail in Docker container
Solution: Ensure you're using --privileged and mounting cgroups correctly as shown in the run examples
Each build generates a manifest in manifests/ containing:
- Build timestamp
- Git commit hash
- Image metadata
- Custom variables used
Example: manifests/2025-11-13 11:07:49.json
- Create a feature branch
- Test changes locally with QEMU or Docker
- Validate Packer templates:
packer validate [template] - Test Docker builds:
docker build -f docker/[os]/Dockerfile . - Submit pull request
See CONTRIBUTING.md for details.
See LICENSE file for details.
Maintained by: Princeton University Library
Repository: https://github.com/pulibrary/vm-builds