Helm operations
Production Helm charts: a practical checklist
Walk this list against a packaged chart before you treat a release as production-ready. Each check has a pass fixture, a known failure, and a command you can rerun.
Chart fidelity
Examples use Universal Helm Chart application 0.4.3 on Artifact Hub. Keys match the published values.yaml and values.schema.json. Hostnames and images are sanitized—replace them before apply. The chart default securityContext: {} is empty; production values must harden it.
Chart 0.4.3 — args / multi-port / service.enabled
From application 0.4.3: main container args (default []); Deployment revisionHistoryLimit (default 10); multi-port via containerPorts and service.ports; optional chart-managed Service via service.enabled (default true). When service.ports is set, Ingress / simple HTTPRoute / NOTES use the first entry (templates/_helpers.tpl application.servicePort). Legacy single-port service.name / service.port / service.protocol / service.appProtocol remain supported. Set service.enabled: false when the workload does not need a chart-managed Service. Do not invent other port keys.
Chart 0.4.3 — route.spec
From application 0.4.3, set route.spec to a complete HTTPRoute.spec (parentRefs, hostnames, rules, filters, multi-backendRefs). When route.spec is non-empty, the chart renders it unchanged and ignores the simple route.hostname / route.path / route.gateway generator. Simple route.* remains the default path; route.gateway is required only when route.enabled: true and route.spec is empty. Do not invent shorthand keys like route.headers or route.cors.
Chart 0.4.3 — service.appProtocol
Optional service.appProtocol (default "") sets Service.spec.ports[].appProtocol on the chart-managed Service (templates/7_service.yaml). For Services created by ingressPlain paths with createService: true, set paths[].service.appProtocol (since 0.4.0). Leave empty when you do not need a protocol hint (for example grpc). Do not invent other service.* protocol keys.
Chart 0.4.2 — externalSecretHooks
Behavior change in application 0.4.2: ExternalSecret objects in extraManifests are ordinary resources by default. The chart does not add Helm hooks automatically. Recommend explicit lifecycle/ordering annotations on the manifest (for example argocd.argoproj.io/sync-wave: "-5"). Set externalSecretHooks.enabled: true (default false) only when legacy automatic helm.sh/hook: pre-install,pre-upgrade / hook-weight: "-5" / hook-delete-policy: before-hook-creation behavior is required (templates/1_extra-manifests.yaml). Do not invent other externalSecret* keys.
Versions used
- Chart:
application 0.4.3
- Helm client: v3.18.4
- API:
apps/v1 Deployment, policy/v1 PDB, gateway.networking.k8s.io/v1 HTTPRoute, networking.k8s.io/v1 Ingress
- Battle reference (historical): Kubernetes v1.35.4 (2026-07-23) with chart
0.3.5
Pass / fail fixtures
Example pack: examples/production-helm-checklist/. Pass path: fixtures/pass-production.yaml (hardened fields). Three fail fixtures cover missing resources, privileged/hostPath, and plaintext secret anti-patterns.
1. Chart.yaml and versioning
- Confirm
apiVersion: v2, type: application, and SemVer version (chart) separate from appVersion (app).
- Bump chart
version for every packaged change consumers depend on.
- Pin GitOps wrappers to an exact chart version (
dependencies[].version: "0.4.3"), not a floating tag.
- Publish a changelog entry for breaking values renames before consumers upgrade.
# Chart.yaml (consumer wrapper)
dependencies:
- name: application
version: "0.4.3"
repository: https://chaser100.github.io/u-helm-chart
2. values.schema.json
Treat the schema as a CI gate, not optional docs. Run it in CI with helm lint / helm template so invalid types fail before merge.
Pass: integer replicaCount, enum imagePullPolicy.
Fail (captured):
# fail-schema-replicaCount.yaml
replicaCount: "two"
# Error: Invalid type. Expected: integer, given: string
# fail-schema-imagePullPolicy.yaml
imagePullPolicy: Sometimes
# Error: must be one of "Always", "IfNotPresent", "Never"
3. Deterministic naming, selectors, labels
- Release name becomes
app.kubernetes.io/instance; keep it stable across upgrades.
- Selector labels must stay immutable for the life of a Deployment (
app.kubernetes.io/name + instance on this chart).
- Do not change
nameOverride / fullnameOverride so that selectors rewrite on a live Deployment—Kubernetes rejects selector mutations.
- Use chart helper labels instead of hand-rolled maps that drift between Service and Pod template.
4. Immutable fields and upgrade hazards
- Treat Service
clusterIP, PVC names, and Deployment selectors as sticky once applied.
- Changing
service.type from ClusterIP to a type that needs recreation requires an explicit cutover plan.
- Job/CronJob renames create new objects; old Jobs may linger unless TTL or hooks clean them.
- CRDs installed from app charts create ownership problems. Prefer platform-owned CRDs outside the application release.
5. Secure pod and container defaults
The chart package default is empty securityContext: {}. Production values must set hardening explicitly:
podSecurityContext:
seccompProfile:
type: RuntimeDefault
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
serviceAccount:
create: true
automount: false
Fail mode: empty security contexts in prod namespaces that enforce Pod Security Standards either reject pods (restricted) or quietly accept weak defaults (baseline without org policy).
6. ServiceAccount and RBAC
- Create a dedicated ServiceAccount per release (
serviceAccount.create: true).
- Set
automount: false unless the workload must call the API.
- This chart manages the ServiceAccount object; Role/RoleBinding are not first-class keys on 0.4.3. Grant API access through platform RBAC or labelled
extraManifests, least privilege only.
7. Probes, resources, disruption
- Define both
livenessProbe and readinessProbe with realistic timeouts. Do not point liveness at a dependency that can flap independently of process health.
- Set
resources.requests and limits; empty resources undermine scheduling fairness and HPA signals.
- PDB is not a first-class key on 0.4.3. Ship
policy/v1 PodDisruptionBudget through extraManifests with selectors that match chart labels (see pass fixture).
topologySpreadConstraints is not a chart key either. Document the intended fragment and apply it with a post-renderer (or wait for a chart key)—do not invent values.topologySpreadConstraints.
8. HPA
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 6
targetCPUUtilizationPercentage: 70
With HPA enabled, HPA owns scale—do not keep overriding replicaCount in GitOps as if it were the source of truth. Keep minReplicas ≥ PDB needs.
9. Hooks, Jobs, and CronJobs
- Use chart
job / cronjob values instead of ad-hoc helm.sh/hook annotations unless you own hook weight and deletion policy.
- Hooks that create CRDs or cluster-scoped objects make rollback harder; keep them out of app charts.
- Set Job
ttlSecondsAfterFinished so failed migrate Jobs do not clutter namespaces forever.
10. Secrets references (never literals)
envSecrets:
enableEnv: true
envs:
- name: DATABASE_URL
secretName: checklist-api-secrets
secretKey: DATABASE_URL
Pass: rendered manifest contains secretKeyRef only (see envSecrets above).
Fail: embedding credentials in values or ConfigMaps. Review rendered YAML for sample literals such as password: and DATABASE_URL: postgres, and assert secretKeyRef is present. That is a fixture guard—not proof that arbitrary secrets are absent. Treat full secret hygiene as a review/policy gate.
11. Tests, lint, render, policy
Minimum local gate:
helm lint application-0.4.3.tgz -f fixtures/pass-production.yaml
helm template checklist-api application-0.4.3.tgz -f fixtures/pass-production.yaml -n apps
- Add
helm unittest or golden diffs in CI when templates change.
- Run policy checks (Kyverno/OPA/Conftest) on rendered YAML for non-root, limits, and forbidden NodePort/hostPath.
- Do not ship deprecated APIs (
extensions/v1beta1, networking.k8s.io/v1beta1 Ingress).
12. GitOps compatibility and rollback
- The chart must render fully from values files in git—no required
--set for production.
- Avoid
lookup in templates for prod paths; it breaks dry-run and GitOps diffs.
- Rollback: revert the Git commit (or Helm revision) that changed chart version/values, then confirm Deployment rollout and HTTPRoute status after reconcile.
- Uninstall: check PVCs, Jobs, and
extraManifests objects that may orphan after release delete.
# Imperative rollback evidence (lab only)
helm rollback checklist-api 1 -n apps
# GitOps rollback: revert the values/chart pin commit and let the reconciler apply
13. Observability
- Enable
serviceMonitor only when an in-cluster Prometheus Operator (or equivalent) exists.
- Expose a stable metrics path; scrape annotations alone are not a substitute for ServiceMonitor in Operator-managed clusters.
- Carry trace/log correlation through the standard
app.kubernetes.io/* labels the chart helpers already emit.
14. Gateway API / kgateway checklist
Prefer simple chart route.* for portable single-host/path HTTPRoute fields; use route.spec when you need the full HTTPRoute.spec. Keep kgateway-specific policies in labelled escape hatches.
- Ownership: platform owns
GatewayClass / Gateway listeners; the app release owns HTTPRoute via route.enabled.
- parentRefs: set
route.gateway (required). Optional route.gatewayNamespace and route.sectionName pin the listener.
- Listener permissions: Gateway
allowedRoutes must allow the app namespace; otherwise the route stays unattached.
- Status: require HTTPRoute
Accepted and ResolvedRefs. For Gateway, also require Programmed when following kgateway lab patterns.
- ReferenceGrant: required when the app
HTTPRoute references a backend Service in another namespace. It is not a chart values key—ship it as platform YAML (see escape-hatch example). Do not treat ReferenceGrant as a generic HTTPRoute→Secret grant.
- TLS: certificate Secret refs belong on the platform-owned
Gateway listener, not on the application HTTPRoute. The app chart must not embed cert material.
- Timeouts: portable via
route.timeouts.request / backendRequest on 0.4.3.
- Retries: not a
route.* key—use kgateway TrafficPolicy via extraManifests and label networking.contract/kind: kgateway-specific.
- ingress-nginx migration: keep
ingress.* and route.* dual-published only during cutover; remove Ingress after HTTPRoute status is healthy. See Ingress NGINX to kgateway.
route:
enabled: true
gateway: external
gatewayNamespace: kgateway-system
sectionName: https
hostname: checklist-api.example.com
path: /
pathMatchType: PathPrefix
timeouts:
request: 75s
backendRequest: 75s
Fail (captured): route.enabled: true without route.gateway → route.gateway is required when route.enabled=true.
15. Positive vs failing examples (summary)
| Case | Result |
pass-production.yaml | Lint OK; renders Deployment, Service, SA, HPA, HTTPRoute, PDB, and ServiceMonitor with hardened securityContext and secret refs |
fail-schema-replicaCount.yaml | Schema rejects string replicaCount |
fail-schema-imagePullPolicy.yaml | Schema rejects invalid enum |
fail-route-missing-gateway.yaml | Template fails requiring route.gateway |
Production gate (copy/paste)
- Chart version pinned; changelog reviewed for breaking values.
helm lint and schema validation green.
- Rendered manifests: non-root, drop ALL, read-only root (or a documented exception), probes, requests/limits.
- Secrets only via Secret references / ExternalSecrets.
- Service type ClusterIP; no NodePort, hostPath, or localhost assumptions.
- Exposure: one of
route.* or ingress.* (dual only during migration).
- HTTPRoute parentRefs and Gateway listener allowlist verified; status Accepted/ResolvedRefs.
- HPA/PDB coherent; topology strategy documented when it is not a first-class key.
- GitOps dry-run/diff clean; rollback owner and revision known.
- Observability scrape path exists when ServiceMonitor is enabled.
One chart vs many → · Gateway API vs Ingress → · Ingress NGINX to kgateway → · Browse examples →
Platform engineering with Helm → · Common Kubernetes deployment mistakes → · Kubernetes Secrets best practices → · One chart vs many → · Gateway API vs Ingress → · Ingress NGINX to kgateway → · Browse examples →