Lab Guide
Three exercises: values layering across environments, Helmfile multi-env management, and secret encryption with SOPS.
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
- Dev has 1 replica with
latesttag anddebuglog level - Prod has 3 replicas with a pinned tag and
warnlog level - The chart templates are identical — only the values differ
- Later
-ffiles override earlier ones (verify by checking the image tag)
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
- Each environment renders with the correct namespace, replica count, and image tag
- The
{{ .Environment.Name }}template resolves to the environment flag value - Values from the Helmfile
environmentsblock merge with chart values files
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
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
- YAML keys (
secrets,dbPassword, etc.) remain in plaintext for readability - Values are replaced with encrypted blobs (starting with
ENC[AES256_GCM,...) - SOPS metadata is appended at the bottom of the file
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.
secrets: entries to your releases. Helmfile supports secrets: as a first-class field that automatically invokes helm-secrets for decryption.