skip to content

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

level: principalimportance: should knowfreq 34%

answer

  1. Ask who owns cluster-scoped state
  2. Count the releases sharing one object
  3. Does the schema keep moving?
  4. Upgradeability has a price in artifacts
  5. Two artifacts need an ordering contract

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.

solid answer

~50 s

The question is really about ownership. A CustomResourceDefinition is cluster-scoped and shared by every release of the chart, while the app chart is namespaced and installed many times — so if the definitions need to change over time, or if application teams are not allowed to write cluster-scoped objects, they do not belong to the app chart at all. A separate chart, whose definitions live in its **templates/** with `helm.sh/resource-policy: keep`, gives you the thing `crds/` cannot: a real upgrade path, a version of its own, and one release that legitimately owns the objects. The costs are a second artifact, an ordering contract between the two, and version skew when a cluster's definition chart lags. Keep `crds/` when one team installs one release per cluster and the schema barely moves — it is fewer moving parts and it removes the ordering race for free.

code

bash · 7 lines
bash
# platform identity, once per cluster
helm upgrade --install digestflow-crds digestflow/digestflow-crds \
  --version 1.4.2 -n digest-system --create-namespace

# each team, no cluster-scoped permission needed
helm upgrade --install digestflow digestflow/digestflow \
  --version 2.6.1 -n team-alpha --skip-crds -f digestflow-values.yaml

go deeper

for a junior

Know the three possible homes for a CustomResourceDefinition in Helm — the chart's crds/, the chart's templates/, or a chart of its own — and that only the first is install-once and never upgraded.

for a middle

Be able to explain the mechanical consequences of each placement: what uninstall does, whether upgrades reach the object, and why a second release of the same chart collides on a cluster-scoped resource.

for a senior

Argue the case for a fleet you have operated: who is allowed to write cluster-scoped objects, how the ordering between the two charts is enforced, and how you detect a cluster whose definitions lag the application.

for a principal

Own the policy, not the preference. State the rule you would write down for the whole organisation, the blast radius it bounds, and an honest account of the coordination cost that a second artifact adds.

### Three placements, not two For `digestflow` — a chart wrapping a vendor email-digest builder image, driven by a values-generated config file, installed by 9 teams across 23 clusters — the `DigestSchedule` definition can live in exactly three places, and the decision is a governance decision more than a packaging one. **In the app chart's `crds/`.** Applied once at install, skipped if present, never upgraded, never deleted. Simplest possible story: one artifact, no ordering to get wrong, and a second release in another namespace installs cleanly because the existing definition is left alone. The price is that the schema is effectively frozen after the first install on each cluster, and every installer needs cluster-scoped write permission on definitions. **In the app chart's `templates/`.** Now it is an ordinary release resource: templated, upgraded, deletable. Three consequences follow immediately. Uninstall takes the definition and every custom resource of that kind with it, unless `helm.sh/resource-policy: keep` is on the manifest. Only one release can own a cluster-scoped object, so a second install of the same chart in another namespace fails on ownership — `--take-ownership` exists as an escape hatch but it is an admission that two releases are fighting over one object. And a custom resource rendered by the same release can race the registration of its own kind. This placement is defensible only when a chart is installed exactly once per cluster. **In a chart of its own.** The definitions go in that chart's `templates/` with `resource-policy: keep`, so they get versioning and a genuine upgrade path, while the object is owned by one release that everybody agrees is the owner. This is the shape almost every mature multi-tenant platform converges on. ### What should push you to split - **Several releases share the definitions.** Nine teams installing `digestflow` in nine namespaces are nine releases and one CustomResourceDefinition. A shared object with a single owner needs an artifact of its own; the alternative is nine releases each believing they own it, or eight of them skipping it silently. - **The schema will keep moving.** If the definition changes every couple of releases, `crds/` guarantees a permanent, silent skew on every cluster that has ever installed the chart. That failure mode is invisible until someone's field goes missing. - **A different identity writes cluster-scoped objects.** Where app teams are confined to their namespaces, a definitions chart installed by a platform identity plus `--skip-crds` on the app install is a clean split of privilege — and it removes the cluster-scoped grant from every application pipeline. - **Lifecycle divergence.** Definitions must outlive the app: uninstalling `digestflow` from a namespace must never remove `DigestSchedule` objects that another namespace still uses. Separate artifacts make that structural rather than annotation-dependent. ### What it costs Two artifacts, two pipelines, and an **ordering contract** you now have to enforce: the definitions chart must be installed and current before an app release that depends on it. That contract will be violated. Design for it — have the app chart check that the kind exists before rendering anything that uses it and stop with a clear message, rather than emitting objects that fail obscurely. Version skew is the second cost, and the mitigation is a documented compatibility statement plus a check in the pipeline, not hope. Third is discovery: newcomers who install one chart and wonder why nothing works. That is a documentation and defaults problem, and it is why some publishers ship both — `crds/` for the fresh-cluster convenience case and a separate definitions chart as the supported path for fleets. ### The decision I would actually write down Single team, one release per cluster, a schema that rarely moves: keep `crds/`. It is the fewest moving parts, and the install-once behaviour is a feature rather than a limitation at that scale. Multi-tenant, many releases per cluster, or a schema under active development: split. Definitions get their own chart, their own version, and an owner named in the platform's documentation. Application charts are installed with `--skip-crds` so that no application pipeline holds cluster-scoped write permission, and the promotion pipeline installs or upgrades the definitions chart first. What I would not do is put a CustomResourceDefinition in an app chart's `templates/` for a chart that more than one team installs. It looks like it gives you upgrades for free, and it hands you an ownership collision and a deletion hazard the first time two teams install it in one cluster. ### What an interviewer is listening for Not a preference, but the reasoning: who owns cluster-scoped state, what the blast radius of a mistake is, whether the ordering contract is enforced or merely written down, and an honest account of what the split costs in artifacts and coordination.

  • Why do the definitions go in the separate chart's templates/ rather than its crds/?
    Because the whole point of splitting is an upgrade path, and `crds/` has none — it would reproduce the frozen-schema problem in a new artifact. In that chart the definitions are ordinary release resources: templated, patched on upgrade, and owned by exactly one release. `helm.sh/resource-policy: keep` then stops an uninstall of the definitions chart from taking every custom resource in the cluster with it.
  • How do you stop an application release from being installed before the definitions chart?
    Make the app chart refuse rather than rely on documentation: check that the kind is registered before rendering anything that uses it and stop with a message naming the chart to install first. That turns a confusing API-server rejection into an actionable one. Belt and braces is a pipeline stage that installs or upgrades the definitions chart ahead of every app deployment, pinned to a compatible version.
  • Some publishers ship both a crds/ directory and a separate definitions chart. Is that incoherent?
    No, it serves two audiences. `crds/` makes a fresh single-cluster install work with one command, which is what evaluation and demos need. The separate chart is the supported path for fleets, where definitions must be upgradeable and owned by a platform identity. What matters is that the documentation says which path is supported for production and that the two never disagree about the schema for a given version.
  • When is putting the definition in an application chart's templates/ actually the right call?
    When exactly one release of that chart will ever exist per cluster — a single-tenant operator installed by its own team, for example. Then the ownership collision cannot happen, the definition is upgraded with the app, and `helm.sh/resource-policy: keep` covers uninstall. As soon as a second team installs the same chart in the same cluster, that placement breaks and it breaks at install time.

saying these in an interview costs you the question

  • Treats it as packaging taste rather than ownership
  • Puts the CRD in templates/ of a multi-tenant app chart
  • Ignores that only one release can own a cluster-scoped object
  • Splits the chart but leaves the ordering contract undocumented
  • Assumes a separate chart removes version skew
  • Forgets resource-policy keep in the definitions chart

context