A server-side dry run never validates your Helm values
kubectl apply --dry-run=server checks the HelmRelease CRD schema and stops there. Rendering the chart is the only cheap way to catch a mistake inside spec.values.
When a chart is deployed through a GitOps controller, the manifest you actually
write is a HelmRelease: a small custom resource whose spec.values block
carries the entire chart configuration as opaque YAML.
My reflex before merging one is a server-side dry run:
kubectl apply --dry-run=server -f helmrelease.yamlIt passes. It nearly always passes, and it tells you a lot less than it seems to.
What the dry run actually checks
A server-side dry run validates the resource against the CRD’s OpenAPI schema.
For a HelmRelease, that means it confirms spec.chart, spec.interval and
the other top-level fields are well-formed.
spec.values has no schema at all, and it can’t have one, because the
controller doesn’t know the schema of a chart it hasn’t fetched yet. So any
mistake inside it gets through: wrong key names, wrong nesting, values the
chart will silently ignore.
That makes for about the worst kind of failure there is. The resource is
admitted, the controller reports Ready, and the GitOps dashboard is green,
but the thing you configured simply isn’t configured.
Render the chart instead
helm template <release> <repo>/<chart> --version <x.y.z> -n <ns> -f values.yamlThis runs the chart’s own templating, so a mistyped key visibly vanishes from the output instead of being quietly accepted. Grep the rendered result for the value you care about:
helm template ... | grep -A12 'my-config-file'If your setting isn’t in the rendered ConfigMap, it was never going to end up in the cluster either.
Helm accepts unknown keys under a chart’s values and drops them without a warning. Rendering the chart is the only signal you get that this happened, so I don’t treat it as optional.
A worked example
I deployed Node Feature Discovery so that the node holding a USB dongle would
get labelled from detection. That way a nodeSelector could follow the
hardware instead of hard-coding a node name.
The manifest had two mistakes in it, and a dry run couldn’t have caught either.
The first was the config key. NFD’s worker configuration uses
core.featureSources and core.labelSources. I’d reached for core.sources,
which doesn’t exist in that version. Helm accepts it, drops it, and renders
nothing.
The second was the device class. NFD’s usb source only labels devices whose
class is listed in sources.usb.deviceClassWhitelist, which defaults to
["0e","ef","fe","ff"]. The dongle reports class 02 (CDC), like every
USB-serial adapter that presents a CDC-ACM interface. 02 isn’t in the default
list, so you get no label at all. Without a label, the nodeSelector
matches nothing, and the pod sits in Pending for reasons that point nowhere
near NFD.
I’d guessed ff (“vendor-specific”) from the chip’s marketing description.
What I should have done from the start was read it off the hardware:
cat /sys/bus/usb/devices/<dev>/bDeviceClassRendering the chart caught both mistakes. Seeing 02 in the rendered
nfd-worker.conf told me the key name was right and that the value had
landed.
A layered check that costs seconds
kubectl kustomize overlays/production # 1. YAML and kustomize errors
kubectl apply --dry-run=server -f - # 2. CRD schema errors
helm template <release> <repo>/<chart> ... # 3. everything inside spec.valuesOnly the third step tests what you actually wrote. I still run the first two, because they catch other kinds of mistakes, just not the ones in your values.
One wrinkle when you dry-run a whole overlay: a resource can’t be admitted into a namespace that’s only being created in the same batch. For the dry run, either point the resource at an existing namespace or accept that step 2 covers less than the full set.
Quoting that looks wrong and is not
Kustomize re-quotes "02" but leaves 0e, ef, fe and ff bare. That
startled me, but it’s correct. 02 would parse as a number. 0e isn’t a valid
YAML float, since an exponent needs digits after the e, and the others are
plainly strings. Leave it alone.