Edge cluster -- runbook¶
How to build an edge cluster -- a lab VM or the box -- all in one: every
step in order, and who creates which file. New to it? Use step by
step, which splits the same work into phases with a commit
and a check after each. General: the values of a concrete cluster live in its
folder (clusters/edge = edge-tt-test1, LabDA; clusters/edge-test2 =
edge-tt-test2, labul). Every hand-written file with its content:
from-scratch.md; the picture:
architecture.md.
Layout of a cluster folder: who creates what¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | |
DEV = written by hand (from-scratch.md).
GEN = render-cluster-apps output, never edited by hand: change
cluster-apps.yaml, re-render (step 5), commit. A re-render
without changes reproduces the files byte for byte, and every secret value is
a ref+sops into secrets/edge/, so nothing rotates. BOOT = committed by
the Flux bootstrap.
Three layers: edge-infra (wait) → edge-apps and edge-lab. Every layer
substitutes ${EDGE_*} from the ConfigMap cluster-vars, so environments
differ only in cluster-vars.yaml and in whether edge-lab exists. Why the
layers look like this: notes.md.
Recreate from scratch¶
1 2 3 4 5 | |
| # | Step | Tool | Developer writes | Generated / changed |
|---|---|---|---|---|
| 1 | Address + DNS | Clusterbook (lab) / router (box), Hetzner DNS | cluster-vars.yaml |
reservations, DNS records |
| 2 | VM + base OS | Backstage create-vm / request-vm |
-- | VM with sthings.baseos.setup |
| 3 | k3s + Cilium (k3s.md) | Ansible CLI or blueprints/vm execute-ansible-with-export |
k3s/* |
node, kubeconfig |
| 4 | Persistent secrets (once ever) | openssl, sops | secrets/edge/*, edge-root-ca.crt, infra/ca/edge-ca.enc.yaml |
-- |
| 5 | Flux files | blueprints/flux render-cluster-apps |
cluster-apps.yaml, infra/ca.yaml, .sourceignore, lab/ |
apps.yaml, *-platform.yaml, cluster-secrets/, the kustomization.yaml wiring |
| 6 | Flux | blueprints/flux bootstrap |
2 pragma comments |
config.yaml, secrets.yaml (committed) |
| 7 | OpenBao init | nothing to do: self-init on first start | -- | userpass users terraform + admin |
| 8 | OpenBao + MinIO config | dagger/terraform, sign-intermediate.sh |
env files | PKI/ACME, bucket + user |
| 9 | Lab-specific extras | e.g. a lab Vault issuer | -- | -- |
| 10 | Verify | kubectl, curl | -- | -- |
1. Address and DNS¶
Two addresses on the node's network: the Gateway VIP (internal names
*.EDGE_DOMAIN) and the players' VIP (public names *.EDGE_PLAY_DOMAIN).
Write them into clusters/$CLUSTER/cluster-vars.yaml
(from-scratch.md, 2.1).
Lab: Clusterbook. Either you already know two free addresses, or ask
Clusterbook -- with a) curl or b) Dagger. reserve with an ip claims it
only if free (409 otherwise) and never overwrites; assign overwrites a
holder and withdraws its DNS record -- use it only on purpose.
| Lab | Clusterbook | Network | DNS zone |
|---|---|---|---|
| LabDA | clusterbook.sthings-infra.4sthings.tiab.ssc.sva.de |
10.100.136 |
4sthings.tiab.ssc.sva.de |
| labul | clusterbook.infra.sthings-vsphere.labul.sva.de |
10.31.102 |
sthings-vsphere.labul.sva.de |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | |
Box: the router's dnsmasq (architecture.md, DNS commands).
Public players' name: a record in the Hetzner zone sthings-edge.com
(* → the players' VIP, or *.<sub> per lab cluster, e.g. *.test2);
cert-manager gets the wildcard from Let's Encrypt via DNS-01. API commands:
architecture.md, Public: Hetzner DNS.
2. VM + base OS¶
Lab: a Backstage template (create-vm with use case baseos: Crossplane
NativeProxmoxVM on machinery, stuttgart-things#3474; or request-vm: Dapr
worker + Terraform, stuttgart-things#3415). The order's AnsibleRun applies
sthings.baseos.setup. Nothing in this repo builds the VM. Box: Ubuntu on the
eMMC by hand, then sthings.baseos.setup (Moving to the hardware).
A DHCP address that belonged to another VM before: ssh-keygen -R <ip> and
compare the new host key with the console.
3. k3s + Cilium¶
k3s.md -- the three files of clusters/$CLUSTER/k3s/
(inventory, vars, collections) as cat <<EOF blocks, what the role sets up,
both deployment options (1: Ansible CLI, the kubeconfig lands on your machine; 2: Dagger
execute-ansible-with-export, the kubeconfig is exported from the container),
the checks and what a second run changes. Result: node Ready
(v1.35.9+k3s1, sqlite), Cilium 1.20.2 with Gateway API v1.6.1,
local-path, the CLIs on the node (k9s, flux, sops, age -- Tools on the
node), the kubeconfig in $KUBECONFIG -- and encrypted in
secrets/edge/kubeconfig-<node>.enc.yaml (SOPS + age).
4. Persistent secrets -- once ever¶
Skip this when they exist -- a second edge cluster reuses them. Every file with its content, the value generators and the SOPS encryption: from-scratch.md, section 1. They make a reinstall, another cluster or the move to the box invisible to clients and devices: same root, same seal key, same passwords.
5. Flux files¶
The hand-written files (from-scratch.md, section 2), then:
1 2 3 4 5 6 7 8 9 | |
Without Dagger: write the same files by hand (from-scratch.md,
2B) instead of cluster-apps.yaml.
Leave out --existing-secrets on the very first render (there is no
cluster-secrets/ yet). The wiring (kustomization.yaml at the root, in
infra/ and apps/) is generated too (spec.wiring); hand-written files are
listed there as extraResources. Merge to main: the FluxInstance syncs
refs/heads/main, and a path that only exists on a branch leaves flux-system
at path not found.
Later, to move to a newer flux release: bump spec.source.tag in
cluster-apps.yaml and re-render.
6. Flux¶
blueprints/flux v3.6.1 or newer (flux-operator 0.61.0, Flux 2.9.6):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | |
It commits config.yaml and secrets.yaml; pull them and add the two
# pragma: allowlist secret comments. env -u SSH_AUTH_SOCK: a stale agent
socket makes Dagger fail with failed to list SSH agent identities.
edge-infra is Ready after about 8 minutes; homerun2 takes about 7 more
(redis-stack).
7. OpenBao init -- nothing to do¶
OpenBao initialises itself on its first start (OPENBAO_INIT:
self-init-userpass, flux#640): userpass with terraform (PKI only, for
step 8) and admin (break-glass, by hand only), passwords from
secrets/edge/app-values.enc.yaml. No root token, no recovery keys; the
static seal unseals every start. If self-init fails (e.g. a password
missing), the server refuses to unseal: fix the secret, then delete PVC
data-openbao-0 and the pod. Details:
clusters/edge/terraform/openbao.
8. OpenBao + MinIO configuration¶
Both with dagger/terraform v0.136.0+, state in the cluster (kubernetes
backend), --extra-files <cluster>/edge-root-ca.crt,<root>/env/<env>.auto.tfvars.json
-- the environment file holds the addresses (and, for OpenBao, the ACME
domains and DNS resolver; from-scratch.md, section 3);
there are no defaults:
terraform/openbao (PKI
mount, role devices, ACME), then terraform/openbao/sign-intermediate.sh
(key generated inside OpenBao, CSR signed with the offline root; refuses if
the mount already has an issuer), and
terraform/minio (bucket
schmetterpause-cnpg, user + policy for the backups). Until MinIO is
configured, schmetterpause's WAL archiving retries.
9. Lab-specific extras¶
Whatever only one lab needs, documented in that cluster's README -- e.g.
edge-tt-test1's Vault issuer on the LabDA Vault and its CoreDNS forward for
the LabDA zone (clusters/edge/README.md).
10. Verify¶
1 2 3 4 5 6 7 8 | |
The first immediate CNPG backup of a fresh rollout fails (the plugin is not
registered yet); start a manual one, see notes.md. The backup
protects against database mistakes, not against losing the node: an
off-box copy is still open (harvester#364). The device path and the apps:
lab-testing.md.
Moving to the hardware¶
Same artifact, same persistent secrets, its own cluster folder. What changes:
cluster-apps.yaml: noedge-lablayer (and nolab/); re-render.cluster-vars.yaml: the box's VIPs and domain (edge.sthings.lab).- Own SOPS key: the box decrypts with its own cluster key
(
cluster-secrets/sops-age.enc.yaml, escrowed for the master key), not with the master key. Get it withdagger call -m github.com/stuttgart-things/blueprints/secrets cluster-age-key --existing clusters/$CLUSTER/cluster-secrets --master-age-key env:SOPS_AGE_KEY plaintextand bootstrap with it as--sops-age-key. Hand-encrypted files the box decrypts (infra/ca/edge-ca.enc.yaml) must be encrypted for that key too (sops updatekeys). Keep--escrow-recipientsso the master key can still read everything. - OS: Ubuntu on the eMMC by hand, then
sthings.baseos.setup, then k3s with the box's address. A static lease for the node (Ciliumk8sServiceHost), plus free addresses for the VIPs. - Terraform:
env/box.auto.tfvars.json(OpenBaoacme_dns_resolver= the router). - Steps 2 (VM) and 9 do not apply; 7 and 8 run as on any new instance.
DNS on the edge¶
*.<domain> has to resolve to the VIP on every client: the router's dnsmasq
(edge.sthings.lab, local=/edge.sthings.lab/, static leases, the public
players' names answered locally) -- architecture.md.
Until the router is set up: /etc/hosts on the few clients.
Still online¶
OS packages, the k3s binary, cilium-cli, Gateway API CRDs, and every image
and chart come from the internet at install or reconcile time. Fully offline
is a separate step: the role's air-gap vars (k3s_airgapped_*,
cilium_airgapped_*), a registry on the node with k3s registries.yaml
mirrors, the flux artifact mirrored into it, and the FluxInstance's
spec.sync switched to OCI.