Skip to content

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).


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 dependency
  • ansible/roles/cilium/handlers/main.yml — optional handler for forced agent restart
  • ansible/roles/cilium/tasks/main.yml — install Gateway API CRDs → helm install → apply CiliumNodeConfig → wait for readiness
  • ansible/roles/cilium/templates/values.yaml.j2 — Helm values rendered from defaults
  • ansible/roles/cilium/templates/cilium-node-config.yaml.j2CiliumNodeConfig CR
  • ansible/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 — add cilium_* variable overrides (or leave defaults to drive)
  • ansible/CLAUDE.md — add cilium row 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 definition
  • argocd/app-configs/gateway-api/kustomization.yaml — the kustomization pointing at upstream v1.5.1 Experimental
  • argocd/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.


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.


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 repository
cilium_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: 24
cilium_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 4789
cilium_routing_mode: tunnel
cilium_tunnel_protocol: vxlan
cilium_tunnel_port: 8473
cilium_auto_direct_node_routes: false
# Migration toggles - PR2 values shown; PR4 flips a subset per spec Phase B
cilium_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: true
cilium_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: true
cilium_hubble_relay_enabled: true
cilium_hubble_ui_enabled: true
cilium_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: 30
cilium_retry_delay: 10
cilium_install_retry_count: 60
cilium_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)


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:

Terminal window
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"
done

Expected 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: true
ipv6:
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:

Terminal window
# Render the Jinja2 template manually using ansible's debug to verify
ansible 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 YAML
uvx --from yamllint yamllint /tmp/cilium-values-test.yaml

Expected: 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
Terminal window
helm repo add cilium https://helm.cilium.io/ 2>/dev/null || true
helm 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 0
echo "Exit code: $?"
# Expect: rendered output has 100+ resources
wc -l /tmp/cilium-rendered.yaml
# Expect: no schema errors
test -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
Terminal window
# Verify cni.customConf is true
rg '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 enabled
rg 'enable-ipv6' /tmp/cilium-rendered.yaml | head -3
# Verify tunnel-port is 8473
rg 'tunnel-port' /tmp/cilium-rendered.yaml | head -3

Each 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/v2
kind: CiliumNodeConfig
metadata:
namespace: {{ cilium_namespace }}
name: cilium-default
spec:
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)
Terminal window
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.yaml
rm /tmp/cnc-test.yaml

Expected: 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)


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
- calico

Add 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)


Files:

  • Modify: ansible/CLAUDE.md — add cilium row to Roles Reference and Phases tables

  • Step 1: Add cilium to 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):

Terminal window
uvx --from yamllint yamllint ansible/roles/cilium/ ansible/k3s-playbook.yml
uvx --from rumdl rumdl check ansible/CLAUDE.md docs/engineering/specs/2026-05-10-calico-to-cilium-design.md
uvx --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:

Terminal window
git push -u origin feat/cilium-migration
gh 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.

  • Order matters within tasks/main.yml. Gateway API CRDs MUST be installed before the Helm task because gatewayAPI.enabled: true against a cluster without the CRDs makes cilium-operator fail to start.
  • The kubectl and helm CLIs are NOT required by this PR. All cluster operations go through kubernetes.core Ansible modules, which use the Python kubernetes/helm libraries. However, the validation steps in Task 5 use helm template directly — install helm locally if not present (brew install helm on macOS).
  • cilium-cli is 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.

Plan complete and saved to docs/engineering/plans/2026-05-11-cilium-pr1-ansible-role.md. Two execution options:

  1. Subagent-Driven (recommended) — Dispatch a fresh subagent per task, review between tasks, fast iteration.
  2. Inline Execution — Execute tasks in this session using executing-plans, batch execution with checkpoints.

Which approach?