How does Helm treat a chart's crds/ directory differently from templates/?
answer
- Two directories, two lifecycles
- One of them is never rendered
- Applied ahead of everything else
- Already present means left alone
- Uninstall does not take it away
basics
~10 sFiles under crds/ are plain YAML that Helm never renders as templates. Helm applies them before anything in templates/, skips any CustomResourceDefinition already present in the cluster, and never updates or deletes them.
solid answer
~50 s`templates/` **is** the release: every file there is rendered by the template engine, stored in the release record, patched on `helm upgrade` and deleted on `helm uninstall`. `crds/` is deliberately outside that lifecycle. Its files are read as literal YAML — a `{{ ... }}` action in one is never evaluated — and Helm applies them, plus every subchart's `crds/`, before it renders the rest of the chart, then refreshes its view of the kinds the API server knows so the chart may use the new kind. If a CustomResourceDefinition of that name already exists, Helm leaves it exactly as it is: it does not diff it, patch it on upgrade, or remove it on uninstall. `--skip-crds` on `helm install` suppresses the step for estates where a platform identity applies CRDs out of band. They are not in `helm get manifest` either, because they were never part of the release manifest.
code
yaml · 25 lines# digestflow/crds/digestschedules.yaml
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: digestschedules.digest.example.com
spec:
group: digest.example.com
scope: Namespaced
names:
kind: DigestSchedule
plural: digestschedules
singular: digestschedule
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
cron:
type: stringgo deeper
Be ready to name the four rules in one breath: not templated, applied first, skipped if it already exists, never updated or deleted. Knowing that crds/ sits outside the release is the whole answer at this level.
Explain the mechanics behind the rules: rendered templates are stored in the release record and diffed on upgrade, while crds/ files never enter that record, which is why helm get manifest and a plain helm template do not show them.
Show that you have felt the consequences in production — the cluster-scoped RBAC a crds/ directory silently demands, why --skip-crds exists, and how a rendered-diff gate in CI misses CRD changes entirely.
Own the tradeoff you are handing to consumers of your chart: crds/ buys an install-once guarantee and gives up templating, upgrades and per-release ownership. Say when a chart should instead cede CRDs to a platform-owned artifact.
### Two directories, two lifecycles A chart can contain both `templates/` and `crds/`, and Helm treats them as if they came from different tools. Everything under `templates/` is rendered by the template engine with the chart's values bound to the dot, concatenated into one manifest, applied to the cluster, and **stored in the release record** — the Secret named `sh.helm.release.v1.<name>.v<rev>` in the release namespace. Because the previous revision's manifest is stored, the next `helm upgrade` can work out what changed, write the difference, and delete what the chart no longer renders. Those objects are release-owned: they carry `app.kubernetes.io/managed-by: Helm` plus the `meta.helm.sh/release-name` and `meta.helm.sh/release-namespace` annotations, and `helm uninstall` deletes them. `crds/` opts out of every part of that. Four rules, and they are absolute: **1. Never templated.** Files under `crds/` are read as literal YAML. `{{ .Values.crds.enabled }}` in one is not a value reference; it is text that will be posted to the API server and rejected. You cannot gate a CRD on a value, inject `.Release.Namespace`, or reuse a partial from `_helpers.tpl`. A chart author has no switch here at all — the only switch is the caller's `--skip-crds`. **2. Installed first.** On `helm install`, Helm collects `crds/` from the chart **and from every subchart**, applies those files before it renders or applies anything else, then refreshes its view of the kinds the API server serves. That last step is why a chart may legally ship both a CustomResourceDefinition in `crds/` and a custom resource of that new kind in `templates/`. **3. Skipped when already present.** If a CustomResourceDefinition with that name exists, Helm leaves the cluster's copy alone and moves on. It does not compare the two, does not patch, and does not fail. Installing the chart a second time in another namespace therefore just works, which is precisely the behaviour you want for a cluster-scoped object that several releases share. **4. Never upgraded, never deleted.** `helm upgrade` does not touch an existing CRD however much the chart's copy has changed, and `helm uninstall` leaves it — along with every custom resource of that kind — behind. Both are intentional: a CRD is cluster-scoped shared state, an edit to it can invalidate objects other releases already stored, and removing one takes every object of that kind with it. ### Where they show up and where they do not This catches people out in tooling more than in the cluster: - `helm get manifest` shows the stored release manifest, so CRDs installed from `crds/` are **absent** from it. - `helm template` renders `templates/` and leaves `crds/` out unless you pass `--include-crds`, so a rendered-diff check in CI silently reports "no change" for a CRD edit. - `helm show crds CHART` is the command that prints them, and it works straight off a packaged chart or a repository reference. ### The permission it quietly needs Creating a CustomResourceDefinition is a cluster-scoped write. A chart that would otherwise install fine with rights inside one namespace suddenly needs `create` on `customresourcedefinitions` in `apiextensions.k8s.io`. In estates where application teams are confined to their own namespaces, that is the usual reason CRDs are applied by a platform identity and the app chart is installed with `--skip-crds`. ### The alternative, and what it costs Nothing forces you to use `crds/`. A CustomResourceDefinition placed in `templates/` behaves like any other resource: it is templated, it can be gated on a value, and it is patched on upgrade — which is exactly what people want. It also becomes release-owned, so `helm uninstall` deletes it and every custom resource of that kind unless you annotate it `helm.sh/resource-policy: keep`; a second release of the same chart elsewhere in the cluster collides with the first release's ownership of that one cluster-scoped object; and a custom resource rendered by the same release can race the registration of its own kind. `crds/` trades all of that away for an install-once guarantee. The short version to say out loud: `templates/` is managed state, `crds/` is a one-shot bootstrap, and everything that surprises people about CRDs in Helm follows from that one sentence.
- Can a chart author make a CRD in crds/ optional, so users who already have it can opt out?No. Files in `crds/` never reach the template engine, so `.Values` is unavailable and an `{{ if }}` guard would be posted to the API server as literal text. The only switch is `--skip-crds`, which belongs to whoever runs `helm install`, not to the chart. Author-controlled CRDs mean putting them in `templates/` or shipping them as a separate chart.
- Does helm template print the chart's crds/ files?Not by default — `helm template` renders `templates/` only, so a rendered-diff gate in CI shows nothing when a CRD changes. `--include-crds` adds them to the output. `helm get manifest` never shows them at all, because CRDs installed from `crds/` are not part of the stored release manifest; `helm show crds` is the command that reads them out of a chart.
- What permission does a chart with a crds/ directory need that a namespace-only chart does not?Creating a CustomResourceDefinition is a cluster-scoped write, so the installing identity needs create and get on `customresourcedefinitions` in `apiextensions.k8s.io` — not just rights inside the release namespace. Where application teams are confined to their own namespaces, that is exactly why a platform identity applies the CRDs and the app chart is installed with `--skip-crds`.
crds/ is the building's foundation and templates/ is the flat you rent: Helm pours the foundation once if it is not already there, then only ever renovates the flat.
saying these in an interview costs you the question
- Says crds/ files can reference .Values like any other template
- Thinks helm upgrade re-applies the crds/ directory
- Expects helm uninstall to remove the chart's CRDs
- Assumes helm get manifest lists the chart's CRDs
- Believes a second install overwrites the existing CRD
- Confuses crds/ with a hook-annotated manifest in templates/