Cilium PR2 — Install Cilium Alongside Calico (Migration Mode) Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Install Cilium 1.19.3 alongside the existing Calico deployment in upstream’s prescribed live-migration mode. Pre-stage the bastion’s CiliumNetworkPolicy (which won’t enforce until PR4 due to policyEnforcementMode: "never"). No workload pod is migrated in this PR — that’s PR3.
Architecture: Run the ansible/roles/cilium role added in PR1. Cilium’s Helm install lands with cni.customConf: true so it doesn’t write a CNI config file; the CiliumNodeConfig CR (also applied) matches no nodes yet. Calico continues full enforcement of all existing NetworkPolicies. The bastion’s new CiliumNetworkPolicy is created in cf-ssh-bastion namespace via ArgoCD; it does NOT enforce until PR4.
Tech Stack: Ansible playbook from PR1, Velero, ArgoCD, cilium-cli (local workstation), kubectl.
Reference spec: docs/engineering/specs/2026-05-10-calico-to-cilium-design.md (commit fe05a87c).
File Structure
Section titled “File Structure”Files created:
argocd/app-configs/cf-ssh-bastion/cilium-network-policy.yaml— pre-stagedCiliumNetworkPolicytranslating the existing Calico policy.
Files modified:
argocd/app-configs/cf-ssh-bastion/kustomization.yaml— add the new policy file to the kustomization.
Cluster state changes:
- New namespace state in
kube-system: cilium DaemonSet, cilium-operator Deployment, hubble-relay, hubble-ui, CiliumNodeConfig resource - New cluster-scoped CRDs: Cilium’s own (CiliumNode, CiliumEndpoint, CiliumIdentity, CiliumNetworkPolicy, CiliumClusterwideNetworkPolicy, CiliumNodeConfig, CiliumEnvoyConfig, etc.) + Gateway API v1.4.1 Standard CRDs
Task 1: Operator workstation prerequisite — cilium-cli
Section titled “Task 1: Operator workstation prerequisite — cilium-cli”Files: none (workstation setup)
- Step 1: Verify cilium-cli is installed
Run: cilium version --client 2>&1 | head -3
If not installed:
# macOSbrew install cilium-cli
# Linux (x86_64) — see https://github.com/cilium/cilium-cli/releases for currentCILIUM_CLI_VERSION="v0.16.20" # check releases page for version matching Cilium 1.19.xcurl -L --remote-name-all "https://github.com/cilium/cilium-cli/releases/download/${CILIUM_CLI_VERSION}/cilium-linux-amd64.tar.gz"tar xzf cilium-linux-amd64.tar.gzsudo mv cilium /usr/local/bin/rm cilium-linux-amd64.tar.gzVerify: cilium version --client returns a version line. Expected client version: matches or exceeds 0.16.x.
- Step 2: Verify connectivity to the cluster (read-only)
Run: kubectl --context fzymgc-house get nodes
Expected: 8 nodes listed, all Ready, all running Calico (look for calico-system pods on each).
Task 2: Pre-flight — Velero backup and Gateway API CRD check
Section titled “Task 2: Pre-flight — Velero backup and Gateway API CRD check”Files: none (read-only verification + backup)
- Step 1: Take a Velero backup
BACKUP_NAME="pre-cilium-install-$(date +%Y%m%d-%H%M%S)"velero backup create "${BACKUP_NAME}" --waitecho "Backup name: ${BACKUP_NAME}"Expected: Velero reports backup completed Completed with no errors. Save the backup name; you’ll reference it if rollback is needed.
- Step 2: Clean up orphaned Gateway API resources from PR1’s ownership transfer
PR1 removed argocd/app-configs/gateway-api/ and argocd/cluster-app/templates/gateway-api.yaml, transferring Gateway API ownership to the Cilium Ansible role. ArgoCD prunes the Application object on PR1 merge, but the managed CRDs + the safe-upgrades ValidatingAdmissionPolicy do NOT cascade (per repo memory argocd-app-deletion-doesnt-cascade-without-finalizer). This step deletes them so the Cilium role’s v1.4.1 Standard CRD install can succeed.
# Delete the safe-upgrades policy + binding first (they block CRD changes below)kubectl delete validatingadmissionpolicybinding safe-upgrades.gateway.networking.k8s.io --ignore-not-foundkubectl delete validatingadmissionpolicy safe-upgrades.gateway.networking.k8s.io --ignore-not-found
# Delete the 12 orphaned Gateway API CRDs from the prior gateway-api Applicationkubectl delete crd \ listenersets.gateway.networking.k8s.io \ xmeshes.gateway.networking.x-k8s.io \ xbackendtrafficpolicies.gateway.networking.x-k8s.io \ tcproutes.gateway.networking.k8s.io \ udproutes.gateway.networking.k8s.io \ tlsroutes.gateway.networking.k8s.io \ backendtlspolicies.gateway.networking.k8s.io \ gateways.gateway.networking.k8s.io \ gatewayclasses.gateway.networking.k8s.io \ httproutes.gateway.networking.k8s.io \ grpcroutes.gateway.networking.k8s.io \ referencegrants.gateway.networking.k8s.io \ --ignore-not-foundVerify clean state:
kubectl get crd | rg 'gateway\.networking' | wc -l# Expected: 0
kubectl get validatingadmissionpolicy | rg gateway | wc -l# Expected: 0If either count is non-zero, halt and investigate — there’s an unexpected Gateway API installer in the cluster.
Safety check: confirm zero Gateway API resources are in use (this was verified once during PR1 execution, but re-verify in case the cluster changed since):
for kind in gateway gatewayclass httproute grpcroute referencegrant tlsroute tcproute udproute backendtlspolicy; do COUNT=$(kubectl get "${kind}" -A --no-headers 2>/dev/null | wc -l) echo "${kind}: ${COUNT}"doneExpected: all zeros (or “the server doesn’t have a resource type” if the CRD was already deleted, which is fine). If any returns > 0, HALT — there are Gateway API resources in use that would be destroyed.
Note: the safe-upgrades.gateway.networking.k8s.io ValidatingAdmissionPolicy is
deleted here, but the Cilium role re-installs it in Phase 1 with the bundle-version
floor adjusted to v1.4.0 (the version Cilium manages). The cluster’s guard against
unsafe Gateway API CRD downgrades is preserved.
- Step 3: Verify existing NetworkPolicy inventory
kubectl get networkpolicy -Aecho "---"kubectl get networkpolicies.crd.projectcalico.org -Aecho "---"kubectl get globalnetworkpolicies.crd.projectcalico.org -AExpected: Calico policies under cf-ssh-bastion namespace only (6 policies: bastion-ingress-cloudflared, bastion-allow-kube-dns, bastion-allow-k8s-api, bastion-allow-node-ssh, bastion-allow-icmp, bastion-allow-external-https). No NetworkPolicy (networking.k8s.io) resources expected. Document the actual list in the PR description for traceability.
Task 3: Run the cilium-install Ansible task
Section titled “Task 3: Run the cilium-install Ansible task”Files: none (operational)
- Step 1: Run the playbook with cilium-install tag
From the worktree root:
# Capture playbook start time BEFORE running (must be before tee, because tee updates mtime)date +%s > /tmp/cilium-install.startansible-playbook \ -i ansible/inventory/hosts.yml \ ansible/k3s-playbook.yml \ --tags cilium-install \ -v 2>&1 \ | tee /tmp/cilium-install.logWriting the start timestamp to a dedicated file before the playbook runs ensures Step 5 captures the correct start time. (tee updates the log file’s mtime on every write, so the log’s mtime reflects the END of the run, not the start.)
This runs Phase 5b (Cilium install) from PR1: Gateway API CRDs → Helm install → CiliumNodeConfig apply → DaemonSet/operator/Hubble Relay readiness waits.
Expected: each task reports ok or changed; final play recap shows failed=0. Total runtime: ~5-10 minutes (DaemonSet rollout + Hubble Relay startup).
If a task fails:
Install Gateway API ... Standard CRDsfailure → check CRD YAML validity, network connectivity tokubectlproxy, kubeconfig path.Install or upgrade Cilium via Helmfailure → runhelm templatelocally with the same values to see schema errors.Wait for Cilium DaemonSettimeout → checkkubectl get pods -n kube-system -l k8s-app=ciliumandkubectl describe pods -n kube-system -l k8s-app=ciliumfor the failing pods.- Step 2: Verify cilium-cli reports OK
cilium status --waitExpected: Cilium: 8/8 Ready, Operator: 2/2 Ready, Hubble Relay: 1/1 Ready, Cluster Pods: <count>/<count> managed by Cilium — note: many pods at this point are still Calico-managed because no node has been migrated yet. The “managed by Cilium” count reflects pods that landed AFTER the cilium-operator started watching, NOT a migrated count.
cilium status should report no failures.
- Step 3: Verify Cilium DaemonSet pods on all 8 nodes
kubectl get pods -n kube-system -l k8s-app=cilium -o wide --no-headers | wc -lExpected: 8.
kubectl get pods -n kube-system -l k8s-app=cilium -o wide --no-headers \ | awk '{print $NF, $3}' | sortExpected: 8 lines, each showing a node name and Running status.
- Step 4: Verify CiliumNodeConfig is applied (matching zero nodes)
kubectl get ciliumnodeconfig -n kube-system cilium-default -o yaml | rg -A 3 'nodeSelector'Expected: matchLabels: { io.cilium.migration/cilium-default: "true" } is present.
kubectl get nodes -l 'io.cilium.migration/cilium-default=true' --no-headers | wc -lExpected: 0 (no nodes labeled yet — that’s PR3’s job).
- Step 5: Confirm no workload pod restart from this install
# Read the captured start time (NOT the log's mtime — tee updates mtime on every write)START_TS=$(cat /tmp/cilium-install.start 2>/dev/null) \ || { echo "ERROR: /tmp/cilium-install.start missing — was the playbook run with the start-capture step from Task 3 Step 1?"; exit 1; }echo "Playbook start ts: ${START_TS}"
# Count workload pods that have started since the playbook began (excludes kube-system)kubectl get pods -A --field-selector status.phase=Running -o json \ | jq --argjson since "${START_TS}" \ '[.items[] | select(.metadata.namespace != "kube-system") | select((.status.startTime // "1970-01-01T00:00:00Z" | fromdateiso8601) > $since) | .metadata.namespace + "/" + .metadata.name] | length'This shows count of non-kube-system pods started since the playbook began. Expected: 0. The kube-system exclusion isolates the Cilium/Hubble pods from this count; inspect them separately with kubectl get pods -n kube-system --sort-by=.status.startTime. If there are unexpected workload pod restarts, investigate kubectl describe pod/X -n Y for the eviction reason.
- Step 6: Verify no node is on the Cilium pod CIDR yet
kubectl get pods -A -o json \ | jq -r '[.items[] | select(.status.podIP // "" | startswith("10.245."))] | length'Expected: 0 (no pods on Cilium CIDR yet — Cilium has cni.customConf: true and no per-node override is active).
Task 4: Create the bastion CiliumNetworkPolicy file
Section titled “Task 4: Create the bastion CiliumNetworkPolicy file”Files:
-
Create:
argocd/app-configs/cf-ssh-bastion/cilium-network-policy.yaml -
Step 1: Read the existing Calico policy to anchor the translation
Run: cat argocd/app-configs/cf-ssh-bastion/calico-network-policy.yaml — confirm the 6 policies (cloudflared-ingress, kube-dns egress, k8s-api egress, node-ssh egress, ICMP egress, external-https egress).
- Step 2: Create the translated CiliumNetworkPolicy file
# CiliumNetworkPolicies for cf-ssh-bastion# Translated from calico-network-policy.yaml (Calico projectcalico.org/v3) to# Cilium cilium.io/v2. Pre-staged in PR2; does NOT enforce until PR4 flips# policyEnforcementMode from "never" to "default".## Sources/destinations preserved verbatim from the Calico policy:# - Cloudflared ingress (port 2222)# - kube-dns egress# - kubernetes API egress# - SSH egress to specific nodes# - ICMP egress to specific nodes# - HTTPS egress to non-RFC1918---apiVersion: cilium.io/v2kind: CiliumNetworkPolicymetadata: name: bastion-ingress-cloudflared namespace: cf-ssh-bastionspec: endpointSelector: matchLabels: app: cf-ssh-bastion ingress: - fromEndpoints: - matchLabels: k8s:io.kubernetes.pod.namespace: cloudflared toPorts: - ports: - port: "2222" protocol: TCP---apiVersion: cilium.io/v2kind: CiliumNetworkPolicymetadata: name: bastion-allow-kube-dns namespace: cf-ssh-bastionspec: endpointSelector: {} egress: - toEndpoints: - matchLabels: k8s:io.kubernetes.pod.namespace: kube-system k8s-app: kube-dns toPorts: - ports: - port: "53" protocol: UDP - port: "53" protocol: TCP---apiVersion: cilium.io/v2kind: CiliumNetworkPolicymetadata: name: bastion-allow-k8s-api namespace: cf-ssh-bastionspec: endpointSelector: {} egress: - toEntities: - kube-apiserver---apiVersion: cilium.io/v2kind: CiliumNetworkPolicymetadata: name: bastion-allow-node-ssh namespace: cf-ssh-bastionspec: endpointSelector: {} egress: - toCIDRSet: - cidr: 192.168.20.1/32 - cidr: 192.168.20.141/32 - cidr: 192.168.20.142/32 - cidr: 192.168.20.143/32 - cidr: 192.168.20.144/32 - cidr: 192.168.20.151/32 - cidr: 192.168.20.152/32 - cidr: 192.168.20.153/32 - cidr: 192.168.20.154/32 - cidr: 192.168.23.109/32 toPorts: - ports: - port: "22" protocol: TCP---apiVersion: cilium.io/v2kind: CiliumNetworkPolicymetadata: name: bastion-allow-icmp namespace: cf-ssh-bastionspec: endpointSelector: {} egress: - toCIDRSet: - cidr: 192.168.20.1/32 - cidr: 192.168.20.141/32 - cidr: 192.168.20.142/32 - cidr: 192.168.20.143/32 - cidr: 192.168.20.144/32 - cidr: 192.168.20.151/32 - cidr: 192.168.20.152/32 - cidr: 192.168.20.153/32 - cidr: 192.168.20.154/32 - cidr: 192.168.23.109/32 icmps: - fields: - type: 8 family: IPv4 - type: 128 family: IPv6---apiVersion: cilium.io/v2kind: CiliumNetworkPolicymetadata: name: bastion-allow-external-https namespace: cf-ssh-bastionspec: endpointSelector: {} egress: - toCIDRSet: - cidr: 0.0.0.0/0 except: - 10.0.0.0/8 - 172.16.0.0/12 - 192.168.0.0/16 toPorts: - ports: - port: "443" protocol: TCP- Step 3: Validate yamllint
Run: uvx --from yamllint yamllint argocd/app-configs/cf-ssh-bastion/cilium-network-policy.yaml
Expected: clean.
- Step 4: Validate against the cilium.io/v2 schema (kubectl dry-run)
kubectl --context fzymgc-house apply --dry-run=server \ -f argocd/app-configs/cf-ssh-bastion/cilium-network-policy.yamlExpected: 6 lines like ciliumnetworkpolicy.cilium.io/<name> created (server dry run). No schema errors. (Cilium 1.19’s CRDs are now installed in the cluster after Task 3, so server-side dry-run works.)
- Step 5: Commit
Commit message: feat(cf-ssh-bastion): pre-stage CiliumNetworkPolicy for migration (PR2/5)
Task 5: Add the new policy to the kustomization
Section titled “Task 5: Add the new policy to the kustomization”Files:
-
Modify:
argocd/app-configs/cf-ssh-bastion/kustomization.yaml -
Step 1: Inspect the current kustomization
Run: cat argocd/app-configs/cf-ssh-bastion/kustomization.yaml
Note where calico-network-policy.yaml appears under resources:.
- Step 2: Add the new file
Edit argocd/app-configs/cf-ssh-bastion/kustomization.yaml. In the resources: list, find the line - calico-network-policy.yaml (or similar) and add IMMEDIATELY AFTER it:
- cilium-network-policy.yamlBoth policy files coexist from PR2 through PR4. PR5 removes the Calico one.
- Step 3: Verify kustomize renders cleanly
kubectl --context fzymgc-house kustomize argocd/app-configs/cf-ssh-bastion/ > /tmp/bastion-kustomize.yamlecho "Rendered $(rg -c '^kind:' /tmp/bastion-kustomize.yaml) resources"rg '^kind:' /tmp/bastion-kustomize.yaml | sort | uniq -crm /tmp/bastion-kustomize.yamlExpected: rendered output contains both NetworkPolicy (Calico) and CiliumNetworkPolicy kinds, plus the bastion’s other resources (Deployment, Service, etc.). No kustomize errors.
- Step 4: Validate yamllint
Run: uvx --from yamllint yamllint argocd/app-configs/cf-ssh-bastion/kustomization.yaml
Expected: clean.
- Step 5: Commit
Commit message: feat(cf-ssh-bastion): add CiliumNetworkPolicy to kustomization (PR2/5)
Task 6: Verify ArgoCD sync and CiliumNetworkPolicy state
Section titled “Task 6: Verify ArgoCD sync and CiliumNetworkPolicy state”Files: none (verification — will run after the PR is merged to main, but the dry-run below validates the chain works)
- Step 1: Run the ArgoCD app diff locally
kubectl --context fzymgc-house get application cf-ssh-bastion -n argocd -o yaml \ | rg -A 2 'syncPolicy|targetRevision'Note the current sync policy. If automated sync is enabled, the new CRs will be applied after PR2 merges to main. If manual, an operator triggers the sync.
- Step 2: After PR2 merges, verify the bastion CiliumNetworkPolicies exist
(This step runs post-merge; document the expected post-merge state in the PR description.)
kubectl --context fzymgc-house get ciliumnetworkpolicies.cilium.io -n cf-ssh-bastionExpected: 6 policies listed (bastion-ingress-cloudflared, bastion-allow-kube-dns, bastion-allow-k8s-api, bastion-allow-node-ssh, bastion-allow-icmp, bastion-allow-external-https).
- Step 3: Verify the policies do NOT enforce yet (
policyEnforcementMode: "never")
kubectl --context fzymgc-house get cm -n kube-system cilium-config -o jsonpath='{.data.enable-policy}'echo ""Expected: never. Cilium is installed but policy enforcement is fully disabled cluster-wide. PR4 flips this to default.
Task 7: Create the cilium-migration.md migration log (start the running runbook)
Section titled “Task 7: Create the cilium-migration.md migration log (start the running runbook)”Files:
-
Create:
docs/operations/cilium-migration.md -
Step 1: Create the migration log skeleton
# Cilium Migration Log
This document is the chronological log of the Calico→Cilium migrationdescribed in `docs/engineering/specs/2026-05-10-calico-to-cilium-design.md`.It is updated iteratively during PR3 (per-node migration) and finalized in PR4.
After PR4 merges, this log is retained as a historical artifact (not deleted)so future debugging can reference what was done and when.
## PR2 — Cilium installed alongside Calico
- **Date:** <fill in at execution>- **Operator:** <fill in>- **Velero backup:** `pre-cilium-install-<timestamp>` (succeeded)- **Cilium version:** 1.19.3- **Cluster state pre-install:** 8 nodes on Calico CNI; no Calico-only NetworkPolicies outside `cf-ssh-bastion`.- **Install duration:** <fill in>- **Post-install verification:** - `cilium status --wait` reports OK - Cilium DaemonSet 8/8 Ready - cilium-operator 2/2 Ready - Hubble Relay 1/1 Ready - `CiliumNodeConfig cilium-default` applied; matches 0 nodes - `policyEnforcementMode: never` confirmed via cilium-config ConfigMap - 0 pods on `10.245.0.0/16` (no nodes migrated yet) - 6 `CiliumNetworkPolicy` resources created in `cf-ssh-bastion` (not enforcing)
## Constraint reminder
**Between PR2 merge and PR4 merge, no new `NetworkPolicy` or`CiliumNetworkPolicy` resources may merge to `argocd/`.** During thiswindow, `policyEnforcementMode: "never"` makes Cilium ignore all policiescluster-wide, so newly-added policies would silently fail to enforce andcreate a security regression. The bastion's pre-staged policies (this PR)are exempt because they're the migration target, not new functionality.
## PR3 — Per-node migration
(Entries appended per-node as each migration completes.)
## PR4 — Decommission Calico
(Entry appended when PR4 merges.)- Step 2: Validate markdown lint
Run: uvx --from rumdl rumdl check docs/operations/cilium-migration.md
Expected: clean (if rumdl reports MD code-fence-language warnings, accept — the file is operational, not user-facing).
- Step 3: Commit
Commit message: docs(cilium): start migration log (PR2/5)
Task 8: Final verification + open PR
Section titled “Task 8: Final verification + open PR”Files: none (final checks)
- Step 1: Run all repo linters
lefthook run pre-commitExpected: all checks pass.
- Step 2: Verify worktree is clean and ready
git statusExpected: clean working tree, branch feat/cilium-migration (or a PR2-specific branch if you’ve split the worktree).
- Step 3: Open the PR
git push -u origin feat/cilium-migration # or PR2-specific branchgh pr create \ --title "feat(cilium): install Cilium alongside Calico in migration mode (PR2/5)" \ --body "$(cat <<'EOF'## Summary
- Runs `ansible-playbook --tags cilium-install` to install Cilium 1.19.3 in migration mode (cni.customConf: true, policyEnforcementMode: never, bpf.hostLegacyRouting: true).- Pre-stages `CiliumNetworkPolicy` resources for `cf-ssh-bastion` (does not enforce until PR4).- Starts `docs/operations/cilium-migration.md` runbook.
## Cluster impact
- Cilium DaemonSet running on all 8 nodes (no pods migrated yet).- Calico continues full enforcement of existing NetworkPolicies.- Gateway API v1.4.1 Standard CRDs installed.
## Constraint
**No new NetworkPolicy / CiliumNetworkPolicy may merge to `argocd/` between this PR and PR4.** During the migration window, Cilium's policyEnforcementMode is "never" cluster-wide.
## Spec / prior PR
- Spec: `docs/engineering/specs/2026-05-10-calico-to-cilium-design.md`- PR1 (Ansible role): #<PR1-number>- Next: PR3 (per-node migration, iterative)
## Test plan
- [x] Velero backup completed- [x] `ansible-playbook --tags cilium-install` succeeded- [x] `cilium status --wait` reports OK- [x] 6 CiliumNetworkPolicies present in cf-ssh-bastion (not enforcing)- [x] No workload pod restart from install
🤖 Generated with [Claude Code](https://claude.com/claude-code)EOF)"Self-Review Checklist
Section titled “Self-Review Checklist”- Spec coverage — Spec’s PR2 section: pre-flight Velero (T2), Gateway API CRD check (T2), Helm install via Ansible (T3), CiliumNodeConfig apply (T3), DaemonSet readiness (T3), bastion CiliumNetworkPolicy create (T4), kustomization update (T5), ArgoCD verify (T6), migration log (T7).
- Placeholder scan — no “TODO/TBD/etc.”; every command is explicit. The migration-log entry has
<fill in>placeholders for operator/date — these are values the operator fills at execution time, not plan-author gaps; they’re explicitly bracketed as such. - Type/symbol consistency —
cf-ssh-bastionnamespace and policy names match the spec;k8s-app=ciliumselector matches PR1’s template. - No invented references — every kubectl/helm/velero/ansible command refers to real commands and verified flags.
Notes for the implementer
Section titled “Notes for the implementer”- The bastion
CiliumNetworkPolicytranslation is L3/L4 only. Spec is explicit that DNS-based egress (L7) is out of scope for this migration. If you find yourself wanting to adddnsEgressrules, stop — that’s a follow-up security-hardening PR. endpointSelector: {}matches all pods in the namespace (Cilium equivalent of Calico’sselector: all()). Don’t change this to a more specific selector.bastion-allow-k8s-apiusestoEntities: kube-apiserverwhich is a Cilium reserved entity. This is cleaner than the Calicoservicesapproach (which named thekubernetesservice indefaultnamespace) and is the Cilium-idiomatic way to allow API server traffic.bastion-allow-kube-dnsusestoEndpointswith k8s-app label rather thantoServices. Cilium’stoServicesis more recent and historically less reliable than direct endpoint matching; the spec’s CiliumNetworkPolicy dialect decision favors this approach.
Execution handoff
Section titled “Execution handoff”Plan complete and saved to docs/engineering/plans/2026-05-11-cilium-pr2-install-alongside-calico.md. Two execution options:
-
Subagent-Driven (recommended for code tasks) — fresh subagent per task, review between tasks. Note: PR2 has multiple human-in-the-loop operational steps (Velero backup, Ansible playbook against the live cluster); a subagent can drive these but you should be reviewing each step’s output.
-
Inline Execution — execute tasks in this session using executing-plans, with explicit human approval at each cluster-affecting step.
Which approach?