Migrate a Helm Chart to a Helm-Based Kubernetes Operator

Tech reviewed: Deepak Prasad
Migrate a Helm Chart to a Helm-Based Kubernetes Operator

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.

Migration from a traditional Helm release to a Helm-based Kubernetes Operator showing values.yaml becoming a Custom Resource spec, watches.yaml connecting the CRD to the embedded Helm chart, and the operator managing Helm install and upgrade lifecycle


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 install or 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:

  1. A CRD describing the instance API (spec mirrors what used to be top-level Helm values).
  2. watches.yaml mapping Group/Version/Kindlocal chart directory in the image (reference).
  3. 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.yaml top-level fields so templates need minimal edits. If the chart uses image.repository, the CR should expose spec.image.repository (nested) rather than inventing unrelated names.
  • Tighten the CRD over time: replace x-kubernetes-preserve-unknown-fields: true with real OpenAPI properties so kubectl explain and webhooks (if you add them later) stay honest.
  • Defaults: chart values.yaml still supplies defaults for omitted CR fields; CRD default annotations are optional and should agree with the chart to avoid two sources of truth.

Secrets and large config

  • Never require raw secrets in .spec if 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):

  1. Template {{ default ... }} inside the chart.
  2. Chart values.yaml.
  3. CR .spec.
  4. overrideValues in watches.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:

  1. Install CRDs and operator Deployment (manifests, OLM Subscription, or your GitOps app-of-apps).
  2. Grant RBAC the chart needs (often ClusterRole if cluster-scoped resources exist—RBAC lesson).
  3. Apply a minimal CR; confirm Helm status conditions (Deployed, etc.) on the CR status.
  4. 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

  1. Deploy the operator (new controller) without deleting the old release yet.
  2. Create CRs under new release names or in a staging namespace; validate workloads.
  3. Cut traffic (Ingress, Service selectors, consumers) to the new stack.
  4. 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)

  1. Uninstall or pause the old Helm release while keeping PVCs / external data.
  2. 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 fullnameOverride strategy.
  • Verify RBAC for any cluster-scoped kinds in the chart.
  • Run parallel path first on staging; document rollback (kubectl delete CR vs helm rollback semantics pre-cutover).
  • Update runbooks from helm upgrade to kubectl 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


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.

Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with over a decade of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, …

  • Red Hat Certified System Administrator in Red Hat OpenStack
  • Certified Kubernetes Application Developer (CKAD)
  • Red Hat Certified Specialist in Ansible Automation
  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security