Helm charts often manage Custom Resource Definitions (CRDs), and getting their installation and upgrades right is trickier than it seems.

Let’s see a basic CRD in action. Imagine we’re deploying a hypothetical MyApp resource.

# crds/myapp.crd.yaml
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: myapps.stable.example.com
spec:
  group: stable.example.com
  versions:
    - name: v1
      served: true
      storage: true
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                replicas:
                  type: integer
                  minimum: 1
            status:
              type: object
              properties:
                availableReplicas:
                  type: integer
  scope: Namespaced
  names:
    plural: myapps
    singular: myapp
    kind: MyApp
    shortNames:
      - ma

When you helm install or helm upgrade a chart containing this CRD, Helm has a specific, and sometimes surprising, way of handling it. The most counterintuitive aspect is that Helm doesn’t truly upgrade a CRD if it already exists and has the same spec.group, spec.names.plural, and spec.names.kind. It will simply report "CRD already exists" and move on, even if the spec.versions or spec.schema have changed. This means your updated CRD definition might not be applied to the cluster.

Here’s how a simple MyApp resource would look after the CRD is in place:

# templates/myapp-instance.yaml
apiVersion: stable.example.com/v1
kind: MyApp
metadata:
  name: my-first-app
spec:
  replicas: 3

To see this in action, you’d first have a chart with the CRD definition (e.g., in a crds/ directory or directly in templates/). Then, you’d run:

helm install my-release ./my-chart

And create an instance:

kubectl apply -f myapp-instance.yaml

The mental model for CRDs in Helm is that they are treated as declarative resources that Helm should ensure exist. However, the "ensure" part has a quirk: Helm’s primary concern for CRDs is existence, not necessarily an exact match to the current definition in the chart if the CRD is already present. If the CRD exists, Helm assumes it’s managed correctly and doesn’t attempt to patch or replace it with the new definition from the chart. This is a safety mechanism to prevent accidental deletion or breaking changes to CRDs that might be used by other applications, but it can lead to stale CRD definitions if not handled carefully.

The exact levers you control are the group, versions, scope, and names fields within the CRD’s spec. Changes to these fields are what define your custom resource. Helm’s behavior is tied to the identity formed by group and names.plural/names.kind.

What most people don’t realize is that Helm will attempt to install a CRD if it doesn’t exist, and it will not delete a CRD when you helm uninstall the chart. This means CRDs installed by Helm persist even after the chart is removed, which is generally desirable for custom resources but can lead to confusion if you’re expecting a clean slate.

The next concept you’ll likely grapple with is how to manage CRD versions and ensure backward compatibility when upgrading your application that relies on those CRDs.

Want structured learning?

Take the full Helm course →