Lesson 17 Multi-Environment Values
Theme:

Lab Guide

Three exercises: values layering across environments, Helmfile multi-env management, and secret encryption with SOPS.

Prerequisites: Helm 3.x installed (helm version). A running Kubernetes cluster (minikube, kind, or similar). For Exercise 2, install Helmfile: brew install helmfile (macOS) or download from GitHub releases. For Exercise 3, install SOPS (brew install sops), age (brew install age), and the helm-secrets plugin (helm plugin install https://github.com/jkroepke/helm-secrets).

Exercise 1 Values Layering Across Environments

In this exercise you will create a Helm chart with a base values file and environment-specific overlays for dev, staging, and production. You will deploy each environment and verify the correct values are applied.

Step 1 — Create the chart and directory structure

helm create multienv-app
cd multienv-app
mkdir -p values/envs

Step 2 — Create the base values file

Create values/base.yaml with shared defaults across all environments:

# values/base.yaml
replicaCount: 1

image:
  repository: nginx
  pullPolicy: IfNotPresent
  tag: "1.25"

service:
  type: ClusterIP
  port: 80

resources:
  requests:
    cpu: 50m
    memory: 64Mi
  limits:
    cpu: 200m
    memory: 128Mi

env:
  LOG_LEVEL: info
  FEATURE_DEBUG: "false"

Step 3 — Create environment-specific overlays

Create values/envs/dev.yaml:

# values/envs/dev.yaml
replicaCount: 1

image:
  tag: "latest"

resources:
  requests:
    cpu: 25m
    memory: 32Mi
  limits:
    cpu: 100m
    memory: 64Mi

env:
  LOG_LEVEL: debug
  FEATURE_DEBUG: "true"

Create values/envs/staging.yaml:

# values/envs/staging.yaml
replicaCount: 2

image:
  tag: "1.25-rc1"

resources:
  requests:
    cpu: 100m
    memory: 128Mi
  limits:
    cpu: 500m
    memory: 256Mi

env:
  LOG_LEVEL: info
  FEATURE_DEBUG: "false"

Create values/envs/prod.yaml:

# values/envs/prod.yaml
replicaCount: 3

image:
  tag: "1.25.4"

resources:
  requests:
    cpu: 250m
    memory: 256Mi
  limits:
    cpu: "1"
    memory: 512Mi

env:
  LOG_LEVEL: warn
  FEATURE_DEBUG: "false"

Step 4 — Add environment variables to the deployment template

Edit templates/deployment.yaml to inject the env values. Add this block inside the container spec, after the ports section:

          env:
            {{- range $key, $value := .Values.env }}
            - name: {{ $key }}
              value: {{ $value | quote }}
            {{- end }}

Step 5 — Render and compare each environment

# Render dev
helm template myapp . -f values/base.yaml -f values/envs/dev.yaml > /tmp/dev.yaml

# Render staging
helm template myapp . -f values/base.yaml -f values/envs/staging.yaml > /tmp/staging.yaml

# Render prod
helm template myapp . -f values/base.yaml -f values/envs/prod.yaml > /tmp/prod.yaml

# Compare dev vs prod
diff /tmp/dev.yaml /tmp/prod.yaml

What to observe

Step 6 — Deploy to your cluster

# Deploy the dev environment
helm upgrade --install myapp-dev . \
  -f values/base.yaml \
  -f values/envs/dev.yaml \
  --namespace dev --create-namespace

# Verify
kubectl get deployment -n dev myapp-dev-multienv-app -o jsonpath='{.spec.replicas}'
# Should output: 1

# Deploy the staging environment
helm upgrade --install myapp-staging . \
  -f values/base.yaml \
  -f values/envs/staging.yaml \
  --namespace staging --create-namespace

# Verify
kubectl get deployment -n staging myapp-staging-multienv-app -o jsonpath='{.spec.replicas}'
# Should output: 2

Step 7 — Test the --set override precedence

# --set always wins over -f files
helm template myapp . \
  -f values/base.yaml \
  -f values/envs/prod.yaml \
  --set replicaCount=99 | grep "replicas:"
# Should show: replicas: 99

Success: Each environment renders with the correct overrides, and --set takes highest precedence.

Exercise 2 Helmfile for Multi-Environment Management

You will create a Helmfile that manages the same chart across dev, staging, and production environments using a single declarative configuration.

Step 1 — Install Helmfile and the diff plugin

# Install Helmfile (macOS)
brew install helmfile

# Install the helm-diff plugin (required for helmfile diff/apply)
helm plugin install https://github.com/databus23/helm-diff

# Verify
helmfile version
helm plugin list | grep diff

Step 2 — Create the Helmfile

In the multienv-app parent directory, create helmfile.yaml:

# helmfile.yaml
environments:
  dev:
    values:
      - namespace: dev
      - imageTag: latest
  staging:
    values:
      - namespace: staging
      - imageTag: 1.25-rc1
  prod:
    values:
      - namespace: prod
      - imageTag: 1.25.4

---

releases:
  - name: myapp-{{ .Environment.Name }}
    chart: ./multienv-app
    namespace: {{ .Values.namespace }}
    createNamespace: true
    values:
      - multienv-app/values/base.yaml
      - multienv-app/values/envs/{{ .Environment.Name }}.yaml
    set:
      - name: image.tag
        value: {{ .Values.imageTag }}

Step 3 — Preview with helmfile template

# Render manifests for each environment
helmfile -e dev template
helmfile -e staging template
helmfile -e prod template

What to observe

Step 4 — Run helmfile diff

# See what would change in the cluster (requires helm-diff plugin)
helmfile -e dev diff

If this is a fresh deployment, diff will show all resources as additions (green).

Step 5 — Deploy with helmfile sync and apply

# sync: installs or upgrades unconditionally
helmfile -e dev sync

# apply: runs diff first, only syncs if there are changes
helmfile -e staging apply

# Verify both deployments
kubectl get deployments -n dev
kubectl get deployments -n staging

Step 6 — Add a second release

Extend your helmfile.yaml to deploy a Redis sidecar alongside the main app:

repositories:
  - name: bitnami
    url: https://charts.bitnami.com/bitnami

releases:
  - name: myapp-{{ .Environment.Name }}
    chart: ./multienv-app
    namespace: {{ .Values.namespace }}
    createNamespace: true
    values:
      - multienv-app/values/base.yaml
      - multienv-app/values/envs/{{ .Environment.Name }}.yaml
    set:
      - name: image.tag
        value: {{ .Values.imageTag }}

  - name: redis-{{ .Environment.Name }}
    chart: bitnami/redis
    namespace: {{ .Values.namespace }}
    version: 19.x
    values:
      - architecture: standalone
      - auth:
          enabled: false
# Deploy both releases for dev
helmfile -e dev sync

# List all releases managed by Helmfile
helmfile -e dev list

Step 7 — Clean up

# Destroy all releases for an environment
helmfile -e dev destroy
helmfile -e staging destroy

Success: Helmfile manages multiple releases across environments from a single YAML file. Sync, diff, and apply all work correctly.

Exercise 3 Secret Management with helm-secrets and SOPS

You will encrypt environment-specific secrets with SOPS and deploy them transparently using the helm-secrets plugin.

Step 1 — Install the tools

# Install SOPS and age
brew install sops age

# Install helm-secrets plugin
helm plugin install https://github.com/jkroepke/helm-secrets

# Verify
sops --version
age --version
helm secrets --help

Step 2 — Generate an age key pair

# Generate a key pair
age-keygen -o key.txt

# Note the public key from the output (starts with "age1...")
# Export for SOPS to find the private key
export SOPS_AGE_KEY_FILE=$(pwd)/key.txt

# Display the public key
grep "public key" key.txt
Security: Never commit key.txt to Git. Add it to your .gitignore immediately. In production, the private key lives in CI secrets or a key management service.

Step 3 — Create a SOPS configuration

Create .sops.yaml in the project root to define encryption rules:

# .sops.yaml
creation_rules:
  - path_regex: values/secrets/.*\.yaml$
    age: "age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p"  # replace with YOUR public key

Step 4 — Create plaintext secret values

mkdir -p multienv-app/values/secrets

Create multienv-app/values/secrets/dev.yaml:

# Plaintext -- will be encrypted
secrets:
  dbPassword: dev-password-123
  apiKey: dev-api-key-abc
  redisUrl: redis://localhost:6379

Create multienv-app/values/secrets/prod.yaml:

# Plaintext -- will be encrypted
secrets:
  dbPassword: super-secret-prod-pw-!@#
  apiKey: prod-api-key-xyz-789
  redisUrl: redis://prod-redis.internal:6379

Step 5 — Encrypt the secrets files

# Encrypt dev secrets
sops --encrypt --in-place multienv-app/values/secrets/dev.yaml

# Encrypt prod secrets
sops --encrypt --in-place multienv-app/values/secrets/prod.yaml

# Inspect the encrypted file -- values are encrypted, keys are plaintext
cat multienv-app/values/secrets/dev.yaml

What to observe

Step 6 — Add a Secret template to the chart

Create multienv-app/templates/app-secrets.yaml:

{{- if .Values.secrets }}
apiVersion: v1
kind: Secret
metadata:
  name: {{ include "multienv-app.fullname" . }}-secrets
  labels:
    {{- include "multienv-app.labels" . | nindent 4 }}
type: Opaque
stringData:
  {{- range $key, $value := .Values.secrets }}
  {{ $key }}: {{ $value | quote }}
  {{- end }}
{{- end }}

Step 7 — Deploy with helm secrets

# helm secrets transparently decrypts .enc.yaml or SOPS-encrypted files
helm secrets template myapp ./multienv-app \
  -f multienv-app/values/base.yaml \
  -f multienv-app/values/envs/dev.yaml \
  -f multienv-app/values/secrets/dev.yaml

# Deploy for real
helm secrets upgrade --install myapp-dev ./multienv-app \
  -f multienv-app/values/base.yaml \
  -f multienv-app/values/envs/dev.yaml \
  -f multienv-app/values/secrets/dev.yaml \
  --namespace dev --create-namespace

Step 8 — Verify the secret was created

# Check the Secret exists
kubectl get secret -n dev myapp-dev-multienv-app-secrets -o yaml

# Decode a value to confirm it matches the plaintext
kubectl get secret -n dev myapp-dev-multienv-app-secrets \
  -o jsonpath='{.data.dbPassword}' | base64 -d
# Should output: dev-password-123

Step 9 — Edit encrypted secrets safely

# Open the encrypted file in your editor -- SOPS decrypts in-place
sops multienv-app/values/secrets/dev.yaml

# Make a change, save, and close. The file is re-encrypted automatically.
# Verify the diff shows only encrypted blob changes
git diff multienv-app/values/secrets/dev.yaml

Success: Secrets are encrypted at rest in Git, decrypted transparently during deployment, and the Kubernetes Secret contains the correct plaintext values.

Bonus: Try integrating helm-secrets into your Helmfile by adding secrets: entries to your releases. Helmfile supports secrets: as a first-class field that automatically invokes helm-secrets for decryption.