Why does helm template omit an operator chart's CustomResourceDefinitions from its output?
answer
- Part of the chart never reaches the render
- Two directories, two kinds of YAML
- crds/ is not templates/
- A flag adds them back to the output
- Opt in on render, opt out on install
basics
~20 sFiles under crds/ are plain YAML rather than templates, so the rendering engine never touches them and helm template leaves them out of its output by default. Pass --include-crds to have them emitted alongside the rendered manifests.
solid answer
~50 sA chart holds Kubernetes YAML in two places that Helm treats differently. `templates/` is rendered: values are substituted and the result is the manifest stream. `crds/` is not rendered at all — the files are shipped as written, which is why `{{ .Values.x }}` inside one is not substituted. Because `crds/` is outside the render, `helm template` does not print those files unless you pass `--include-crds`, and it gives no warning that it left them out. That bites when the render is going somewhere: piping it at a cluster installs the custom resources without the definitions they depend on; diffing it against a live cluster shows the CRDs as phantom drift; and a committed golden render hides every CRD change from review. On the install path the default is reversed — CRDs are applied, and `--skip-crds` is the flag that suppresses them.
code
bash · 5 lines# the CRDs in crds/ are silently absent from this output
helm template idx ./operator-chart | grep -c CustomResourceDefinition
# include them, as-is, alongside the rendered manifests
helm template idx ./operator-chart --include-crds | grep -c CustomResourceDefinitiongo deeper
Remember that a chart can hold YAML in two directories and that only one of them is rendered. If a render seems to be missing definitions an operator chart clearly ships, the flag is what you are looking for.
Explain the mechanism: crds/ bypasses the template engine entirely, so it neither substitutes values nor appears in render output, and --include-crds emits those files verbatim rather than rendering them.
Recognise the downstream symptoms — a piped render rejected for unknown kinds, phantom drift in a cluster diff, CRD changes invisible in review — and make the flag part of any render that leaves your own terminal.
Decide how your organisation ships CRDs at all: inside application charts, in a separate chart with its own lifecycle, or owned by the platform team, given that a render flag cannot compensate for the constraints the crds/ directory imposes.
### `crds/` is not `templates/` A Helm chart directory has two places that hold Kubernetes YAML, and they are treated completely differently. Files under **`templates/`** are Go templates. Helm executes them with the release's values bound, and their output is the manifest stream — the thing `helm template` prints and the thing an install applies. Files under **`crds/`** are **not templates**. Helm does not run them through the rendering engine at all. `{{ .Values.something }}` in a file under `crds/` is not substituted; it ships as those literal characters and produces invalid YAML on the cluster. The directory exists so a chart can carry the CustomResourceDefinitions its own templates depend on, and it has special install-time handling: those CRDs are installed before the rest of the chart so that custom resources rendered from `templates/` have a kind to be validated against. ### Why the render looks incomplete Because `crds/` is outside the render, **`helm template` does not include those files in its output by default**. Run it against an operator chart that ships a handful of CRDs and you will see the operator Deployment, its RBAC, its Service and any custom resources the chart defines — but not the CRDs that define those custom resources' kinds. Nothing warns you. The output simply does not contain them. The flag that changes this is **`--include-crds`**: with it, `helm template` emits the `crds/` files alongside the rendered manifests, as-is. ### When it actually bites Three situations, all of them real: **Piping a render at a cluster.** Someone decides to bypass Helm's release machinery and do `helm template ... | kubectl apply -f -`. Without `--include-crds` the CRDs never ship, so the custom resources in the same stream are rejected for referring to kinds the cluster does not have. The error looks like a cluster problem; it is a missing flag. **Diffing a render against reality.** If you compare a fresh render with what a cluster currently holds, the CRDs show as "present in cluster, absent from chart", which reads like drift or like something an operator created behind your back. Adding the flag makes the two sides comparable. **Golden-render review.** A team that commits a rendered snapshot for review is reviewing a document with a silent hole in it, and a change to a CRD — a new version, a changed schema, an added field — is invisible in the diff. ### The knob on the other side On the install path the default is the opposite: CRDs from `crds/` **are** applied, and `--skip-crds` is the flag that suppresses that. This is the pairing worth memorising, because the two names look like a matched set and are not symmetric in effect: on a render you opt CRDs *in*, on an install you opt them *out*. ### What `--include-crds` does not do It does not template them. The files come out byte-for-byte as they are in the chart, so if you were hoping to parameterise a CRD's name or a field with `.Values`, the flag will not help — anything that must vary per release has to live under `templates/` instead, with the consequences that carries. It also does not change how those CRDs are treated over a release's life. The output of a render is text; what an install or upgrade does with `crds/` is a separate matter of Helm's install behaviour, and it is not something a render flag reaches. ### The practical rule If your render is going anywhere other than your own eyes — into a diff, into another tool, into a file that stands for "what this chart installs" — pass `--include-crds` so the artefact is complete. If you are just reading templates, leave it off and enjoy the shorter output.
- What is the install-side counterpart of `--include-crds`?`--skip-crds` on `helm install`. The defaults are opposite: a render leaves `crds/` out until you ask for it, while an install applies those files unless you ask it not to. Remembering that asymmetry saves a confusing hour when a render and an install disagree about what the chart contains.
- Why is `{{ .Values.x }}` inside a file under `crds/` not substituted?Because Helm never runs that directory through the template engine — it is a directory of literal manifests, not templates. The braces ship as characters and produce invalid YAML on the cluster. Anything about a CRD that has to vary per release must live under `templates/` instead, which is a deliberate design decision rather than an oversight.
saying these in an interview costs you the question
- Assumes helm template prints everything the chart would install
- Thinks files in crds/ are rendered with .Values like templates
- Confuses --include-crds with --skip-crds
- Believes --include-crds also templates the crds/ files
- Blames the cluster when a piped render is missing its CRDs