Compare commits

..
11 Commits
Author SHA1 Message Date
kasun f8f14db80a Merge pull request 'changed grafana persistence to postgresql db' (#29) from persist-grafana-data-on-postgres into main
Deploy Monitoring / Pulumi Preview (push) Has been skipped
Deploy Monitoring / Pulumi Deploy (push) Successful in 1m37s
2026-07-08 22:47:53 +02:00
kasun ea35bb55e4 changed grafana persistence to postgresql db
Deploy Monitoring / Pulumi Preview (pull_request) Successful in 54s
Deploy Monitoring / Pulumi Deploy (pull_request) Has been skipped
2026-07-08 22:44:30 +02:00
kasun 7a54674ba2 added 2nd replica to prometheus
Deploy Monitoring / Pulumi Preview (push) Has been skipped
Deploy Monitoring / Pulumi Deploy (push) Successful in 1m6s
2026-07-06 00:14:59 +02:00
kasun 93a5eac028 set longer scrape and evaluation intervals
Deploy Monitoring / Pulumi Preview (push) Has been skipped
Deploy Monitoring / Pulumi Deploy (push) Successful in 1m2s
2026-07-05 21:41:47 +02:00
kasun 21c3d0402e added prometheus retention details
Deploy Monitoring / Pulumi Preview (push) Has been skipped
Deploy Monitoring / Pulumi Deploy (push) Successful in 1m2s
2026-07-05 21:24:23 +02:00
kasun 189fe17cf6 added taints to keep regular workloads of master nodes
Deploy k8s Infra / Pulumi Preview (push) Has been skipped
Deploy k8s Infra / Pulumi Deploy (push) Successful in 54s
2026-07-05 19:17:54 +02:00
kasun 0d233c1544 updated CLAUDE.MD with new Longhorn settings for dedicated mounts and persistence only set in worker nodes
Deploy k8s Infra / Pulumi Preview (push) Has been skipped
Deploy k8s Infra / Pulumi Deploy (push) Successful in 55s
2026-07-05 18:33:20 +02:00
kasun 71732df894 Merge pull request 'Bug/fix longhorn disk pressure' (#28) from bug/fix-longhorn-diskPressure into main
Deploy k8s Infra / Pulumi Preview (push) Has been skipped
Deploy k8s Infra / Pulumi Deploy (push) Successful in 1m11s
2026-07-05 03:52:05 +02:00
kasun 135458c9ac added patch to longhorn to use dedicated mount for longhorn to avoid using root disk and causing diskPressure
Deploy k8s Bootstrap / Pulumi Preview (pull_request) Successful in 3m18s
Deploy k8s Bootstrap / Bootstrap k3s Cluster (pull_request) Has been skipped
Deploy k8s Infra / Pulumi Preview (pull_request) Successful in 1m7s
Deploy k8s Infra / Pulumi Deploy (pull_request) Has been skipped
2026-07-05 03:46:42 +02:00
kasun b803ec937c added mounting of dedicated longhorn storage to worker nodes. 2026-07-05 03:45:06 +02:00
kasun bcccd79148 decreased replicas for prometheus from 3 to 2 2026-07-05 02:08:43 +02:00
5 changed files with 113 additions and 10 deletions
+2 -1
View File
@@ -11,7 +11,8 @@ Bootstraps a k3s cluster on the Proxmox VMs created by `proxmox-infra`. Starts V
3. Installs k3s on `k3s-master-1` with `--cluster-init --tls-san <master1Ip>` 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 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 5. Joins `k3s-worker-1` and `k3s-worker-2` as agent nodes
6. Reads `/etc/rancher/k3s/k3s.yaml` from master-1 via SSH, patches the server URL, and exports it as the secret stack output `kubeconfig` 6. Formats the workers' dedicated `scsi1` disk (ext4, if not already formatted) and mounts it at `/mnt/longhorn-extra` (via `/etc/fstab`) — this is the disk `03-k8s-infra` later registers as an extra Longhorn disk, to keep Longhorn off the root filesystem and avoid `DiskPressure`
7. Reads `/etc/rancher/k3s/k3s.yaml` from master-1 via SSH, patches the server URL, and exports it as the secret stack output `kubeconfig`
VM IDs and the CI runner SSH private key are read automatically from the `proxmox-infra` stack output via StackReference — no manual setup needed for those. VM IDs and the CI runner SSH private key are read automatically from the `proxmox-infra` stack output via StackReference — no manual setup needed for those.
+26 -1
View File
@@ -215,7 +215,32 @@ const joinWorker2 = new command.remote.Command(
); );
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Step 5: Read kubeconfig from master-1, patch server URL for external access // Step 5: Format and mount Longhorn disks
// ---------------------------------------------------------------------------
const workers = [worker1Ip, worker2Ip];
const workerSshReader = [waitWorker1Ssh, waitWorker2Ssh];
const mountLonghornDisks = workers.map((worker, i) => {
return new command.remote.Command(
`mount-longhorn-disk-${i}`,
{
connection: conn(worker),
create: `set -euo pipefail;
DISK_ID=$(ls /dev/disk/by-id/ | grep 'scsi1');
DISK_PATH="/dev/disk/by-id/$DISK_ID";
sudo blkid "$DISK_PATH" >/dev/null 2>&1 || sudo mkfs.ext4 "$DISK_PATH";
sudo mkdir -p /mnt/longhorn-extra;
echo ""$DISK_PATH" /mnt/longhorn-extra ext4 defaults,nofail 0 2" | sudo tee -a /etc/fstab;
sudo mount -a;
df -h /mnt/longhorn-extra`,
},
{ dependsOn: workerSshReader[i] },
);
});
// ---------------------------------------------------------------------------
// Step 6: Read kubeconfig from master-1, patch server URL for external access
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const getKubeconfig = new command.remote.Command( const getKubeconfig = new command.remote.Command(
+1 -1
View File
@@ -21,7 +21,7 @@ CI installs the `helm` CLI (`azure/setup-helm`) before running Pulumi.
| `truenas-nfs` StorageClass | Backed by an NFS share on TrueNAS (`Retain` reclaim policy) | | `truenas-nfs` StorageClass | Backed by an NFS share on TrueNAS (`Retain` reclaim policy) |
| cert-manager | Certificate lifecycle management | | cert-manager | Certificate lifecycle management |
| `letsencrypt-prod` ClusterIssuer | DNS-01 via Cloudflare, issues Let's Encrypt TLS certs | | `letsencrypt-prod` ClusterIssuer | DNS-01 via Cloudflare, issues Let's Encrypt TLS certs |
| Longhorn | Default block storage (`longhorn` StorageClass, 3 replicas) — used by app/monitoring PVCs | | Longhorn | Default block storage (`longhorn` StorageClass, 2 replicas) — used by app/monitoring PVCs. Worker nodes get an extra `longhorn-extra` disk at `/mnt/longhorn-extra` (a dedicated mount set up by `k8s-bootstrap`, patched onto the Longhorn `Node` CR here) so Longhorn data stays off the root disk and doesn't cause `DiskPressure`. `nodeDownPodDeletionPolicy` deletes both the StatefulSet and Deployment pod on node-down so stateful workloads reschedule automatically |
| kube-vip | DaemonSet that ARP-announces a virtual IP for LoadBalancer Services | | kube-vip | DaemonSet that ARP-announces a virtual IP for LoadBalancer Services |
| kube-vip-cloud-provider | Allocates LoadBalancer IPs from a single-address pool (`kubeVipAddress`) — k3s's built-in ServiceLB is not used | | kube-vip-cloud-provider | Allocates LoadBalancer IPs from a single-address pool (`kubeVipAddress`) — k3s's built-in ServiceLB is not used |
| `traefik` ServicePatch | Pins Traefik's LoadBalancer IP to `kubeVipAddress` | | `traefik` ServicePatch | Pins Traefik's LoadBalancer IP to `kubeVipAddress` |
+51 -3
View File
@@ -122,7 +122,7 @@ new k8s.apiextensions.CustomResource(
// ── Longhorn ────────────────────────────────────────────────────────────── // ── Longhorn ──────────────────────────────────────────────────────────────
new k8s.helm.v3.Release( const longhorn = new k8s.helm.v3.Release(
"longhorn", "longhorn",
{ {
name: "longhorn", name: "longhorn",
@@ -133,14 +133,46 @@ new k8s.apiextensions.CustomResource(
version: "1.12.0", version: "1.12.0",
values: { values: {
defaultSettings: { defaultSettings: {
defaultReplicaCount: 3, defaultReplicaCount: 2,
podDeletionPolicyWhenNodeIsDown: "delete-pod-and-force-delete-pod", nodeDownPodDeletionPolicy: "delete-both-statefulset-and-deployment-pod",
createDefaultDiskLabeledNodes: true,
}, },
}, },
}, },
opts(), opts(),
); );
// Patches Longhorn instances to use the mountend Longhorn storage on the worker nodes.
["k3s-worker-1", "k3s-worker-2"].forEach(
(nodeName) =>
new k8s.apiextensions.CustomResourcePatch(
`${nodeName}-longhorn-disks`,
{
apiVersion: "longhorn.io/v1beta2",
kind: "Node",
metadata: {
name: nodeName,
namespace: "longhorn-system",
annotations: { "pulumi.com/patchForce": "true" },
},
spec: {
disks: {
"longhorn-extra": {
allowScheduling: true,
diskDriver: "",
diskType: "filesystem",
evictionRequested: false,
path: "/mnt/longhorn-extra",
storageReserved: 0,
tags: [],
},
},
},
},
opts({ dependsOn: [longhorn] }),
),
);
// ── kube-vip ──────────── // ── kube-vip ────────────
const kubeVipNamespace = "kube-system"; const kubeVipNamespace = "kube-system";
@@ -149,6 +181,22 @@ const controlPlaneTolerations = [
{ key: "node-role.kubernetes.io/control-plane", effect: "NoSchedule" }, { key: "node-role.kubernetes.io/control-plane", effect: "NoSchedule" },
]; ];
// Keeps regular workloads off the master nodes; kube-vip and
// kube-vip-cloud-provider below explicitly tolerate these same taints.
["k3s-master-1", "k3s-master-2", "k3s-master-3"].forEach((nodeName) => {
new k8s.core.v1.NodePatch(
`${nodeName}-control-plane-taint`,
{
metadata: {
name: nodeName,
annotations: { "pulumi.com/patchForce": "true" },
},
spec: { taints: controlPlaneTolerations },
},
opts(),
);
});
const kubeVipServiceAccount = new k8s.core.v1.ServiceAccount( const kubeVipServiceAccount = new k8s.core.v1.ServiceAccount(
"kube-vip", "kube-vip",
{ metadata: { name: "kube-vip", namespace: kubeVipNamespace } }, { metadata: { name: "kube-vip", namespace: kubeVipNamespace } },
+33 -4
View File
@@ -13,6 +13,10 @@ const appsRef = new pulumi.StackReference(
const kubeconfig = bootstrapRef.requireOutput("kubeconfig"); const kubeconfig = bootstrapRef.requireOutput("kubeconfig");
const domain = appsRef.requireOutput("domain"); const domain = appsRef.requireOutput("domain");
const grafanaAdminPassword = config.requireSecret("grafanaAdminPassword"); const grafanaAdminPassword = config.requireSecret("grafanaAdminPassword");
const grafanaDbUser = config.requireSecret("grafanaDbUser");
const grafanaDbPassword = config.requireSecret("grafanaDbPassword");
const grafanaDbHost = config.requireSecret("grafanaDbHost");
const grafanaDbName = config.require("grafanaDbName");
const k8sProvider = new k8s.Provider("k3s", { kubeconfig }); const k8sProvider = new k8s.Provider("k3s", { kubeconfig });
@@ -24,6 +28,15 @@ const monitoringNs = new k8s.core.v1.Namespace(
const grafanaHost = pulumi.interpolate`grafana.${domain}`; const grafanaHost = pulumi.interpolate`grafana.${domain}`;
const grafanaDbSecret = new k8s.core.v1.Secret(
"grafana-db-credentials",
{
metadata: { namespace: monitoringNs.metadata.name },
stringData: { password: grafanaDbPassword },
},
{ provider: k8sProvider },
);
new k8s.helm.v3.Release( new k8s.helm.v3.Release(
"kube-prometheus-stack", "kube-prometheus-stack",
{ {
@@ -38,6 +51,7 @@ new k8s.helm.v3.Release(
values: { values: {
grafana: { grafana: {
adminPassword: grafanaAdminPassword, adminPassword: grafanaAdminPassword,
replicas: 2,
ingress: { ingress: {
enabled: true, enabled: true,
ingressClassName: "traefik", ingressClassName: "traefik",
@@ -45,14 +59,29 @@ new k8s.helm.v3.Release(
hosts: [grafanaHost], hosts: [grafanaHost],
tls: [{ secretName: "grafana-tls", hosts: [grafanaHost] }], tls: [{ secretName: "grafana-tls", hosts: [grafanaHost] }],
}, },
persistence: { env: {
enabled: true, GF_DATABASE_TYPE: "postgres",
storageClassName: "longhorn", GF_DATABASE_HOST: grafanaDbHost,
size: "2Gi", GF_DATABASE_NAME: grafanaDbName,
GF_DATABASE_USER: grafanaDbUser,
GF_DATABASE_SSL_MODE: "disable",
},
envValueFrom: {
GF_DATABASE_PASSWORD: {
secretKeyRef: {
name: grafanaDbSecret.metadata.name,
key: "password",
},
},
}, },
}, },
prometheus: { prometheus: {
prometheusSpec: { prometheusSpec: {
retention: "7d",
retentionSize: "15GB",
scrapeInterval: "60s",
evaluationInterval: "60s",
replicas: "2",
storageSpec: { storageSpec: {
volumeClaimTemplate: { volumeClaimTemplate: {
spec: { spec: {