fix(controller): own the metrics bind address, keep the proxy out of readiness - #212
Merged
Merged
Conversation
…readiness Three problems with the kube-rbac-proxy block added in #210. The loopback contract was documented and not enforced. The template said "the controller is expected to bind it on 127.0.0.1" and backed that up by not rendering a containerPort -- but containerPort is metadata and restricts no bind. What actually held the contract was one identical line copied into all sixteen consuming charts, and with controllerMetricsPort and controllerMetricsProxyPort defaulting to the same number, a single edit to ":8080" in any of them turns into either an unauthenticated /metrics on the pod IP or two containers racing for the port in the shared netns. The library now emits METRICS_BIND_ADDRESS itself, next to LOG_LEVEL and CONTROLLER_NAMESPACE: loopback when a proxy fronts the endpoint, the pod IP otherwise. Consuming charts must drop their own METRICS_BIND_ADDRESS from additionalControllerEnvs when they move to this version -- a second entry of the same name is what env-variables-duplicates reports. The proxy carried a readinessProbe, which made a metrics sidecar able to take the pod out of the endpoints of every Service that selects it. This template renders the webhook Service next to the Deployment, admission configurations default to failurePolicy: Fail, and in storage-foundation and state-snapshotter that same Service also backs an APIService. So a proxy that failed to come up stopped cluster-wide StorageClass writes in one module, Rook reconciliation in another, and an aggregated API group in two more. A wedged proxy is still restarted by the liveness probe, and one that is not up yet is already excluded from scrape targets by the readiness filter in the monitors. nodeMetricsPort is removed. No module in the fleet passes it, and its first user would have got the CSI node metrics published on every node interface without authentication, because the DaemonSet is hostNetwork by default -- the same defect this series fixed in the sds-replicated-volume agent. Signed-off-by: v.oleynikov <vasily.oleynikov@flant.com>
The comment claimed a proxy that is not up yet is kept out of the scrape targets by the readiness filter in the monitors. With the readinessProbe gone that is no longer true: the pod is Ready as soon as the controller is, so a scrape or two can fail while the proxy starts. Say so, and say which side of the trade that is. Signed-off-by: v.oleynikov <vasily.oleynikov@flant.com>
krpsh123
approved these changes
Aug 24, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Three follow-ups to #210, found while reviewing the sixteen storage-module MRs that consume it.
helm_lib_module_controller_manifestsemitsMETRICS_BIND_ADDRESSitself.127.0.0.1:<controllerMetricsPort>whencontrollerMetricsProxyPortis set,:<controllerMetricsPort>otherwise. The parameter documentation now also states that a consuming chart must not pass this variable throughadditionalControllerEnvs.The
kube-rbac-proxycontainer loses itsreadinessProbe. ThelivenessProbestays.nodeMetricsPortis removed fromhelm_lib_csi_node_manifests, together with its two test cases.Chart version bumped to 1.72.18.
Why do we need it, and what problem does it solve?
The loopback contract was documented and not enforced
#210 says "the controller is expected to bind it on 127.0.0.1" and backs that up by not rendering a
containerPortfor the controller's own metrics port. ButcontainerPortis metadata — it restricts no bind, and dropping it does not keep a process off the pod IP. What actually held the contract was one identical line copied into all sixteen consuming charts:plus sixteen copies of a
const defaultMetricsBindAddress = "127.0.0.1:8080"in the Go configs, each with the same five-line comment. Classic Shotgun Surgery: one rule, sixteen places to get it wrong.And getting it wrong is cheap, because
controllerMetricsPortandcontrollerMetricsProxyPortdefault to the same number (8080) in fourteen of those modules. It works today only because the two sockets differ in the host part:127.0.0.1:8080and<podIP>:8080. Write":8080"in any one of the sixteen charts — or drop the line, reasoning that the library handles it — and you get one of:/metricsanswering on the pod IP, which is exactly what the proxy was added to prevent;listen tcp :8080: bind: address already in usein the shared network namespace, killing whichever container starts second.Neither is caught by a linter or a template test. Both surface in production.
A metrics sidecar could take cluster-wide admission down
The proxy carried a
readinessProbe. This template renders the webhookServicenext to theDeployment, that Service selectsapp: controller, and an EndpointSlice contains only Ready pods.helm_lib_module_validating_webhook_configurationemits nofailurePolicy, and theadmissionregistration.k8s.io/v1default isFail.So a
kube-rbac-proxythat fails to become ready — image pull, OOM at the 25Mi VPA floor, a port typo in a future chart edit — removes the pod from the webhook Service's endpoints and every guarded API write starts failing. Twelve pods in the fleet are shaped this way. Concretely:csi-nfsguardsstorage.k8s.io/v1/storageclasseswithoperations: ["*"], cluster scope. A dead metrics sidecar stops every StorageClass create and update in the cluster, including other modules'.sds-elasticguardscephclusters,cephobjectstoresand sixteen more Rook kinds onCREATE/UPDATE/DELETE. The same failure halts Rook reconciliation.It gets worse for two modules where that same Service also backs an aggregated API:
storage-foundation—v1.subresources.storage-foundation.deckhouse.ioandv1.subresources.snapshot.storage.k8s.iostate-snapshotter—v1alpha1.subresources.state-snapshotter.deckhouse.ioAn
APIServicewith no ready endpoints takes its whole API group out of discovery.Before #210 the pod's readiness was decided by the controller and the webhook — the two containers the webhook actually needs. Adding a purely observability sidecar to that gate is a blast-radius increase, and a wedged proxy is still restarted by the liveness probe without it.
The removal does have a cost, stated plainly: with no readinessProbe the pod is Ready before the proxy listens, so during a rollout a scrape or two can fail (the readiness filter the monitors carry —
__meta_kubernetes_endpointslice_endpoint_conditions_ready/__meta_kubernetes_pod_ready— still excludes terminating pods and pods whose controller is not ready, but no longer "proxy not up yet"). That is the trade: a few gaps in a metric against an admission or discovery outage.nodeMetricsPorthas no user and a sharp edgeNo module passes it — verified across all sixteen storage modules plus the
sds-local-volumepilot the sweep started from. It shipped into a shared library with zero consumers.Worse than dead:
_csi_node.tpldefaultscsiNodeHostNetworkto"true", so the first module to use the parameter "as intended" publishes the CSI node's/metricson every interface of the node, unauthenticated. That is precisely the defect the same sweep just fixed in thesds-replicated-volumeagent, which had been serving:4270node-wide past the kube-rbac-proxy that exists to gate it. A second effect is that the port number silently becomes a node-level resource: two modules picking the samenodeMetricsPortcannot both schedule on one node.Better to delete it now and reintroduce it, if ever, with the loopback + proxy shape spelled out in the helper.
What is the expected result?
METRICS_BIND_ADDRESSappears on thecontrollercontainer whenevercontrollerMetricsPortis set:A chart that sets neither port renders exactly as before.
The
kube-rbac-proxycontainer renders withoutreadinessProbeand withlivenessProbeunchanged.helm_lib_csi_node_manifestsno longer acceptsnodeMetricsPort; since nothing passes it, no consumer output changes.Backward compatibility
Checked against every consumer I can see — all
ourmodules/*,flant/deckhouse(full checkout, vendoring 1.72.14),flant/deckhouse-cse(modules/990-*andmodules-ext/*), plus a GitHub-wide code search:nodeMetricsPortremovedhelm_lib_csi_node_manifests, not the storage fleet, and no public hit. A chart that did pass it would not fail to render either: an unknown$configkey is simply nil.METRICS_BIND_ADDRESSemittedcontrollerMetricsPortwithout passing the variable:sds-local-volume(8080, no proxy) and thesds-node-configuratorsnapshots indeckhouse-cse(8080, no proxy). Both get":8080"— identical tosds-local-volume's ownDefaultMetricsBindAddress, and ignored outright by the cse snapshot, whose binary readsMETRICS_PORT. No rendered behaviour change.dmt'senv-variables-duplicatesreports it, which is the forcing function for step 3.controllercontainer only. Thewebhookscontainer is untouched, so a chart passingMETRICS_BIND_ADDRESSthroughadditionalWebhooksEnvs(the example #210 used) does not collide. Verified across all sixteen rendered charts: the webhooks container's env is["LOG_LEVEL"]everywhere.CONTROLLER_NAMESPACEandadditionalControllerEnvs. No downstream repository asserts an exact env list for this container — the only such assertion was this library's ownadditionalWebhooksEnvscase, updated here.readinessProberemovedRollout order for the consuming modules — this matters, because a module on 1.72.17 that vendors 1.72.18 without the second step gets a duplicate env entry:
charts/deckhouse_lib_helm-1.72.18.tgzin the sixteen modules;METRICS_BIND_ADDRESSfrom each module'sadditionalControllerEnvs.Until step 2 those modules keep their own copy of the variable; the value is byte-identical, so behaviour does not change in the meantime. After step 2 and before step 3,
dmt'senv-variables-duplicatesrule reports the duplicate — which is the intended forcing function rather than a silent override.Covered by three new cases in
helm_lib_module_controller_manifests_test.yaml: loopback bind behind a proxy together with the absentreadinessProbeand the presentlivenessProbe, wildcard bind with no proxy, and no variable at all when the module declares no metrics port. TheadditionalWebhooksEnvsordering case from #210 stops usingMETRICS_BIND_ADDRESSas its example, since the library now owns that name.helm unittest ./tests/— 373 passed, 84 suites.go run tools/build-doc.go --diffclean.Checklist