You already ship an application with Helm. Product or platform now wants a Kubernetes API (MyProduct) for self-service installs, a stable upgrade story on the cluster, or a path toward OLM—without rewriting templates in Go. The Helm-based operator (Operator SDK helm.sdk.operatorframework.io/v1) wraps the same chart behind a CR; reconcile is still helm install / upgrade / uninstall.
This article is the brownfield bridge between Helm-based operator Part 1 (scaffold, CRD, watches.yaml) and Part 2 (lifecycle, drift, hooks, ceiling). Read those first if you have not built a Helm operator yet.
When Helm alone is enough vs when an operator helps
Stay on plain Helm (or Helm + GitOps) when
- One release per concern is enough and Argo CD / Flux already owns desired state from Git.
- Consumers are cluster admins who run
helm installor sync HelmRelease objects—there is no need for a namespaced "product API." - You do not need OLM catalog resolution,
Subscription-style upgrades across channels, or a CRD surface for multi-tenant self-service. - All coordination is chart-internal (subcharts, hooks) and no cluster-wide operator logic is required.
Reach for a Helm-based operator when
- You want
kubectl apply -f myproduct.yaml(or a UI / self-service portal) instead of handing teams a values file and Helm flags. - Same chart, many instances per namespace (or selected namespaces) with RBAC-scoped CR access—platform installs the operator once; tenants create CRs.
- You are heading toward bundles and OLM (OLM bundles & OperatorHub) where the install unit is an operator, not a loose chart tarball.
- You need the Operator SDK machinery (bundle pipeline, scorecard, project layout) while keeping 100% of templates in Helm.
When you have already outgrown the pre-built operator
If you need custom status, finalizers, cross-CR coordination, or dynamic values from external APIs, the pre-built reconciler hits its ceiling—Helm hybrid (Go + chart) is the next step. This migration article still applies to how you shape the CR and cut over workloads; only the reconciler implementation changes.
| Need | Plain Helm / GitOps | Helm-based operator | Go / hybrid operator |
|---|---|---|---|
| Declarative install from Git | Excellent | CR YAML instead of HelmRelease | Same + custom logic |
| Self-service tenant API | Awkward | Natural (MyKind CR) |
Same, richer status |
| Custom reconcile / webhooks | No | Hooks only (limited) | Yes |
| OLM / OperatorHub | Indirect | Straightforward bundle | Straightforward bundle |
Target end state (what actually changes)
After migration the chart is still the source of rendered manifests. The operator binary adds:
- A CRD describing the instance API (
specmirrors what used to be top-level Helm values). watches.yamlmappingGroup/Version/Kind→ local chart directory in the image (reference).- RBAC + Deployment that runs the generic helm reconciler.
User-facing change: instead of helm upgrade myrelease -f prod.yaml ./chart, tenants apply a CR whose .spec carries the same data prod.yaml used to carry. CI/CD might emit CR manifests (or Helm wraps the operator install—both patterns exist).
Mapping values.yaml → operator CRD spec
The reconciler passes the entire CR .spec to Helm as the values map (see Part D in Helm-based operator Part 2 for precedence and examples). Your design job is to make that map line up with what templates expect.
Shape and naming
- Prefer the same keys as
values.yamltop-level fields so templates need minimal edits. If the chart usesimage.repository, the CR should exposespec.image.repository(nested) rather than inventing unrelated names. - Tighten the CRD over time: replace
x-kubernetes-preserve-unknown-fields: truewith real OpenAPI properties sokubectl explainand webhooks (if you add them later) stay honest. - Defaults: chart
values.yamlstill supplies defaults for omitted CR fields; CRDdefaultannotations are optional and should agree with the chart to avoid two sources of truth.
Secrets and large config
- Never require raw secrets in
.specif anyone with namespace read can read CRs. Patterns:spec.existingSecret, reference-only IDs, or mount ConfigMaps the chart already templates. - Etcd size: very large blobs in a CR are a smell—split configuration into a ConfigMap/Secret the chart mounts, and keep the CR as references + high-level knobs.
Versioning the API
When you rename or flatten fields, treat it like any CRD evolution: additive minor changes, conversion or parallel fields for breaking changes—see CRD version upgrades. Helm charts tolerate extra values keys; missing required keys fail at render time—surface those failures clearly in runbooks.
watches.yaml, overrideValues, chart version vs operator version
watches.yaml essentials
Each stanza needs group, version, kind, and chart (local path baked into the image). Optional fields you will use in migration:
overrideValues— operator-enforced values layered on top of the CR (see precedence below).watchDependentResources/reconcilePeriod— drift and periodic resync (Part 2).selector— limit which CRs this operator instance reconciles in multi-tenant setups.
Charts must be vendored into the repo; the chart field cannot point to a remote repo URL at runtime.
Precedence (effective Helm values)
Order from losing to winning (full table in Part 2):
- Template
{{ default ... }}inside the chart. - Chart
values.yaml. - CR
.spec. overrideValuesinwatches.yaml(plus env substitution where configured).
Use overrideValues for non-negotiables: internal image mirrors, mandatory labels, feature flags the customer must not override. Keep the list short and auditable.
Chart semver vs operator image semver
Track two versions deliberately:
| Artifact | What it versions | Bump when |
|---|---|---|
Chart (Chart.yaml version) |
Template and default values behavior | Breaking template changes, new subcharts, default behavior changes |
| Operator image (git tag / CI digest) | Bundled chart snapshot + reconciler + watches.yaml |
Any change to chart copy, overrides, RBAC, or binary |
Rule of thumb: ship a new operator build whenever the embedded chart changes, even if the CRD apiVersion stays the same—users reason about "operator 1.4.2 includes chart 2.3.1." Document that mapping in release notes.
Day-0 install (greenfield)
For a net-new cluster or namespace:
- Install CRDs and operator Deployment (manifests, OLM
Subscription, or your GitOps app-of-apps). - Grant RBAC the chart needs (often
ClusterRoleif cluster-scoped resources exist—RBAC lesson). - Apply a minimal CR; confirm Helm status conditions (
Deployed, etc.) on the CR status. - Run your smoke tests the same way you did for
helm install—URLs, probes, data plane.
Release name is typically derived from the CR metadata (name/namespace); align with how your chart expects fullnameOverride or similar so Service DNS stays predictable.
Day-2 upgrade path for existing Helm releases
Teams rarely migrate from zero; they migrate from helm install history, HelmRelease objects, or packaged zip installs.
Inventory before touching production
helm list -n <ns>— release name, chart version, app version.helm get values -n <ns> <release> -a— effective values (including defaults merged by Helm client where applicable).helm get manifest— ground truth of what is running.- Identify immutable fields (StatefulSet ordinals, PVC binding, Service
clusterIP) and hooks that ran once.
Upgrade driven by CR spec changes
Once the operator owns the release, any spec patch triggers helm upgrade with the merged values map. Plan:
- Backward-compatible chart changes with the same values shape → safe operator upgrade + rolling CR edits.
- Breaking value shape → bump CRD version or document a one-shot migration Job (Helm hook or external playbook) before tenants apply new fields.
Existing release name and namespace
If the goal is one continuous release (same Helm secret history), you must ensure the operator's Helm driver uses the same release name and namespace your chart templates expect—and that no second controller applies overlapping manifests. The pre-built operator does not magically import foreign release ownership; treat "takeover" as a custom migration project (often parallel install is safer).
Cutover strategy: parallel install vs in-place migration
Parallel install (recommended default)
- Deploy the operator (new controller) without deleting the old release yet.
- Create CRs under new release names or in a staging namespace; validate workloads.
- Cut traffic (Ingress, Service selectors, consumers) to the new stack.
- Decommission the old Helm release when idle.
Pros: clear rollback (flip traffic back), no surprise ownership fights. Cons: duplicate resource cost during overlap; data components need replication or import steps.
In-place migration (high risk)
- Uninstall or pause the old Helm release while keeping PVCs / external data.
- Immediately let the operator create a release that adopts the same workload definitions.
Risks: duplicate helm.sh/release secrets if names collide; annotations/labels Helm adds differ between CLI and operator; immutable API errors on upgrade; finalizers on old resources blocking delete. Only attempt with a written procedure, backups, and a maintenance window.
Checklist (condensed)
- Export current values and manifests; store in Git.
- Diff chart used by operator vs chart used by last successful deploy.
- Decide release naming and
fullnameOverridestrategy. - Verify RBAC for any cluster-scoped kinds in the chart.
- Run parallel path first on staging; document rollback (
kubectl deleteCR vshelm rollbacksemantics pre-cutover). - Update runbooks from
helm upgradetokubectl patch/ GitOps CR changes.
FAQ
Can one operator watch multiple charts?
Multiple entries in watches.yaml are supported—each Kind maps to its own chart path. Migration scope is still "one chart → one Kind" in most products.
Do we need OLM for an internal migration?
No. OLM is optional catalog glue; bare manifests or GitOps are fine.
What if teams still want raw Helm?
Ship a supported path only (usually CR or OLM). Maintaining Helm CLI and operator on the same release doubles incidents.
See also
- Helm-based operator Part 1 — chart, CRD, watches.yaml
- Helm-based operator Part 2 — lifecycle, drift, hooks, scope
- Helm operator vs Flux vs Argo CD
- From commit to cluster
- OLM bundles & OperatorHub
- Operator SDK — Helm operator overview
Bottom line: migrating to a Helm-based operator is mostly API and process work—reshape values as CR spec, bake the chart into the operator image, use overrideValues sparingly for platform mandates, version chart and operator independently but document the pair, prefer parallel install before any risky in-place takeover, and keep GitOps or runbooks pointed at the CR as the new source of truth.

