Skip to main content
RunBook Academy

KubernetesCIV · HelmHelm

Helm values and templating — Go template patterns in production

Advanced⏱ ~17 minhelm

What you'll learn

  • Design values.yaml structures that are environment-portable
  • Apply Go template patterns (conditionals, loops, functions)
  • Choose between --set and -f for value overrides
  • Apply the operational discipline of treating values files as production configuration

Prerequisites

Verified against Kubernetes 1.34.x · kubeadm 1.34.x · kubectl 1.34.x · etcd 3.6.x · CoreDNS 1.11.x · containerd 1.7.x / 2.x · 2026-08-16

Not yet marked complete on this device.

Helm values are the chart’s configuration. This lesson walks the values.yaml structure, Go template patterns, values merging, the —set vs -f trade-off, value validation, and the operational discipline.

The values structure

# values.yaml
replicaCount: 3
image:
  repository: myapp
  tag: "1.0.0"
  pullPolicy: IfNotPresent
service:
  type: ClusterIP
  port: 80
ingress:
  enabled: false
  className: nginx
  annotations: {}
  hosts:
    - host: myapp.example.com
      paths:
        - path: /
          pathType: Prefix
resources:
  requests:
    cpu: 100m
    memory: 128Mi
  limits:
    cpu: 500m
    memory: 512Mi
autoscaling:
  enabled: false
  minReplicas: 1
  maxReplicas: 100
  targetCPUUtilizationPercentage: 80
nodeSelector: {}
tolerations: []
affinity: {}

The values structure:

  • Nested maps for related values (image.repository, image.tag).
  • Lists for arrays (ingress.hosts).
  • Booleans for feature toggles (ingress.enabled, autoscaling.enabled).
  • Defaults for sensible production values.

The discipline is to structure values logically; the template uses dot notation ({{ .Values.image.tag }}) to access them.

Go template patterns

# Conditional
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: {{ include "mychart.fullname" . }}
spec:
  rules:
  {{- range .Values.ingress.hosts }}
    - host: {{ .host | quote }}
      http:
        paths:
        {{- range .paths }}
          - path: {{ .path }}
            pathType: {{ .pathType }}
            backend:
              service:
                name: {{ include "mychart.fullname" $ }}
                port:
                  number: {{ $.Values.service.port }}
        {{- end }}
  {{- end }}
{{- end }}

Common patterns:

  • Conditional. {{- if .Values.X }}{{- end }} renders the block only if the condition is true.
  • Loop. {{- range .Values.X }}{{- end }} iterates over a list.
  • Variable capture. {{ $ := . }} captures the root context; use $ to reference it inside a range.
  • Function call. {{ .host | quote }} pipes the value to the quote function.
  • YAML rendering. {{- toYaml .Values.resources | nindent 12 }} converts a value to YAML and indents it.

Values merging

flowchart LR
    A[values.yaml defaults] --> D[Final values]
    B[-f prod-values.yaml] --> D
    C[--set replicaCount=5] --> D

The merge order (later overrides earlier):

  1. Chart’s values.yaml (defaults).
  2. Files passed with -f (in order).
  3. Values passed with --set (highest priority).

This allows a base values file plus environment- specific overrides:

helm install myrelease mychart/ \
  -f values.yaml \
  -f values-prod.yaml \
  --set image.tag=2.0.0

The --set image.tag=2.0.0 overrides anything in the files.

—set vs -f

flowchart LR
    A[--set replicaCount=5] --> B[Hard to review]
    A --> C[Not in version control]
    A --> D[Best for ad-hoc overrides]
    E[-f values-prod.yaml] --> F[Easy to review]
    E --> G[In version control]
    E --> H[Best for production]

The trade-off:

  • —set. Quick; convenient for ad-hoc overrides and tests. Hard to review; not in version control.
  • -f. Reviewable; version-controlled. Best for production.

Value validation (values.schema.json)

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "required": ["image", "service"],
  "properties": {
    "image": {
      "type": "object",
      "required": ["repository", "tag"],
      "properties": {
        "repository": {"type": "string"},
        "tag": {"type": "string"}
      }
    },
    "replicaCount": {"type": "integer", "minimum": 1},
    "service": {
      "type": "object",
      "properties": {
        "port": {"type": "integer", "minimum": 1, "maximum": 65535}
      }
    }
  }
}

A JSON schema in values.schema.json validates the values during install/upgrade. Required fields, type checks, and range constraints prevent common errors.

helm install myrelease mychart/ -f values-prod.yaml
# Error: values don't satisfy schema

Quiz

Knowledge check · 4 questions

  1. Q1. Given `-f base.yaml -f prod.yaml` with the same key in both, which value applies?

  2. Q2. A list value in a later values file is appended to the list from an earlier file.

  3. Q3. Account for a production memory limit that appears in no values file, and bring it under version control.

    `payments` in `prod-app` runs with `resources.limits.memory: 8Gi`, but every values file in the repository says `2Gi` and nobody can find the change. `helm get values payments -n prod-app` lists `resources.limits.memory: 8Gi` among the user-supplied values, and the CI job that last ran `helm upgrade` passes `--set resources.limits.memory=$MEM_LIMIT` from a pipeline variable.

  4. Q4. Which command shows the values a release is actually running with including the chart's own defaults, and what does the same command show without that flag?

Passing score: 75%. Answers are checked in this browser.

The operational discipline

Helm values in production rest on five non-negotiable elements:

  • Values files in version control. Production values are in Git (or another VCS).
  • Pin chart and app versions. Never use latest.
  • Validate with schema. values.schema.json catches errors before apply.
  • Use -f for production. Reserve —set for development.
  • Document the values. Comment the values file; explain non-obvious values.

Helm values are production configuration. Treat them with the same rigour as Kubernetes manifests.