Homelab Infrastructure as Code
A Pulumi-based IaC template for managing a Proxmox homelab. The goal is to replace manual GUI configuration and ad-hoc YAML stacks (LXC, VM, Docker, etc.) with version-controlled, reproducible infrastructure — starting with a highly available k3s cluster across multiple Proxmox nodes.
This repo is intentionally abstract: credentials are never hardcoded, making it easy to fork and adapt as a template for your own homelab.
Why IaC for a homelab?
- Reproducibility — rebuild your entire environment from scratch with a single command
- Version history — every change is tracked in git; roll back at any time
- Auditability — diff infrastructure changes before applying them (
pulumi preview) - Automation — CI/CD handles deploys; no manual SSH into nodes
- Portability — swap node names, datastores, or credentials without touching logic
Repository layout
.
├── proxmox-infra/ # Pulumi TypeScript stack — VMs & LXC on Proxmox
│ ├── index.ts # All Pulumi resources
│ ├── Pulumi.yaml # Stack project definition
│ ├── Pulumi.dev.yaml # Encrypted stack config (gitignored)
│ └── sdks/
│ └── pfsense/ # Locally bundled @pulumi/pfsense SDK
├── .gitea/
│ └── workflows/
│ └── deploy-proxmox-infra.yaml # Gitea Actions CI/CD pipeline
Current stack: proxmox-infra
Provisions a 5-node k3s cluster spread across two Proxmox hosts (pve and pve-bckp, third bare metal host to be added later for actual parity):
| VM name | Role | Proxmox node |
|---|---|---|
| k3s-master-1 | master | pve |
| k3s-master-2 | master | pve |
| k3s-worker-1 | worker | pve |
| k3s-master-3 | master | pve-bckp |
| k3s-worker-2 | worker | pve-bckp |
Each node is a full clone of an Ubuntu Noble (24.04) cloud-image template, with cloud-init injecting hostname, user credentials, and SSH keys at boot. Each VM's MAC address is registered as a DHCPv4 static mapping in pfSense so that nodes always receive their designated IPs.
An ED25519 SSH key pair is generated once and stored in Pulumi state. The public key is injected into every VM at boot; the private key is exported as a stack output so k8s-bootstrap can consume it via StackReference without any manual key distribution.
Tech stack:
- Pulumi with TypeScript
@muhlba91/pulumi-proxmoxvev8.x community provider@pulumi/pfsense— locally bundled SDK bridged from the Terraform pfSense provider; installed automatically vianpm install@pulumi/tls— SSH key pair generation- Self-hosted Pulumi state backend (PostgreSQL)
- Gitea Actions for CI/CD
Prerequisites
- Pulumi CLI installed
- Node.js 18+ and npm
- Access to a Proxmox node with an API token
- pfSense instance with API credentials (used for DHCPv4 static mappings)
- A self-hosted Pulumi state backend (PostgreSQL connection string)
- Gitea instance for CI/CD (optional for local use)
Getting started
1. Clone and install
git clone <your-repo-url>
cd proxmox-infra
npm install
pfSense SDK — The
@pulumi/pfsenseSDK is bundled locally undersdks/pfsense/and referenced as afile:dependency inpackage.json. Runningnpm installcompiles it automatically via its postinstall hook. No separate installation or build step is required.
2. Configure credentials
All secrets are stored as encrypted Pulumi config values — never in plain environment variables or committed files.
# Proxmox API credentials
pulumi config set --secret pve1Endpoint https://<proxmox-host-1>:8006
pulumi config set --secret pve1ApiToken <user>@pam!<token-id>=<uuid>
pulumi config set --secret pve2Endpoint https://<proxmox-host-2>:8006
pulumi config set --secret pve2ApiToken <user>@pam!<token-id>=<uuid>
# VM credentials
pulumi config set --secret k3sVmPassword <vm-password>
pulumi config set --secret sshPvePublicKey "ssh-ed25519 AAAA..."
# pfSense credentials (used for DHCPv4 static mappings)
pulumi config set --secret pfSenseUrl https://<pfsense-host>
pulumi config set --secret pfSenseUser <admin-username>
pulumi config set --secret pfSensePassword <admin-password>
# Static IP addresses assigned to each k3s node
pulumi config set --secret master1Ip <ip-for-k3s-master-1>
pulumi config set --secret master2Ip <ip-for-k3s-master-2>
pulumi config set --secret worker1Ip <ip-for-k3s-worker-1>
pulumi config set --secret master3Ip <ip-for-k3s-master-3>
pulumi config set --secret worker2Ip <ip-for-k3s-worker-2>
Pulumi encrypts these values into Pulumi.dev.yaml using your PULUMI_CONFIG_PASSPHRASE.
3. Set the state backend
export PULUMI_BACKEND_URL=postgresql://<user>:<pass>@<host>/<db>
export PULUMI_CONFIG_PASSPHRASE=<your-passphrase>
4. Preview and deploy
# See what will change before touching anything
pulumi preview
# Sync Pulumi state with actual Proxmox state (run after any manual GUI changes)
pulumi refresh --yes
# Deploy
pulumi refresh --yes && pulumi up --yes
CI/CD (Gitea Actions)
The workflow at .gitea/workflows/deploy-proxmox-infra.yaml runs automatically:
| Event | Action |
|---|---|
Pull request → main |
pulumi preview (read-only) |
Push to main |
pulumi refresh + pulumi up |
| Manual trigger | pulumi refresh + pulumi up |
Required Gitea secrets
Configure these under Settings → Actions → Secrets in your Gitea repo:
| Secret | Description |
|---|---|
PULUMI_BACKEND_URL |
PostgreSQL connection string for the state backend |
PULUMI_CONFIG_PASSPHRASE |
Passphrase to decrypt secrets in Pulumi.dev.yaml |
PULUMI_DEV_YAML |
Base64-encoded content of Pulumi.dev.yaml |
Pulumi.dev.yaml is gitignored because it contains your encryption salt. Whenever it changes (e.g. after adding or rotating a secret), re-encode it and paste the output into the Gitea secret:
base64 -w 0 proxmox-infra/Pulumi.dev.yaml
Adapting this as a template
- Fork or copy the repo
- Update node names (
pve,pve-bckp) and datastore IDs inindex.tsto match your setup - Add or remove VMs from the
nodeConfigsarray - Set your own secrets with
pulumi config set --secret - Point the CI/CD workflow at your own Git instance
Roadmap
- LXC container management
- Docker / Compose stack provisioning
- Firewall rules (pfSense)
- Automated k3s bootstrapping (kubeconfig export)
- Additional worker nodes and storage volumes
- Migrate secrets management to OpenBao — replace
PULUMI_CONFIG_PASSPHRASEand manualPulumi.dev.yamlencoding with a self-hosted vault - Add a third bare metal proxmox instance to create an actual 3 node parity.