Files

412 lines
16 KiB
Markdown

# RKE2 Upgrade Attempt & Tarball Recovery — 2026-07-12
## Goal
Update K8s cluster: OS updates on all VMs, RKE2 upgrade v1.35.2→v1.35.6,
Helm chart updates, fix broken components. Scope: K8s only (no PVE hosts).
## Phase 1: OS Updates (COMPLETED)
All 7 VMs updated via `qm guest exec` through Proxmox jumphost chain.
### VM→PVE-Node Discovery
Initial assumption that all VMs were on proxmox3 was WRONG. Had to scan
all PVE nodes to find actual placement:
```
118 (CP-01) → proxmox5 (NOT proxmox3)
130 (CP-02) → proxmox3
129 (CP-03) → proxmox2
128 (Worker-01) → proxmox5 (later moved to proxmox)
132 (Worker-02) → proxmox2
131 (Worker-03) → proxmox3
200 (mgmt-runner) → proxmox3
```
### Update Process
- VM200: Direct SSH (10.0.30.124) — 0 updates remaining
- VM118, VM129: Updated via qm guest exec on respective PVE nodes
- VM132: dpkg interrupted (timeout) — fixed with `dpkg --configure -a`
- All VMs: 0 remaining updates after completion
- Cluster stayed healthy throughout (6/6 nodes Ready)
### Command Pattern
```bash
ssh -i ~/.ssh/id_ed25519_proxmox root@10.0.20.10 \
"ssh -o ConnectTimeout=5 root@PVE_NODE 'qm guest exec VMID --timeout 600 \
-- sh -c \"apt-get update && apt-get upgrade -y\"'"
```
## Phase 2: RKE2 Upgrade (PARTIAL — TARBALL DISASTER)
### Initial Approach (WRONG)
Attempted manual tarball installation:
1. Downloaded `rke2.linux-amd64.tar.gz` (v1.35.6) to VM118
2. Extracted with `tar xf rke2.linux-amd64.tar.gz -C /`**FATAL ERROR**
### Tarball Destruction
The RKE2 tarball contains:
```
bin/ → RKE2 binaries
lib/ → systemd unit files
share/ → RKE2 data files
```
Extracting to `/` on Debian 12 (which uses usrmerge) replaced:
- `/bin` symlink (→ usr/bin) with a real directory containing only Rke2 binaries
- `/lib` symlink (→ usr/lib) with a real directory containing only systemd files
- Created bogus `/share` directory
Result: ALL system binaries inaccessible (`ls`, `sh`, `bash`, `systemctl`,
`mount`). `qm guest exec` returns exit code 29. VM was still running
(kernel + RKE2 processes in memory) but no new processes could start.
### Recovery: Ceph RBD Offline Mount
1. **Stopped VM118** (`qm stop 118`) — safe because 2/3 CP nodes still
provide etcd quorum
2. **Mapped RBD image** on proxmox5:
```bash
rbd map vm_disks/vm-118-disk-0 # → /dev/rbd1
sleep 2 # Wait for udev
```
3. **Found partitions**: `rbd1p1` (root fs 39.9G), `rbd1p14` (BIOS boot),
`rbd1p15` (EFI 124M)
4. **Mounted root partition**: `mount /dev/rbd1p1 /mnt/vm118-rescue`
5. **Verified damage**:
- `/mnt/vm118-rescue/bin` = real directory with only `rke2`, `rke2-killall.sh`, `rke2-uninstall.sh`
- `/mnt/vm118-rescue/lib` = real directory with only `systemd/`
- `/mnt/vm118-rescue/usr/bin/bash` = INTACT (original system binaries fine)
6. **Fixed symlinks**:
```bash
# Save RKE2 binaries
cp /mnt/vm118-rescue/bin/rke2 /mnt/vm118-rescue/usr/local/bin/
cp /mnt/vm118-rescue/bin/rke2-*.sh /mnt/vm118-rescue/usr/local/bin/
# Save systemd units
mkdir -p /mnt/vm118-rescue/usr/lib/systemd/system
cp -a /mnt/vm118-rescue/lib/systemd/system/rke2-*.service /mnt/vm118-rescue/usr/lib/systemd/system/
cp -a /mnt/vm118-rescue/lib/systemd/system/rke2-*.env /mnt/vm118-rescue/usr/lib/systemd/system/
# Restore symlinks
rm -rf /mnt/vm118-rescue/bin
ln -s usr/bin /mnt/vm118-rescue/bin
rm -rf /mnt/vm118-rescue/lib
ln -s usr/lib /mnt/vm118-rescue/lib
rm -rf /mnt/vm118-rescue/share
```
7. **Unmounted, unmapped, started VM**:
```bash
umount /mnt/vm118-rescue
rbd unmap /dev/rbd1
qm start 118
```
8. **Verified**: Guest agent responded after 60s boot. Shell works.
BUT RKE2 server stuck "activating" — v1.35.6 binary couldn't find
v1.35.6 runtime images (cached images were v1.35.2).
### Second Fix: Restore Matching Binary
Downloaded v1.35.2 tarball and extracted CORRECTLY:
```bash
wget https://github.com/rancher/rke2/releases/download/v1.35.2%2Brke2r1/rke2.linux-amd64.tar.gz
tar xf rke2.linux-amd64.tar.gz -C /usr/local/ # CORRECT!
rke2 --version # v1.35.2+rke2r1
systemctl start rke2-server
```
Node came back as Ready v1.35.2+rke2r1. Cluster fully healthy again.
### RBD Pitfalls Encountered
- **Device disappearance**: First `rbd map` created `/dev/rbd1` but it
vanished before mount. Had to unmap stale mappings and re-map.
- **No kpartx**: PVE node didn't have `kpartx`. Partition devices were
created by kernel automatically after `sleep 2` post-mapping.
- **partprobe failed**: `partprobe /dev/rbd1` gave "Could not stat device"
because the device had already disappeared. Re-mapping fixed it.
## Phase 2b: Proper Upgrade Path (PREREQS IDENTIFIED, BLOCKED ON SSH)
### IaC Repo Analysis
The `epic-2-k8s/ansible/playbook.yml` uses `lablabs.rke2` role v1.50.1:
```yaml
rke2_version: v1.35.2+rke2r1 # ← Change this to upgrade
rke2_token: "{{ lookup('env', 'RKE2_TOKEN') }}"
rke2_ha_mode: true
rke2_api_ip: 10.0.30.50
```
Play 2 does rolling restart: Workers (serial:1) → Masters (serial:1).
### Prerequisites Status
1. ✅ Ansible role installed on VM200 (`ansible-galaxy install -r requirements.yml -p /root/.ansible/roles`)
2. ✅ Inventory generated manually (tofu state shows VM IDs, IPs known)
3. ✅ RKE2 token extracted from VM130 config (`/etc/rancher/rke2/config.yaml` → `token:` line)
4. ❌ **SSH access from VM200 to K8s VMs** — `Permission denied (publickey)`
- mgmt-runner's SSH key NOT in `debian@VMs/.ssh/authorized_keys`
- Ansible inventory uses `ansible_user: debian`
- Need to deploy VM200's public key to all 6 K8s VMs before Ansible can run
### Tofu State Details
Tofu state at `/opt/tofu-state/rke2-cluster.tfstate` has VM IDs:
```
rke2-cp-01: vm_id=118, node=proxmox (original placement, HA may move)
rke2-cp-02: vm_id=130, node=proxmox2
rke2-cp-03: vm_id=129, node=proxmox3
rke2-worker-01: vm_id=128, node=proxmox
rke2-worker-02: vm_id=132, node=proxmox2
rke2-worker-03: vm_id=131, node=proxmox3
```
### User Directive
User instructed: "Nutze GitOps!" and "Neue nodes mit neuer version
erstellen. Cluster join. Verify. Workloads migrieren. Alte nodes löschen"
This means: prefer GitOps/Ansible for upgrades, or blue-green with new
VMs if rolling upgrade is too risky. NEVER manual tarball installation.
## Phase 3: openclaw-memory Removal (COMPLETED via GitOps)
### Context
User: "Openclaw memory kann weg. Nutze ich nicht mehr"
The `openclaw-memory` namespace had 3 broken pods:
- `memory-api` — CreateContainerConfigError (missing secret)
- `qdrant-0` — CreateContainerConfigError (missing secret)
- `ollama` — Running but unused
Two ExternalSecrets in `SecretSyncedError` state (wrong 1Password vault).
### Removal Steps (Clean GitOps)
1. **Deleted from Git repo**:
```bash
rm clusters/main/apps/memory.yaml # ArgoCD Application
rm -rf clusters/main/memory/ # All manifests
rm .github/workflows/epic6_prepare-memory-secrets.yml
git commit -m "chore: remove openclaw-memory (unused, deprecated)"
git push origin main # commit ebc4da5
```
2. **Deleted ArgoCD Application** (cascade=foreground):
```bash
kubectl --server=https://10.0.30.52:6443 \
delete application memory -n argocd --cascade=foreground
```
3. **Deleted namespace** (pods cleared, namespace went Terminating → Gone):
```bash
kubectl --server=https://10.0.30.52:6443 delete ns openclaw-memory
```
4. **Verified**: Namespace NotFound, 0 non-running pods, ArgoCD apps
all Healthy except `backups` (OutOfSync, non-critical).
### Key Observations
- ArgoCD Application `--cascade=foreground` cleaned up managed resources
(deployments, services) but namespace remained — needed manual `kubectl delete ns`.
- No operator CRs involved, so no finalizer issues (unlike MariaDB removal).
- `backups` ArgoCD app remains OutOfSync (pre-existing, not related).
## Phase 2c: SSH Key Deployment & Ansible Launch (COMPLETED)
### Problem
Ansible on VM200 could not SSH to K8s VMs — `Permission denied (publickey)`.
VM200 had no SSH keypair at all (`~/.ssh/` only had `authorized_keys` and
`known_hosts`).
### Fix
1. **Generated SSH key on VM200**:
```bash
ssh root@10.0.30.124 "ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -N '' -C 'vm200-ansible'"
```
2. **Confirmed actual VM→PVE-Node mapping** (all 6 nodes scanned):
```
118 (CP-01) → proxmox5
130 (CP-02) → proxmox3
129 (CP-03) → proxmox2
128 (Worker-01) → proxmox5 (later: proxmox)
131 (Worker-03) → proxmox3
132 (Worker-02) → proxmox2
```
3. **Distributed public key to all 6 VMs** via `qm guest exec` — each VM
got the key appended to `/home/debian/.ssh/authorized_keys` with
correct ownership (debian:debian) and permissions (700/600).
All 6 returned OK.
4. **Verified Ansible connectivity**:
```bash
cd /root/iac-homelab/epic-2-k8s/ansible && ansible all -m ping
```
All 6 nodes: SUCCESS => { "ping": "pong" }
### Ansible Playbook Execution
With all prerequisites met, launched the upgrade:
```bash
cd /root/iac-homelab/epic-2-k8s/ansible
export RKE2_TOKEN='EyMCxVDobAkWjgyPDcwvXRJQHgWHoH6qgDcvFhQUkd'
ansible-playbook playbook.yml -v
```
The playbook:
- Play 1: `lablabs.rke2` role applies `rke2_version: v1.35.6+rke2r1` to
all nodes (writes config, downloads/installs new binaries)
- Play 2: Rolling restart Workers (serial: 1) — each node restarted,
waits for RKE2 agent active + node Ready
- Play 3: Rolling restart Masters (serial: 1) — same pattern for CP
GitOps trail:
- Commit `6cb1b3c`: `feat(rke2): upgrade v1.35.2 → v1.35.6+rke2r1`
- Commit `ebc4da5`: `chore: remove openclaw-memory (unused, deprecated)`
## Phase 2d: dpkg Lock Blocking Ansible (RESOLVED)
### Problem
First Ansible playbook run failed on 5 of 6 nodes. Only Worker-01
succeeded (it had been updated cleanly earlier). The other 5 failed
at the pre_tasks stage with:
```
E: dpkg was interrupted, you must manually run 'sudo dpkg --configure -a'
```
Root cause: Previous OS updates via `qm guest exec` left dpkg in a
half-configured state on 5 nodes. The `openssh-server` package was
mid-upgrade (held a config file conflict prompt that hung without a TTY).
### Fix Sequence
Had to fix each node individually via `qm guest exec` (could not use
Ansible ad-hoc commands because Ansible itself triggers apt):
```bash
# For each broken node, on its hosting PVE node:
ssh root@PVE_NODE "qm guest exec VMID --timeout 120 -- bash -c \"
fuser -k /var/lib/dpkg/lock-frontend 2>/dev/null;
fuser -k /var/lib/dpkg/lock 2>/dev/null;
fuser -k /var/cache/debconf/config.dat 2>/dev/null;
sleep 2;
rm -f /var/lib/dpkg/lock-frontend /var/lib/dpkg/lock;
DEBIAN_FRONTEND=noninteractive dpkg --configure -a &&
apt-get install -y ceph-common rbd-nbd &&
echo FIXED
\""
```
All 5 nodes fixed (CP-01, CP-02, CP-03, Worker-02, Worker-03). Then
re-ran `ansible-playbook playbook.yml` successfully.
### Key Insight
The `lablabs.rke2` role's pre_tasks install `ceph-common` and `rbd-nbd`
via apt. If any node has dpkg in an interrupted state, the ENTIRE
playbook fails on that node. Must fix dpkg on ALL nodes before running
the upgrade playbook. Use `DEBIAN_FRONTEND=noninteractive` to avoid
config file conflict prompts hanging under `qm guest exec` (no TTY).
## Phase 2e: Upgrade Completion & Transient CP Failure (COMPLETED)
### Result
Second Ansible playbook run succeeded. All 6 nodes upgraded to
v1.35.6+rke2r1. PLAY RECAP showed `failed=1` on CP-03 (VM129) — but
investigation via `qm guest exec 129` confirmed the service was already
active and running v1.35.6+rke2r1. The failure was a timing race:
Ansible's `systemctl restart rke2-server` raced with RKE2's internal
reload from the binary swap. The service had already started before
Ansible tried to restart it, causing a transient "already activating"
error.
### Verification
```bash
kubectl get nodes -o wide
# All 6 nodes: Ready, v1.35.6+rke2r1, containerd 2.2.5-k3s2
kubectl get pods -A | grep -v Running | grep -v Completed
# Empty — 0 broken pods
```
### Lesson: Transient CP restart failures can be benign
If a CP node shows `failed=1` in the PLAY RECAP but is `Ready` and
running the target version, the failure was a race condition between
Ansible's `systemctl restart` and RKE2's internal reload during the
binary swap. No remediation needed — verify cluster state with
`kubectl get nodes` rather than blindly re-running the playbook.
## Remaining Work
1. ✅ OS Updates — DONE
2. ✅ RKE2 Upgrade v1.35.2→v1.35.6 — COMPLETED (commit 6cb1b3c)
3. ✅ Helm Chart Updates — COMPLETED (commits 36eb5c4, acd3231)
See `references/helm-chart-upgrades-2026-07.md` for full detail.
ArgoCD v2→v3, External Secrets, Ceph CSI RBD, CNPG, Velero all upgraded.
4. ✅ openclaw-memory — REMOVED via GitOps (commit ebc4da5)
5. ✅ ArgoCD `backups` app OutOfSync — ROOT CAUSE FOUND (cosmetic:
ESO default fields in live spec not in Git YAML). Fix pending
user approval (read-only investigation completed).
## Lessons Learned
1. **NEVER extract RKE2 tarball to `/`** — always use `/usr/local/`
2. **Debian 12 usrmerge**: `/bin`, `/lib`, `/sbin` are symlinks, not
real directories. Any tarball that creates `bin/` or `lib/` at root
will destroy the system.
3. **Ceph RBD offline mount** is viable for VM filesystem repair when
guest agent is broken — stop VM, map RBD, mount partition, fix, unmount, start.
4. **Always discover VM→PVE-Node mapping dynamically** — tofu state and
actual placement diverge due to HA migrations.
5. **Etcd quorum**: With 3 CP nodes, stopping 1 is safe (2/3 quorum).
Never stop 2 simultaneously.
6. **RKE2 version must match cached runtime images** — a newer binary
without matching images will loop "activating" forever.
7. **GitOps-first**: User expects cluster changes through IaC repo, not
manual interventions. Manual SSH/qm-guest-exec should be last resort.
8. **Non-operator app removal via GitOps** is straightforward: delete
manifests → push → delete ArgoCD App (cascade) → delete namespace.
No finalizer complications unlike operator-managed resources.
9. **Ansible SSH prerequisite**: mgmt-runner (VM200) needs its SSH public
key in `debian@VMs/.ssh/authorized_keys` before Ansible can reach K8s nodes.
Generate keypair on VM200, distribute via `qm guest exec` to all 6 VMs,
verify with `ansible all -m ping` before running the playbook.
10. **GitOps-first principle reinforced**: User explicitly directed to use
the IaC repo (`rke2_version` variable in Ansible playbook) for upgrades
rather than manual operations. All cluster changes should go through
`dominik/iac-homelab` on Gitea → commit → push → Ansible/ArgoCD.
11. **dpkg interrupted state blocks Ansible**: If OS updates via `qm guest
exec` leave dpkg half-configured (e.g. openssh-server mid-upgrade with
a config file conflict prompt hanging without TTY), the Ansible
playbook's pre_tasks (`apt install ceph-common rbd-nbd`) will fail on
that node. Fix: `fuser -k` on dpkg AND debconf locks, then
`DEBIAN_FRONTEND=noninteractive dpkg --configure -a` on each affected
node BEFORE running the upgrade playbook.
12. **debconf has its own lock**: Beyond `/var/lib/dpkg/lock-frontend` and
`/var/lib/dpkg/lock`, debconf maintains `/var/cache/debconf/config.dat`.
A stale process holding this lock causes `dpkg --configure -a` to fail
even after clearing dpkg locks. Must `fuser -k` all three.
13. **Transient CP restart failures can be benign**: Ansible's
`systemctl restart rke2-server` can race with RKE2's internal reload
during the binary swap. If a CP node shows `failed=1` in PLAY RECAP
but is `Ready` and running the target version, the failure was a
timing race — no remediation needed. Always verify with
`kubectl get nodes -o wide` rather than blindly re-running the playbook.