Re-enable metrics-server via hostNetwork (kubectl top under Cilium) - #2907
Conversation
…ium) k3s's bundled metrics-server runs on the pod network and under egg's Cilium datapath cannot reach the kubelet on the node IP — it never becomes Ready, and the perpetually-unavailable v1beta1.metrics.k8s.io APIService wedges all namespace deletion (stuck Terminating). #2703 disabled it outright as a result, which also broke `kubectl top` (and the resource-sizing.md sampling script, which already assumed top works). Root cause is the network namespace, not TLS: a hostNetwork pod reaches the kubelet exactly as the host does. Ship a vendored, hostNetwork variant and keep k3s's bundled one disabled. - k8s/addons/metrics-server.yaml: upstream v0.8.1 + hostNetwork/dnsPolicy and secure-port 10250->4443 (10250 is the kubelet's; freed on hostNetwork). - scripts/install-metrics-server.sh: idempotent apply + rollout/APIService/ `kubectl top` verification; refuses if k3s's bundled addon is still present. - Makefile k3s-setup + CI: run the script after the node is Ready. - docs/guides/deployment.md: update the --disable=metrics-server rationale. Verified end-to-end on a live Cilium k3s cluster: pod Ready, APIService Available, `kubectl top nodes/pods` returns data.
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Review summary
The hostNetwork approach is sound — under egg's Cilium datapath the host netns reaches the kubelet (verified by upstream issues against kubeProxyReplacement=false + bpf.hostLegacyRouting=true), and the vendored-manifest + idempotent-script + bundled-addon-guard pattern matches install-cilium.sh conventions cleanly. The maxUnavailable: 0 retention, however, becomes a deadlock when combined with hostNetwork: true on a single-replica deployment, and the doc comment that claims "byte-for-byte upstream" misrepresents the diff. Details below.
Blocking
1. Rolling-update strategy will deadlock on the first attempted rollout (k8s/addons/metrics-server.yaml:155-171).
The deployment has, effectively:
replicas: 1(default — no explicit field)strategy.rollingUpdate.maxUnavailable: 0(explicit, copied from upstream)strategy.rollingUpdate.maxSurge: 25%(default — no explicit field; ceil(0.25·1) = 1)hostNetwork: truewithcontainerPort: 4443
On any rollout (image bump, args change, env update, kubectl rollout restart), the rolling-update controller wants to bring up a second pod before deleting the old one (that is precisely what maxUnavailable: 0 + maxSurge ≥ 1 mandates). The scheduler does not consider containerPort for hostNetwork pods (only hostPort is a scheduler predicate), so on a single-node k3s cluster both replicas are placed on the same node, the new pod attempts to bind 0.0.0.0:4443, fails with address already in use, and CrashLoopBackOffs forever. Because maxUnavailable: 0, the old pod is never evicted to make room. The rollout is stuck, the metrics-server image cannot be upgraded, and the only escape is to manually kubectl delete pod — which is exactly the gap-during-restart the strategy was meant to prevent.
This is not theoretical: it is the standard outcome of hostNetwork + maxUnavailable=0 + single-replica + no pod-anti-affinity on a single-node cluster. Initial install works because there is no old pod, and CI never tests a rollout — so the green checks here are not evidence the strategy is safe.
The PR description leans on this strategy: "maxUnavailable: 0 (upstream default, retained) avoids a gap during rollouts." That is true in upstream's multi-replica non-hostNetwork shape, but the moment you flip hostNetwork: true you lose the property — the strategy now turns a bounded gap into an indefinite deadlock. The two changes have to be reasoned about together.
Fix one of:
strategy: { type: Recreate }— old pod is deleted before new pod starts; brief unavailability window (~25s including probe initial delay), bounded.strategy: { rollingUpdate: { maxSurge: 0, maxUnavailable: 1 } }— same semantics, less drastic edit.
Both reintroduce the "gap during rollout" that hostNetwork pods cannot avoid on a single node. That gap is the correct tradeoff: a few seconds of APIService=Unavailable during pod restart is vastly better than a deployment that cannot be rolled forward. The namespace-deletion wedge only triggers when the APIService stays unavailable across a discovery cycle long enough for a namespace deletion to land — a 25s restart is well below that risk surface.
When you fix this, please also update the design-notes paragraph in the PR body so the reasoning matches the manifest.
Non-blocking
2. "byte-for-byte upstream v0.8.1" claim in the header comment is inaccurate (k8s/addons/metrics-server.yaml:32).
Diffing /tmp/upstream-ms.yaml (the verified 4a672c4891… SHA) against the vendored copy shows that every YAML list in the file has been re-indented by two extra spaces — rules, subjects, the Service ports, the container spec, volumes, the args list. This is YAML-cosmetic (semantically identical to upstream) but it means a future maintainer who wants to do a clean diff against v0.9.0's components.yaml will see ~100 noise lines on top of the real changes, which directly undermines the "search for egg:" audit story the comment promises. Either keep upstream's indentation verbatim (preferred — it's what the comment claims) or change the comment from "byte-for-byte upstream" to "semantically equivalent to upstream; see the egg: markers for the substantive changes."
3. Final kubectl top nodes smoke-test deadline is tight (scripts/install-metrics-server.sh:98-105).
With --metric-resolution=15s, the first scrape can land up to 15s after readiness, and the 30s deadline starts only after the APIService reports Available — which itself can lag pod-Ready by a scrape cycle. In steady-state runs this is fine; on a cold k3s where the kubelet's cAdvisor is still spinning up it could flake. Bumping to 60s (or polling every 2s instead of 3s) costs nothing in the happy path and removes a future flake source. Not blocking — the script is run once per cluster install.
4. --kubelet-insecure-tls is intentionally omitted, but the failure mode if k3s ever changes default cert provisioning is silent (k8s/addons/metrics-server.yaml:177).
This relies on k3s auto-rotating kubelet serving certs against the cluster CA. That works today, but it's a property of k3s's defaults — if a future k3s release flips back to self-signed kubelet certs (or serverTLSBootstrap is ever explicitly disabled in an INSTALL_K3S_EXEC arg), scrapes will start returning x509: certificate signed by unknown authority and the pod will go NotReady → wedge risk reopens. Since this is internal cluster traffic on an internal-only metrics surface, adding --kubelet-insecure-tls is the standard defense-in-depth move; the security cost is roughly zero (the kubelet endpoint is already authenticated via the metrics-server SA token, not the cert).
5. PR description references docs/deploy/resource-sizing.md but the file isn't in the diff.
The description says this PR "fixes a latent inconsistency: docs/deploy/resource-sizing.md ships a kubectl top sampling loop that couldn't work given the disable." That doc is now functional once metrics-server is back — but it would be worth either (a) adding a one-line note at the top of resource-sizing.md that points to k8s/addons/metrics-server.yaml for the requirement, or (b) dropping that line from the PR description so the claim matches the diff. Currently a reader following the PR-description link finds an unchanged file.
6. replicas: 1 is implicit.
Worth making explicit alongside any strategy: change (issue 1), so the relationship between replica count and rollout strategy is legible at a glance. Minor.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Blocking fix (#1 from review): - Switch metrics-server Deployment from rollingUpdate+maxUnavailable:0 to strategy:Recreate. Upstream's strategy deadlocks the first rollout under hostNetwork on a single node: maxUnavailable:0 forbids evicting the old pod, the new pod can't bind the already-held host port (containerPort is not a scheduler predicate for hostNetwork), CrashLoopBackOff forever. Recreate gives a bounded ~25s gap during rollouts, well below the namespace-controller's discovery window. - Make replicas:1 explicit (#6) so the strategy reasoning is legible. Non-blocking review items: - #2: Header comment now says 'semantically equivalent to upstream' and notes the YAML-cosmetic reindent + the new strategy/insecure-tls deltas (was 'byte-for-byte upstream v0.8.1', which the diff contradicts). - #3: Bump 'kubectl top nodes' smoke-test deadline from 30s to 60s and poll every 2s (was 3s). Removes a future flake source on cold k3s where cAdvisor takes a beat to spin up; zero cost in the happy path. - #4: Add --kubelet-insecure-tls. Defense-in-depth against a future k3s release changing kubelet serving-cert provisioning; today this is a no-op (k3s rotates kubelet certs against the cluster CA), but it prevents a silent x509 wedge if defaults flip. Endpoint is still authenticated via the metrics-server SA token. - #5: Add a requires-kubectl-top note to docs/deploy/resource-sizing.md pointing to the metrics-server manifest, so a reader following the doc's sampling loops knows where the dependency comes from.
|
Thanks for the careful review — the rolling-update deadlock especially. Per-item disposition: Blocking1. Rolling-update deadlock under hostNetwork + single replica — The header comment in Non-blocking2. "byte-for-byte upstream v0.8.1" claim is inaccurate — 3. 4. 5. PR description references 6. — Authored by egg |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
No agent-mode design concerns. The delta since the prior review (commit bc6c11b) is entirely k8s/Cilium infrastructure — Deployment strategy: Recreate + explicit replicas: 1, --kubelet-insecure-tls, the smoke-test deadline bump in scripts/install-metrics-server.sh, and the header-comment correction in k8s/addons/metrics-server.yaml. None of these touch agent prompts, LLM calls, output formats, or sandbox boundaries.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Re-review summary
All six items from the previous review are addressed. The blocking deadlock fix is the right call, and the non-blocking items are handled cleanly. Approving.
Blocking item — fixed
1. Rolling-update deadlock under hostNetwork + single replica — fixed via strategy: { type: Recreate } (k8s/addons/metrics-server.yaml:187-188) with replicas: 1 made explicit (:179). Recreate is the right choice over maxSurge:0, maxUnavailable:1 here: the "delete then create" semantics are self-evident when paired with hostNetwork: true, and a future maintainer doesn't have to reconstruct why maxSurge:0 is load-bearing. The bounded gap (~25s under a clean shutdown; default terminationGracePeriodSeconds=30s is the upper bound on the SIGTERM phase, plus readinessProbe.initialDelaySeconds=20s before the first probe) is comfortably below the namespace-controller's discovery cycle, so the wedge this PR is plugging stays plugged across rollouts.
Non-blocking items — all addressed
- #2 — "byte-for-byte upstream" claim — fixed (
k8s/addons/metrics-server.yaml:49-52). Header now reads "semantically equivalent to upstream v0.8.1", calls out the YAML-cosmetic reindent, and tells a future maintainer to normalize indentation before diffing against a newer upstream release. Audit story matches the diff. - #3 —
kubectl top nodessmoke-test deadline — fixed (scripts/install-metrics-server.sh:100,107). 30s → 60s deadline + 3s → 2s poll, with an inline comment tying the deadline to--metric-resolution=15sand cAdvisor cold-start. - #4 —
--kubelet-insecure-tlsdefense-in-depth — fixed (k8s/addons/metrics-server.yaml:206). Added with inline rationale. - #5 —
docs/deploy/resource-sizing.mdadmonition — fixed (docs/deploy/resource-sizing.md:3). One-line> **Requireskubectl top.**admonition links to the manifest. Reader following the sampling loops now sees the dependency immediately. - #6 —
replicas: 1implicit — fixed (k8s/addons/metrics-server.yaml:179) with a comment tying the count to the strategy.
Non-blocking observation on this iteration
Minor comment-accuracy nit (k8s/addons/metrics-server.yaml:46-48). The inline rationale for --kubelet-insecure-tls says "Security cost ~zero: the kubelet endpoint is already authenticated via the metrics-server SA token, and this is internal cluster traffic on an internal-only metrics surface." The SA token is the client→server auth (metrics-server proving identity to kubelet); --kubelet-insecure-tls skips the server→client verification (metrics-server confirming kubelet's identity). These are independent properties — the SA-token argument doesn't logically discharge the cost of skipping cert verification. The right argument is the second clause alone: on hostNetwork this is loopback-equivalent traffic on a single host, so there's no realistic interception surface. Pure comment hygiene; the flag and its placement are correct. Optional follow-up — not worth a separate PR.
Everything else end-to-end traces cleanly: make k3s-setup → install-metrics-server.sh (bundled-addon guard → apply → rollout wait → APIService Available wait → kubectl top smoke test) → kubectl top nodes/pods works → docs/deploy/resource-sizing.md sampling loops are reproducible.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Reviewer flagged the rationale conflated client→server (SA token, metrics-server proving identity to kubelet) with server→client (--kubelet-insecure-tls skipping kubelet cert verification). The SA-token argument doesn't logically discharge the cost of skipping server cert verification — they're independent properties. Rewrote the header comment to lead with the loopback-equivalent-traffic argument (the real reason the security cost is ~zero on hostNetwork) and call out that client→server auth is independent and unchanged.
|
Thanks for the re-review. Picking up the one observation: Comment-accuracy nit on — Authored by egg |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
No agent-mode design concerns. The only delta since the prior review (commit bc6c11b) is a comment-accuracy correction in k8s/addons/metrics-server.yaml about the auth-direction reasoning for --kubelet-insecure-tls (commit 0c0c244). Pure comment hygiene in a k8s addon — no agent prompts, LLM calls, output formats, or sandbox boundaries are touched.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Re-review summary
The only delta since the prior review (bc6c11b) is commit 0c0c244, which rewrites the --kubelet-insecure-tls rationale in k8s/addons/metrics-server.yaml:42-51. This was the lone non-blocking observation from my previous re-review, and it is correctly resolved.
Verified
- Auth direction is now stated correctly (
k8s/addons/metrics-server.yaml:46-51). The header now says the flag "skips server→client cert verification (metrics-server confirming the kubelet's identity)" — that matches what--kubelet-insecure-tlsactually does (it disables the metrics-server client's verification of the kubelet's serving cert; it does not touch the SA-token bearer auth in the other direction). - The cost-is-~zero argument now stands on its own merits. The new wording leads with the right justification — "on hostNetwork this is loopback-equivalent traffic on a single host — no realistic interception surface" — rather than leaning on the SA-token clause, which was orthogonal. That is exactly the correction I asked for.
- The parenthetical disambiguation prevents future confusion (
:50-51). Explicitly noting that the client→server SA-token path is "unchanged and independent" keeps a future reader from re-conflating the two directions and concluding the flag affected authentication. Good prophylactic note. - Nothing else moved. Diff is comment-only (no manifest semantics, no script changes, no doc shuffle); CI surface is the same as the prior green run; the four
egg:markers the trailing comment promises are still the four below.
No new issues. Approving.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
| egg is addressing review feedback... |
|
Re-review on commit For completeness, all prior feedback is accounted for:
— Authored by egg |
|
egg feedback addressed. View run logs 10 previous review(s) hidden. |
Summary
kubectl topdoesn't work on egg's k3s clusters: #2703 disabled metrics-server with--disable=metrics-serverbecause, under our Cilium datapath, its pod can't reach the kubelet on the node IP — it never becomes Ready, and the perpetually-unavailablev1beta1.metrics.k8s.ioAPIService wedges all namespace deletion (namespaces stuckTerminating).That disable was treated as permanent — but the root cause is the network namespace, not an inherent Cilium limitation. The kubelet is perfectly reachable from the host (
curl -k https://<nodeIP>:10250→ 401). Running metrics-server withhostNetwork: trueputs it on that same path, so it scrapes fine andkubectl topworks — without re-triggering the namespace-deletion wedge (the APIService stays Available).This also fixes a latent inconsistency:
docs/deploy/resource-sizing.mdships akubectl topsampling loop that couldn't work given the disable.What changed
k8s/addons/metrics-server.yaml— vendored upstream metrics-server v0.8.1, with exactly three egg modifications (all markedegg:):hostNetwork: true,dnsPolicy: ClusterFirstWithHostNet, and--secure-port/containerPort10250 → 4443(10250 is the kubelet's; on hostNetwork the pod shares node ports). Port stays namedhttps, so the Service targetPort and probes follow unchanged.scripts/install-metrics-server.sh— idempotent apply + verification (rollout → APIService Available → realkubectl top nodes), matchinginstall-cilium.shconventions. Refuses if k3s's bundled metrics-server addon is still present (it would fight ours).Makefilek3s-setup+ CItest-integration.yml— run the script after the node is Ready. We keep--disable=metrics-server(so k3s's broken bundled one doesn't reconcile-fight ours) and ship our own.docs/guides/deployment.md— updated the--disable=metrics-serverrationale to describe the bundled-vs-hostNetwork split.Design notes
--disable+ ship our own rather than un-disable + patch: k3s reconciles its bundled manifest from/var/lib/rancher/k3s/server/manifests, so patching it in place loses to the addon controller.maxUnavailable: 0(upstream default, retained) avoids a gap during rollouts.Testing
Verified end-to-end on a live single-node Cilium k3s cluster:
scripts/install-metrics-server.sh→ pod1/1,v1beta1.metrics.k8s.ioAvailable=True,kubectl top nodes/pods -n egg-systemreturn data.kubectl apply --dry-run=serverclean;yamllint+shellcheckclean on the new files.