diff --git a/.gitea/workflows/deploy-proxmox-infra.yaml b/.gitea/workflows/deploy-proxmox-infra.yaml index bfbd7b6..8e335c5 100644 --- a/.gitea/workflows/deploy-proxmox-infra.yaml +++ b/.gitea/workflows/deploy-proxmox-infra.yaml @@ -33,7 +33,11 @@ jobs: run: echo "${{ secrets.PROXMOX_INFRA_PULUMI_DEV_YAML }}" | base64 -d > proxmox-infra/Pulumi.dev.yaml - name: Install Dependencies - run: npm install + run: npm ci + working-directory: proxmox-infra + + - name: Generate Local pfSense SDK + run: pulumi package add terraform-provider marshallford/pfsense working-directory: proxmox-infra - name: Preview @@ -63,7 +67,11 @@ jobs: run: echo "${{ secrets.PROXMOX_INFRA_PULUMI_DEV_YAML }}" | base64 -d > proxmox-infra/Pulumi.dev.yaml - name: Install Dependencies - run: npm install + run: npm ci + working-directory: proxmox-infra + + - name: Generate Local pfSense SDK + run: pulumi package add terraform-provider marshallford/pfsense working-directory: proxmox-infra - name: Refresh State diff --git a/.gitignore b/.gitignore index 6df2fcb..340f7d1 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,6 @@ .vscode node_modules/ bin/ -Pulumi.dev.yaml \ No newline at end of file +Pulumi.dev.yaml +sdks/ +.env \ No newline at end of file diff --git a/README.md b/README.md index 4eaf679..2673fd1 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,9 @@ This repo is intentionally abstract: credentials are never hardcoded, making it ├── 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) +│ ├── 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 @@ -37,12 +39,16 @@ Provisions a 5-node k3s cluster spread across two Proxmox hosts (`pve` and `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. +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](https://www.pulumi.com/) with TypeScript - [`@muhlba91/pulumi-proxmoxve`](https://github.com/muhlba91/pulumi-provider-proxmoxve) v8.x community provider +- [`@pulumi/pfsense`](https://github.com/marshallford/terraform-provider-pfsense) — locally bundled SDK bridged from the Terraform pfSense provider; installed automatically via `npm install` +- [`@pulumi/tls`](https://www.pulumi.com/registry/packages/tls/) — SSH key pair generation - Self-hosted Pulumi state backend (PostgreSQL) - Gitea Actions for CI/CD @@ -51,6 +57,7 @@ Each node is a full clone of an Ubuntu Noble (24.04) cloud-image template, with - [Pulumi CLI](https://www.pulumi.com/docs/install/) 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) @@ -64,20 +71,34 @@ 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. ```bash -# Set Proxmox API credentials +# Proxmox API credentials pulumi config set --secret pve1Endpoint https://:8006 pulumi config set --secret pve1ApiToken @pam!= pulumi config set --secret pve2Endpoint https://:8006 pulumi config set --secret pve2ApiToken @pam!= -# Set VM credentials +# VM credentials pulumi config set --secret k3sVmPassword pulumi config set --secret sshPvePublicKey "ssh-ed25519 AAAA..." + +# pfSense credentials (used for DHCPv4 static mappings) +pulumi config set --secret pfSenseUrl https:// +pulumi config set --secret pfSenseUser +pulumi config set --secret pfSensePassword + +# Static IP addresses assigned to each k3s node +pulumi config set --secret master1Ip +pulumi config set --secret master2Ip +pulumi config set --secret worker1Ip +pulumi config set --secret master3Ip +pulumi config set --secret worker2Ip ``` Pulumi encrypts these values into `Pulumi.dev.yaml` using your `PULUMI_CONFIG_PASSPHRASE`. @@ -140,7 +161,7 @@ base64 -w 0 proxmox-infra/Pulumi.dev.yaml - LXC container management - Docker / Compose stack provisioning -- Network and firewall rules +- Firewall rules (pfSense) - Automated k3s bootstrapping (kubeconfig export) - Additional worker nodes and storage volumes - Migrate secrets management to [OpenBao](https://openbao.org/) — replace `PULUMI_CONFIG_PASSPHRASE` and manual `Pulumi.dev.yaml` encoding with a self-hosted vault diff --git a/proxmox-infra/Pulumi.yaml b/proxmox-infra/Pulumi.yaml index 70ec6e3..8929124 100644 --- a/proxmox-infra/Pulumi.yaml +++ b/proxmox-infra/Pulumi.yaml @@ -8,3 +8,9 @@ config: pulumi:tags: value: pulumi:template: typescript +packages: + pfsense: + source: terraform-provider + version: 1.1.3 + parameters: + - marshallford/pfsense diff --git a/proxmox-infra/index.ts b/proxmox-infra/index.ts index ed51045..2644234 100644 --- a/proxmox-infra/index.ts +++ b/proxmox-infra/index.ts @@ -1,18 +1,29 @@ import * as pulumi from "@pulumi/pulumi"; import * as proxmox from "@muhlba91/pulumi-proxmoxve"; import * as tls from "@pulumi/tls"; +import * as pfsense from "@pulumi/pfsense"; const config = new pulumi.Config(); -// --------------------------------------------------------------------------- -// Providers — one per standalone Proxmox machine -// --------------------------------------------------------------------------- - const pve1Endpoint = config.requireSecret("pve1Endpoint"); const pve1ApiToken = config.requireSecret("pve1ApiToken"); const pve2Endpoint = config.requireSecret("pve2Endpoint"); const pve2ApiToken = config.requireSecret("pve2ApiToken"); +const pfSenseUrl = config.requireSecret("pfSenseUrl"); +const pfSenseUser = config.requireSecret("pfSenseUser"); +const pfSensePassword = config.requireSecret("pfSensePassword"); + +const master1Ip = config.requireSecret("master1Ip"); +const master2Ip = config.requireSecret("master2Ip"); +const worker1Ip = config.requireSecret("worker1Ip"); +const master3Ip = config.requireSecret("master3Ip"); +const worker2Ip = config.requireSecret("worker2Ip"); + +// --------------------------------------------------------------------------- +// Providers — one per standalone Proxmox machine +// --------------------------------------------------------------------------- + const pveProvider = new proxmox.Provider("pve", { endpoint: pve1Endpoint, apiToken: pve1ApiToken, @@ -25,6 +36,17 @@ const pveBckpProvider = new proxmox.Provider("pve-bckp", { insecure: true, }); +// --------------------------------------------------------------------------- +// Providers — PfSense +// --------------------------------------------------------------------------- + +const pfSenseProvider = new pfsense.Provider("pfsense", { + url: pfSenseUrl, + username: pfSenseUser, + password: pfSensePassword, + tlsSkipVerify: true, +}); + // --------------------------------------------------------------------------- // CI runner SSH keypair — generated once, stored in Pulumi state backend. // Public key goes into every VM; private key is exported for k8s-bootstrap. @@ -150,6 +172,7 @@ interface NodeConfig { provider: proxmox.Provider; template: proxmox.VmLegacy; diskDatastore: string; + ip: pulumi.Output; } const nodeConfigs: NodeConfig[] = [ @@ -160,6 +183,7 @@ const nodeConfigs: NodeConfig[] = [ provider: pveProvider, template: pveTemplate, diskDatastore: "local-lvm", + ip: master1Ip, }, { name: "k3s-master-2", @@ -168,6 +192,7 @@ const nodeConfigs: NodeConfig[] = [ provider: pveProvider, template: pveTemplate, diskDatastore: "local-lvm", + ip: master2Ip, }, { name: "k3s-worker-1", @@ -176,6 +201,7 @@ const nodeConfigs: NodeConfig[] = [ provider: pveProvider, template: pveTemplate, diskDatastore: "local-lvm", + ip: worker1Ip, }, { name: "k3s-master-3", @@ -184,6 +210,7 @@ const nodeConfigs: NodeConfig[] = [ provider: pveBckpProvider, template: pveBckpTemplate, diskDatastore: "local", + ip: master3Ip, }, { name: "k3s-worker-2", @@ -192,6 +219,7 @@ const nodeConfigs: NodeConfig[] = [ provider: pveBckpProvider, template: pveBckpTemplate, diskDatastore: "local", + ip: worker2Ip, }, ]; @@ -235,9 +263,9 @@ const k3sVms = nodeConfigs.map( username: "ubuntu", password: k3sVmPassword, keys: [ - sshPvePublicKey.apply((k) => k.trim()), - ciRunnerKey.publicKeyOpenssh.apply((k) => k.trim()), - ], + sshPvePublicKey.apply((k) => k.trim()), + ciRunnerKey.publicKeyOpenssh.apply((k) => k.trim()), + ], }, }, networkDevices: [{ bridge: "vmbr0", model: "virtio" }], @@ -256,12 +284,22 @@ const k3sVms = nodeConfigs.map( ), ); -export const clusterInfo = k3sVms.map((vm, index) => ({ - nodeName: vm.nodeName, - vmId: vm.vmId, - name: nodeConfigs[index].name, - role: nodeConfigs[index].role, -})); +k3sVms.forEach((vmResource, i) => { + const assignedMac = vmResource.networkDevices.apply( + (nic) => nic[0].macAddress, + ); + + return new pfsense.Dhcpv4Staticmapping( + `${nodeConfigs[i].name}-dhcp`, + { + interface: "lan", + macAddress: assignedMac, + ipAddress: nodeConfigs[i].ip, + hostname: nodeConfigs[i].name, + }, + { dependsOn: vmResource, provider: pfSenseProvider }, + ); +}); // Individual vmId exports — used by k8s-bootstrap to start VMs. // Order matches nodeConfigs: master-1, master-2, worker-1, master-3, worker-2. @@ -278,3 +316,6 @@ export const ciRunnerPrivateKey = pulumi.secret(ciRunnerKey.privateKeyOpenssh); // Proxmox API credentials — consumed by k8s-bootstrap via StackReference. export { pve1Endpoint, pve1ApiToken, pve2Endpoint, pve2ApiToken }; + +//k3s instance ips consumed by k8s-bootstrap. +export { master1Ip, master2Ip, worker1Ip, master3Ip, worker2Ip }; diff --git a/proxmox-infra/package-lock.json b/proxmox-infra/package-lock.json index d0d3640..ccdabcf 100644 --- a/proxmox-infra/package-lock.json +++ b/proxmox-infra/package-lock.json @@ -7,6 +7,7 @@ "name": "proxmox-infra", "dependencies": { "@muhlba91/pulumi-proxmoxve": "^8.2.1", + "@pulumi/pfsense": "file:sdks/pfsense", "@pulumi/pulumi": "^3.113.0", "@pulumi/tls": "^5.5.0" }, @@ -703,6 +704,10 @@ "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", "license": "BSD-3-Clause" }, + "node_modules/@pulumi/pfsense": { + "resolved": "sdks/pfsense", + "link": true + }, "node_modules/@pulumi/pulumi": { "version": "3.243.0", "resolved": "https://registry.npmjs.org/@pulumi/pulumi/-/pulumi-3.243.0.tgz", @@ -2674,6 +2679,44 @@ "engines": { "node": ">=12" } + }, + "sdks/pfsense": { + "name": "@pulumi/pfsense", + "version": "0.22.0", + "hasInstallScript": true, + "dependencies": { + "@pulumi/pulumi": "^3.238.0", + "@types/node": "^20", + "typescript": "^4.7.0" + } + }, + "sdks/pfsense/node_modules/@types/node": { + "version": "20.19.41", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz", + "integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "sdks/pfsense/node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "sdks/pfsense/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" } } } diff --git a/proxmox-infra/package.json b/proxmox-infra/package.json index 784b56d..18fd875 100644 --- a/proxmox-infra/package.json +++ b/proxmox-infra/package.json @@ -7,7 +7,12 @@ }, "dependencies": { "@muhlba91/pulumi-proxmoxve": "^8.2.1", + "@pulumi/pfsense": "file:sdks/pfsense", "@pulumi/pulumi": "^3.113.0", "@pulumi/tls": "^5.5.0" + }, + "imports": { + "#pfsense": "./sdks/pfsense/index.js", + "#pfsense/*": "./sdks/pfsense/*" } }