HelmChartConfig/traefik carries a dead service.loadBalancerIP key, and hand-applied Service drift #910
Labels
No labels
burndown-2026-06
burndown-2026-08
autonomy
async-consult
autonomy
epic
autonomy
headless
autonomy
live-collab
coherence-core
priority
P0
priority
P1
priority
P2
priority
P3
priority
P4
qa-fixture
role/advocate
role/director
role/exec
role/frontend
role/gamedev
role/human
role/platform
role/qa
role/science
role/sysadmin
state
ambient
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
coilyco-flight-deck/infrastructure#910
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The landmine
HelmChartConfig/traefikinkube-systemon kai-server currently carries:service.loadBalancerIPis a dead key in traefik chart 34.x. It moved toservice.spec.loadBalancerIP. The chart ignores the old spelling silently, with no warning and no error.Proof, from decoding the live Helm release
sh.helm.release.v1.traefik.v3(charttraefik-34.2.1+up34.2.0). The release config containsservice.loadBalancerIP: 99.110.50.213, and the manifest the chart actually rendered contains noloadBalancerIPfield at all:The live Service has
loadBalancerIP: 99.110.50.213only because someone hand-applied over the Helm-managed object. That history is recorded in its own annotation:Why this bites
Any edit to
HelmChartConfig/traefikcauses k3s to run a Helm upgrade job. That upgrade re-renders the Service from the chart, which dropsloadBalancerIP, which drops the public IP binding for every public hostname on the cluster. As of 2026-08-24 that is 20+ ingresses across authelia, aws-ssm-mcp, bluesky-mcp, coilysiren-eco-app, coilysiren-eco-gnome, discord-mcp, factory-game, forgejo, forgejo-mcp, galaxy-gen, glama-mcp, node-stats-mcp, registry, skillsmp-mcp, steam-mcp, trello-mcp, and website.The failure mode is nasty because the HelmChartConfig looks like it pins the IP. Nothing about reading the file suggests the value is inert.
The same trap applies to a k3s upgrade or anything else that re-runs the
helm-install-traefikjob, independent of whether a human edits the config.Fix
Move the key under
service.specso the chart honors it, and so the live Service stops depending on a hand-applied patch:Confirm
service.specpassthrough exists in chart 34.2.1 before relying on it, rather than trusting this issue. Verify by rendering, not by applying: check that the re-rendered Service still carriesloadBalancerIP: 99.110.50.213before letting the upgrade job touch the live object.Until that lands, anyone editing
HelmChartConfig/traefikfor any reason must make this correction in the same edit.How it was found
While preparing the
externalTrafficPolicy: Localchange for #909. The HelmChartConfig was the obvious route for that change and would have caused a cluster-wide ingress outage. That change was instead prepared as a direct Service apply, which does not trigger a Helm upgrade. See #909 for the incident this came out of.Discovered by inspection, not by triggering it. No outage occurred.
Editing HelmChartConfig/traefik would drop the public loadBalancerIP for every serviceto HelmChartConfig/traefik carries a dead service.loadBalancerIP key, and hand-applied Service driftSeverity correction: this is drift, not an outage risk
I filed this claiming a Helm upgrade would "drop the public IP binding for every public hostname on the cluster." That is wrong. I traced the dead key correctly and then asserted an impact I had not traced at all. Retitled accordingly. The original description's "Why this bites" section should be read as retracted.
The dead-key finding stands. The consequence does not.
What actually publishes the public IP
spec.loadBalancerIPon the traefik Service participates in nothing. Traced end to end:1. k3s ServiceLB ignores the field. The Service carries
loadBalancerIP: 99.110.50.213, but its status advertises the node's InternalIP:klipper-lb advertises node IPs and does not implement
spec.loadBalancerIPassignment. Every Ingress inherits that same address,100.69.164.66.2. external-dns never reads this Service. It runs
--source=ingress, not--source=service:3. The public IP comes from a per-ingress annotation. Each managed Ingress pins its own target, which is why the published record is
99.110.50.213and not the100.69.164.66in ingress status:Confirmed in DNS:
4. Forgejo is not even external-dns managed. Its Ingress carries no labels, and
extdns-forgejo.coilysiren.mehas no TXT record.forgejo.coilysiren.me A 99.110.50.213is a manual Route53 record. 15 of 26 ingresses are external-dns managed;forgejois not among them (theforgejomatch in the managed set isforgejo-mcp).So the real chain is: DNS (per-ingress annotation, or manual) ->
99.110.50.213is the router WAN IP -> port-forward 80/443 -> kai-server -> klipper-lb hostPort -> traefik. The traefik Service'sloadBalancerIPfield appears nowhere in it. Deleting it would be cosmetic.The dead key itself, now properly evidenced
Confirmed from the chart embedded in the release secret rather than from the version number. Chart default
serviceblock:No
loadBalancerIPkey exists. Andtemplates/_service.tplreferences.service.loadBalancerClass,.service.spec,.service.loadBalancerSourceRanges,.service.externalIPs,.service.ipFamilyPolicy,.service.ipFamilies-- and never.service.loadBalancerIP.service.specis dumped verbatim:That also resolves the open question from the original description:
service.specpassthrough is real in 34.2.1, so it is the correct home for bothloadBalancerIPandexternalTrafficPolicy.What is actually worth fixing
Two real but mild problems:
A comprehension hazard. The HelmChartConfig reads as though it pins the public IP. It does not, and it never has. The next person to touch it will reasonably believe otherwise, and may make a change premised on it. Delete the key or move it under
service.specso the file states something true.Hand-applied Service drift. The live Service carries two hand-applied fields the chart does not know about:
loadBalancerIP(inert) and nowexternalTrafficPolicy: Local(applied 2026-08-24, see #909). A Helm upgrade reverts both. TheexternalTrafficPolicyrevert is the one with actual behavior attached, and per #909 that transition costs roughly 12 minutes of degraded in-cluster access to public hostnames while the ServiceLB pod is recreated. So a routine k3s upgrade carries a small unannounced disruption, which is worth folding intoservice.specto make it survive.Neither is urgent. Neither is the cluster-wide outage I originally described.
Fix prepared and dry-run verified, blocked on permission
Kai sequenced this ahead of the Traefik analytics work, because enabling access logs and fixing the metrics scrape both require editing this HelmChartConfig, and doing that today would silently revert the
externalTrafficPolicy: Localapplied in #909.Prepared, dry-run verified, and blocked by the operator permission classifier. Not applied.
The dry run, rendered rather than reasoned
Previous comments argued from the chart template. This renders it. The chart is embedded in the release secret
sh.helm.release.v1.traefik.v3, so it can be reconstructed and rendered locally with the exact values k3s supplies (release.config, which is the k3s base merged with this HelmChartConfig).Rendered Service spec under current values:
No
externalTrafficPolicy. NoloadBalancerIP. This confirms the drift claim directly: a Helm upgrade today revertsexternalTrafficPolicytoClusterand drops the IP field.Rendered Service spec under proposed values:
service.specpassthrough works as expected, andservice.ipFamilyPolicyfrom the k3s base values survives the deep merge alongside it.Blast radius: none
Diffing the entire rendered chart output between the two value sets, the only delta is two lines:
No Deployment change, no RBAC change, nothing else.
Diffing the proposed render against the live cluster (
kubectl diffon the rendered Service and Deployment), the only difference is the annotation added by hand in #909:So the Helm upgrade this triggers is a genuine no-op against live. No Deployment roll, no Service mutation, and therefore none of the roughly 12 minute svclb recreation window that the direct Service apply cost in #909. That window happened because the Service spec actually changed; here it does not.
The change
kubectl diffon the CR itself:Full manifest, recorded so it survives the session:
loadBalancerIPis deliberately kept rather than deleted, even though it is inert. Keeping it makes the rendered Service byte-match live, which is what makes the upgrade a no-op. Removing it is a separate, deliberate decision and should not ride along with this one.After applying
The k3s
helm-install-traefikjob runs a Helm upgrade. Expected observations:externalTrafficPolicystaysLocal,loadBalancerIPstays99.110.50.213.svclb-traefikpod, because the Service spec does not change.Rollback is
rollback-helmchartconfig-traefik-current.yaml, or restoring the two-lineservice.loadBalancerIPform.What this unblocks
Once landed,
HelmChartConfig/traefikis safe to edit, which is the prerequisite for the Traefik observability work: correcting the metrics scrape port and enabling access logs. Both are currently dark. See the analytics discussion for detail.Applied and verified. Resolved.
Kai applied it at 00:14 UTC on 2026-08-25. The k3s
helm-install-traefikjob ran a Helm upgrade and completed. Every prediction from the dry run held.After-state
1. The hand-applied field survived a real Helm upgrade. This is the whole point of the change, and it is now demonstrated rather than modelled:
Before this change, the same upgrade would have reverted the policy to
Clusterand dropped the IP, as the rendered dry run showed.2. Allocations preserved, as predicted for Helm's three-way merge with nodePort absent from both old and new release manifests:
3. No ServiceLB recreation. Same pod as before the upgrade:
This is the key operational difference from #909. That change mutated the Service spec and cost roughly 12 minutes of degraded in-cluster access while klipper-lb rebuilt. This one did not touch the spec, so it cost nothing.
4. No Deployment roll.
traefik-59f5fc745f-8rphd, 27d old, unchanged.5. Public edge healthy. All six sampled hostnames returned 200. First probes ran 0.21-0.43s, elevated against the 0.026-0.13s baseline, but a re-probe 20 seconds later settled to normal:
Cold connections immediately after the Helm job, not a regression.
6. Nothing else disturbed. Forgejo
forgejo-6fb99c98fc-8hsfpat 0 restarts and 5h22m. Both Flux GitRepositoriesREADY=True.Disposition
The dead key is gone, the values now render a Service that matches live, and
HelmChartConfig/traefikis safe to edit. Closing.Two things deliberately left undone, neither blocking:
loadBalancerIPis retained underservice.specand remains functionally inert, since k3s ServiceLB advertises node IPs regardless. It is now at least in the location the chart reads, and the config carries a comment saying so. Deleting it is a separate decision.Unblocks the Traefik observability work: the metrics scrape port correction and enabling access logs, both of which require editing this HelmChartConfig and are currently dark.