Skip to content

Commit 30013a2

Browse files
web-flowclaude
andcommitted
docs(blog): add K3s cluster rebuild post-mortem with GitOps recovery
Chronicle of the January 2026 cluster disaster: - Disk failure on still-fawn triggered full rebuild - Documented the TLS certificate mismatch root cause - Explained why k3s-uninstall.sh is critical (not rm -rf) - Showed GitOps recovery in action with Flux - Added timing metrics and lessons learned Key takeaway: GitOps makes disaster recovery boring (in a good way) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 98aa8ff commit 30013a2

1 file changed

Lines changed: 196 additions & 0 deletions

File tree

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
# How I Blew Up My K3s Cluster and Didn't Panic (Thanks to GitOps)
2+
3+
**Date**: January 24, 2026
4+
**Time to full recovery**: ~2 hours
5+
**Panic level**: Surprisingly low
6+
7+
## The Disaster
8+
9+
It started with a simple disk failure on `still-fawn`, one of my Proxmox nodes. The 512GB SSD that held the ZFS root pool decided it had enough. The node wouldn't boot - just dropped into initramfs with "cannot import rpool".
10+
11+
No big deal, I thought. I have PBS backups. I'll just restore VM 108 (my K3s control plane node on still-fawn).
12+
13+
**Plot twist**: The PBS restore was painfully slow (2 MB/s due to chunk-based deduplication + ZFS random I/O). After 23 minutes at 5% progress, I made a decision: create a fresh VM instead.
14+
15+
That's when things got interesting.
16+
17+
## The Cascade
18+
19+
My K3s cluster was a 2-node HA setup with embedded etcd:
20+
- `pumped-piglet` (VM 105) - 32GB RAM, RTX 3070
21+
- `still-fawn` (VM 108) - 32GB RAM, AMD GPU, Coral TPU
22+
23+
With still-fawn down, etcd lost quorum. The cluster was effectively dead.
24+
25+
**What I tried (and failed at):**
26+
27+
1. Reset pumped-piglet to single-node with `k3s server --cluster-reset`
28+
2. Create fresh VM 108 via Crossplane ✓
29+
3. Join still-fawn to the cluster... ✗
30+
31+
The join kept failing with TLS certificate errors:
32+
```
33+
"rejected connection on peer endpoint","error":"remote error: tls: bad certificate"
34+
```
35+
36+
Every attempt would:
37+
1. Add a learner member to pumped-piglet's etcd
38+
2. Fail TLS handshake
39+
3. Leave stale learner in etcd
40+
4. Crash pumped-piglet's etcd (stuck trying to reach unreachable member)
41+
42+
I was going in circles. Reset pumped-piglet, try to join, fail, reset again. Classic insanity.
43+
44+
## The Root Cause
45+
46+
After much frustration (and some colorful language), I finally diagnosed it:
47+
48+
**Mixed CA certificates**. still-fawn had OLD CA files from previous failed attempts mixed with NEW client certs:
49+
50+
```
51+
/var/lib/rancher/k3s/server/tls/etcd/peer-ca.crt 06:30 (OLD!)
52+
/var/lib/rancher/k3s/server/tls/etcd/client.crt 06:59 (new)
53+
```
54+
55+
K3s downloads CA certs from the existing cluster during join. But if OLD CA files exist on disk, they're used instead, causing TLS mismatch.
56+
57+
**Why did the old files persist?** Because I was using `rm -rf /var/lib/rancher/k3s` instead of the proper uninstall script. The systemd service would restart and recreate partial state before the join completed.
58+
59+
## The Fix
60+
61+
One command: `/usr/local/bin/k3s-uninstall.sh`
62+
63+
This script removes EVERYTHING:
64+
- All `/var/lib/rancher/k3s/*`
65+
- All `/etc/rancher/k3s/*`
66+
- systemd service files
67+
- symlinks (`kubectl`, `crictl`, `ctr`)
68+
- CNI state
69+
- iptables rules
70+
71+
After a proper uninstall and fresh join:
72+
73+
```
74+
$ kubectl get nodes
75+
NAME STATUS ROLES AGE
76+
k3s-vm-pumped-piglet-gpu Ready control-plane,etcd,master 40m
77+
k3s-vm-still-fawn Ready control-plane,etcd,master 55s
78+
```
79+
80+
## Why I Didn't Panic: GitOps
81+
82+
Here's the thing - my entire cluster state is in Git. When the K3s cluster came back online, I just needed to:
83+
84+
1. Install Flux
85+
2. Create two secrets (SOPS age key + GitHub deploy key)
86+
3. Apply the GitOps sync config
87+
88+
```bash
89+
# Install Flux
90+
flux install
91+
92+
# Create SOPS decryption secret
93+
kubectl create secret generic sops-age \
94+
--namespace=flux-system \
95+
--from-file=age.agekey=~/.config/sops/age/keys.txt
96+
97+
# Create GitHub deploy key secret
98+
kubectl create secret generic flux-system \
99+
--namespace=flux-system \
100+
--from-file=identity=~/.ssh/flux-homeiac-home \
101+
--from-file=identity.pub=~/.ssh/flux-homeiac-home.pub \
102+
--from-literal=known_hosts="$(ssh-keyscan github.com 2>/dev/null)"
103+
104+
# Apply sync config
105+
kubectl apply -f gitops/clusters/homelab/flux-system/gotk-sync.yaml
106+
```
107+
108+
Within minutes, Flux was reconciling:
109+
110+
```
111+
$ flux get kustomizations
112+
NAME READY MESSAGE
113+
cert-manager-config True Applied revision: master@sha1:98aa8fff
114+
flux-system True Applied revision: master@sha1:98aa8fff
115+
metallb-config True Applied revision: master@sha1:98aa8fff
116+
traefik-config True Applied revision: master@sha1:98aa8fff
117+
```
118+
119+
HelmReleases started deploying automatically:
120+
- cert-manager ✓
121+
- MetalLB ✓
122+
- kube-prometheus-stack ✓
123+
- Crossplane ✓
124+
- external-dns ✓
125+
- Traefik ✓
126+
127+
All my apps, all my configs, all my secrets (SOPS-encrypted in Git) - everything came back.
128+
129+
## What GitOps Saved Me From
130+
131+
Without GitOps, I would have needed to:
132+
133+
1. Remember which Helm charts were installed and their versions
134+
2. Recreate all the Helm values files
135+
3. Remember the order of installation (CRDs before resources)
136+
4. Manually create all secrets
137+
5. Reconfigure all ingresses
138+
6. Hope I didn't forget anything
139+
140+
Instead, I just pointed Flux at my Git repo and went to make coffee.
141+
142+
## Lessons Learned
143+
144+
### 1. Use the Uninstall Script
145+
Never `rm -rf /var/lib/rancher/k3s`. Always use `/usr/local/bin/k3s-uninstall.sh`. It exists for a reason.
146+
147+
### 2. Dependency Ordering Matters
148+
Flux's server-side dry-run will fail if CRDs don't exist yet. Solution: separate Flux Kustomizations with `dependsOn`:
149+
150+
```yaml
151+
apiVersion: kustomize.toolkit.fluxcd.io/v1
152+
kind: Kustomization
153+
metadata:
154+
name: metallb-config
155+
spec:
156+
dependsOn:
157+
- name: flux-system # Wait for MetalLB HelmRelease
158+
path: ./infrastructure-config/metallb-config/resources
159+
```
160+
161+
### 3. Keep Your Age Key Safe
162+
The SOPS age private key (`~/.config/sops/age/keys.txt`) is the master key to all your encrypted secrets. Back it up. Seriously.
163+
164+
### 4. Document the Recovery Process
165+
I now have a complete runbook at `docs/runbooks/still-fawn-recovery-2026-01.md` with every command needed to rebuild from scratch.
166+
167+
### 5. GitOps is Not Just Hype
168+
When your cluster dies at 2 AM, you don't want to be piecing together configs from memory. Git is your source of truth. Flux makes it real.
169+
170+
## The Numbers
171+
172+
| Metric | Value |
173+
|--------|-------|
174+
| Time from disk failure to cluster rebuild | ~90 minutes |
175+
| Time from Flux install to apps running | ~5 minutes |
176+
| Manual kubectl commands after Flux | 0 |
177+
| Secrets recreated manually | 2 (age key + deploy key) |
178+
| Configs recreated manually | 0 |
179+
| Sleep lost | Some, but not as much as expected |
180+
181+
## Final Thoughts
182+
183+
Disasters happen. Disks fail. Clusters die. The question isn't *if* but *when*.
184+
185+
GitOps doesn't prevent disasters - it makes recovery boring. And boring is exactly what you want at 2 AM.
186+
187+
My cluster is now running again with all services restored. The only evidence of the disaster is this blog post and a slightly updated runbook.
188+
189+
If you're still doing `kubectl apply -f` by hand, consider this your sign to embrace GitOps. Future-you will thank present-you.
190+
191+
---
192+
193+
**Stack**: K3s, Flux, SOPS, Proxmox, PBS
194+
**Repo**: [homeiac/home](https://github.com/homeiac/home)
195+
196+
**Tags**: k3s, kubernetes, gitops, flux, disaster-recovery, etcd, homelab, infrastructure-as-code, sops

0 commit comments

Comments
 (0)