changed naming and order of folders and files to represent deployment order
Deploy Proxmox Infra / Pulumi Preview (push) Has been skipped
Deploy Proxmox Infra / Pulumi Deploy (push) Failing after 50s
Deploy k8s Bootstrap / Pulumi Preview (push) Has been skipped
Deploy k8s Bootstrap / Bootstrap k3s Cluster (push) Failing after 7s
Deploy k8s Infra / Pulumi Preview (push) Has been skipped
Deploy k8s Infra / Pulumi Deploy (push) Failing after 7s
Deploy Proxmox Infra / Pulumi Preview (push) Has been skipped
Deploy Proxmox Infra / Pulumi Deploy (push) Failing after 50s
Deploy k8s Bootstrap / Pulumi Preview (push) Has been skipped
Deploy k8s Bootstrap / Bootstrap k3s Cluster (push) Failing after 7s
Deploy k8s Infra / Pulumi Preview (push) Has been skipped
Deploy k8s Infra / Pulumi Deploy (push) Failing after 7s
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
This is a Pulumi TypeScript project (`proxmox-infra`) for provisioning VMs and LXC containers in Proxmox using the `@muhlba91/pulumi-proxmoxve` provider. The stack name is `dev`.
|
||||
|
||||
## Common Commands
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Preview infrastructure changes
|
||||
pulumi preview
|
||||
|
||||
# Sync Pulumi state with actual Proxmox state (run before up if resources were changed manually)
|
||||
pulumi refresh --yes
|
||||
|
||||
# Deploy infrastructure
|
||||
pulumi refresh --yes && pulumi up --yes
|
||||
|
||||
# Destroy infrastructure
|
||||
pulumi destroy
|
||||
|
||||
# View current stack outputs
|
||||
pulumi stack output
|
||||
|
||||
# View stack config
|
||||
pulumi config
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
- **Entry point**: `index.ts` — all Pulumi resources are declared here
|
||||
- **Provider**: `@muhlba91/pulumi-proxmoxve` v8.x — community Proxmox provider (not an official Pulumi provider)
|
||||
- **Stack**: `dev` — configured in `Pulumi.dev.yaml`
|
||||
- **Runtime**: Node.js with `npm`, TypeScript compiled to `bin/` (excluded from git)
|
||||
|
||||
## CI/CD (Gitea Actions)
|
||||
|
||||
Workflow file: `../.gitea/workflows/deploy-proxmox-infra.yaml`
|
||||
|
||||
Triggers: push to `main` and pull requests targeting `main`, scoped to changes under `proxmox-infra/**` or `.gitea/workflows/**`.
|
||||
|
||||
- **Pull request** → `pulumi preview` (no changes deployed)
|
||||
- **Push to main** → `pulumi refresh` then `pulumi up`
|
||||
|
||||
Secrets required in Gitea (`Settings → Actions → Secrets`):
|
||||
|
||||
- `PULUMI_BACKEND_URL` — PostgreSQL connection string for the self-hosted state backend
|
||||
- `PULUMI_CONFIG_PASSPHRASE` — passphrase used to decrypt secrets in `Pulumi.dev.yaml`
|
||||
- `PULUMI_DEV_YAML` — base64-encoded content of `Pulumi.dev.yaml`; update manually whenever the file changes: `base64 -w 0 Pulumi.dev.yaml`
|
||||
|
||||
## Key Notes
|
||||
|
||||
- Credentials for both Proxmox nodes are stored as encrypted secrets in `Pulumi.dev.yaml` and decrypted at runtime using `PULUMI_CONFIG_PASSPHRASE`. Do not pass Proxmox credentials via environment variables — the code uses `config.requireSecret()`.
|
||||
- There are two Proxmox providers: `pveProvider` (main node `pve`) and `pveBckpProvider` (backup node `pve-bckp`). Always pass the correct provider when adding resources.
|
||||
- `Pulumi.dev.yaml` contains the encryption salt — never delete it or secrets become unrecoverable.
|
||||
- TypeScript is compiled with strict mode, `nodenext` module resolution, and `noImplicitReturns` — all functions must have explicit return types when TypeScript cannot infer them.
|
||||
- Don't add a co-author when committing to git.
|
||||
@@ -0,0 +1,16 @@
|
||||
name: proxmox-infra
|
||||
description: Setting up VM and LXC in Proxmox
|
||||
runtime:
|
||||
name: nodejs
|
||||
options:
|
||||
packagemanager: npm
|
||||
config:
|
||||
pulumi:tags:
|
||||
value:
|
||||
pulumi:template: typescript
|
||||
packages:
|
||||
pfsense:
|
||||
source: terraform-provider
|
||||
version: 1.1.3
|
||||
parameters:
|
||||
- marshallford/pfsense
|
||||
@@ -0,0 +1,321 @@
|
||||
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();
|
||||
|
||||
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,
|
||||
insecure: true,
|
||||
});
|
||||
|
||||
const pveBckpProvider = new proxmox.Provider("pve-bckp", {
|
||||
endpoint: pve2Endpoint,
|
||||
apiToken: pve2ApiToken,
|
||||
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.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const ciRunnerKey = new tls.PrivateKey("ci-runner-key", {
|
||||
algorithm: "ED25519",
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Download Ubuntu Noble cloud image to each node's ISO storage
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const ubuntuNobleUrl =
|
||||
"https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img";
|
||||
|
||||
const ubuntuImagePve = new proxmox.download.File(
|
||||
"ubuntu-noble-pve",
|
||||
{
|
||||
nodeName: "pve",
|
||||
datastoreId: "pve-local-ext1",
|
||||
contentType: "import",
|
||||
fileName: "noble-server-cloudimg-amd64.qcow2",
|
||||
url: ubuntuNobleUrl,
|
||||
overwrite: false,
|
||||
overwriteUnmanaged: true,
|
||||
},
|
||||
{ provider: pveProvider },
|
||||
);
|
||||
|
||||
const ubuntuImagePveBckp = new proxmox.download.File(
|
||||
"ubuntu-noble-pve-bckp",
|
||||
{
|
||||
nodeName: "pve-bckp",
|
||||
datastoreId: "local",
|
||||
contentType: "import",
|
||||
fileName: "noble-server-cloudimg-amd64.qcow2",
|
||||
url: ubuntuNobleUrl,
|
||||
overwrite: false,
|
||||
overwriteUnmanaged: true,
|
||||
},
|
||||
{ provider: pveBckpProvider },
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// VM templates — one per node, cloned from the downloaded cloud image.
|
||||
// Templates are not started and have no cloud-init config; that is applied
|
||||
// per-clone so each node gets its own hostname (derived from VM name).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const templateSettings = {
|
||||
template: true,
|
||||
started: false,
|
||||
stopOnDestroy: true,
|
||||
scsiHardware: "virtio-scsi-pci",
|
||||
cpu: {
|
||||
cores: 2,
|
||||
sockets: 1,
|
||||
type: "host",
|
||||
numa: true,
|
||||
},
|
||||
memory: {
|
||||
dedicated: 2048,
|
||||
floating: 0,
|
||||
},
|
||||
networkDevices: [{ bridge: "vmbr0", model: "virtio" }],
|
||||
serialDevices: [{}],
|
||||
vga: { type: "serial0" },
|
||||
agent: { enabled: true },
|
||||
};
|
||||
|
||||
const pveTemplate = new proxmox.VmLegacy(
|
||||
"k3s-template-pve",
|
||||
{
|
||||
...templateSettings,
|
||||
nodeName: "pve",
|
||||
name: "k3s-ubuntu-noble-template",
|
||||
disks: [
|
||||
{
|
||||
interface: "scsi0",
|
||||
datastoreId: "local-lvm",
|
||||
importFrom: pulumi.interpolate`${ubuntuImagePve.datastoreId}:import/${ubuntuImagePve.fileName}`,
|
||||
size: 10,
|
||||
ssd: true,
|
||||
discard: "on",
|
||||
},
|
||||
],
|
||||
},
|
||||
{ provider: pveProvider },
|
||||
);
|
||||
|
||||
const pveBckpTemplate = new proxmox.VmLegacy(
|
||||
"k3s-template-pve-bckp",
|
||||
{
|
||||
...templateSettings,
|
||||
nodeName: "pve-bckp",
|
||||
name: "k3s-ubuntu-noble-template",
|
||||
disks: [
|
||||
{
|
||||
interface: "scsi0",
|
||||
datastoreId: "local",
|
||||
importFrom: pulumi.interpolate`${ubuntuImagePveBckp.datastoreId}:import/${ubuntuImagePveBckp.fileName}`,
|
||||
size: 10,
|
||||
ssd: true,
|
||||
discard: "on",
|
||||
},
|
||||
],
|
||||
},
|
||||
{ provider: pveBckpProvider },
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// k3s nodes — full clones of their respective templates
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const k3sVmPassword = config.requireSecret("k3sVmPassword");
|
||||
const sshPvePublicKey = config.requireSecret("sshPvePublicKey");
|
||||
|
||||
interface NodeConfig {
|
||||
name: string;
|
||||
role: "master" | "worker";
|
||||
nodeName: string;
|
||||
provider: proxmox.Provider;
|
||||
template: proxmox.VmLegacy;
|
||||
diskDatastore: string;
|
||||
ip: pulumi.Output<string>;
|
||||
}
|
||||
|
||||
const nodeConfigs: NodeConfig[] = [
|
||||
{
|
||||
name: "k3s-master-1",
|
||||
role: "master",
|
||||
nodeName: "pve",
|
||||
provider: pveProvider,
|
||||
template: pveTemplate,
|
||||
diskDatastore: "local-lvm",
|
||||
ip: master1Ip,
|
||||
},
|
||||
{
|
||||
name: "k3s-master-2",
|
||||
role: "master",
|
||||
nodeName: "pve",
|
||||
provider: pveProvider,
|
||||
template: pveTemplate,
|
||||
diskDatastore: "local-lvm",
|
||||
ip: master2Ip,
|
||||
},
|
||||
{
|
||||
name: "k3s-worker-1",
|
||||
role: "worker",
|
||||
nodeName: "pve",
|
||||
provider: pveProvider,
|
||||
template: pveTemplate,
|
||||
diskDatastore: "local-lvm",
|
||||
ip: worker1Ip,
|
||||
},
|
||||
{
|
||||
name: "k3s-master-3",
|
||||
role: "master",
|
||||
nodeName: "pve-bckp",
|
||||
provider: pveBckpProvider,
|
||||
template: pveBckpTemplate,
|
||||
diskDatastore: "local",
|
||||
ip: master3Ip,
|
||||
},
|
||||
{
|
||||
name: "k3s-worker-2",
|
||||
role: "worker",
|
||||
nodeName: "pve-bckp",
|
||||
provider: pveBckpProvider,
|
||||
template: pveBckpTemplate,
|
||||
diskDatastore: "local",
|
||||
ip: worker2Ip,
|
||||
},
|
||||
];
|
||||
|
||||
const k3sVms = nodeConfigs.map(
|
||||
(node) =>
|
||||
new proxmox.VmLegacy(
|
||||
node.name,
|
||||
{
|
||||
nodeName: node.nodeName,
|
||||
name: node.name,
|
||||
description: "k3s " + node.role + " node — managed by Pulumi",
|
||||
tags: ["k3s", node.role],
|
||||
clone: {
|
||||
vmId: node.template.vmId,
|
||||
full: true,
|
||||
datastoreId: node.diskDatastore,
|
||||
},
|
||||
cpu: {
|
||||
cores: 2,
|
||||
sockets: 1,
|
||||
type: "host",
|
||||
numa: true,
|
||||
},
|
||||
memory: {
|
||||
dedicated: 2048,
|
||||
floating: 0,
|
||||
},
|
||||
disks: [
|
||||
{
|
||||
interface: "scsi0",
|
||||
datastoreId: node.diskDatastore,
|
||||
size: 10,
|
||||
ssd: true,
|
||||
discard: "on",
|
||||
},
|
||||
],
|
||||
initialization: {
|
||||
datastoreId: node.diskDatastore,
|
||||
ipConfigs: [{ ipv4: { address: "dhcp" } }],
|
||||
userAccount: {
|
||||
username: "ubuntu",
|
||||
password: k3sVmPassword,
|
||||
keys: [
|
||||
sshPvePublicKey.apply((k) => k.trim()),
|
||||
ciRunnerKey.publicKeyOpenssh.apply((k) => k.trim()),
|
||||
],
|
||||
},
|
||||
},
|
||||
networkDevices: [{ bridge: "vmbr0", model: "virtio" }],
|
||||
scsiHardware: "virtio-scsi-pci",
|
||||
serialDevices: [{}],
|
||||
vga: { type: "serial0" },
|
||||
agent: { enabled: true },
|
||||
started: false,
|
||||
stopOnDestroy: true,
|
||||
},
|
||||
{
|
||||
provider: node.provider,
|
||||
retainOnDelete: true,
|
||||
ignoreChanges: ["clone", "started"],
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
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.
|
||||
export const vmIds = {
|
||||
master1: k3sVms[0].vmId,
|
||||
master2: k3sVms[1].vmId,
|
||||
worker1: k3sVms[2].vmId,
|
||||
master3: k3sVms[3].vmId,
|
||||
worker2: k3sVms[4].vmId,
|
||||
};
|
||||
|
||||
// CI runner SSH private key — consumed by k8s-bootstrap via StackReference.
|
||||
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 };
|
||||
Generated
+2722
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "proxmox-infra",
|
||||
"main": "index.ts",
|
||||
"devDependencies": {
|
||||
"@types/node": "^18",
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"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/*"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"outDir": "bin",
|
||||
"target": "es2020",
|
||||
"module": "nodenext",
|
||||
"moduleResolution": "nodenext",
|
||||
"sourceMap": true,
|
||||
"experimentalDecorators": true,
|
||||
"pretty": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitReturns": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"files": [
|
||||
"index.ts"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user