Cilium PR1 — Ansible Role 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: Add the ansible/roles/cilium Ansible role to the repository as code-only (no cluster change). This produces the artifact that PR2 will use to install Cilium 1.19.3 alongside Calico in the official live-migration mode.
Architecture: Mirror the existing ansible/roles/calico lifecycle position (Phase 5 of k3s-playbook.yml) but use Helm-based install via kubernetes.core.helm instead of operator manifests. Vendor Gateway API v1.4.1 CRDs into the role’s files/ directory so cluster bootstrap is offline-capable and version-pinned. Add a templated CiliumNodeConfig CR that PR2 applies (initially matching no nodes) and PR3 activates per-node via label.
Tech Stack: Ansible 2.15+, kubernetes.core collection 5.0+, Helm chart cilium/cilium 1.19.3, upstream Gateway API CRDs v1.4.1 Standard channel.
Reference spec: docs/engineering/specs/2026-05-10-calico-to-cilium-design.md (commit fe05a87c on branch feat/cilium-migration).
File Structure
Section titled “File Structure”Files created:
ansible/roles/cilium/defaults/main.yml— role variables (version, CIDRs, feature flags, migration toggles)ansible/roles/cilium/meta/main.yml— galaxy metadata, kubernetes.core collection dependencyansible/roles/cilium/handlers/main.yml— optional handler for forced agent restartansible/roles/cilium/tasks/main.yml— install Gateway API CRDs → helm install → apply CiliumNodeConfig → wait for readinessansible/roles/cilium/templates/values.yaml.j2— Helm values rendered from defaultsansible/roles/cilium/templates/cilium-node-config.yaml.j2—CiliumNodeConfigCRansible/roles/cilium/files/gateway-api-crds/gatewayclasses.yaml(vendored from upstream)ansible/roles/cilium/files/gateway-api-crds/gateways.yaml(vendored)ansible/roles/cilium/files/gateway-api-crds/httproutes.yaml(vendored)ansible/roles/cilium/files/gateway-api-crds/grpcroutes.yaml(vendored)ansible/roles/cilium/files/gateway-api-crds/referencegrants.yaml(vendored)
Files modified:
ansible/k3s-playbook.yml— add Phase 5b (Cilium install, tagged off by default at this PR)ansible/inventory/group_vars/tp_cluster_nodes.yml— addcilium_*variable overrides (or leave defaults to drive)ansible/CLAUDE.md— addciliumrow to “Roles Reference” and “k3s-playbook.yml Phases” tables
Files DELETED (Gateway API ownership transfer — see spec “Gateway API ownership transfer” section):
argocd/cluster-app/templates/gateway-api.yaml— the legacy Application definitionargocd/app-configs/gateway-api/kustomization.yaml— the kustomization pointing at upstream v1.5.1 Experimentalargocd/app-configs/gateway-api/— the (now empty) directory
After PR1 merges, the orphaned cluster-side CRDs + safe-upgrades policy will be cleaned via a one-time kubectl pre-flight before PR2 runs (documented in PR2’s pre-flight steps).
Files unchanged (but referenced): ansible/roles/calico/ stays intact through PR3; deleted in PR4.
Task 1: Create the role skeleton
Section titled “Task 1: Create the role skeleton”Files:
-
Create:
ansible/roles/cilium/meta/main.yml -
Step 1: Create the meta file
# SPDX-License-Identifier: MIT-0# code: language=ansible---galaxy_info: author: fzymgc-house description: Cilium CNI installation via Helm with Gateway API CRDs and live-migration support license: MIT-0 min_ansible_version: "2.15" platforms: - name: Debian versions: - bookworm - name: Ubuntu versions: - jammy - noble
dependencies: []collections: - kubernetes.core- Step 2: Verify YAML syntax
Run: uvx --from yamllint yamllint ansible/roles/cilium/meta/main.yml
Expected: no output, exit 0.
- Step 3: Commit
Commit message: feat(cilium): add Ansible role meta (PR1/5)
Use commit-commands:commit skill per ~/.claude/CLAUDE.md.
Task 2: Create the role defaults
Section titled “Task 2: Create the role defaults”Files:
-
Create:
ansible/roles/cilium/defaults/main.yml -
Step 1: Create the defaults file with all variables locked in per spec
# SPDX-License-Identifier: MIT-0# code: language=ansible---# defaults file for cilium
# Cilium version - pinned to latest stable at design time (2026-05-11)cilium_version: "1.19.3"
# Kubeconfig path for kubectl/helm operations (mirror calico role)cilium_kubeconfig: "{{ lookup('env', 'HOME') }}/.kube/configs/{{ k8s_context }}-admin.yml"
# Cilium runs in kube-system (chart default; non-default placement invites footguns)cilium_namespace: kube-system
# Helm repositorycilium_helm_repo_url: "https://helm.cilium.io/"cilium_helm_repo_name: cilium
# IPAM - new cluster pool CIDR (non-overlapping with Calico's 10.42.0.0/16)cilium_cluster_pool_ipv4_cidr: "10.245.0.0/16"cilium_cluster_pool_ipv4_mask_size: 24cilium_cluster_pool_ipv6_cidr: "fd00:10:245::/48"cilium_cluster_pool_ipv6_mask_size: 64
# Routing - tunnel mode with VXLAN, port distinct from Calico's 4789cilium_routing_mode: tunnelcilium_tunnel_protocol: vxlancilium_tunnel_port: 8473cilium_auto_direct_node_routes: false
# Migration toggles - PR2 values shown; PR4 flips a subset per spec Phase Bcilium_cni_custom_conf: true # PR2: true; PR4: false (the cutover toggle)cilium_cni_uninstall: false # PR2: false; PR4: true (default)cilium_policy_enforcement_mode: "never" # PR2: "never"; PR4: "default"cilium_operator_unmanaged_pod_watcher_restart: false # PR2: false; PR4: truecilium_bpf_host_legacy_routing: true # PR2: true; PR4: STAYS true (flip is deferred follow-up)
# Feature scope (constant across PR2 -> PR4)# NOTE: Cilium 1.19's Helm schema accepts kubeProxyReplacement as a string,# not a boolean. The template renders this as a quoted string.cilium_kube_proxy_replacement: "false"cilium_hubble_enabled: truecilium_hubble_relay_enabled: truecilium_hubble_ui_enabled: truecilium_gateway_api_enabled: true
# Gateway API CRDs are pre-installed by a separate role task (chart does NOT# auto-install). Pinned version - matches upstream's k8s-install-migration docs.cilium_gateway_api_crd_version: "v1.4.1"
# Retry configuration (mirror calico_*_retry_count pattern)cilium_retry_count: 30cilium_retry_delay: 10cilium_install_retry_count: 60cilium_install_retry_delay: 10- Step 2: Verify YAML syntax
Run: uvx --from yamllint yamllint ansible/roles/cilium/defaults/main.yml
Expected: no output, exit 0.
- Step 3: Commit
Commit message: feat(cilium): add role defaults (PR1/5)
Task 3: Create the handlers file
Section titled “Task 3: Create the handlers file”Files:
-
Create:
ansible/roles/cilium/handlers/main.yml -
Step 1: Create handlers file
# SPDX-License-Identifier: MIT-0# code: language=ansible---# handlers file for cilium# Helm handles rollouts; this handler is for the rare case where a config# change requires forcing an agent restart outside the Helm upgrade flow.
- name: Restart Cilium DaemonSet kubernetes.core.k8s: kubeconfig: "{{ cilium_kubeconfig }}" state: present definition: apiVersion: apps/v1 kind: DaemonSet metadata: name: cilium namespace: "{{ cilium_namespace }}" annotations: kubectl.kubernetes.io/restartedAt: "{{ ansible_date_time.iso8601 }}" delegate_to: localhost become: false run_once: true listen: restart-cilium-ds- Step 2: Verify YAML syntax
Run: uvx --from yamllint yamllint ansible/roles/cilium/handlers/main.yml
Expected: no output, exit 0.
- Step 3: Commit
Commit message: feat(cilium): add role handlers (PR1/5)
Task 4: Download and vendor Gateway API v1.4.1 CRDs
Section titled “Task 4: Download and vendor Gateway API v1.4.1 CRDs”Files:
- Create:
ansible/roles/cilium/files/gateway-api-crds/gatewayclasses.yaml - Create:
ansible/roles/cilium/files/gateway-api-crds/gateways.yaml - Create:
ansible/roles/cilium/files/gateway-api-crds/httproutes.yaml - Create:
ansible/roles/cilium/files/gateway-api-crds/grpcroutes.yaml - Create:
ansible/roles/cilium/files/gateway-api-crds/referencegrants.yaml - Step 1: Make the directory and download all 5 CRDs
Run from the worktree root:
mkdir -p ansible/roles/cilium/files/gateway-api-crds
GAPI_VERSION="v1.4.1"BASE="https://raw.githubusercontent.com/kubernetes-sigs/gateway-api/${GAPI_VERSION}/config/crd/standard"
for crd in gatewayclasses gateways httproutes grpcroutes referencegrants; do curl -fsSL \ "${BASE}/gateway.networking.k8s.io_${crd}.yaml" \ -o "ansible/roles/cilium/files/gateway-api-crds/${crd}.yaml" echo "Downloaded ${crd}.yaml"doneExpected output: 5 lines “Downloaded X.yaml” + 5 files exist.
- Step 2: Verify each CRD has the correct version annotation
Run: rg -l 'gateway.networking.k8s.io/bundle-version' ansible/roles/cilium/files/gateway-api-crds/ | wc -l
Expected: 5 (all 5 files contain the version annotation).
Run: rg 'gateway.networking.k8s.io/bundle-version: v1.4.1' ansible/roles/cilium/files/gateway-api-crds/ -l | wc -l
Expected: 5 (all 5 files pin v1.4.1).
- Step 3: Verify YAML syntax for each CRD
Run: uvx --from yamllint yamllint ansible/roles/cilium/files/gateway-api-crds/
Expected: no output, exit 0. (If lint warns about line length, that’s acceptable for vendored upstream files; suppress per .yamllint.yaml if needed but try clean first.)
- Step 4: Commit
Commit message: feat(cilium): vendor Gateway API v1.4.1 Standard CRDs (PR1/5)
Task 5: Create the values.yaml.j2 template
Section titled “Task 5: Create the values.yaml.j2 template”Files:
-
Create:
ansible/roles/cilium/templates/values.yaml.j2 -
Step 1: Create the template
# Cilium Helm values - rendered by ansible/roles/cilium from defaults/main.yml# Most values are upstream-mandated for the migration window per# https://docs.cilium.io/en/v1.19/installation/k8s-install-migration/# Deviations from upstream are marked with comments.
operator: replicas: 2 unmanagedPodWatcher: restart: {{ cilium_operator_unmanaged_pod_watcher_restart | lower }} # Pin operator to control-plane nodes - they're stable and the operator # is not on the data path. Required for "don't migrate two operator-hosting # nodes in parallel" constraint per spec. nodeSelector: node-role.kubernetes.io/control-plane: "" tolerations: - key: node-role.kubernetes.io/control-plane operator: Exists effect: NoSchedule
routingMode: {{ cilium_routing_mode }}tunnelProtocol: {{ cilium_tunnel_protocol }}tunnelPort: {{ cilium_tunnel_port }}
cni: customConf: {{ cilium_cni_custom_conf | lower }} uninstall: {{ cilium_cni_uninstall | lower }}
ipam: mode: cluster-pool operator: clusterPoolIPv4PodCIDRList: - "{{ cilium_cluster_pool_ipv4_cidr }}" clusterPoolIPv4MaskSize: {{ cilium_cluster_pool_ipv4_mask_size }} clusterPoolIPv6PodCIDRList: - "{{ cilium_cluster_pool_ipv6_cidr }}" clusterPoolIPv6MaskSize: {{ cilium_cluster_pool_ipv6_mask_size }}
# Dual-stack supplement (not in upstream's prescribed migration values;# this project's choice for family-preservation; upstream marks IP-family# CHANGES as untested but is silent on family-preservation)ipv4: enabled: trueipv6: enabled: true
policyEnforcementMode: "{{ cilium_policy_enforcement_mode }}"
bpf: hostLegacyRouting: {{ cilium_bpf_host_legacy_routing | lower }}
# String value, not bool (Cilium 1.19 Helm schema)kubeProxyReplacement: "{{ cilium_kube_proxy_replacement }}"
hubble: enabled: {{ cilium_hubble_enabled | lower }} relay: enabled: {{ cilium_hubble_relay_enabled | lower }} ui: enabled: {{ cilium_hubble_ui_enabled | lower }} service: type: ClusterIP
gatewayAPI: enabled: {{ cilium_gateway_api_enabled | lower }}- Step 2: Verify the template renders with default values
Run a quick Helm dry-run using the upstream chart and the rendered template:
# Render the Jinja2 template manually using ansible's debug to verifyansible localhost -m template \ -a "src=ansible/roles/cilium/templates/values.yaml.j2 dest=/tmp/cilium-values-test.yaml" \ -e "@ansible/roles/cilium/defaults/main.yml" \ -e "k8s_context=fzymgc-house"
# Validate the rendered output is valid YAMLuvx --from yamllint yamllint /tmp/cilium-values-test.yamlExpected: ansible task succeeds, yamllint shows no errors (or only acceptable warnings for line length on long CIDR lists).
- Step 3: Verify the rendered values pass Helm chart schema validation
helm repo add cilium https://helm.cilium.io/ 2>/dev/null || truehelm repo update cilium
helm template cilium-test cilium/cilium \ --version 1.19.3 \ --namespace kube-system \ --values /tmp/cilium-values-test.yaml \ --dry-run \ > /tmp/cilium-rendered.yaml 2>/tmp/cilium-helm-stderr.txt
# Expect: helm template exits 0echo "Exit code: $?"# Expect: rendered output has 100+ resourceswc -l /tmp/cilium-rendered.yaml# Expect: no schema errorstest -s /tmp/cilium-helm-stderr.txt && cat /tmp/cilium-helm-stderr.txt || echo "(stderr empty - OK)"Expected: exit code 0, large rendered output, empty stderr.
- Step 4: Spot-check the rendered Helm output for key values
# Verify cni.customConf is truerg 'customConf' /tmp/cilium-rendered.yaml | head -3
# Verify policyEnforcementMode is "never"rg 'enable-policy' /tmp/cilium-rendered.yaml | head -3# (Cilium translates policyEnforcementMode to enable-policy config; "never" -> "never")
# Verify ipv6 is enabledrg 'enable-ipv6' /tmp/cilium-rendered.yaml | head -3
# Verify tunnel-port is 8473rg 'tunnel-port' /tmp/cilium-rendered.yaml | head -3Each should return at least one matching line consistent with the defaults.
- Step 5: Clean up tmp files
Run: rm /tmp/cilium-values-test.yaml /tmp/cilium-rendered.yaml /tmp/cilium-helm-stderr.txt
- Step 6: Commit
Commit message: feat(cilium): add Helm values template (PR1/5)
Task 6: Create the CiliumNodeConfig template
Section titled “Task 6: Create the CiliumNodeConfig template”Files:
-
Create:
ansible/roles/cilium/templates/cilium-node-config.yaml.j2 -
Step 1: Create the template
# CiliumNodeConfig - applied by the role at install time.# Initially matches NO nodes (the migration label is not set on any node# at PR2-install time). PR3 applies the label per-node to migrate that# node from non-exclusive to exclusive Cilium mode.# Per https://docs.cilium.io/en/v1.19/installation/k8s-install-migration/apiVersion: cilium.io/v2kind: CiliumNodeConfigmetadata: namespace: {{ cilium_namespace }} name: cilium-defaultspec: nodeSelector: matchLabels: io.cilium.migration/cilium-default: "true" defaults: write-cni-conf-when-ready: /host/etc/cni/net.d/05-cilium.conflist custom-cni-conf: "false" cni-chaining-mode: "none" cni-exclusive: "true"- Step 2: Verify YAML syntax (render with default vars)
ansible localhost -m template \ -a "src=ansible/roles/cilium/templates/cilium-node-config.yaml.j2 dest=/tmp/cnc-test.yaml" \ -e "@ansible/roles/cilium/defaults/main.yml" \ -e "k8s_context=fzymgc-house"uvx --from yamllint yamllint /tmp/cnc-test.yamlrm /tmp/cnc-test.yamlExpected: ansible succeeds, yamllint clean.
- Step 3: Commit
Commit message: feat(cilium): add CiliumNodeConfig template (PR1/5)
Task 7: Create the tasks/main.yml — Gateway API CRDs
Section titled “Task 7: Create the tasks/main.yml — Gateway API CRDs”Files:
-
Create:
ansible/roles/cilium/tasks/main.yml -
Step 1: Create the first task block — Gateway API CRDs
This is the FIRST file you write under tasks/. You’ll append additional tasks in subsequent tasks of this plan.
# SPDX-License-Identifier: MIT-0# code: language=ansible---# tasks file for cilium# NOTE: This role runs delegated to localhost (kubectl/helm operations)
# --- Phase 1: Gateway API CRDs ---# Cilium 1.19 requires Gateway API v1.4.1 Standard channel CRDs to be# pre-installed (chart does NOT auto-install). Vendored under# files/gateway-api-crds/ for offline-capable bootstrap.
- name: Install Gateway API {{ cilium_gateway_api_crd_version }} Standard CRDs kubernetes.core.k8s: kubeconfig: "{{ cilium_kubeconfig }}" state: present src: "{{ role_path }}/files/gateway-api-crds/{{ item }}.yaml" server_side_apply: field_manager: ansible loop: - gatewayclasses - gateways - httproutes - grpcroutes - referencegrants delegate_to: localhost become: false run_once: true tags: - cilium-install - cilium-gateway-api-crds- Step 2: Validate yamllint
Run: uvx --from yamllint yamllint ansible/roles/cilium/tasks/main.yml
Expected: clean.
- Step 3: Commit
Commit message: feat(cilium): add Gateway API CRD install task (PR1/5)
Task 8: Append Helm install task to tasks/main.yml
Section titled “Task 8: Append Helm install task to tasks/main.yml”Files:
-
Modify:
ansible/roles/cilium/tasks/main.yml(append) -
Step 1: Append the Helm repo + install tasks
Append the following AFTER the existing Gateway API CRD task block:
# --- Phase 2: Helm install ---
- name: Add Cilium Helm repository kubernetes.core.helm_repository: name: "{{ cilium_helm_repo_name }}" repo_url: "{{ cilium_helm_repo_url }}" delegate_to: localhost become: false run_once: true tags: - cilium-install
- name: Install or upgrade Cilium via Helm kubernetes.core.helm: kubeconfig: "{{ cilium_kubeconfig }}" name: cilium chart_ref: "{{ cilium_helm_repo_name }}/cilium" chart_version: "{{ cilium_version }}" release_namespace: "{{ cilium_namespace }}" create_namespace: false values: "{{ lookup('template', 'values.yaml.j2') | from_yaml }}" wait: true wait_timeout: 10m delegate_to: localhost become: false run_once: true tags: - cilium-install- Step 2: Validate yamllint
Run: uvx --from yamllint yamllint ansible/roles/cilium/tasks/main.yml
Expected: clean.
- Step 3: Commit
Commit message: feat(cilium): add Helm install task (PR1/5)
Task 9: Append CiliumNodeConfig apply task to tasks/main.yml
Section titled “Task 9: Append CiliumNodeConfig apply task to tasks/main.yml”Files:
-
Modify:
ansible/roles/cilium/tasks/main.yml(append) -
Step 1: Append the CiliumNodeConfig task
Append AFTER the Helm install task:
# --- Phase 3: Apply CiliumNodeConfig ---# This CR is the mechanism that translates the migration label# (io.cilium.migration/cilium-default=true) into per-node Cilium# behavior. Until PR3 applies the label to nodes, this matches no# nodes and has no effect.
- name: Apply CiliumNodeConfig for live migration kubernetes.core.k8s: kubeconfig: "{{ cilium_kubeconfig }}" state: present definition: "{{ lookup('template', 'cilium-node-config.yaml.j2') | from_yaml }}" server_side_apply: field_manager: ansible delegate_to: localhost become: false run_once: true tags: - cilium-install - cilium-node-config- Step 2: Validate yamllint
Run: uvx --from yamllint yamllint ansible/roles/cilium/tasks/main.yml
Expected: clean.
- Step 3: Commit
Commit message: feat(cilium): add CiliumNodeConfig apply task (PR1/5)
Task 10: Append readiness wait tasks to tasks/main.yml
Section titled “Task 10: Append readiness wait tasks to tasks/main.yml”Files:
-
Modify:
ansible/roles/cilium/tasks/main.yml(append) -
Step 1: Append the three readiness wait tasks
Append AFTER the CiliumNodeConfig task:
# --- Phase 4: Wait for readiness ---
- name: Wait for Cilium DaemonSet to be Ready on all nodes kubernetes.core.k8s_info: kubeconfig: "{{ cilium_kubeconfig }}" kind: DaemonSet name: cilium namespace: "{{ cilium_namespace }}" register: cilium_ds until: - cilium_ds.resources | length > 0 - cilium_ds.resources[0].status.numberReady | default(0) == cilium_ds.resources[0].status.desiredNumberScheduled | default(-1) - cilium_ds.resources[0].status.desiredNumberScheduled | default(0) > 0 retries: "{{ cilium_install_retry_count }}" delay: "{{ cilium_install_retry_delay }}" delegate_to: localhost become: false run_once: true tags: - cilium-install
- name: Wait for cilium-operator Deployment to be Ready kubernetes.core.k8s_info: kubeconfig: "{{ cilium_kubeconfig }}" kind: Deployment name: cilium-operator namespace: "{{ cilium_namespace }}" register: cilium_operator until: - cilium_operator.resources | length > 0 - cilium_operator.resources[0].status.readyReplicas | default(0) > 0 retries: "{{ cilium_retry_count }}" delay: "{{ cilium_retry_delay }}" delegate_to: localhost become: false run_once: true tags: - cilium-install
- name: Wait for Hubble Relay Deployment to be Ready (when enabled) kubernetes.core.k8s_info: kubeconfig: "{{ cilium_kubeconfig }}" kind: Deployment name: hubble-relay namespace: "{{ cilium_namespace }}" register: hubble_relay until: - hubble_relay.resources | length > 0 - hubble_relay.resources[0].status.readyReplicas | default(0) > 0 retries: "{{ cilium_retry_count }}" delay: "{{ cilium_retry_delay }}" when: cilium_hubble_relay_enabled | bool delegate_to: localhost become: false run_once: true tags: - cilium-install- Step 2: Validate yamllint
Run: uvx --from yamllint yamllint ansible/roles/cilium/tasks/main.yml
Expected: clean.
- Step 3: Validate full role ansible-lint
Run: uvx --from ansible-lint ansible-lint ansible/roles/cilium/
Expected: no errors (warnings about role README missing are acceptable).
- Step 4: Commit
Commit message: feat(cilium): add Cilium readiness wait tasks (PR1/5)
Task 11: Add Phase 5b to k3s-playbook.yml
Section titled “Task 11: Add Phase 5b to k3s-playbook.yml”Files:
-
Modify:
ansible/k3s-playbook.yml— insert new phase block between Phase 5 (Calico) and Phase 6 (CSI) -
Step 1: Insert the Phase 5b block
Find the existing Phase 5 block in ansible/k3s-playbook.yml:
# Phase 5: Install Calico CNI- name: Install Calico CNI hosts: tp_cluster_controlplane[0] gather_facts: false roles: - role: calico tags: - k3s-calico - calicoAdd this NEW block immediately after it (before “Phase 6”):
# Phase 5b: Install Cilium CNI (live migration mode through PR3; flips to# production mode in PR4 when migration is complete and Calico is removed)- name: Install Cilium CNI hosts: tp_cluster_controlplane[0] gather_facts: false roles: - role: cilium tags: - k3s-cilium - cilium - cilium-install- Step 2: Syntax check the modified playbook
Run: ansible-playbook -i ansible/inventory/hosts.yml ansible/k3s-playbook.yml --syntax-check
Expected: playbook: ansible/k3s-playbook.yml, exit 0.
- Step 3: Validate yamllint on the playbook
Run: uvx --from yamllint yamllint ansible/k3s-playbook.yml
Expected: clean.
- Step 4: Commit
Commit message: feat(cilium): add Phase 5b to k3s-playbook.yml (PR1/5)
Task 12: Update ansible/CLAUDE.md
Section titled “Task 12: Update ansible/CLAUDE.md”Files:
-
Modify:
ansible/CLAUDE.md— addciliumrow to Roles Reference and Phases tables -
Step 1: Add
ciliumto the Roles Reference table
Find the row that begins with | `calico` | in ansible/CLAUDE.md and insert this row immediately after it:
| `cilium` | CNI installation via Helm (live migration mode until PR4) | First control plane |- Step 2: Add Phase 5b to the Phases table
Find the row beginning with | 5. Calico CNI | and insert this row immediately after it:
| 5b. Cilium CNI (migration mode) | `k3s-cilium`, `cilium`, `cilium-install` |- Step 3: Verify markdown lint
Run: uvx --from rumdl rumdl check ansible/CLAUDE.md
Expected: success, no issues.
- Step 4: Commit
Commit message: docs(cilium): add cilium role to Ansible CLAUDE.md tables (PR1/5)
Task 13: Final validation — full role syntax + dry-run
Section titled “Task 13: Final validation — full role syntax + dry-run”Files: none (validation only)
- Step 1: Run ansible-playbook —syntax-check on the full playbook
Run: ansible-playbook -i ansible/inventory/hosts.yml ansible/k3s-playbook.yml --syntax-check
Expected: clean, exit 0.
- Step 2: Dry-run the cilium-install tag (will fail to connect to cluster but should validate task structure)
Run: ansible-playbook -i ansible/inventory/hosts.yml ansible/k3s-playbook.yml --tags cilium-install --check --diff 2>&1 | head -50
Expected: ansible reports each task it would run; any errors should be about missing kubeconfig or unreachable hosts, NOT about task-structure problems. If you see “ERROR! couldn’t resolve module” or “ERROR! ‘X’ is undefined”, fix the role before proceeding.
- Step 3: Run all repo-level linters
Run: lefthook run pre-commit (this runs yamllint + rumdl + ansible-lint + shellcheck against staged files)
Expected: success on the role + playbook + CLAUDE.md changes.
Alternative (if lefthook doesn’t run on already-committed files, run each individually):
uvx --from yamllint yamllint ansible/roles/cilium/ ansible/k3s-playbook.ymluvx --from rumdl rumdl check ansible/CLAUDE.md docs/engineering/specs/2026-05-10-calico-to-cilium-design.mduvx --from ansible-lint ansible-lint ansible/roles/cilium/- Step 4: Open the PR
Push the branch and open a PR with title feat(cilium): add Ansible role for Cilium 1.19 migration (PR1/5). Body should reference the spec (docs/engineering/specs/2026-05-10-calico-to-cilium-design.md) and list:
- 11 new files (role + vendored CRDs + templates)
- 2 modified files (
k3s-playbook.yml,ansible/CLAUDE.md) - “No cluster change” — playbook is tagged off by default
- Next PR: install Cilium alongside Calico
Use the commit-commands:commit-push-pr skill if available; otherwise:
git push -u origin feat/cilium-migrationgh pr create --title "feat(cilium): add Ansible role for Cilium 1.19 migration (PR1/5)" --body "$(cat <<'EOF'## Summary
- Adds `ansible/roles/cilium/` — Helm-based install with vendored Gateway API v1.4.1 CRDs- Adds Phase 5b to `k3s-playbook.yml` (tagged `cilium-install`, not run by default)- Updates `ansible/CLAUDE.md` Roles Reference + Phases tables
No cluster change in this PR. Next: PR2 installs Cilium alongside Calico in migration mode.
## Spec
See `docs/engineering/specs/2026-05-10-calico-to-cilium-design.md` (commit fe05a87c) — PR1 of 5.
## Test plan
- [x] ansible-playbook --syntax-check passes- [x] yamllint clean- [x] rumdl clean- [x] ansible-lint clean- [x] Helm template renders cleanly with role's values- [ ] Reviewer verifies vendored CRDs match upstream v1.4.1
🤖 Generated with [Claude Code](https://claude.com/claude-code)EOF)"Self-Review Checklist (run after writing the plan, before handoff)
Section titled “Self-Review Checklist (run after writing the plan, before handoff)”- Spec coverage — PR1 sections of the spec map to tasks: role skeleton (T1-3), Gateway API CRDs (T4, T7), Helm values (T5, T8), CiliumNodeConfig (T6, T9), readiness waits (T10), playbook integration (T11), CLAUDE.md update (T12), validation (T13).
- Placeholder scan — no “TODO”, “TBD”, “fill in details” present. Every command is explicit; every code block is complete.
- Type/symbol consistency — variable names (
cilium_cni_custom_conf,cilium_bpf_host_legacy_routing) appear identically in defaults, template, and tasks. Tag names (cilium-install) are consistent across tasks and playbook block. - No invented references — every file path exists in the worktree or is created in this plan. Every command (
uvx,ansible-playbook,helm,kubectl,rg) is in scope per the repo’s CLAUDE.md.
Notes for the implementer
Section titled “Notes for the implementer”- Order matters within tasks/main.yml. Gateway API CRDs MUST be installed before the Helm task because
gatewayAPI.enabled: trueagainst a cluster without the CRDs makes cilium-operator fail to start. - The
kubectlandhelmCLIs are NOT required by this PR. All cluster operations go throughkubernetes.coreAnsible modules, which use the Python kubernetes/helm libraries. However, the validation steps in Task 5 usehelm templatedirectly — installhelmlocally if not present (brew install helmon macOS). cilium-cliis NOT required by PR1. It’s needed by PR2/PR3 for verification but not by the role itself. The cilium-cli prerequisite is documented in the PR2 plan.- Phase 5b stays in the playbook through PR4. Don’t try to “clean up” the dual-phase configuration during this PR. PR4 removes Phase 5 (Calico) and trims any Phase 5b “migration mode” comments.
Execution handoff
Section titled “Execution handoff”Plan complete and saved to docs/engineering/plans/2026-05-11-cilium-pr1-ansible-role.md. Two execution options:
- Subagent-Driven (recommended) — Dispatch a fresh subagent per task, review between tasks, fast iteration.
- Inline Execution — Execute tasks in this session using executing-plans, batch execution with checkpoints.
Which approach?