Lesson 17 Multi-Environment Values
Theme:

Multi-Environment Values

Managing dev, staging, and production from a single Helm chart

The Multi-Environment Challenge

Every application eventually runs in more than one environment. Development needs hot reloading and verbose logging. Staging mirrors production topology at smaller scale. Production demands high availability, strict resource limits, and encrypted secrets. The question is not whether you need different configurations — it is how you manage them without duplicating your entire chart for each environment.

Helm solves this through values layering: a single set of templates combined with environment-specific values files. The chart itself never changes between environments. Only the values that feed into it differ. This principle — one chart, many values files — is the foundation of multi-environment Helm management.

Values Layering with -f Flags

When you pass multiple -f (or --values) flags to helm install or helm upgrade, Helm deep-merges them from left to right. Values in later files override those in earlier files at every key level.

helm upgrade myapp ./mychart \
  -f values/base.yaml \
  -f values/prod.yaml \
  --set image.tag="v2.4.1"

The merge order here is: chart values.yaml (defaults) → base.yaml → prod.yaml → --set flags. Each layer can override any key from the previous layer.

Merge Semantics

Understanding how Helm merges values is critical to avoiding surprises:

List replacement gotcha: If your base file defines three environment variables as a list and your prod overlay only sets one, you will lose the other two. Design values as maps where possible, or repeat the full list in each overlay.

What Changes Per Environment

While every project differs, these categories cover the vast majority of environment-specific values:

CategoryDevStagingProd
Replica count123–10+
CPU / memory requestsLowMediumRight-sized
Image taglatest or branchRC tagPinned release
Ingress hostdev.internalstaging.example.comapp.example.com
TLSDisabled or self-signedLet's Encrypt stagingLet's Encrypt prod
Feature flagsAll onSelectiveStable only
Log leveldebuginfowarn
External endpointsMock / localStaging servicesProduction services

Keep your base values file as the "dev-friendly default" so developers can helm install with zero overrides and get a working local deployment.

Directory Structure Patterns

Flat Pattern

For simple projects with two or three environments, store values files alongside the chart:

mychart/
  Chart.yaml
  values.yaml            # defaults (doubles as dev)
  values-staging.yaml
  values-prod.yaml
  templates/

Deploy with: helm upgrade myapp . -f values-prod.yaml

Layered Pattern

For teams managing multiple services across regions and clusters, separate the chart from the deployment configuration:

deploy/
  charts/
    mychart/             # the chart itself
  values/
    base.yaml            # shared across all envs
    envs/
      dev.yaml
      staging.yaml
      prod.yaml
    regions/
      us-east.yaml
      eu-west.yaml
    secrets/
      prod-secrets.enc.yaml
  helmfile.yaml

Deploy with: helm upgrade myapp charts/mychart -f values/base.yaml -f values/envs/prod.yaml -f values/regions/us-east.yaml

Tip: The layered pattern works especially well with Helmfile, which can compose these layers declaratively per environment.

Helmfile: Declarative Multi-Environment Management

Helmfile is a declarative wrapper around Helm that manages multiple releases across environments from a single YAML file. Instead of writing shell scripts that call helm upgrade with the right flags, you declare what you want and let Helmfile figure out the commands.

Core Structure

# helmfile.yaml
repositories:
  - name: bitnami
    url: https://charts.bitnami.com/bitnami

environments:
  dev:
    values:
      - values/envs/dev.yaml
  staging:
    values:
      - values/envs/staging.yaml
  prod:
    values:
      - values/envs/prod.yaml

releases:
  - name: myapp
    chart: ./charts/mychart
    namespace: {{ .Values.namespace }}
    values:
      - values/base.yaml
      - values/envs/{{ .Environment.Name }}.yaml
    set:
      - name: image.tag
        value: {{ .Values.imageTag | default "latest" }}

The environments block defines named environments with their own values. The releases block lists every Helm release. Helmfile supports Go templating (via {{ }}) so you can reference environment names and values dynamically.

Key Commands

Helmfile and CI: In a CI pipeline, helmfile -e $ENV apply is often the only deploy command you need. The environment name comes from the pipeline variable (e.g., the Git branch or a manual trigger).

Secret Management Strategies

Secrets are the hardest part of multi-environment configuration. They must differ per environment, must never appear in Git as plaintext, and must be available at deploy time. Three patterns dominate:

1. CI Variable Injection

The simplest approach: store secrets in your CI system (GitHub Actions secrets, GitLab CI variables, etc.) and inject them at deploy time with --set:

helm upgrade myapp ./mychart \
  -f values/base.yaml \
  -f values/prod.yaml \
  --set secrets.dbPassword="${DB_PASSWORD}" \
  --set secrets.apiKey="${API_KEY}"

Pros: no extra tooling, secrets never touch Git. Cons: does not scale well beyond a handful of secrets; difficult to audit.

2. SOPS + helm-secrets Plugin

SOPS (Secrets OPerationS) encrypts YAML values files so they can be committed to Git safely. The helm-secrets plugin decrypts them transparently during helm install/upgrade:

# Encrypt with age (modern, simple key management)
sops --encrypt --age age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p \
  values/prod-secrets.yaml > values/prod-secrets.enc.yaml

# Deploy with transparent decryption
helm secrets upgrade myapp ./mychart \
  -f values/base.yaml \
  -f values/prod.yaml \
  -f values/prod-secrets.enc.yaml

SOPS supports age, PGP, AWS KMS, GCP KMS, and Azure Key Vault as key backends. The encrypted file lives in Git; only the decryption key stays in your CI environment or developer keyring.

3. vals and External Secret Operators

For teams already using a secrets manager (Vault, AWS Secrets Manager, etc.), the vals tool or Kubernetes External Secrets Operator can pull secrets directly at deploy time without storing them in values files at all.

Rule of thumb: Start with CI injection for small teams. Move to SOPS when you have more than five secrets or need auditability. Adopt External Secrets Operator when you run multiple clusters and need centralized rotation.

Kustomize + Helm: A Comparison

Kustomize and Helm both solve the multi-environment problem, but from opposite directions. Helm uses templating: you write parameterized templates and fill in values. Kustomize uses patching: you write complete YAML manifests and overlay patches per environment.

In practice, many teams combine both. helm template renders a chart into raw YAML, and Kustomize overlays apply cluster-specific patches (like injecting a sidecar or changing a namespace annotation). ArgoCD supports this "Helm + Kustomize" pattern natively.

Choose Helm values layering when your chart is a reusable package deployed across teams. Choose Kustomize when you need surgical patches to third-party manifests you do not control.

The CI Pipeline Pattern

The recommended production workflow ties everything together:

  1. Build — CI builds and tags the container image (e.g., v2.4.1)
  2. Render — helmfile -e $ENV template renders manifests for review
  3. Diff — helmfile -e $ENV diff shows what will change in the cluster
  4. Apply — helmfile -e $ENV apply --set image.tag=$TAG deploys
  5. Verify — smoke tests run against the deployed environment

Each environment has its own pipeline stage or trigger. Dev deploys on every push, staging on merge to main, production on manual approval or tag creation. The chart artifact is identical across all environments — only the values file changes.

Golden rule: If you find yourself editing templates to accommodate a specific environment, you are doing it wrong. Move the difference into a values file instead.