CRD Ordering: kubectl First, Helm Second
| Field | Value |
|---|---|
| Type | Skill Resource |
| Source | ~/.copilot/skills/platform/references/helm/crd-ordering.md |
| Description | Not specified |
Source Content
CRD Ordering: kubectl First, Helm Second
The single hardest rule in this skill. CRDs are cluster-scoped schema; Helm is a release manager. Letting Helm own CRD lifecycle means an uninstall or a chart bump can delete or mutate the schema that other releases depend on. So CRDs install out of band, by a kubectl apply task, before any dependent Helm release. This is ADR-028 (platform foundations).
Contents
The ordering
1. kubectl apply -f <crd-source> # cluster-scoped schema, idempotent2. wait for CRDs to be Established3. helm upgrade --install <release> # controllers + namespaced resourcesStep 1 runs as a dedicated Taskfile target. Step 3’s chart is configured to install no CRDs. The two steps never merge.
Disabling in-chart CRDs
The opt-out key differs per chart — always confirm it on Artifact Hub. Common forms:
| Chart family | Opt-out value |
|---|---|
| cert-manager | installCRDs: false (also crds.enabled: false in newer charts) |
| ingress-nginx | no CRDs shipped — nothing to disable |
| Prometheus (kube-prometheus-stack) | crds.enabled: false |
| external-secrets | installCRDs: false |
| CloudNativePG | install the operator CRDs via kubectl first; chart references them |
Backstops that always work regardless of chart:
helm install --skip-crds— Helm skips the chart’scrds/directory.- Do not template CRDs into
templates/yourself; keep them in akubectl-applied source.
The Taskfile pattern
This target belongs in the project Taskfile (author it with the go-task skill). It applies CRDs, waits for them, then runs Helm. Mirror the kubectl-task style from go-task’s Taskfile.kubectl.yml.
version: '3'
# Included under the `helm:` namespace by the root Taskfile:# task helm:crds# task helm:deploy ENV=prod
vars: ENV: '{{.ENV | default "dev"}}' NAMESPACE: '{{.NAMESPACE | default "p3-app"}}' RELEASE: '{{.RELEASE | default "app"}}' CHART: '{{.CHART | default "./chart"}}' # Pin the CRD source to an exact version — never a moving ref. CERT_MANAGER_CRDS: 'https://github.com/cert-manager/cert-manager/releases/download/v1.16.2/cert-manager.crds.yaml'
tasks: crds: desc: "Apply CRDs out of band, before any Helm release depends on them" cmds: - kubectl apply --server-side -f {{.CERT_MANAGER_CRDS}} - kubectl wait --for=condition=Established --timeout=120s crd --all
deploy: desc: "Apply CRDs first, then install/upgrade the Helm release" deps: [crds] cmds: - >- helm upgrade --install {{.RELEASE}} {{.CHART}} --namespace {{.NAMESPACE}} --create-namespace --skip-crds --values values.yaml --values values-{{.ENV}}.yamlNotes:
deps: [crds]makes the CRD apply a hard prerequisite of every deploy.--server-sideapply is the safe default for large CRDs (avoids the last-applied-annotation size limit).--skip-crdsis the backstop even when the chart’s values already disable CRDs.--values values.yaml --values values-{{.ENV}}.yamllayers the env override on the base.
Why not Helm hooks
Helm CRD hooks and the crds/ directory both tie CRD lifetime to release lifetime. A helm uninstall can then remove CRDs that other releases still need, and a CRD schema change becomes a release-coupled migration instead of a deliberate cluster operation. Applying CRDs with kubectl keeps them cluster-scoped, idempotent, and independent of any single release — which is exactly what ADR-028 requires.