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 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.