Back to Blog

Automating Proxmox VM Lifecycle with Ansible and Semaphore

7 min read

Manually SSH-ing into each of my 18 hosts to apply updates, create users, or change configurations doesn’t scale — even in a homelab. Ansible handles the automation, and Semaphore provides a web UI that makes running playbooks as simple as clicking a button.

Why Semaphore?

Ansible on its own is powerful but requires command-line access, inventory management, and remembering the right flags for each playbook. Semaphore adds:

  • Web-based UI: Run playbooks from a browser — no terminal needed
  • Task History: See what ran, when, and whether it succeeded
  • Scheduled Runs: Trigger playbooks on a cron schedule
  • Access Control: Grant team members playbook access without SSH keys
  • Inventory Management: Visual inventory editor instead of flat files

For a homelab, it turns Ansible from a power tool into an appliance.

Architecture

Semaphore runs on a dedicated host and stores its playbooks, inventory, and credentials in a local database. The playbooks themselves live in a Git repository, pulled automatically when a task runs.

Semaphore Web UI → Ansible Engine → SSH → Target Hosts

All Ansible commands execute from Semaphore’s working directory at semaphore/opt/semaphore/tmp/proxmox/, which contains the playbooks, roles, and inventory files.

The Playbook Library

VM Creation: pve_create_vm.yml

Creating a new VM in Proxmox involves dozens of API calls — specifying CPU, memory, disk, network, cloud-init parameters, and more. This playbook automates the entire process:

  • Provisions a VM on the specified Proxmox node
  • Configures hardware resources (CPU, RAM, disk)
  • Attaches to the correct VLAN/network
  • Applies cloud-init for initial OS configuration
  • Starts the VM and waits for SSH availability

What used to take 15 minutes of clicking through the Proxmox UI now takes 30 seconds.

VM Onboarding: pve_onboard.yml

After a VM is created, it needs to be prepared for Ansible management. The onboarding playbook handles:

  1. Creates the ansible user with SSH key authentication
  2. Configures passwordless sudo for the ansible user
  3. Deploys SSH authorized keys from a central key store
  4. Sets the hostname to match the inventory name
  5. Configures basic networking (DNS, NTP)

This is the one playbook I run manually the first time (since the new VM doesn’t have the ansible user yet). After onboarding, all subsequent automation uses the ansible user.

Common Linux Configuration: doug-common-linux-playbook.yml

This is the workhorse playbook that applies my standard Linux baseline. It uses a custom role called doug-common-linux with modular task files:

Date/NTP Configuration:

  • Sets timezone to America/New_York
  • Configures NTP synchronization
  • Ensures consistent timestamps across all hosts

Hostname Management:

  • Sets hostname from inventory
  • Updates /etc/hosts with the correct hostname mapping

User Management:

  • Creates standard user accounts
  • Configures SSH keys
  • Sets password policies
  • Manages sudo access

Package Installation:

  • Installs a standard package set (htop, curl, wget, jq, etc.)
  • Removes unnecessary default packages
  • Configures unattended security updates

Mail Configuration:

  • Configures outbound mail relay
  • Sets up mail aliases for system notifications

System Configuration:

  • Kernel parameter tuning
  • Sysctl settings for network performance
  • Log rotation configuration

System Updates: update_linux_servers.yml

Rolling updates across all managed hosts:

ansible-playbook -i inventory update_linux_servers.yml

This playbook:

  • Runs apt update && apt upgrade on Debian-based hosts
  • Handles package holds and pinned versions
  • Reports which packages were updated
  • Optionally reboots hosts that require it (kernel updates)
  • Processes hosts in serial to avoid taking down all services simultaneously

Running this through Semaphore means I can trigger updates from my phone, watch the progress in real-time, and review the results later — all without opening a terminal.

Additional Playbooks

  • install_alloy.yml: Deploys Grafana Alloy agent for metrics collection
  • install-nvim.yml: Installs and configures Neovim with my preferred settings
  • ping.yml: Simple connectivity test across all hosts (great for verifying after network changes)

The Enigma Secrets Manager

Ansible playbooks often need credentials — root passwords, API tokens, service accounts. Hardcoding these in playbooks or inventory files is a security risk.

I use a custom Ansible lookup plugin called Enigma that reads credentials from ~/.enigma.json:

# In a playbook
common_users_root_password: "{{ lookup('enigma', '7604', 'password') }}"

Enigma stores credentials as a JSON key-value store with numeric IDs. Each entry can have arbitrary fields (username, password, bot_token, chat_id, etc.). The file lives outside the Git repository, so credentials never end up in version control.

This pattern keeps secrets management simple — no HashiCorp Vault server to maintain, no cloud secrets manager to pay for. Just a JSON file with restrictive permissions on the Semaphore host.

Custom Roles

doug-common-linux

This is my primary Ansible role, organized into modular task files:

roles/doug-common-linux/
├── tasks/
│   ├── main.yml          # Imports all task files
│   ├── date-ntp.yml      # Timezone and NTP
│   ├── hostname.yml      # Hostname configuration
│   ├── users.yml         # User management
│   ├── system.yml        # System configuration
│   ├── packages.yml      # Package management
│   ├── mail.yml          # Mail relay setup
│   └── domain.yml        # Domain joining (optional)
├── defaults/
│   └── main.yml          # Default variables
├── handlers/
│   └── main.yml          # Service restart handlers
└── templates/
    └── ...               # Configuration file templates

Each task file is self-contained and can be skipped with tags. Need to update just the NTP configuration across all hosts? Run the playbook with --tags ntp and only that task file executes.

Workflow Example: Adding a New Host

Here’s the typical workflow when I spin up a new LXC container or VM:

  1. Create the VM/Container (Proxmox UI or pve_create_vm.yml)
  2. Onboard — Run pve_onboard.yml to create the ansible user and configure SSH
  3. Configure — Run doug-common-linux-playbook.yml to apply the standard baseline
  4. Deploy Service — Install the specific service (Docker, Zabbix Agent, etc.)
  5. Monitor — Add to Zabbix monitoring (usually automatic via discovery)

Steps 2–4 are all Semaphore button clicks. A new host goes from bare OS to fully configured in about 10 minutes, with consistent configuration every time.

Lessons Learned

1. Idempotency Is Everything

Every task in every playbook must be idempotent — running it twice produces the same result as running it once. This means using state: present instead of shell commands, checking before modifying, and avoiding destructive operations.

2. Serial Execution for Updates

Never run updates on all hosts simultaneously. If an update breaks something, you want to catch it on the first host before it propagates. Set serial: 1 or serial: 3 in update playbooks.

3. Test in Dev, Deploy in Prod

I have template VMs specifically for testing playbook changes. New role modifications get tested on a throwaway VM before running against production hosts. Semaphore’s task history makes it easy to compare test runs against production runs.

4. Keep Roles Modular

The doug-common-linux role started as a monolithic task file. Breaking it into separate files for users, packages, NTP, etc. made it dramatically easier to maintain and debug. Each file is small enough to understand at a glance.

5. Git-Tracked Playbooks

All playbooks and roles live in a Git repository. Semaphore pulls from Git on each run, so the latest changes are always applied. This also provides full audit history — who changed what, when, and why.

What’s Next

  • Expanded Zabbix Agent deployment: Automate Zabbix Agent 2 installation and PSK configuration as part of the onboarding playbook
  • Backup verification: Playbook to verify Proxmox Backup Server integrity and test restores
  • Certificate rotation: Automate Let’s Encrypt certificate renewal tracking

For more on the monitoring that works alongside this automation, see Monitoring Everything: Zabbix 7 in a Homelab. For the complete infrastructure overview, check out Building My Homelab.