Add workload placement node labels

This commit is contained in:
juvdiaz 2026-05-26 23:02:36 -06:00
parent dfe7bbf4a7
commit 7b0b060a1c
4 changed files with 116 additions and 5 deletions

View File

@ -174,6 +174,24 @@ worker_nodes = {
Stateful apps currently pin retained local PVs to the `debian` node. Move or Stateful apps currently pin retained local PVs to the `debian` node. Move or
duplicate those PV manifests when you want storage on another node. duplicate those PV manifests when you want storage on another node.
## Workload Placement
`bootstrap/cluster` labels nodes with homelab placement metadata:
- `homelab.dev/node-role=control-plane` and `homelab.dev/storage=local` on the
Debian control plane
- `homelab.dev/node-role=edge-app` and `homelab.dev/storage=local` on the
Raspberry Pi worker
- `homelab.dev/node-role=app` and `homelab.dev/storage=nvme` on automated Pimox
worker clones
Override `control_plane_node_labels`, `worker_node_labels`,
`LAB_RASPBERRY_NODE_LABELS_JSON`, or `LAB_PIMOX_WORKER_NODE_LABELS_JSON` when
the physical layout changes. The current website, demos, registry, and Gitea
manifests are not moved automatically because the public NodePort path and
retained OpenEBS hostpath PVs are node-local. Move workloads only after their
storage and edge path are ready on the target node.
The website and demos NodePorts are reachable from the OCI jump box through the The website and demos NodePorts are reachable from the OCI jump box through the
Raspberry Pi Tailscale interface. `bootstrap/cluster` installs a persistent Raspberry Pi Tailscale interface. `bootstrap/cluster` installs a persistent
`homelab-tailscale-nodeport.service` on the configured worker to restore the `homelab-tailscale-nodeport.service` on the configured worker to restore the

View File

@ -635,6 +635,69 @@ EOT
} }
} }
locals {
control_plane_node_label_pairs = [
for label, value in var.control_plane_node_labels : {
node_name = var.control_plane_node_name
label = label
value = value
}
]
worker_node_label_pairs = flatten([
for worker_key, worker in var.worker_nodes : [
for label, value in lookup(var.worker_node_labels, worker_key, {}) : {
node_name = worker.node_name
label = label
value = value
}
]
])
node_label_pairs = concat(local.control_plane_node_label_pairs, local.worker_node_label_pairs)
}
resource "null_resource" "node_labels" {
for_each = {
for pair in local.node_label_pairs : "${pair.node_name}/${pair.label}" => pair
}
depends_on = [
null_resource.kubeadm_control_plane,
null_resource.kubeadm_worker,
]
triggers = {
kubeconfig_path = var.kubeconfig_path
node_name = each.value.node_name
label = each.value.label
value = each.value.value
}
provisioner "local-exec" {
interpreter = ["/bin/bash", "-lc"]
environment = {
KUBECONFIG_PATH = self.triggers.kubeconfig_path
NODE_NAME = self.triggers.node_name
NODE_LABEL = self.triggers.label
NODE_LABEL_VALUE = self.triggers.value
}
command = <<EOT
set -euo pipefail
for _ in $(seq 1 60); do
if kubectl --kubeconfig "$${KUBECONFIG_PATH}" get node "$${NODE_NAME}" >/dev/null 2>&1; then
break
fi
sleep 5
done
kubectl --kubeconfig "$${KUBECONFIG_PATH}" get node "$${NODE_NAME}" >/dev/null
kubectl --kubeconfig "$${KUBECONFIG_PATH}" label node "$${NODE_NAME}" "$${NODE_LABEL}=$${NODE_LABEL_VALUE}" --overwrite
EOT
}
}
output "kubeconfig_path" { output "kubeconfig_path" {
value = var.kubeconfig_path value = var.kubeconfig_path
} }

View File

@ -3,6 +3,14 @@ variable "control_plane_node_name" {
default = "debian" default = "debian"
} }
variable "control_plane_node_labels" {
type = map(string)
default = {
"homelab.dev/node-role" = "control-plane"
"homelab.dev/storage" = "local"
}
}
variable "control_plane_advertise_address" { variable "control_plane_advertise_address" {
type = string type = string
default = "192.168.100.68" default = "192.168.100.68"
@ -62,6 +70,17 @@ variable "worker_nodes" {
} }
} }
variable "worker_node_labels" {
type = map(map(string))
default = {
raspberrypi = {
"homelab.dev/node-role" = "edge-app"
"homelab.dev/storage" = "local"
}
}
}
variable "tailscale_nodeport_access" { variable "tailscale_nodeport_access" {
type = object({ type = object({
enabled = bool enabled = bool

21
lab.sh
View File

@ -270,10 +270,12 @@ write_cluster_worker_var_file() {
local var_file="$2" local var_file="$2"
LAB_INCLUDE_RASPBERRY_WORKER="${LAB_INCLUDE_RASPBERRY_WORKER:-true}" \ LAB_INCLUDE_RASPBERRY_WORKER="${LAB_INCLUDE_RASPBERRY_WORKER:-true}" \
LAB_RASPBERRY_HOST="${LAB_RASPBERRY_HOST:-192.168.100.89}" \ LAB_RASPBERRY_HOST="${LAB_RASPBERRY_HOST:-192.168.100.89}" \
LAB_RASPBERRY_USER="${LAB_RASPBERRY_USER:-jv}" \ LAB_RASPBERRY_USER="${LAB_RASPBERRY_USER:-jv}" \
LAB_RASPBERRY_NODE_NAME="${LAB_RASPBERRY_NODE_NAME:-raspberry}" \ LAB_RASPBERRY_NODE_NAME="${LAB_RASPBERRY_NODE_NAME:-raspberry}" \
LAB_RASPBERRY_SSH_KEY_PATH="${LAB_RASPBERRY_SSH_KEY_PATH:-/home/jv/.ssh/id_ed25519}" \ LAB_RASPBERRY_SSH_KEY_PATH="${LAB_RASPBERRY_SSH_KEY_PATH:-/home/jv/.ssh/id_ed25519}" \
LAB_RASPBERRY_NODE_LABELS_JSON="${LAB_RASPBERRY_NODE_LABELS_JSON:-{\"homelab.dev/node-role\":\"edge-app\",\"homelab.dev/storage\":\"local\"}}" \
LAB_PIMOX_WORKER_NODE_LABELS_JSON="${LAB_PIMOX_WORKER_NODE_LABELS_JSON:-{\"homelab.dev/node-role\":\"app\",\"homelab.dev/storage\":\"nvme\"}}" \
python3 - "${spec_file}" "${var_file}" <<'PY' python3 - "${spec_file}" "${var_file}" <<'PY'
import json import json
import os import os
@ -281,6 +283,13 @@ import sys
spec_file, var_file = sys.argv[1:3] spec_file, var_file = sys.argv[1:3]
nodes = {} nodes = {}
node_labels = {}
try:
raspberry_labels = json.loads(os.environ["LAB_RASPBERRY_NODE_LABELS_JSON"])
pimox_labels = json.loads(os.environ["LAB_PIMOX_WORKER_NODE_LABELS_JSON"])
except json.JSONDecodeError as exc:
raise SystemExit(f"Invalid node label JSON: {exc}") from exc
if os.environ["LAB_INCLUDE_RASPBERRY_WORKER"].lower() not in {"0", "false", "no", "off", "disabled"}: if os.environ["LAB_INCLUDE_RASPBERRY_WORKER"].lower() not in {"0", "false", "no", "off", "disabled"}:
nodes["raspberrypi"] = { nodes["raspberrypi"] = {
@ -289,6 +298,7 @@ if os.environ["LAB_INCLUDE_RASPBERRY_WORKER"].lower() not in {"0", "false", "no"
"node_name": os.environ["LAB_RASPBERRY_NODE_NAME"], "node_name": os.environ["LAB_RASPBERRY_NODE_NAME"],
"ssh_key_path": os.environ["LAB_RASPBERRY_SSH_KEY_PATH"], "ssh_key_path": os.environ["LAB_RASPBERRY_SSH_KEY_PATH"],
} }
node_labels["raspberrypi"] = raspberry_labels
with open(spec_file, encoding="utf-8") as handle: with open(spec_file, encoding="utf-8") as handle:
for line in handle: for line in handle:
@ -302,9 +312,10 @@ with open(spec_file, encoding="utf-8") as handle:
"node_name": node_name, "node_name": node_name,
"ssh_key_path": ssh_key_path, "ssh_key_path": ssh_key_path,
} }
node_labels[key] = pimox_labels
with open(var_file, "w", encoding="utf-8") as handle: with open(var_file, "w", encoding="utf-8") as handle:
json.dump({"worker_nodes": nodes}, handle, indent=2) json.dump({"worker_nodes": nodes, "worker_node_labels": node_labels}, handle, indent=2)
handle.write("\n") handle.write("\n")
PY PY
} }