kasun 8eb59643cf
Deploy k8s Bootstrap / Pulumi Preview (pull_request) Successful in 37s
Deploy k8s Bootstrap / Bootstrap k3s Cluster (pull_request) Has been skipped
format
2026-06-01 03:15:15 +02:00
2026-06-01 03:15:15 +02:00

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-proxmoxve v8.x community provider
  • @pulumi/pfsense — locally bundled SDK bridged from the Terraform pfSense provider; installed automatically via npm 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/pfsense SDK is bundled locally under sdks/pfsense/ and referenced as a file: dependency in package.json. Running npm install compiles 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

  1. Fork or copy the repo
  2. Update node names (pve, pve-bckp) and datastore IDs in index.ts to match your setup
  3. Add or remove VMs from the nodeConfigs array
  4. Set your own secrets with pulumi config set --secret
  5. 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_PASSPHRASE and manual Pulumi.dev.yaml encoding with a self-hosted vault
  • Add a third bare metal proxmox instance to create an actual 3 node parity.
S
Description
I set up my Proxmox homelab with Pulumi and Gitea Actions to utilize IaC.
Readme 295 KiB
Languages
TypeScript 100%