11 Commits

Author SHA1 Message Date
kasun 5305061e7b added missing pulumi cli installation
Deploy Proxmox Infra / Pulumi Preview (pull_request) Successful in 1m5s
Deploy Proxmox Infra / Pulumi Deploy (pull_request) Has been skipped
2026-06-01 00:56:11 +02:00
kasun 136e6c9eec added pfsense provider for automating static ip setup
Deploy Proxmox Infra / Pulumi Preview (pull_request) Failing after 12s
Deploy Proxmox Infra / Pulumi Deploy (pull_request) Has been skipped
2026-06-01 00:51:22 +02:00
kasun e09ec50687 changed pulumi dev secret name 2026-06-01 00:47:00 +02:00
kasun 7815e1e4f2 Delete .env.local
accidentially commited. don't worry token is not valid anymore
2026-05-31 18:50:31 +02:00
kasun 6a70000c62 Merge pull request 'removed netcat dependency with /dev/tcp' (#2) from bug/fix-missing-dependencies-k8s-bootstrap into main
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
Reviewed-on: #2
2026-05-31 18:33:17 +02:00
kasun 5ac4cb592c fix: removed failing propagation. will add it later
Deploy k8s Bootstrap / Pulumi Preview (pull_request) Successful in 33s
Deploy k8s Bootstrap / Bootstrap k3s Cluster (pull_request) Has been skipped
Deploy Proxmox Infra / Pulumi Preview (pull_request) Successful in 42s
Deploy Proxmox Infra / Pulumi Deploy (pull_request) Has been skipped
2026-05-30 19:53:04 +02:00
kasun 980e24c606 fix: added missing sudo
Deploy k8s Bootstrap / Pulumi Preview (pull_request) Successful in 38s
Deploy k8s Bootstrap / Bootstrap k3s Cluster (pull_request) Has been skipped
2026-05-30 19:35:37 +02:00
kasun 2f6635aa73 fix: added sudo to kubectl commands
Deploy k8s Bootstrap / Pulumi Preview (pull_request) Successful in 31s
Deploy k8s Bootstrap / Bootstrap k3s Cluster (pull_request) Has been skipped
2026-05-30 19:16:36 +02:00
kasun 7212e0e3ef fix: add triggers and --node-name to k8s-bootstrap commands
Without triggers, commands cached in Pulumi state don't re-run when VMs
are deleted and recreated with new IDs. This caused stale state where
start/install commands were skipped while the new VMs were never
bootstrapped, leading to SSH "no route to host" failures.

All command resources now carry triggers: [vmId] so they are
automatically replaced (and re-run) whenever the underlying VM changes.

Also adds --node-name to every k3s install/join command so nodes register
under the expected name (k3s-master-1 etc.) regardless of the VM's actual
hostname, which cloud-init does not set explicitly.
2026-05-30 19:06:11 +02:00
kasun e9765bb073 removed netcat dependency with /dev/tcp
Deploy k8s Bootstrap / Pulumi Preview (pull_request) Successful in 41s
Deploy k8s Bootstrap / Bootstrap k3s Cluster (pull_request) Has been skipped
2026-05-30 17:24:49 +02:00
kasun f16bdf7d3b Merge pull request 'Feature/setting up k3s on all nodes' (#1) from feature/setting-up-k3s-on-all-nodes into main
Deploy k8s Bootstrap / Pulumi Preview (push) Has been skipped
Deploy Proxmox Infra / Pulumi Preview (push) Has been skipped
Deploy Proxmox Infra / Pulumi Deploy (push) Successful in 16m0s
Deploy k8s Bootstrap / Bootstrap k3s Cluster (push) Failing after 5m44s
Reviewed-on: #1
2026-05-29 18:50:54 +02:00
11 changed files with 298 additions and 117 deletions
-3
View File
@@ -1,3 +0,0 @@
GITEA_API_URL=https://gitea.kasuns.website/kasun/homelab-infrastructure-as-code.git
GITEA_TOKEN=ba3fd0f4851aa627e2088da1f94a596646ba2de7
@@ -85,31 +85,3 @@ jobs:
cloud-url: ${{ secrets.PULUMI_BACKEND_URL }}
env:
PULUMI_CONFIG_PASSPHRASE: ${{ secrets.PULUMI_CONFIG_PASSPHRASE }}
# Propagate kubeconfig to the downstream stacks so their next deploy picks it up
- name: Propagate kubeconfig to k8s-infra
run: |
echo "${{ secrets.K8S_INFRA_DEV_YAML }}" | base64 -d > k8s-infra/Pulumi.dev.yaml
cd k8s-infra && npm install
KUBECONFIG=$(cd ../k8s-bootstrap && pulumi stack output kubeconfig --show-secrets \
--cloud-url "${{ secrets.PULUMI_BACKEND_URL }}" -s dev)
pulumi config set --secret kubeconfig "$KUBECONFIG" \
--cloud-url "${{ secrets.PULUMI_BACKEND_URL }}" -s dev
# Re-encode updated config for the secret (update manually in Gitea after first run)
base64 -w 0 Pulumi.dev.yaml
working-directory: .
env:
PULUMI_CONFIG_PASSPHRASE: ${{ secrets.PULUMI_CONFIG_PASSPHRASE }}
- name: Propagate kubeconfig to k8s-apps
run: |
echo "${{ secrets.K8S_APPS_DEV_YAML }}" | base64 -d > k8s-apps/Pulumi.dev.yaml
cd k8s-apps && npm install
KUBECONFIG=$(cd ../k8s-bootstrap && pulumi stack output kubeconfig --show-secrets \
--cloud-url "${{ secrets.PULUMI_BACKEND_URL }}" -s dev)
pulumi config set --secret kubeconfig "$KUBECONFIG" \
--cloud-url "${{ secrets.PULUMI_BACKEND_URL }}" -s dev
base64 -w 0 Pulumi.dev.yaml
working-directory: .
env:
PULUMI_CONFIG_PASSPHRASE: ${{ secrets.PULUMI_CONFIG_PASSPHRASE }}
+18 -4
View File
@@ -30,10 +30,17 @@ jobs:
node-version: '24'
- name: Restore Stack Config
run: echo "${{ secrets.PULUMI_DEV_YAML }}" | base64 -d > proxmox-infra/Pulumi.dev.yaml
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: Install Pulumi CLI
run: curl -fsSL https://get.pulumi.com | sh && echo "$HOME/.pulumi/bin" >> $GITHUB_PATH
- name: Generate Local pfSense SDK
run: pulumi package add terraform-provider marshallford/pfsense
working-directory: proxmox-infra
- name: Preview
@@ -60,10 +67,17 @@ jobs:
node-version: '24'
- name: Restore Stack Config
run: echo "${{ secrets.PULUMI_DEV_YAML }}" | base64 -d > proxmox-infra/Pulumi.dev.yaml
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: Install Pulumi CLI
run: curl -fsSL https://get.pulumi.com | sh && echo "$HOME/.pulumi/bin" >> $GITHUB_PATH
- name: Generate Local pfSense SDK
run: pulumi package add terraform-provider marshallford/pfsense
working-directory: proxmox-infra
- name: Refresh State
+3 -1
View File
@@ -3,4 +3,6 @@
.vscode
node_modules/
bin/
Pulumi.dev.yaml
Pulumi.dev.yaml
sdks/
.env
+26 -5
View File
@@ -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://<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
# 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`.
@@ -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
+9 -7
View File
@@ -5,7 +5,7 @@ Bootstraps a k3s cluster on the 5 Proxmox VMs created by `proxmox-infra`. Starts
## How it works
1. Starts all 5 VMs via `POST /api2/json/nodes/{node}/qemu/{vmid}/status/start`
2. Waits for port 22 to open on each VM (`nc -z`)
2. Waits for port 22 to open on each VM (bash `/dev/tcp`)
3. Installs k3s on `k3s-master-1` with `--cluster-init --tls-san <master1Ip>`
4. Joins `k3s-master-2` and `k3s-master-3` as embedded etcd nodes
5. Joins `k3s-worker-1` and `k3s-worker-2` as agent nodes
@@ -18,12 +18,6 @@ VM IDs and the CI runner SSH private key are read automatically from the `proxmo
Run in this directory after `pulumi stack init dev`:
```bash
# Same as proxmox-infra
pulumi config set --secret pve1Endpoint "https://192.168.1.x:8006"
pulumi config set --secret pve1ApiToken "root@pam!pulumi=<token>"
pulumi config set --secret pve2Endpoint "https://192.168.1.y:8006"
pulumi config set --secret pve2ApiToken "root@pam!pulumi=<token>"
# Pre-shared k3s token — any strong random string
pulumi config set --secret k3sToken "$(openssl rand -hex 32)"
@@ -35,6 +29,14 @@ pulumi config set worker1Ip "192.168.1.x"
pulumi config set worker2Ip "192.168.1.x"
```
Proxmox credentials (`pve1Endpoint`, `pve1ApiToken`, `pve2Endpoint`, `pve2ApiToken`) are read automatically from the `proxmox-infra` stack outputs via StackReference — do **not** set them here.
## Deployment order
`proxmox-infra` must be deployed **before** k8s-bootstrap. The proxmox-infra stack exports the Proxmox credentials and VM IDs that k8s-bootstrap reads via StackReference. If proxmox-infra hasn't been re-deployed after its latest code changes, those outputs won't exist and k8s-bootstrap will fail.
In CI, both workflows run as previews on PRs (no deploy). Trigger `workflow_dispatch` on `deploy-proxmox-infra` first, then run k8s-bootstrap.
After setting config, re-encode `Pulumi.dev.yaml` and update the Gitea secret `K8S_BOOTSTRAP_DEV_YAML`:
```bash
base64 -w 0 Pulumi.dev.yaml
+134 -56
View File
@@ -33,123 +33,201 @@ const worker2Ip = config.require("worker2Ip");
const k3sToken = config.requireSecret("k3sToken");
// VM IDs and CI runner SSH key — read from proxmox-infra stack outputs
const vmIdsOutput = infraRef.requireOutput("vmIds") as pulumi.Output<Record<string, number>>;
const ciRunnerPrivateKey = infraRef.requireOutput("ciRunnerPrivateKey") as pulumi.Output<string>;
const vmIdsOutput = infraRef.requireOutput("vmIds") as pulumi.Output<
Record<string, number>
>;
const ciRunnerPrivateKey = infraRef.requireOutput(
"ciRunnerPrivateKey",
) as pulumi.Output<string>;
const master1VmId = vmIdsOutput.apply(ids => String(ids.master1));
const master2VmId = vmIdsOutput.apply(ids => String(ids.master2));
const master3VmId = vmIdsOutput.apply(ids => String(ids.master3));
const worker1VmId = vmIdsOutput.apply(ids => String(ids.worker1));
const worker2VmId = vmIdsOutput.apply(ids => String(ids.worker2));
const master1VmId = vmIdsOutput.apply((ids) => String(ids.master1));
const master2VmId = vmIdsOutput.apply((ids) => String(ids.master2));
const master3VmId = vmIdsOutput.apply((ids) => String(ids.master3));
const worker1VmId = vmIdsOutput.apply((ids) => String(ids.worker1));
const worker2VmId = vmIdsOutput.apply((ids) => String(ids.worker2));
// SSH connection helper
function conn(ip: string): command.types.input.remote.ConnectionArgs {
return { host: ip, user: "ubuntu", privateKey: ciRunnerPrivateKey };
return { host: ip, user: "ubuntu", privateKey: ciRunnerPrivateKey };
}
// ---------------------------------------------------------------------------
// Step 1: Start all VMs (fire-and-forget — VMs are owned by proxmox-infra)
// triggers: vmId — forces re-run when VMs are deleted and recreated
// ---------------------------------------------------------------------------
const startMaster1 = new command.local.Command("start-master-1", {
create: pulumi.interpolate`curl -sf -k -X POST -H "Authorization: PVEAPIToken=${pve1ApiToken}" "${pve1Endpoint}/api2/json/nodes/pve/qemu/${master1VmId}/status/start" 2>/dev/null || true`,
interpreter: ["/bin/bash", "-c"],
create: pulumi.interpolate`curl -sf -k -X POST -H "Authorization: PVEAPIToken=${pve1ApiToken}" "${pve1Endpoint}/api2/json/nodes/pve/qemu/${master1VmId}/status/start" 2>/dev/null || true`,
triggers: [master1VmId],
interpreter: ["/bin/bash", "-c"],
});
const startMaster2 = new command.local.Command("start-master-2", {
create: pulumi.interpolate`curl -sf -k -X POST -H "Authorization: PVEAPIToken=${pve1ApiToken}" "${pve1Endpoint}/api2/json/nodes/pve/qemu/${master2VmId}/status/start" 2>/dev/null || true`,
interpreter: ["/bin/bash", "-c"],
create: pulumi.interpolate`curl -sf -k -X POST -H "Authorization: PVEAPIToken=${pve1ApiToken}" "${pve1Endpoint}/api2/json/nodes/pve/qemu/${master2VmId}/status/start" 2>/dev/null || true`,
triggers: [master2VmId],
interpreter: ["/bin/bash", "-c"],
});
const startWorker1 = new command.local.Command("start-worker-1", {
create: pulumi.interpolate`curl -sf -k -X POST -H "Authorization: PVEAPIToken=${pve1ApiToken}" "${pve1Endpoint}/api2/json/nodes/pve/qemu/${worker1VmId}/status/start" 2>/dev/null || true`,
interpreter: ["/bin/bash", "-c"],
create: pulumi.interpolate`curl -sf -k -X POST -H "Authorization: PVEAPIToken=${pve1ApiToken}" "${pve1Endpoint}/api2/json/nodes/pve/qemu/${worker1VmId}/status/start" 2>/dev/null || true`,
triggers: [worker1VmId],
interpreter: ["/bin/bash", "-c"],
});
const startMaster3 = new command.local.Command("start-master-3", {
create: pulumi.interpolate`curl -sf -k -X POST -H "Authorization: PVEAPIToken=${pve2ApiToken}" "${pve2Endpoint}/api2/json/nodes/pve-bckp/qemu/${master3VmId}/status/start" 2>/dev/null || true`,
interpreter: ["/bin/bash", "-c"],
create: pulumi.interpolate`curl -sf -k -X POST -H "Authorization: PVEAPIToken=${pve2ApiToken}" "${pve2Endpoint}/api2/json/nodes/pve-bckp/qemu/${master3VmId}/status/start" 2>/dev/null || true`,
triggers: [master3VmId],
interpreter: ["/bin/bash", "-c"],
});
const startWorker2 = new command.local.Command("start-worker-2", {
create: pulumi.interpolate`curl -sf -k -X POST -H "Authorization: PVEAPIToken=${pve2ApiToken}" "${pve2Endpoint}/api2/json/nodes/pve-bckp/qemu/${worker2VmId}/status/start" 2>/dev/null || true`,
interpreter: ["/bin/bash", "-c"],
create: pulumi.interpolate`curl -sf -k -X POST -H "Authorization: PVEAPIToken=${pve2ApiToken}" "${pve2Endpoint}/api2/json/nodes/pve-bckp/qemu/${worker2VmId}/status/start" 2>/dev/null || true`,
triggers: [worker2VmId],
interpreter: ["/bin/bash", "-c"],
});
const allStarts = [startMaster1, startMaster2, startMaster3, startWorker1, startWorker2];
const allStarts = [
startMaster1,
startMaster2,
startMaster3,
startWorker1,
startWorker2,
];
// ---------------------------------------------------------------------------
// Step 2: Wait for SSH on master-1, install k3s
// ---------------------------------------------------------------------------
const waitMaster1Ssh = new command.local.Command("wait-ssh-master-1", {
create: `for i in $(seq 1 60); do nc -z -w 5 ${master1Ip} 22 && exit 0; sleep 5; done; exit 1`,
const waitMaster1Ssh = new command.local.Command(
"wait-ssh-master-1",
{
create: `for i in $(seq 1 60); do (timeout 5 bash -c "echo > /dev/tcp/${master1Ip}/22") 2>/dev/null && exit 0; sleep 5; done; exit 1`,
triggers: [master1VmId],
interpreter: ["/bin/bash", "-c"],
}, { dependsOn: allStarts });
},
{ dependsOn: allStarts },
);
const installMaster1 = new command.remote.Command("install-k3s-master-1", {
const installMaster1 = new command.remote.Command(
"install-k3s-master-1",
{
connection: conn(master1Ip),
create: pulumi.interpolate`curl -sfL https://get.k3s.io | K3S_TOKEN='${k3sToken}' sh -s - server --cluster-init --tls-san ${master1Ip}`,
}, { dependsOn: [waitMaster1Ssh] });
create: pulumi.interpolate`curl -sfL https://get.k3s.io | sudo K3S_TOKEN='${k3sToken}' sh -s - server --cluster-init --tls-san ${master1Ip} --node-name k3s-master-1`,
triggers: [master1VmId],
},
{ dependsOn: [waitMaster1Ssh] },
);
const waitK3sMaster1Ready = new command.remote.Command("wait-k3s-master-1-ready", {
const waitK3sMaster1Ready = new command.remote.Command(
"wait-k3s-master-1-ready",
{
connection: conn(master1Ip),
create: `for i in $(seq 1 60); do kubectl --kubeconfig /etc/rancher/k3s/k3s.yaml get node k3s-master-1 --no-headers 2>/dev/null | grep -q " Ready" && exit 0; sleep 10; done; exit 1`,
}, { dependsOn: [installMaster1] });
create: `for i in $(seq 1 60); do sudo kubectl --kubeconfig /etc/rancher/k3s/k3s.yaml get node k3s-master-1 --no-headers 2>/dev/null | grep -q " Ready" && exit 0; sleep 10; done; exit 1`,
triggers: [master1VmId],
},
{ dependsOn: [installMaster1] },
);
// ---------------------------------------------------------------------------
// Step 3: Wait for SSH on remaining masters, join cluster
// ---------------------------------------------------------------------------
const waitMaster2Ssh = new command.local.Command("wait-ssh-master-2", {
create: `for i in $(seq 1 60); do nc -z -w 5 ${master2Ip} 22 && exit 0; sleep 5; done; exit 1`,
const waitMaster2Ssh = new command.local.Command(
"wait-ssh-master-2",
{
create: `for i in $(seq 1 60); do (timeout 5 bash -c "echo > /dev/tcp/${master2Ip}/22") 2>/dev/null && exit 0; sleep 5; done; exit 1`,
triggers: [master2VmId],
interpreter: ["/bin/bash", "-c"],
}, { dependsOn: [waitK3sMaster1Ready] });
},
{ dependsOn: [waitK3sMaster1Ready] },
);
const waitMaster3Ssh = new command.local.Command("wait-ssh-master-3", {
create: `for i in $(seq 1 60); do nc -z -w 5 ${master3Ip} 22 && exit 0; sleep 5; done; exit 1`,
const waitMaster3Ssh = new command.local.Command(
"wait-ssh-master-3",
{
create: `for i in $(seq 1 60); do (timeout 5 bash -c "echo > /dev/tcp/${master3Ip}/22") 2>/dev/null && exit 0; sleep 5; done; exit 1`,
triggers: [master3VmId],
interpreter: ["/bin/bash", "-c"],
}, { dependsOn: [waitK3sMaster1Ready] });
},
{ dependsOn: [waitK3sMaster1Ready] },
);
const joinMaster2 = new command.remote.Command("join-k3s-master-2", {
const joinMaster2 = new command.remote.Command(
"join-k3s-master-2",
{
connection: conn(master2Ip),
create: pulumi.interpolate`curl -sfL https://get.k3s.io | K3S_TOKEN='${k3sToken}' sh -s - server --server https://${master1Ip}:6443`,
}, { dependsOn: [waitMaster2Ssh] });
create: pulumi.interpolate`curl -sfL https://get.k3s.io | sudo K3S_TOKEN='${k3sToken}' sh -s - server --server https://${master1Ip}:6443 --node-name k3s-master-2`,
triggers: [master2VmId],
},
{ dependsOn: [waitMaster2Ssh] },
);
const joinMaster3 = new command.remote.Command("join-k3s-master-3", {
const joinMaster3 = new command.remote.Command(
"join-k3s-master-3",
{
connection: conn(master3Ip),
create: pulumi.interpolate`curl -sfL https://get.k3s.io | K3S_TOKEN='${k3sToken}' sh -s - server --server https://${master1Ip}:6443`,
}, { dependsOn: [waitMaster3Ssh, joinMaster2] });
create: pulumi.interpolate`curl -sfL https://get.k3s.io | sudo K3S_TOKEN='${k3sToken}' sh -s - server --server https://${master1Ip}:6443 --node-name k3s-master-3`,
triggers: [master3VmId],
},
{ dependsOn: [waitMaster3Ssh, joinMaster2] },
);
// ---------------------------------------------------------------------------
// Step 4: Join worker nodes
// ---------------------------------------------------------------------------
const waitWorker1Ssh = new command.local.Command("wait-ssh-worker-1", {
create: `for i in $(seq 1 60); do nc -z -w 5 ${worker1Ip} 22 && exit 0; sleep 5; done; exit 1`,
const waitWorker1Ssh = new command.local.Command(
"wait-ssh-worker-1",
{
create: `for i in $(seq 1 60); do (timeout 5 bash -c "echo > /dev/tcp/${worker1Ip}/22") 2>/dev/null && exit 0; sleep 5; done; exit 1`,
triggers: [worker1VmId],
interpreter: ["/bin/bash", "-c"],
}, { dependsOn: [joinMaster3] });
},
{ dependsOn: [joinMaster3] },
);
const waitWorker2Ssh = new command.local.Command("wait-ssh-worker-2", {
create: `for i in $(seq 1 60); do nc -z -w 5 ${worker2Ip} 22 && exit 0; sleep 5; done; exit 1`,
const waitWorker2Ssh = new command.local.Command(
"wait-ssh-worker-2",
{
create: `for i in $(seq 1 60); do (timeout 5 bash -c "echo > /dev/tcp/${worker2Ip}/22") 2>/dev/null && exit 0; sleep 5; done; exit 1`,
triggers: [worker2VmId],
interpreter: ["/bin/bash", "-c"],
}, { dependsOn: [joinMaster3] });
},
{ dependsOn: [joinMaster3] },
);
const joinWorker1 = new command.remote.Command("join-k3s-worker-1", {
const joinWorker1 = new command.remote.Command(
"join-k3s-worker-1",
{
connection: conn(worker1Ip),
create: pulumi.interpolate`curl -sfL https://get.k3s.io | K3S_URL=https://${master1Ip}:6443 K3S_TOKEN='${k3sToken}' sh -s -`,
}, { dependsOn: [waitWorker1Ssh] });
create: pulumi.interpolate`curl -sfL https://get.k3s.io | sudo K3S_URL=https://${master1Ip}:6443 K3S_TOKEN='${k3sToken}' sh -s - --node-name k3s-worker-1`,
triggers: [worker1VmId],
},
{ dependsOn: [waitWorker1Ssh] },
);
const joinWorker2 = new command.remote.Command("join-k3s-worker-2", {
const joinWorker2 = new command.remote.Command(
"join-k3s-worker-2",
{
connection: conn(worker2Ip),
create: pulumi.interpolate`curl -sfL https://get.k3s.io | K3S_URL=https://${master1Ip}:6443 K3S_TOKEN='${k3sToken}' sh -s -`,
}, { dependsOn: [waitWorker2Ssh] });
create: pulumi.interpolate`curl -sfL https://get.k3s.io | sudo K3S_URL=https://${master1Ip}:6443 K3S_TOKEN='${k3sToken}' sh -s - --node-name k3s-worker-2`,
triggers: [worker2VmId],
},
{ dependsOn: [waitWorker2Ssh] },
);
// ---------------------------------------------------------------------------
// Step 5: Read kubeconfig from master-1, patch server URL for external access
// ---------------------------------------------------------------------------
const getKubeconfig = new command.remote.Command("get-kubeconfig", {
const getKubeconfig = new command.remote.Command(
"get-kubeconfig",
{
connection: conn(master1Ip),
create: `cat /etc/rancher/k3s/k3s.yaml`,
}, { dependsOn: [joinWorker1, joinWorker2] });
create: `sudo cat /etc/rancher/k3s/k3s.yaml`,
triggers: [master1VmId],
},
{ dependsOn: [joinWorker1, joinWorker2] },
);
export const kubeconfig = pulumi.secret(
getKubeconfig.stdout.apply(kc => kc.replace(/127\.0\.0\.1/g, master1Ip).trim()),
getKubeconfig.stdout.apply((kc) =>
kc.replace(/127\.0\.0\.1/g, master1Ip).trim(),
),
);
+6
View File
@@ -8,3 +8,9 @@ config:
pulumi:tags:
value:
pulumi:template: typescript
packages:
pfsense:
source: terraform-provider
version: 1.1.3
parameters:
- marshallford/pfsense
+54 -13
View File
@@ -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<string>;
}
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 };
+43
View File
@@ -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"
}
}
}
+5
View File
@@ -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/*"
}
}