Files
homelab/docs/fastpass-rebuild-runbook.md
2026-05-18 13:20:42 -05:00

434 lines
12 KiB
Markdown

# fastpass cluster rebuild runbook
Root cause: Talos 1.13 defaults to `enforce: baseline` for Pod Security admission
control. Cilium requires `NET_ADMIN`, `NET_RAW`, and `SYS_ADMIN` — all blocked
under baseline. Fix: `enforce: privileged` in talconfig.yaml before generating
machine configs. Everything else in this runbook follows from a clean wipe.
All commands run from `city-hall` (10.1.71.38) in the `talos/talhelper/`
directory unless otherwise noted.
---
## Phase 0 — pre-flight
Confirm tools are present and on the right versions:
```bash
talhelper --version # expect 3.1.10
talosctl version --client # expect v1.13.2
kubectl version --client # expect v1.36.x
helm version # expect v3.x
```
Confirm the age key and sops config are in place:
```bash
cat ~/.config/sops/age/keys.txt | head -1 # should show "# created: ..."
cat /opt/git/homelab/.sops.yaml # should reference your age pubkey
```
---
## Phase 1 — wipe and redeploy Talos nodes
### 1.1 Reset all six nodes
Run from `talos/talhelper/`. If the nodes are currently in a broken state,
reset them to wipe the install and reboot into maintenance mode:
```bash
# Control plane nodes
for node in 10.1.71.66 10.1.71.67 10.1.71.68; do
talosctl reset \
--talosconfig clusterconfig/talosconfig \
--nodes $node \
--graceful=false \
--reboot \
--system-labels-to-wipe STATE \
--system-labels-to-wipe EPHEMERAL
done
# Worker nodes
for node in 10.1.71.69 10.1.71.70 10.1.71.71; do
talosctl reset \
--talosconfig clusterconfig/talosconfig \
--nodes $node \
--graceful=false \
--reboot \
--system-labels-to-wipe STATE \
--system-labels-to-wipe EPHEMERAL
done
```
Wait ~2 minutes. Nodes boot into maintenance mode (no OS installed, just the
Talos installer kernel running in RAM). You can verify via Proxmox console —
the node should show the Talos maintenance screen.
If nodes are completely unreachable (talosctl can't connect), boot them from
the Talos ISO via Proxmox and they'll come up in maintenance mode automatically.
### 1.2 Regenerate machine configs
The talconfig.yaml now has `enforce: privileged`. Regenerate everything:
```bash
cd /opt/git/homelab/talos/talhelper
talhelper genconfig
```
This writes fresh configs to `clusterconfig/`. Commit the regenerated configs
before applying:
```bash
cd /opt/git/homelab
git add talos/
git commit -m "fix: set enforce:privileged for Cilium compatibility"
git push
```
### 1.3 Apply machine configs
```bash
cd /opt/git/homelab/talos/talhelper
talhelper gencommand apply --extra-flags="--insecure" | bash
```
Expected output for each node: `Applied configuration without a reboot`
The nodes will install Talos to disk and reboot automatically. Watch progress
on one control plane node:
```bash
talosctl -n 10.1.71.66 --talosconfig clusterconfig/talosconfig dmesg --follow
```
Wait for all six nodes to finish installing and come back up (~3-5 min).
### 1.4 Bootstrap etcd
Run bootstrap exactly once — on the first control plane node only:
```bash
talhelper gencommand bootstrap | bash
```
This initializes etcd on `space-mountain`. The other two control plane nodes
join automatically. Do not run bootstrap again — it will corrupt the cluster.
### 1.5 Get kubeconfig
```bash
talhelper gencommand kubeconfig | bash
# or directly:
talosctl kubeconfig \
--talosconfig clusterconfig/talosconfig \
--nodes 10.1.71.66 \
~/.kube/config
```
### 1.6 Verify all 6 nodes are Ready
```bash
kubectl get nodes -o wide --watch
```
Wait until all six show `Ready`. This takes 3-5 minutes after bootstrap.
Do not proceed to Phase 2 until all six nodes are `Ready`.
Expected output:
```
NAME STATUS ROLES AGE VERSION
big-thunder-mountain Ready control-plane 5m v1.32.3
haunted-mansion Ready <none> 4m v1.32.3
jungle-cruise Ready <none> 4m v1.32.3
peter-pans-flight Ready <none> 4m v1.32.3
space-mountain Ready control-plane 5m v1.32.3
splash-mountain Ready control-plane 5m v1.32.3
```
### 1.7 Approve all pending CSRs
```bash
kubectl get csr --no-headers | grep Pending | awk '{print $1}' | xargs kubectl certificate approve
```
Nodes will show `NotReady` until Cilium is installed — that's expected. The
condition clears after Phase 2.
---
## Phase 2 — install Cilium
Cilium is Helm-only. It is never managed by ArgoCD.
### 2.1 Add the Helm repo
```bash
helm repo add cilium https://helm.cilium.io/
helm repo update
```
### 2.2 Install Cilium
```bash
helm upgrade --install cilium cilium/cilium \
--version 1.17.3 \
--namespace kube-system \
--values /opt/git/homelab/talos/cilium/cilium-values.yaml \
--wait \
--timeout 5m
```
The `--wait` flag blocks until all Cilium pods are Running. If it times out,
check what's happening:
```bash
kubectl -n kube-system get pods -l app.kubernetes.io/name=cilium-agent
kubectl -n kube-system describe pod <cilium-agent-pod>
```
### 2.3 Apply IP pools
```bash
kubectl apply -f /opt/git/homelab/talos/cilium/ip-pools.yaml
```
### 2.4 Verify Cilium health
```bash
# Overall status
cilium status --wait
# All nodes should show cilium-agent as OK
kubectl -n kube-system get pods -l app.kubernetes.io/name=cilium-agent
# Confirm L2 announcement policy is active
kubectl get ciliuml2announcementpolicy
# Confirm IP pools are ready
kubectl get ciliumloadbalancerippool
```
### 2.5 Verify all nodes are Ready
```bash
kubectl get nodes
```
All six must show `Ready` before proceeding. If any are still `NotReady` after
Cilium is healthy, drain and uncordon to force a kubelet re-check:
```bash
kubectl drain <node> --ignore-daemonsets --delete-emptydir-data
kubectl uncordon <node>
```
---
## Phase 3 — bootstrap ArgoCD
ArgoCD is installed manually once. After that, everything else is GitOps.
### 3.1 Create the argocd namespace
```bash
kubectl create namespace argocd
```
### 3.2 Install ArgoCD
```bash
kubectl apply -n argocd \
-f https://raw.githubusercontent.com/argoproj/argo-cd/v3.4.2/manifests/install.yaml
```
Wait for ArgoCD to come up:
```bash
kubectl -n argocd rollout status deployment argocd-server --timeout=3m
```
### 3.3 Apply server config (insecure mode for ingress TLS termination)
ArgoCD needs to run with `--insecure` so ingress-nginx handles TLS. This is in
your repo at `cluster/argocd/argocd-cmd-params-cm.yaml`:
```bash
kubectl apply -f /opt/git/homelab/cluster/argocd/argocd-cmd-params-cm.yaml
kubectl -n argocd rollout restart deployment argocd-server
kubectl -n argocd rollout status deployment argocd-server --timeout=2m
```
The ConfigMap should contain:
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-cmd-params-cm
namespace: argocd
data:
server.insecure: "true"
```
### 3.4 Bootstrap 1Password Connect secret
This is the only manual `kubectl apply` after ArgoCD install. Everything else
comes from Git.
```bash
# Create the secret from your 1Password Connect credentials file
kubectl create namespace external-secrets
kubectl create secret generic onepassword-connect-secret \
--namespace external-secrets \
--from-file=1password-credentials.json=/path/to/1password-credentials.json \
--from-literal=token=<your-1password-connect-token>
```
Verify:
```bash
kubectl -n external-secrets get secret onepassword-connect-secret
```
### 3.5 Configure ArgoCD to access Gitea via SSH
Add the Gitea SSH key to ArgoCD's known hosts and create the repo secret:
```bash
# Get Gitea's SSH host key
ssh-keyscan -t ed25519 mad-tea-party.local.mk-labs.cloud >> /tmp/gitea-known-hosts
# Create the repo credentials secret
kubectl apply -f - <<EOF
apiVersion: v1
kind: Secret
metadata:
name: gitea-repo-creds
namespace: argocd
labels:
argocd.argoproj.io/secret-type: repository
stringData:
type: git
url: git@mad-tea-party.local.mk-labs.cloud:rblundon/homelab.git
sshPrivateKey: |
$(cat ~/.ssh/argocd_gitea_ed25519 | sed 's/^/ /')
EOF
```
Add Gitea to ArgoCD known hosts:
```bash
argocd cert add-ssh mad-tea-party.local.mk-labs.cloud \
--ssh-known-hosts-data "$(ssh-keyscan -t ed25519 mad-tea-party.local.mk-labs.cloud 2>/dev/null)"
```
### 3.6 Apply the root Application
This is the app-of-apps entry point. It points ArgoCD at `cluster/platform/`
and `cluster/applications/`:
```bash
kubectl apply -f /opt/git/homelab/cluster/argocd/application.yaml
```
The argocd Application in that file must have `prune: false` to prevent ArgoCD
from ever pruning its own namespace:
```yaml
# cluster/argocd/application.yaml (key fields)
spec:
project: default
source:
repoURL: git@mad-tea-party.local.mk-labs.cloud:rblundon/homelab.git
targetRevision: main
path: cluster/platform
directory:
exclude: "application.yaml" # prevents self-reference loop
destination:
server: https://kubernetes.default.svc
syncPolicy:
automated:
prune: false # NEVER prune argocd namespace
selfHeal: true
```
### 3.7 Watch the platform sync
```bash
# Get initial admin password
kubectl -n argocd get secret argocd-initial-admin-secret \
-o jsonpath='{.data.password}' | base64 -d; echo
# Port-forward to access UI (before ingress is up)
kubectl port-forward svc/argocd-server -n argocd 8080:443
# Or use CLI
argocd login localhost:8080 --username admin --insecure
# Watch sync status
argocd app list
argocd app get platform
```
Platform apps will sync in wave order (sync-wave annotation). Expected order:
1. Wave 0: cert-manager CRDs
2. Wave 1: cert-manager, external-secrets
3. Wave 2: ingress-nginx
4. Wave 3: external-dns
5. Wave 4: application workloads
---
## Phase 4 — verify platform
```bash
# All platform pods running
kubectl get pods -A | grep -v Running | grep -v Completed
# Ingress controller has a LB IP from the pool
kubectl -n ingress-nginx get svc ingress-nginx-controller
# cert-manager is issuing certs
kubectl get clusterissuers
# External secrets operator is syncing
kubectl -n external-secrets get pods
# external-dns is running
kubectl -n external-dns get pods
```
---
## Known gotchas (learned the hard way)
**ArgoCD self-prune:** The `argocd-config` Application must have `prune: false`.
If prune is enabled and ArgoCD syncs itself, it will delete its own resources
and take itself down. Recovery requires manual re-apply.
**Multi-source self-reference loop:** If an Application's source path contains
its own `application.yaml`, ArgoCD enters a sync loop. Fix: add
`directory.exclude: "application.yaml"` to the source spec.
**ArgoCD Helm valueFiles:** The correct field name is `valueFiles` (plural), not
`valuesFile` or `valuesFiles`. Wrong field name causes silent failure — ArgoCD
syncs green but ignores your values.
**Cilium managed by Helm only:** Never add Cilium to ArgoCD. If ArgoCD manages
Cilium and there's a sync conflict, ArgoCD can restart Cilium mid-cluster
operation, breaking all networking. Helm is the correct tool here.
**IP pool API version:** CiliumLoadBalancerIPPool must use `cilium.io/v2alpha1`.
Wrong version creates the object but it never activates — LB IPs are assigned
but not advertised.
**Talos VIP and NIC name:** The VIP (10.1.71.65) is configured per-node in the
`networkInterfaces` section. Each control plane node declares the VIP on `ens18`.
Workers do not declare the VIP. If the NIC name changes on your Proxmox
VMs, check with: `talosctl -n <ip> get addresses --talosconfig clusterconfig/talosconfig`
**Bootstrap once only:** `talhelper gencommand bootstrap` initializes etcd.
Running it a second time on an already-bootstrapped cluster corrupts etcd.
If you accidentally run it twice, you need to wipe and start over.