Files
kasun 136e6c9eec
Deploy Proxmox Infra / Pulumi Preview (pull_request) Failing after 12s
Deploy Proxmox Infra / Pulumi Deploy (pull_request) Has been skipped
added pfsense provider for automating static ip setup
2026-06-01 00:51:22 +02:00

322 lines
8.8 KiB
TypeScript

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: true,
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: true,
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"],
},
),
);
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 };