Helm V2 CRD Management
Published: Estimated reading time: ~4 minutes
With the last feature release of Helm V2 just published, and Helm V3 on the horizon, we’re still stuck with managing CRDs. CRD management in helm is, to be nice about it, utterly horrible.
The Problem with helm
When you have a chart that both installs a CRD and installs a resource that requires that CRD, there’s a race condition where the resource installation can fail because the CRD is not yet recognized as defined by the API server. To work around this, helm added a hook.
apiVersion: ...
kind: ....
metadata:
annotations:
"helm.sh/hook": "pre-install"
...
This hook can only be used at install, not upgrade, not delete, and will still fail if the CRD is already defined.
Why this is a problem
If you want to reinstall a chart and helm delete --purge
the release, the CRD is not uninstalled. This is a good thing. Uninstalling CRDs can result in data loss and since the helm tool doesn’t know the value of your data, doing the safe thing is smart. The problem comes when you try to helm install
again. Because the CRD is defined, the installation fails. The expectation by the helm community is that this is a human interface problem and the human can then choose to run the command with the --no-crd-hook
flag.
The expectation that the helm CLI will always be run by a human is flawed. At enterprise scale, almost nothing is done by humans. There are scripts, controllers, configuration management tools that are all doing this work and making runtime choices based on the failure of a command can be complex and should be avoided. That’s why declarative systems (Ansible, Kubernetes, etc) exist. Declare the desired state and the tools should take the actions necessary to produce consistency.
The workaround
Wrap the CRD in commands to only install it if the API server doesn’t have the capability provided by that CRD.
templates/mycrd.yaml
{{- if not (.Capabilities.APIVersions.Has "mygroup.dom/v1beta1/MyResource") }}
---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: myresource.mygroup.dom
annotations:
"helm.sh/hook": crd-install
...
Now if the API server already has this capability, the template will render an empty file and the error will not occur.
Unfortunately, this means that helm will never manage that CRD ever again. If you add an optional field, or change the additionalPrinterColumns, they will never be updated.
Just use a Job to kubectl apply
the CRD
Unfortunately, this won’t work. Yes, you can make the job run before the resource creation that needs the CRD by using pre-install/pre-upgrade hooks, but helm validates the resources that are going to be installed to ensure that the API server can accept all the resources. Since the capability does not exist in any way that helm can see it, it fails validation and won’t install. The crd-install hook solves this by allowing it to run before validation occurs1.
How to update CRDs that are no longer managed by helm
To solve the CRD abandonment problem, we’ve added a ConfigMap and Job to our templates in addition to the CRD template with the crd-install
hook and capability filtering. Admittedly, this is a very hacky approach but the community has been very resistant to solving this problem in helm v2 with all their focus being on helm v3.
To do this in a maintainable way, we use the .Files
template command and keep the CRD definition, itself, in a files directory.
Examples
mychart/
files/
mycrd.yaml
templates/
configmap.yaml
crd.yaml
job.yaml
myresource.yaml
files/mycrd.yaml
---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: myresource.mygroup.dom
annotations:
"helm.sh/hook": crd-install
...
templates/configmap.yaml
---
apiVersion: v1
kind: ConfigMap
metadata:
name: myresource-crd
annotations:
"helm.sh/hook": pre-install, pre-upgrade
"helm.sh/hook-weight": "-5"
data:
crd.yaml |
{{.Files.Get "files/mycrd.yaml" | indent 6}}
templates/crd.yaml
{{- if not (.Capabilities.APIVersions.Has "mygroup.dom/v1beta1/MyResource") }}
{{.Files.Get "files/mycrd.yaml"}}
{{- endif }}
templates/job.yaml
apiVersion: apps/v1
kind: Job
metadata:
name: myresource-crd-apply
annotations:
"helm.sh/hook": pre-install, pre-upgrade
"helm.sh/hook-weight": "-4"
...
The rest of the Job is just a container that runs kubectl apply on the mounted ConfigMap.