kasun 6a70000c62
Deploy k8s Bootstrap / Pulumi Preview (push) Has been skipped
Deploy k8s Bootstrap / Bootstrap k3s Cluster (push) Successful in 48s
Deploy Proxmox Infra / Pulumi Preview (push) Has been skipped
Deploy Proxmox Infra / Pulumi Deploy (push) Successful in 16m7s
Merge pull request 'removed netcat dependency with /dev/tcp' (#2) from bug/fix-missing-dependencies-k8s-bootstrap into main
Reviewed-on: #2
2026-05-31 18:33:17 +02:00
2026-05-30 19:35:37 +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)
├── .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 key at boot.

Tech stack:

Prerequisites

  • Pulumi CLI installed
  • Node.js 18+ and npm
  • Access to a Proxmox node with an API token
  • 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

2. Configure credentials

All secrets are stored as encrypted Pulumi config values — never in plain environment variables or committed files.

# Set 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>

# Set VM credentials
pulumi config set --secret k3sVmPassword  <vm-password>
pulumi config set --secret sshPvePublicKey "ssh-ed25519 AAAA..."

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
  • Network and firewall rules
  • 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%