skip to content

CRD Lifecycle

Files under crds/ are never templated, installed before everything else, skipped when already present, and never upgraded or deleted. Asked because a CRD change in a later chart version never lands.

part ofHelmoverview, primer and where to startread it →
on this pageshow

questions

4

How does Helm treat a chart's crds/ directory differently from templates/?

level: juniorimportance: must knowfreq 72%

answer

  1. Two directories, two lifecycles
  2. One of them is never rendered
  3. Applied ahead of everything else
  4. Already present means left alone
  5. Uninstall does not take it away

basics

~10 s

Files 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
yaml
# 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: string

go deeper

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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/

context

open as a page

Why does helm upgrade leave a chart's crds/ CustomResourceDefinition untouched?

level: middleimportance: must knowfreq 64%

basics

~20 s

Helm has no safe way to change a CustomResourceDefinition — an edit can invalidate stored objects and a delete takes every custom resource with it. So crds/ is install-only: upgrades skip it, and a new chart version's schema change never lands.

open as a page

Your Helm chart's new crds/ field works on fresh clusters but not on upgraded ones — why?

level: seniorimportance: should knowfreq 46%

basics

~20 s

crds/ is applied only by helm install, so a cluster that already ran the release keeps the old definition and validates new custom resources against it — the added field is pruned or rejected. Apply the definition, then upgrade.

open as a page

When would you ship a Helm chart's CRDs as a separate chart rather than in crds/?

level: principalimportance: should knowfreq 34%

basics

~20 s

Split them out when the definitions are shared by several releases, must be upgradeable, and are written by a different identity than the application. Keeping them in crds/ is right when one team owns one release per cluster and the schema is stable.

open as a page