Week 13: Helm — Packaging Kubernetes Apps

By now you're maintaining a Deployment, a Service, a ConfigMap, a Secret, a PVC and an Ingress — for one app, in one environment. Multiply that by three environments and it's a maintenance problem. Helm templates a whole set of manifests from reusable charts and a single values file per environment, turning "hand-edit twelve YAML files" into "change one line in values-prod.yaml."

Module 13 of 17 Week 13 of 18 ~4 Hours Hands-on Exercise Included

By the end of this week, you'll be able to

  • Explain a chart's structure and how values.yaml drives template rendering
  • Install a public chart and write your own from scratch
  • Manage releases across environments with per-environment values files

1. Charts, Templates & values.yaml

A chart is a package of Kubernetes manifests written as templates, with placeholders filled in from a values.yaml file. Think of it as a parameterized version of everything you've written in Weeks 8–12.

a chart's directory structure
my-app/
├── Chart.yaml          # chart metadata: name, version, description
├── values.yaml          # DEFAULT values — every template placeholder's fallback
└── templates/
    ├── deployment.yaml   # a Deployment, with {{ .Values.X }} placeholders
    ├── service.yaml
    └── ingress.yaml
templates/deployment.yaml — templated
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Release.Name }}-{{ .Chart.Name }}
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      app: {{ .Chart.Name }}
  template:
    metadata:
      labels:
        app: {{ .Chart.Name }}
    spec:
      containers:
        - name: {{ .Chart.Name }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          resources:
            {{- toYaml .Values.resources | nindent 12 }}
values.yaml — the defaults these placeholders pull from
replicaCount: 3
image:
  repository: myrepo/my-app
  tag: "1.5.0"
resources:
  requests:
    cpu: "100m"
    memory: "128Mi"
  limits:
    cpu: "500m"
    memory: "256Mi"

{{ .Values.X }} pulls from values.yaml at render time. This is the whole idea of Helm in one example: the exact same template renders differently just by changing what's in values.yaml — no manifest is ever hand-edited per environment again.

Render templates locally before applying them

helm template my-app ./my-app prints the fully-rendered YAML without touching your cluster at all — invaluable for catching a syntax mistake before it ever reaches kubectl apply or a live release.

2. Installing a Public Chart

Most popular software you'd self-host on Kubernetes — Postgres, Redis, Prometheus, cert-manager itself — already has a well-maintained public chart, saving you from writing dozens of manifests by hand.

installing Prometheus via a public chart
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update

helm search repo prometheus-community
helm install my-prometheus prometheus-community/prometheus

helm list                 # see every installed release
helm status my-prometheus  # detailed status of one
helm uninstall my-prometheus

Before installing blindly, it's worth inspecting what a chart will actually create:

inspecting before installing
helm show values prometheus-community/prometheus   # see every configurable option
helm template my-prometheus prometheus-community/prometheus | less   # see rendered manifests
helm install my-prometheus prometheus-community/prometheus --dry-run --debug
You'll use this exact skill in Week 16

The Prometheus + Grafana observability stack you'll deploy for monitoring is, in practice, almost always installed via Helm rather than hand-written manifests — this section is direct preparation for that.

3. Writing Your Own Chart

Scaffold and build a chart for your own app:

terminal
helm create my-app
# generates a starter chart with sensible defaults you'll trim down or extend

cd my-app
helm template . | less           # preview the rendered output
helm install my-release . --dry-run --debug   # validate without actually installing
helm install my-release .        # install for real
templates/service.yaml — a second templated resource
apiVersion: v1
kind: Service
metadata:
  name: {{ .Release.Name }}-{{ .Chart.Name }}
spec:
  selector:
    app: {{ .Chart.Name }}
  ports:
    - port: {{ .Values.service.port }}
      targetPort: {{ .Values.service.targetPort }}
  type: {{ .Values.service.type }}

Notice the label selector, {{ .Chart.Name }}, matches the Deployment template's Pod labels exactly — the same selector-matching principle from Week 9, just generated by a template instead of typed twice by hand where it could drift out of sync.

helm create's defaults are a starting point, not a finish line

The scaffolded chart includes HPA, Ingress and ServiceAccount templates you may not need yet — trim what you don't use rather than shipping unused, half-understood YAML. Understanding every template you keep matters more than keeping everything the scaffold generated.

4. Releases & Per-environment Values

A Helm release is one installed instance of a chart — you can install the same chart multiple times, under different release names, with different values, side by side.

values-dev.yaml / values-prod.yaml
# values-dev.yaml
replicaCount: 1
image:
  tag: "dev-latest"
resources:
  requests: { cpu: "50m", memory: "64Mi" }

# values-prod.yaml
replicaCount: 5
image:
  tag: "1.5.0"
resources:
  requests: { cpu: "250m", memory: "256Mi" }
  limits: { cpu: "1000m", memory: "512Mi" }
installing the same chart to two environments
helm install my-app-dev . -f values-dev.yaml --namespace dev
helm install my-app-prod . -f values-prod.yaml --namespace prod

# upgrading after a change — this is your Week 8 rolling update, now Helm-driven:
helm upgrade my-app-prod . -f values-prod.yaml --set image.tag=1.6.0

helm rollback my-app-prod 1        # roll back to release revision 1
helm history my-app-prod            # see every past revision, just like kubectl rollout history

helm upgrade and helm rollback are Helm's layer on top of the exact rolling-update and rollback mechanics from Week 8 — Helm tracks its own release history in addition to what Kubernetes tracks natively, giving you one command that manages every resource in the chart together, atomically.

Looking ahead

Week 17's GitOps setup deploys via exactly this pattern — a chart plus per-environment values files, committed to Git, with ArgoCD triggering helm upgrade automatically whenever those files change.

5. Hands-on Exercise

Hands-on

Turn your app's manifests into a chart, then deploy it to two "environments"

Convert the raw YAML from earlier weeks into a proper Helm chart, then use it to deploy two differently-configured releases of the same app side by side.

Part 1 — Build the chart:

  1. Scaffold a chart with helm create, then replace its templates/deployment.yaml and templates/service.yaml with templated versions of your own app's manifests from Weeks 8–9.
  2. Move every value that should vary (image tag, replica count, resource limits) into values.yaml as placeholders.
  3. Run helm template . and confirm the rendered output looks correct — a real, valid Deployment and Service manifest with your values substituted in.
  4. Install it for real with helm install, confirm it works, then helm uninstall it cleanly.
Hint

Delete the scaffolded templates you're not using (HPA, ServiceAccount, tests) rather than leaving them half-configured — an unused template with a broken reference to a values.yaml key you removed is a common source of confusing render errors.

Part 2 — Two environments, one chart:

  1. Create values-dev.yaml and values-prod.yaml with meaningfully different replica counts and resource requests.
  2. Create two namespaces (kubectl create namespace dev, kubectl create namespace prod) and install your chart into each, using the matching values file and a distinct release name.
  3. Confirm with kubectl get deployments -n dev and -n prod that replica counts differ as expected between the two.
  4. Bump the image tag in values-prod.yaml only, run helm upgrade against the prod release, and confirm with helm history that a new revision was recorded — then practice a helm rollback back to the previous one.
Hint

Add -n <namespace> to every helm and kubectl command in this exercise — it's easy to accidentally run a command against the default namespace and wonder why your dev/prod resources "aren't there."

6. Knowledge Check

Four quick questions. Expand each to check your answer.

Q1

What's the relationship between a chart's templates and its values.yaml?

Templates are Kubernetes manifests with placeholders like {{ .Values.X }}. values.yaml supplies the actual values those placeholders resolve to at render time. The same templates render into different manifests depending only on which values file (or overrides) is used.

Q2

What does helm template . do, and why is it useful before a real install?

It renders the chart's templates into final Kubernetes YAML locally, without touching the cluster at all. It's a fast way to catch templating mistakes or unexpected output before running helm install, which actually creates resources.

Q3

Can the same chart be installed multiple times in the same cluster with different configurations?

Yes — each installation is a separate "release" with its own name, typically in its own namespace, and can be given a completely different values file. This is exactly how one chart deploys differently-configured dev and prod releases side by side.

Q4

What does helm rollback do, at a mechanical level?

It re-applies a previous revision's rendered manifests for every resource the chart manages, restoring the release (Deployment, Service, ConfigMap, etc. together) to that earlier state in one atomic operation — the Helm-level equivalent of kubectl rollout undo, but covering the whole chart's resources at once.