Wetopi RKE2 Operations Handbook¶
Version: 0.2 — corrected per approver REQUEST_CHANGES (task t_f1aeb7f4 → fix t_6dcba59b), re-verified live 2026-08-30 03:37-03:40 UTC
Status: command blocks in this handbook were re-verified read-only on the live cluster as ubuntu over the documented SSH path (rc recorded for each in the task evidence; see "Verification method" below). This is operational memory distilled from bring-up — it does not replace the runbook.
Sources of truth (precedence): docs/platform/rke2-apply-runbook.md (merged main 7cc387a) → ADR-0004 → this handbook. Where this handbook and the runbook disagree, the runbook prevails. One known divergence is recorded in §3.2 (etcd certificate form: the runbook's kubectl-exec form references client.crt, which does not exist inside the etcd container at v1.36.3+rke2r1 — use the server-client form below and see the reconciliation note there).
Current state (dated observation — 2026-08-30 03:37 UTC, re-read via kubectl get nodes -L wetopi.io/role on cp01): 6/6 nodes Ready (3 CPs + 3 platform workers, all workers labeled wetopi.io/role=platform), RKE2 v1.36.3+rke2r1 on all nodes, Cilium v1.20.1 present on all 6 nodes (6 CiliumNode objects). Phases B, C, F6 (drills), E (Cilium) and D (workers) are COMPLETE; Phase D2 (full 6-node validation, task t_69d9d0bf) is IN FLIGHT (gates A-E green at 03:07-03:12 UTC, cilium connectivity test still running). Live state moves while D2 runs — re-read before relying on it.
Verification method (what "verified" means here): every command block marked verified below was executed read-only as the ubuntu user over the documented SSH path (§2) at 2026-08-30 03:37-03:40 UTC, with rc recorded. On the CPs/workers ubuntu has (ALL) NOPASSWD: ALL sudo (sudo -l evidence; sudo -n true → rc=0 works in non-interactive contexts — only sudo -v specifically demands a tty for password validation, rc=1). The rc=141 occasionally seen in evidence next to systemctl status invocations is SIGPIPE from piping the pager output into head — systemctl status rke2-server itself returns rc=0 when not piped (verified rc=0 unpiped).
1. Cluster identity¶
| Distribution | RKE2 v1.36.3+rke2r1 (pinned; binary rke2 version → v1.36.3+rke2r1 (c4f306e), kubelet v1.36.3+rke2r1 on every node) |
| Topology | 3 CP (embedded etcd) + 3 platform workers + external bastion/recovery node |
| CPs | rke-cp01 = 10.72.0.21, cp02 = .22, cp03 = .23 (LAN, VLAN 4) |
| Workers | rke-platform01 = 10.72.0.41, 02 = .42, 03 = .43 |
| Bastion | wetopi-ops01 = 10.72.0.10 (WAN 92.222.245.91) — ProxyJump target + separately provisioned phase-E host (NOT the Ansible controller; see §2.1) |
| API VIP | 10.72.0.20 (keepalived on CPs; server URL https://10.72.0.20:9345, API https://10.72.0.20:6443) |
| CNI | none at bootstrap (by design) → Cilium+Hubble as first addon (phase E; live: cilium v1.20.1 DaemonSet 6/6) |
| Cluster CIDRs | pods 10.64.0.0/14, services 10.68.0.0/16 |
2. Access patterns (verified 2026-08-30 03:37 UTC)¶
# Bastion direct (public SSH is rate-limited by in-VM nftables — by design)
ssh -i /opt/data/.ssh/bigardo ubuntu@92.222.245.91
# LAN VMs via ProxyCommand — NOTE: -J does NOT work (drops the -i for the jump host)
ssh -i /opt/data/.ssh/bigardo -o IdentitiesOnly=yes \
-o ProxyCommand="ssh -i /opt/data/.ssh/bigardo -o IdentitiesOnly=yes -W %h:%p ubuntu@92.222.245.91" \
ubuntu@10.72.0.21
2.1 Controller and host roles (corrected — matches runbook §0 item 9)¶
- Controller = the workstation (the box that holds the SSH private keys and runs the Ansible playbooks for phases B/C/D/D2). Per runbook §0 item 9 it REQUIRES the pinned Ansible venv (
requirements-dev.txtpinsansible-core==2.21.3; verify.venv/bin/ansible-playbook --versionreports 2.21.3) to run the B/C/D/D2 playbooks — the playbooks are invoked as.venv/bin/ansible-playbookfrom the infrastructure checkout on the workstation. Live controller caveat (workstation, 2026-08-30): nosudobinary, private key/opt/data/.ssh/bigardopresent. The runbook's controller caveat is about the workstation:become: truedelegated tolocalhostfails there (sudo: not found) — the inventory fix is an ungroupedlocalhosthost withansible_connection: local, ansible_become: false. - wetopi-ops01 is NOT the controller. Per runbook §0 item 9 it is the ProxyJump target (bastion) plus a separately provisioned phase-E host (Cilium Helm op, D2 cilium/hubble CLI acceptance, F2/G commands). Live ops01 (2026-08-30 03:37 UTC):
sudoIS present at/usr/bin/sudo(passwordless for ubuntu:sudo -n true→ OK), Ansible venv PRESENT (~/wetopi-infrastructure/.venv/bin/ansible→ core 2.21.3), infrastructure checkout at~/wetopi-infrastructurehead ce065f2, and the transferred admin kubeconfig present at~/secrets/rke2-admin-kubeconfig(mode 600, 2954 bytes).kubectllives at~/bin/kubectl(v1.36.3, on PATH for login shells; use the absolute path orexport PATH=$HOME/bin:$PATHin scripts). Also on~/bin: cilium-cli v0.19.1, hubble v0.13.6, helm v3.19.0. - Ops quick-checks on ops01 (verified 2026-08-30 03:39 UTC, remote form, rc=0):
(
export KUBECONFIG=~/secrets/rke2-admin-kubeconfig kubectl get nodes # 6/6 Ready, v1.36.3+rke2r1 kubectl -n kube-system get pods | grep -E 'etcd-|apiserver|coredns'kubectl get nodes -L wetopi.io/roleshows the 3 workers carryingplatform.)
3. Verified commands (v1.36.3+rke2r1)¶
All blocks in this section executed read-only as ubuntu over the §2 SSH path, rc recorded (2026-08-30 03:37-03:39 UTC).
3.1 CP-local kubectl (the form the playbook gates use — REQUIRES sudo)¶
/etc/rancher/rke2/rke2.yaml is mode 600 root:root; the bundled kubectl is 755 but cannot read the kubeconfig as ubuntu:
- plain kubectl get nodes as ubuntu → command not found (rc=127; kubectl is not in PATH)
- KUBECONFIG=/etc/rancher/rke2/rke2.yaml /var/lib/rancher/rke2/bin/kubectl get nodes as ubuntu → error loading config file ...: permission denied (rc=1)
- without KUBECONFIG, the bundled kubectl falls back to localhost:8080 and fails (rc=1)
The working CP-local form therefore prefixes sudo (verified, rc=0, 6/6 nodes listed):
sudo /var/lib/rancher/rke2/bin/kubectl --kubeconfig /etc/rancher/rke2/rke2.yaml get nodes
sudo /var/lib/rancher/rke2/bin/kubectl --kubeconfig /etc/rancher/rke2/rke2.yaml get nodes -L wetopi.io/role
sudo /var/lib/rancher/rke2/bin/kubectl --kubeconfig /etc/rancher/rke2/rke2.yaml -n kube-system get pods
Health via the VIP (client kube-apiserver certs on the host, verified 200-equivalent ok):
sudo curl -s --cacert /var/lib/rancher/rke2/server/tls/server-ca.crt \
--cert /var/lib/rancher/rke2/server/tls/client-kube-apiserver.crt \
--key /var/lib/rancher/rke2/server/tls/client-kube-apiserver.key \
https://10.72.0.20:6443/readyz # → ok (rc=0)
# same check through the bundled kubectl (verified rc=0 → "ok")
sudo /var/lib/rancher/rke2/bin/kubectl --kubeconfig /etc/rancher/rke2/rke2.yaml \
--server=https://10.72.0.20:6443 get --raw /readyz
(Ansible runs these as root via become: true on the CP target, so the playbooks' un-prefixed form still works there — the sudo-less forms above are the human quick-check forms.)
3.2 etcd member list (THE version-specific lesson)¶
The live-working form (verified 2026-08-30 03:37 UTC, rc=0, 3 members all started):
sudo /var/lib/rancher/rke2/bin/kubectl --kubeconfig /etc/rancher/rke2/rke2.yaml -n kube-system \
exec etcd-rke-cp01 -- \
etcdctl --endpoints=https://127.0.0.1:2379 \
--cacert=/var/lib/rancher/rke2/server/tls/etcd/server-ca.crt \
--cert=/var/lib/rancher/rke2/server/tls/etcd/server-client.crt \
--key=/var/lib/rancher/rke2/server/tls/etcd/server-client.key \
member list
Pitfalls found live (do not repeat):
- The etcd container neither contains client.crt/client.key nor the env binary. The runbook's kubectl-exec form (rke2-apply-runbook.md, e.g. its Phase B gate) passes env ETCDCTL_API=3 etcdctl ... --cert=/var/lib/rancher/rke2/server/tls/etcd/client.crt --key=.../client.key; executed exactly it fails FIRST at the env prefix itself (OCI runtime exec failed: exec: "env": executable file not found in $PATH, rc=1, verified 2026-08-30 03:39 UTC); and with the env prefix removed but client.crt kept, it fails inside the container with Error: open /var/lib/rancher/rke2/server/tls/etcd/client.crt: no such file or directory (rc=128, verified 03:37 UTC). The HOST directory /var/lib/rancher/rke2/server/tls/etcd/ does contain client.crt/client.key (plus peer-* and server-client.* pairs) — both absences are container-path/mount facts, not host facts. Until the runbook is amended (open PR docs#8 owns that file — follow-up noted, do not edit it here), the server-client form above is the operator-facing source of truth for v1.36.3+rke2r1. It has also been re-verified WITH the env ETCDCTL_API=3 prefix and server-client certs that the env prefix is the sole failure there (rc=1, same missing-env error) — etcdctl alone with full flags is the form that works; the minimal container also has NO sh/ls, so use kubectl-exec passing etcdctl directly, never shell utilities.
- member list plain output includes a table header — for asserts use member list -w json and parse .members | length (verified rc=0, 3 members in JSON; the merged fix, infra PR#7 6d72da5).
3.3 Worker-side facts (verified on rke-platform01, 2026-08-30 03:38 UTC)¶
- The agent's kubelet kubeconfig lives at
/var/lib/rancher/rke2/agent/kubelet.kubeconfig(verified: PRESENT, mode 600 root:root, 464 bytes — contents never read for this check). Sibling agent kubeconfigs in the same dir:kubeproxy.kubeconfig,rke2controller.kubeconfig, plusclient-kubelet.crt/.key,client-kube-proxy.crt/.key,client-rke2-controller.crt/.key. - On workers the same CP-local negative facts hold: plain
kubectlrc=127;KUBECONFIG=/etc/rancher/rke2/rke2.yaml+ bundled kubectl rc=1 (permission denied; no server kubeconfig on workers). sudo /var/lib/rancher/rke2/bin/kubectl --kubeconfig /var/lib/rancher/rke2/agent/kubelet.kubeconfig get nodesfails withForbidden: User "system:node:rke-platform01" ... only its own Node object(rc=1) — the kubelet kubeconfig is node-scoped, NOT an admin credential. Never treat it as one.
3.4 keepalived / VIP¶
- Health-check tied to rke2-server liveness → no VIP holder until a healthy API exists (a holder-less VIP at phase A/B is CORRECT, not drift).
- Priority chain: cp01 150 > cp02 100 > cp03 50 (deterministic failover).
- Failover drill PASSED live (F6-2): cp01 down → VIP to cp02 → cp01 reclaims (preemption) — seconds-scale, verified during B-C.
- VIP
/readyzthrough the surviving quorum after a one-CP outage: PASSED. - Current holder check (verified on cp01, rc=0):
ip -4 addr show eth0 | grep -F 10.72.0.20— the holder showsinet 10.72.0.20/23 scope global secondary eth0.
4. Bootstrap semantics learned¶
NotReadynodes are EXPECTED pre-CNI. Never "fix" them with workarounds; Cilium (phase E) is the fix. CoreDNS cannot start without a CNI — the "workers Ready before Cilium" ordering is an impossible deadlock (caught by approver ESCALATE; runbook now orders E before D).- The playbook is idempotent: after the gate-fix merge we re-ran rke2-cp.yml with cp01 already live — no reinstall, gates re-evaluated, joins proceeded.
- Join token: 0600 file on the controller, read at runtime,
no_log: true, never in Git. Token file existence is a pre-flight assert. - Ansible gate tasks must use the argv list form (
ansible.builtin.command: argv: [...]) —cmd:with a YAML list becomes a literal string argv[0] (found live at phase B). - Config dir
/etc/rancher/rke2is created by our roles since infra PR#6 (d553643) — the official installer does NOT create it; without the role task, templating fails. - Infrastructure
mainat1e8e689+ (d553643 + gate fix) is the verified baseline for all playbooks; live ops01 checkout sits atce065f2(kube-proxy gate fix), phase D/D2 apply usedf841ae7.
5. Disaster-recovery semantics (F6 — from the runbook, which prevails)¶
The full procedure lives in the runbook (Phase F6, merged main 7cc387a): P1-P4 hard STOP prerequisites (external verified snapshot outside cp01's failure domain, independent SHA-256 match, quorum+/readyz evidence, explicit human downtime authorization) gate any server stop. The cluster-reset restore (rke2 server --cluster-reset --cluster-reset-restore-path=<snap>) is DISRUPTIVE — all servers stop, peer DBs are replaced, ~10-20 min full API downtime, NOT idempotent, and the reset-flag must NEVER be deleted manually.
If the reset FAILS: STOP-and-escalate. This is the runbook's fail-closed rule, not a retry ladder. The failure state is reset-flag- and DB-state-dependent: (a) the flag is written only after the snapshot-restore step succeeds (early failures leave NO flag; late failures leave it present), (b) the flag blocks only a bare --cluster-reset (not one with --cluster-reset-restore-path set), and (c) the pre-restore DB is moved aside to db/etcd-old-<timestamp>/ — a failed attempt may leave the DB dir absent or partially restored.
On any failure (reset invocation fails, expected output missing, DB not re-provisioned, peers not rejoining, quorum not re-established, /readyz not OK):
1. Do NOT retry blindly — all CPs are stopped; escalate to the lead with full output; the cluster stays STOPped.
2. The ONLY documented recovery actions are lead-approved, apply-log-recorded: (i) normal start on cp01 (systemctl start rke2-server) — the documented flag-clearing mechanism, which starts from whatever DB state the failed attempt left (restored single member if the restore step succeeded; may fail to start or come up uninitialized if it did not — an uninitialized cluster is an evidence-gathering STOP point, never a "fresh etcd is fine" outcome); (ii) a second reset attempt ONLY after a successful normal start or a coordinated rollback decision; (iii) coordinated Proxmox rollback (qm rollback <vmid> pre-rke2-apply on all CPs); (iv) Rancher support with exact version + logs.
3. A normal start is NOT a "restore to fresh etcd" escape hatch. Whatever DB state remains is what starts; cluster-state loss is only accepted as an explicit, lead-approved, recorded decision (with the F6 evidence trail), never as a default outcome of a failed reset. Now that Cilium and six nodes are live, treating "fresh etcd" as a viable fallback would silently discard cluster state.
6. Ops quick-reference (CP commands verified on cp01 and the worker-only rke2-agent command on worker rke-platform01 at 2026-08-30 03:39-03:40 UTC; rc's: kubectl/grep/journalctl lines and the worker systemctl status rke2-agent rc=0; on cp01 systemctl status rke2-agent returns rc=3 — the unit is absent on CPs, which is expected)¶
# define once per shell (CP-local admin form requires sudo — see §3.1)
RKE2_KUBECTL="sudo /var/lib/rancher/rke2/bin/kubectl --kubeconfig /etc/rancher/rke2/rke2.yaml"
$RKE2_KUBECTL get nodes # 6/6 Ready
$RKE2_KUBECTL get nodes -L wetopi.io/role # workers labeled platform
$RKE2_KUBECTL -n kube-system get pods | grep -E 'etcd-|apiserver|coredns'
# etcd health/members: use the §3.2 server-client kubectl-exec form (NOT client.crt)
# services
systemctl status rke2-server # CPs
systemctl status rke2-agent # workers ONLY (after phase D) — on CPs the unit is absent: rc=3 on cp01 (expected); rc=0 on worker rke-platform01
# keepalived / VIP holder
systemctl status keepalived; ip -4 addr | grep -F 10.72.0.20
# logs
journalctl -u rke2-server -n 50 --no-pager # readable as ubuntu on CPs (rc=0 unprivileged)
# snapshot management (host binary; see runbook F6 for the full gated procedure)
sudo rke2 etcd-snapshot ls # verified rc=0: lists pre-addons-fase6 snapshot; runbook F6 form: save --name <name>, then external copy + dual SHA-256
Remote form from ops01 (for addon/D2/F2/G operations; verified rc=0, 2026-08-30 03:39 UTC):
export KUBECONFIG=~/secrets/rke2-admin-kubeconfig # ops01 ~/bin/kubectl v1.36.3
kubectl get nodes -L wetopi.io/role
7. What is NOT in this handbook¶
Full phase-by-phase apply procedure → the merged runbook (docs/platform/rke2-apply-runbook.md). ADR-0004 for the endpoint decision and its rejection of alternatives. The Ansible roles themselves are the source for node config.
8. Changelog¶
- 0.2 (2026-08-30): approver REQUEST_CHANGES round 1 (t_f1aeb7f4, fix t_6dcba59b) — all six blocking items: (1) every command block re-verified live with the sudo CP-local form / kubectl-exec etcd form, rc recorded; (2) controller/ops01 roles corrected to runbook §0 item 9 (controller = workstation; ops01 = ProxyJump target + provisioned phase-E host with sudo, venv, checkout ce065f2, kubeconfig); (3) worker kubelet kubeconfig path
/var/lib/rancher/rke2/agent/kubelet.kubeconfigadded, verified on rke-platform01; (4) dated current-state statement added (6/6 Ready, B/C/F6/E/D complete, D2 in flight); (5) reset-failure text replaced with the runbook's fail-closed, flag/DB-state-aware STOP-and-escalate semantics; (6) etcd cert guidance reconciled — server-client kubectl-exec form is the operator source of truth, client.crt absence scoped to the etcd container mount, runbook divergence recorded as a follow-up (do not edit the runbook here). - 0.1 (2026-08-30): first edition from live bring-up evidence: B-C quorum green (3 CPs, VIP failover + outage drills passed), gate-defect class (argv/certs/json-count, PR#7), no-CNI NotReady semantics, F6 drill procedure with P1-P4 gates.