Helm & Kubernetes Packaging - Charts, Values, Releases, Rollback, Hooks vs Kustomize, Operators & GitOps
Packaging: Helm charts, values precedence, rendering, releases stored as Secrets, upgrade/rollback, 3-way merge vs server-side apply (Helm 4), hooks, dependencies, library charts, CRD handling; compared with Kustomize, raw manifests, operators and GitOps (Argo CD, Flux) with a decision chart.
- 1Gist
- 2Maps
- 3Q&A
- 4Sandbox
Voice readout needs Web Speech Synthesis in this browser.
Question ladder
L1
What is a Helm release?
Answer
A named installation of a chart in a namespace, with one stored revision per install, upgrade or rollback.
L2
Where does Helm keep release state?
Answer
In Secrets of type helm.sh/release.v1 in the release namespace by default.
L3
In what order are values applied?
Answer
Chart values.yaml, parent values for a subchart, each -f file in order, then --set.
L4
Why a three-way merge?
Answer
So Helm can see live changes, restore drifted fields and keep fields it never managed.
L5
Why does Helm not upgrade CRDs?
Answer
To avoid accidental data loss. Upgrade CRDs separately.
L6
Helm vs Kustomize?
Answer
Helm templates and keeps release history; Kustomize patches plain YAML and keeps no state.
L7
Where does GitOps fit?
Answer
It delivers and repairs drift continuously from Git, using Helm, Kustomize or plain YAML to render.
Failure modes
Replicas drop to 1 when handing them to the HPA
Removing replicas from the chart deletes the field on the next apply, so API defaulting resets it to 1.
Release stuck in pending-upgrade
An interrupted upgrade blocks the next one until you roll back.
GitOps fights the HPA
Without ignoreDifferences the agent keeps resetting replicas or webhook-injected fields.
Misconceptions
helm rollback rewrites history.
It creates a new revision; the failed one stays visible.
Lists in values merge.
Maps merge deeply; lists are replaced whole.
Release Secrets are private to Helm.
Anyone who can read Secrets in that namespace can read rendered values.
Interviewer traps
Templating CRDs in templates/.
Deleting the release then deletes every custom resource of that type.
Running helm upgrade --install without --version.
CI picks up whatever was published last. Pin versions or digests.
Design scenario
Same prompt for every reader.
Requirements
Per-environment config, auditable rollouts, fast rollback, and no drift between Git and the clusters.
Failure assumptions
- A bad chart version fails mid-upgrade.
- An HPA owns replicas on most services.
- A mutating webhook injects sidecars.
Constraints
- Secrets must not appear in release records or CI logs.
- CRDs are shared by several charts.
Prompt
Design packaging and delivery for 30 internal services and 10 third-party components across dev, staging and prod clusters.
API
What does each chart's values interface look like, and what is pinned?
Data
Where does release state live: Helm Secrets, Git, or the GitOps agent?
Architecture
How do the renderer, GitOps agent, CRD pipeline and operators fit together?
Overview
Raw Kubernetes YAML has two problems at scale: you need the same app with different settings in dev, staging and prod, and you need to install, upgrade and roll back a group of objects as one unit. Helm solves both with a chart (templates plus default values) rendered client-side into manifests, and a release (a named, versioned installation whose history Helm stores in the cluster, as Secrets by default). Kustomize solves only the first problem, by patching plain YAML instead of templating it. Operators solve a different problem: ongoing runtime management by a controller. GitOps tools (Argo CD, Flux) solve a fourth: keeping the cluster continuously equal to what is in Git, whatever tool rendered it.
These are layers, not rivals. A common production stack is: a Helm chart or Kustomize overlay renders manifests, Argo CD or Flux applies them and fixes drift, and operators manage the stateful pieces. The concepts that decide whether it works are where state lives, who owns each field, and how drift is detected and repaired.
How Helm works
Flow
- 1
Step 1: chart = Chart.yaml + templates + values.yaml + optional crds folder + dependencies
- nextStep 2: merge values: chart defaults, parent chart, -f files in order, then --set
- 2
Step 2: merge values: chart defaults, parent chart, -f files in order, then --set
- nextStep 3: render Go templates with Values, Release, Chart and Capabilities
- 3
Step 3: render Go templates with Values, Release, Chart and Capabilities
- nextStep 4: run pre-install or pre-upgrade hooks (for example a migration Job) and wait
- 4
Step 4: run pre-install or pre-upgrade hooks (for example a migration Job) and wait
- nextStep 5: apply manifests: 3-way merge in Helm 3, server-side apply by default for new Helm 4 installs
- 5
Step 5: apply manifests: 3-way merge in Helm 3, server-side apply by default for new Helm 4 installs
- nextStep 6: optionally wait for resources to become ready
- nextFailure path: release stuck pending-upgrade, next upgrade refuses until fixed or rolled back
- 6
Step 6: optionally wait for resources to become ready
- nextStep 7: store revision N as a Secret sh.helm.release.v1.NAME.vN, status deployed
- nextFailure path: revision N marked failed; with rollback-on-failure (Helm 4) or atomic (Helm 3) Helm rolls back
- 7
Step 7: store revision N as a Secret sh.helm.release.v1.NAME.vN, status deployed
- nextStep 8: run post hooks, prune history beyond history-max
- 8
Step 8: run post hooks, prune history beyond history-max
- 9
Failure path: revision N marked failed; with rollback-on-failure (Helm 4) or atomic (Helm 3) Helm rolls back
- 10
Failure path: release stuck pending-upgrade, next upgrade refuses until fixed or rolled back
Lesson map
Helm & Kubernetes Packaging - Charts, Values, Releases, Rollback, Hooks vs Kustomize, Operators & GitOps
Packaging: Helm charts, values precedence, rendering, releases stored as Secrets, upgrade/rollback, 3-way merge vs server-side apply (Helm 4), hooks, dependencies, library charts, CRD handling; compared with Kustomize, raw manifests, operators and GitOps (Argo CD, Flux) with a decision chart.
Architecture. Architecture
Select a node to see why it exists, or an edge to see the protocol, direction, effect, and consequence.
Mermaid export
flowchart TB c["Step 1: chart = Chart.yaml + templates + values.yaml + optional crds folder + dependencies"] v["Step 2: merge values: chart defaults, parent chart, -f files in order, then --set"] t["Step 3: render Go templates with Values, Release, Chart and Capabilities"] h["Step 4: run pre-install or pre-upgrade hooks (for example a migration Job) and wait"] a["Step 5: apply manifests: 3-way merge in Helm 3, server-side apply by default for new Helm 4 installs"] w["Step 6: optionally wait for resources to become ready"] s["Step 7: store revision N as a Secret sh.helm.release.v1.NAME.vN, status deployed"] p["Step 8: run post hooks, prune history beyond history-max"] f1["Failure path: revision N marked failed with rollback-on-failure (Helm 4) or atomic (Helm 3) Helm rolls back"] f2["Failure path: release stuck pending-upgrade, next upgrade refuses until fixed or rolled back"] c -->|continues| v v -->|continues| t t -->|continues| h h -->|continues| a a -->|continues| w w -->|continues| s s -->|continues| p w -->|continues| f1 a -->|continues| f2
| Concept | What it is | Why it matters |
|---|---|---|
| Chart | A versioned package: Chart.yaml, values.yaml, templates/, optional crds/, dependencies | The unit you publish and pin, ideally by digest from an OCI registry |
| Values | A tree of settings merged from several sources | The interface of your chart; treat it like an API |
| Release | A named installation of a chart in a namespace | Upgrade and rollback target |
| Revision | One version of a release (install, each upgrade, each rollback) | Rollback creates a new revision; it does not rewrite history |
| Release record | Secret of type helm.sh/release.v1 in the release namespace (default driver) | Readable by anyone who can read Secrets there, and contains rendered manifests and values |
| Hook | A template annotated helm.sh/hook (pre-install, post-install, pre-upgrade, post-upgrade, pre-delete, post-delete, pre-rollback, post-rollback, test) | Run Jobs at lifecycle points; hook resources are not managed as part of the release |
| Library chart | type: library; only defines reusable named templates | Share labels, probes and boilerplate across many app charts |
| Dependency (subchart) | Declared in Chart.yaml, vendored into charts/ | Parent values can override subchart values under the subchart's key |
Two-way or three-way merge on upgrade?
Prefer
Three-way merge (Helm 3 and later)
Compare the old manifest, the live state and the new manifest.
- Drift from kubectl scale --replicas=0 was repaired back to 3.
- The webhook-injected sidecar annotation was kept.
- Fields you stop declaring are deleted, so hand them off in two steps.
Alternative
Two-way merge (Helm 2)
Compare only the old and new chart manifests.
- The live replicas=0 drift was kept.
- Changes made in the cluster are invisible to the upgrade.
- The chart and the cluster silently disagree.
From a chart to a recorded release
Diagram 1 condensed: render, apply, record, and the rollback path.
- 1
Merge values
Chart defaults, -f files in order, then --set. - 2
Render templates
Helm renders manifests on the client. - 3
Apply with a merge
Three-way merge, or server-side apply for new Helm 4 releases. - 4
Record the revision
A release Secret stores the chart, values and manifest. - 5
Fail and roll back
A failed upgrade stays as a revision; rollback creates a new one.
Values precedence
The Helm docs list values sources in order of specificity: the chart's values.yaml, then (for a subchart) the parent chart's values.yaml, then files passed with -f (later files win), then --set parameters. Maps merge deeply, lists are replaced whole, and setting a key to null deletes a default.
"""Simulation: Helm values precedence and release history.
Values: chart values.yaml < parent chart values < -f files (later wins) < --set.
Maps merge deeply, lists are replaced whole, and null deletes a default key.
Releases: each install/upgrade/rollback writes a new revision record (Helm stores
them as Secrets named sh.helm.release.v1.<release>.v<revision> by default);
`helm upgrade --history-max` defaults to 10.
"""
import copy, json
def merge(base, over):
out = copy.deepcopy(base)
for k, v in over.items():
if v is None:
out.pop(k, None) # null removes the default
elif isinstance(v, dict) and isinstance(out.get(k), dict):
out[k] = merge(out[k], v) # maps merge recursively
else:
out[k] = copy.deepcopy(v) # scalars and lists replace
return out
def set_flag(expr): # --set a.b.c=val
path, val = expr.split("=", 1)
val = int(val) if val.isdigit() else val
node = {}
cur = node
keys = path.split(".")
for k in keys[:-1]:
cur[k] = {}; cur = cur[k]
cur[keys[-1]] = val
return node
chart_defaults = {"replicaCount": 1, "image": {"repository": "checkout", "tag": "1.4.2", "pullPolicy": "IfNotPresent"},
"resources": {"requests": {"cpu": "100m"}}, "ingress": {"enabled": False, "hosts": ["checkout.local"]},
"podAnnotations": {"prometheus.io/scrape": "true"}}
prod_file = {"replicaCount": 4, "ingress": {"enabled": True, "hosts": ["checkout.example.com"]}}
region_file = {"resources": {"requests": {"memory": "256Mi"}}, "podAnnotations": None}
layers = [("values.yaml", chart_defaults), ("-f prod.yaml", prod_file), ("-f us-east.yaml", region_file),
("--set image.tag=1.5.0", set_flag("image.tag=1.5.0"))]
vals = {}
for name, layer in layers:
vals = merge(vals, layer)
print(f"after {name:<22} replicas={vals.get('replicaCount')} tag={vals['image']['tag']} "
f"hosts={vals['ingress']['hosts']} annotations={vals.get('podAnnotations', '(removed)')}")
print("resources merged deeply:", json.dumps(vals["resources"]))
# --------- releases --------------------------------------------------------
print("\nrelease history for 'checkout' (history-max 3 to keep output short)")
HISTORY_MAX = 3
secrets = [] # list of dicts, oldest first
def write(action, values, status="deployed"):
for s in secrets:
if s["status"] == "deployed":
s["status"] = "superseded"
rev = (secrets[-1]["rev"] + 1) if secrets else 1
secrets.append({"rev": rev, "action": action, "tag": values["image"]["tag"], "status": status})
while len(secrets) > HISTORY_MAX: # prune oldest revisions
secrets.pop(0)
return rev
write("install", merge(vals, {"image": {"tag": "1.4.2"}}))
write("upgrade", vals)
r = write("upgrade", merge(vals, {"image": {"tag": "1.6.0-bad"}}), status="failed")
secrets[-2]["status"] = "deployed" # a failed upgrade leaves the previous one deployed
target = secrets[-2]
write(f"rollback to {target['rev']}", merge(vals, {"image": {"tag": target["tag"]}}))
for s in secrets:
print(f" Secret sh.helm.release.v1.checkout.v{s['rev']}: {s['action']:<14} tag={s['tag']:<10} {s['status']}")
print(" note: rollback created a NEW revision; revision 1 was pruned by history-max")Output:
after values.yaml replicas=1 tag=1.4.2 hosts=['checkout.local'] annotations={'prometheus.io/scrape': 'true'}
after -f prod.yaml replicas=4 tag=1.4.2 hosts=['checkout.example.com'] annotations={'prometheus.io/scrape': 'true'}
after -f us-east.yaml replicas=4 tag=1.4.2 hosts=['checkout.example.com'] annotations=(removed)
after --set image.tag=1.5.0 replicas=4 tag=1.5.0 hosts=['checkout.example.com'] annotations=(removed)
resources merged deeply: {"requests": {"cpu": "100m", "memory": "256Mi"}}
release history for 'checkout' (history-max 3 to keep output short)
Secret sh.helm.release.v1.checkout.v2: upgrade tag=1.5.0 superseded
Secret sh.helm.release.v1.checkout.v3: upgrade tag=1.6.0-bad failed
Secret sh.helm.release.v1.checkout.v4: rollback to 2 tag=1.5.0 deployed
note: rollback created a NEW revision; revision 1 was pruned by history-maxNotice two things in that output. The us-east file removed the default annotations with null, and replaced nothing else. The ingress.hosts list from prod replaced the default list rather than appending to it. In the history, the failed upgrade stayed visible as revision 3 and the rollback became revision 4. With the real default --history-max of 10 on helm upgrade, Helm keeps ten revisions per release.
Upgrade, rollback and the 3-way merge
Helm 2 compared only the old and new chart manifests (a 2-way merge), so changes made directly in the cluster were invisible to it. Helm 3 uses a three-way strategic merge patch: old manifest, live state, new manifest. Helm 4 defaults new releases to server-side apply, keeps existing releases on whatever method they used before, and renames two flags: --atomic became --rollback-on-failure and --force became --force-replace (the old names still work with a deprecation warning).
// Simulation: two-way vs three-way merge when the live object has drifted.
// Mirrors the Helm 3 docs example: chart says replicas 3, someone scales to 0,
// then the same chart is applied again. Also shows a field added by another
// actor (a mutating webhook's sidecar label) surviving a three-way merge.
type Doc = Record<string, string | number>;
function twoWay(oldM: Doc, newM: Doc, live: Doc): Doc {
const out = { ...live };
for (const k of new Set([...Object.keys(oldM), ...Object.keys(newM)])) {
if (oldM[k] !== newM[k]) { // only chart-to-chart differences are patched
if (k in newM) out[k] = newM[k]; else delete out[k];
}
}
return out;
}
function threeWay(oldM: Doc, newM: Doc, live: Doc): Doc {
const out = { ...live };
for (const k of Object.keys(newM)) {
if (live[k] !== newM[k]) out[k] = newM[k]; // desired value wins over drift
}
for (const k of Object.keys(oldM)) {
if (!(k in newM)) delete out[k]; // we owned it before and dropped it now
}
return out; // keys nobody declared (sidecar) are left alone
}
const chartV1: Doc = { replicas: 3, image: "checkout:1.5.0" };
const live: Doc = { replicas: 0, image: "checkout:1.5.0", "sidecar.istio.io/status": "injected" };
console.log("chart (old == new):", JSON.stringify(chartV1));
console.log("live after kubectl scale --replicas=0 and webhook injection:", JSON.stringify(live));
console.log("two-way result: ", JSON.stringify(twoWay(chartV1, chartV1, live)), "<- drift kept");
console.log("three-way result:", JSON.stringify(threeWay(chartV1, chartV1, live)), "<- drift repaired, sidecar kept");
const chartV2: Doc = { image: "checkout:1.6.0" }; // v2 stops setting replicas (HPA owns it now)
const live2: Doc = { replicas: 7, image: "checkout:1.5.0" };
console.log("\nchart v2 drops 'replicas' so the HPA can own it; live has replicas=7");
const patched = threeWay(chartV1, chartV2, live2);
console.log("three-way result:", JSON.stringify(patched), "<- field you stopped declaring is deleted");
const defaulted = { replicas: 1, ...patched }; // API server defaulting fills spec.replicas
console.log("after API defaulting:", JSON.stringify(defaulted), "<- 7 pods drop to 1 until the HPA reacts");
console.log("lesson: hand replicas to the HPA in two steps (stop owning the field first), per the HPA migration docs");Output:
chart (old == new): {"replicas":3,"image":"checkout:1.5.0"}
live after kubectl scale --replicas=0 and webhook injection: {"replicas":0,"image":"checkout:1.5.0","sidecar.istio.io/status":"injected"}
two-way result: {"replicas":0,"image":"checkout:1.5.0","sidecar.istio.io/status":"injected"} <- drift kept
three-way result: {"replicas":3,"image":"checkout:1.5.0","sidecar.istio.io/status":"injected"} <- drift repaired, sidecar kept
chart v2 drops 'replicas' so the HPA can own it; live has replicas=7
three-way result: {"image":"checkout:1.6.0"} <- field you stopped declaring is deleted
after API defaulting: {"replicas":1,"image":"checkout:1.6.0"} <- 7 pods drop to 1 until the HPA reacts
lesson: hand replicas to the HPA in two steps (stop owning the field first), per the HPA migration docsExpectedchart (old == new): {"replicas":3,"image":"checkout:1.5.0"} live after kubectl scale --replicas=0 and webhook injection: {"replicas":0,"image":"checkout:1.5.0","sidecar.istio.io/status":"injected"} two-way result: {"replicas":0,"image":"checkout:1.5.0","sidecar.istio.io/status":"injected"} <- drift kept three-way result: {"replicas":3,"image":"checkout:1.5.0","sidecar.istio.io/status":"injected"} <- drift repaired, sidecar kept chart v2 drops 'replicas' so the HPA can own it; live has replicas=7 three-way result: {"image":"checkout:1.6.0"} <- field you stopped declaring is deleted after API defaulting: {"replicas":1,"image":"checkout:1.6.0"} <- 7 pods drop to 1 until the HPA reacts lesson: hand replicas to the HPA in two steps (stop owning the field first), per the HPA migration docs
Press Run. Snippets must be self-contained — no network, files, or native modules.
The second half of that run is a real incident pattern. The Kubernetes HPA docs warn that removing spec.replicas from a manifest once an HPA is active causes a one-time drop to the default of 1 replica on the next apply. The fix is to stop owning the field first, then remove it. The HPA side is covered in HPA, VPA & Autoscaling Gotchas.
Helm vs Kustomize vs raw manifests vs operators vs GitOps
| Raw manifests | Helm | Kustomize | Operator | GitOps (Argo CD, Flux) | |
|---|---|---|---|---|---|
| Variation mechanism | Copy and edit | Templates plus values | Overlays and patches on plain YAML | Controller logic reacting to a CR | None itself; runs Helm, Kustomize or plain YAML |
| Where "what is installed" lives | Nowhere | Release Secrets in the cluster | Nowhere (the cluster and Git) | The custom resource and its status | Git is the source; the agent tracks sync status |
| Lifecycle actions | None | Install, upgrade, rollback, hooks, tests | None (kubectl apply -k) | Continuous: backups, failover, upgrades | Continuous sync, optional prune and self-heal |
| Drift handling | None | Repaired only at the next upgrade (3-way merge) | Only at next apply | Continuous, for what it manages | Detects OutOfSync continuously; self-heal re-applies |
| Learning curve | Lowest | Go templates, whitespace, tpl traps | Patch semantics | Writing and running controllers | A platform to operate |
| Failure surface | Typos | Template logic, values sprawl, stuck releases | Patch targeting mistakes | Controller bugs act continuously | A bad commit is applied everywhere quickly |
Templating vs patching. Templates are flexible but turn YAML into a program: conditionals, loops, indentation functions, and a large values surface that becomes a public API. Patching keeps every file valid YAML and makes diffs readable, but anything not anticipated by the base needs a new patch, and complex variation becomes many overlays. Many teams use both: Helm for third-party software, Kustomize overlays for their own services, or Kustomize post-processing a Helm render.
State. Helm's release Secrets are both a feature (rollback without Git, helm history) and a liability: they can drift from Git, they can get stuck in pending-* states, and they hold rendered values, so anyone who can read Secrets in that namespace can read them. GitOps moves the source of truth to Git and treats the cluster as a cache of it. Argo CD can render Helm charts as plain manifests and apply them itself, so the release history lives in Git and in Argo CD rather than in Helm Secrets. Flux's helm-controller, by contrast, performs real Helm releases.
Drift. Only continuous reconcilers (GitOps agents with self-heal, and operators) repair drift between deploys. Helm repairs it only when you next run upgrade. Both need rules for fields owned by someone else: the HPA owns replicas, mutating webhooks add sidecars, and controllers write status. Argo CD supports ignoreDifferences for exactly this; server-side apply's field managers make the ownership explicit.
Hooks, dependencies and library charts
- Hooks run in weight order (
helm.sh/hook-weight, ascending) and Helm waits for hook Jobs to complete. Because hook resources are not part of the release,helm uninstalldoes not remove them. Sethelm.sh/hook-delete-policy(for examplebefore-hook-creation,hook-succeeded) or a Job TTL, or they pile up. Database migrations as pre-upgrade hooks are common; make them backward compatible (expand and contract) so a rollback does not meet a schema it cannot read. - Dependencies are pinned in
Chart.yamland locked inChart.lock. Override subchart values under the subchart's name, useconditionortagsto toggle them, and useglobalfor shared settings. - Library charts hold named templates (
define) only and cannot be installed. They are the DRY tool for a platform team that maintains many service charts.
Pitfalls
- CRDs. Files in a chart's
crds/directory are installed on first install only. Helm does not upgrade or delete them, a deliberate decision to avoid accidental data loss. Upgrade CRDs separately (a dedicated CRD chart, or apply them in CI before the app chart), and never template them intemplates/without understanding that deleting the release then deletes every custom resource of that type. - Rendering secrets. Values passed with
--setor-fend up in the release record, andhelm templateoutput in CI logs. Prefer references to external secret stores, External Secrets or a CSI secret driver (secret stores compared, secret injection), or encrypted values tools with tight RBAC. - Random and lookup functions.
randAlphaNumgenerates a new password on every upgrade unless you guard it withlookup, andlookupalways returns an empty response underhelm templateand does not contact the cluster on client-side dry runs (use--dry-run=serverto test it), so behavior differs between CI and the cluster. - Stuck releases. An interrupted upgrade leaves
pending-upgrade, and the next upgrade fails because another operation appears to be in progress. Roll back to the last good revision instead of deleting release Secrets by hand. --reuse-valuessurprises. It merges your new flags onto the last release's values and ignores new chart defaults;--reset-valuesdoes the opposite.- Unpinned charts.
helm upgrade --install app repo/appwithout--versionmeans CI picks up whatever was published last. Pin versions, or digests for OCI charts as Helm 4 supports.
Decision chart: how should this thing be packaged and delivered?
Decisions
- 1
A
- nextHelm chart pinned by version or digest, values file per environment
- nextStep 2: do environments differ only by a few fields?
- 2
Helm chart pinned by version or digest, values file per environment
- nextStep 3: does it need day-2 automation such as failover, backups, version upgrades?
- ?
Step 2: do environments differ only by a few fields?
- nextKustomize base plus overlays, or one small Helm chart
- nextShared library chart plus thin per-service charts
- 4
Kustomize base plus overlays, or one small Helm chart
- nextStep 3: does it need day-2 automation such as failover, backups, version upgrades?
- 5
Shared library chart plus thin per-service charts
- nextStep 3: does it need day-2 automation such as failover, backups, version upgrades?
- ?
Step 3: does it need day-2 automation such as failover, backups, version upgrades?
- nextUse or build an operator; deliver the operator and its CRs with the same pipeline
- nextStep 4: how do changes reach the cluster?
- 7
Use or build an operator; deliver the operator and its CRs with the same pipeline
- nextStep 4: how do changes reach the cluster?
- ?
Step 4: how do changes reach the cluster?
- nextGitOps agent with self-heal, ignoreDifferences for fields others own
- nextCI runs helm upgrade with rollback-on-failure and wait
- 9
GitOps agent with self-heal, ignoreDifferences for fields others own
- 10
CI runs helm upgrade with rollback-on-failure and wait
What happens if you choose an alternative
| Choice | Instead of | Consequence |
|---|---|---|
CI helm upgrade only | GitOps agent | Simple, but drift lives until the next deploy and multi-cluster rollout is scripted by hand |
GitOps without ignoreDifferences | Scoped ignore rules | The agent fights the HPA or webhooks forever (OutOfSync flapping, replica resets) |
| Templating every field in values.yaml | A small curated values API | Chart becomes unreadable; every change is a breaking change |
| Kustomize for a third-party product | Upstream Helm chart | You re-implement upstream's configuration logic as patches |
| An operator for a stateless app | A Deployment and a chart | A controller to maintain with no day-2 benefit |
| Helm hooks for heavy orchestration | A workflow engine or operator | Hidden, unmanaged resources and brittle upgrade ordering |
Interview Q&A
What is a Helm release and where is its state?
Answer
A named installation of a chart in a namespace. Each install, upgrade or rollback creates a revision, stored by default as a Secret of type helm.sh/release.v1 in that namespace, containing the chart, values and rendered manifest. helm history and helm rollback read those records.
In what order are values applied?
Answer
The chart's values.yaml, then the parent chart's values for a subchart, then each -f file in order, then --set. Maps merge deeply, lists replace, and null removes a key.
Why does Helm 3 use a three-way merge?
Answer
So it can see live changes. Comparing old manifest, live state and new manifest lets Helm restore fields that were changed in the cluster and keep fields it never managed, such as injected sidecars. A two-way merge ignores live drift entirely.
How do you handle CRDs with Helm?
Answer
Put them in crds/ for first install, but manage upgrades separately, because Helm intentionally never upgrades or deletes them. Many teams ship CRDs in their own chart or apply them in a pipeline step before the app chart.
Helm vs Kustomize vs GitOps?
Answer
Helm templates and packages with release history and rollback. Kustomize patches plain YAML with no state. GitOps tools continuously reconcile the cluster to Git and can use either renderer. Choose a renderer for variation, and GitOps for delivery and drift control.
What does `--rollback-on-failure` do?
Answer
In Helm 4 it replaces --atomic: if the upgrade fails or does not become ready within the wait, Helm rolls back to the previous successful release. It implies waiting (defaulting the wait strategy to watching resource status).
Why do Helm hook resources pile up?
Answer
They are not part of the release, so helm uninstall does not remove them. Set a hook-delete-policy or a Job TTL.
What is a library chart for?
Answer
It holds named templates only and cannot be installed, so a platform team can share labels, probes and boilerplate across many service charts.
Check yourself
Render a chart with helm template using two -f files and a --set, then diff the result against your prediction from the precedence rules, paying attention to lists and null.
Elsewhere in the library
These pages stay as they are. This lesson only points at them: Pipeline Anatomy — Stages, Gates, Environments & Promotion, Artifacts & Registries — Digests, Provenance & Immutability, Supply Chain Security — Signing, SBOMs & OIDC Federation, Modules, Composition & Versioning, State, Backends, Locking & Workspaces, Rolling, Blue-Green & Canary — Strategies, PDBs & Blast Radius, Secret Stores Compared — Vault, Cloud Secrets Manager & Kubernetes Secrets, Hermeticity & Reproducible Builds - Sandboxes, Pinned Toolchains, the Nix Store & Bit-for-Bit Output.