Helm & GitOps (Argo CD)
Hand-applying YAML doesn't scale. This lesson is how teams actually manage manifests: Helm templates one chart into many environments with a values.yaml, and GitOps (via Argo CD) makes git the single source of truth — a controller continuously syncs the cluster to what's in the repo and flags drift. This closes the loop back to the CI/CD you built in CD5 and the ch08 DevOps capstone: your pipeline writes to git, and the cluster converges on its own.
Learning objectives
- Explain why raw YAML doesn't scale across environments, and how Helm's templating fixes it.
- Read and write a Helm chart: templates,
values.yaml, releases. - Install/upgrade/rollback a release with the helm CLI.
- Explain GitOps: git as source of truth, pull-based sync, drift detection.
- Define an Argo CD Application and reason about sync, self-heal, and drift.
1 · Why raw YAML doesn't scale
You have the same app in dev, staging, and prod — same shape, different replica counts, image tags, and hostnames. Copy-pasting three near-identical manifest sets means every change is three edits and a chance to drift. You need one templated definition plus per-environment values. That's Helm.
2 · A Helm chart, templated
A chart is a directory: Chart.yaml (metadata), a values.yaml (defaults), and templates/ (manifests with {{ }} placeholders). Helm renders the templates against the values to produce plain Kubernetes YAML.
templates/deployment.yamlapiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}-web
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels: { app: {{ .Release.Name }}-web }
template:
metadata:
labels: { app: {{ .Release.Name }}-web }
spec:
containers:
- name: app
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
ports:
- containerPort: {{ .Values.containerPort }}
values.yaml# values.yaml (defaults)
replicaCount: 2
image:
repository: ghcr.io/example/web
tag: "1.2.0"
containerPort: 8000
---
# values-prod.yaml (overrides only what differs)
replicaCount: 6
image:
tag: "1.2.0" # promote a tested tag to prod
helm install with the default values gives you dev; adding -f values-prod.yaml gives you prod from the same chart. No copy-paste, no drift between environments' base shape.3 · The helm lifecycle
Helm manages a release like a package manager: install it, upgrade it, and roll it back — with history.
helm.shhelm install web ./web-chart # release named "web"
helm install web ./web-chart -f values-prod.yaml # prod values
helm upgrade web ./web-chart --set image.tag=1.3.0 # change one value
helm history web # revisions
helm rollback web 1 # back to revision 1
helm uninstall web
REVISION UPDATED STATUS CHART APP VERSION
1 ... superseded web-0.1.0 1.2.0
2 ... deployed web-0.1.0 1.3.0
--set image.tag=1.3.0 changes state that isn't recorded in git — the classic way environments drift from their definition. In production you want the values in a repo and applied by a pipeline, not typed at a terminal. That's exactly the gap GitOps closes.4 · GitOps — git as the source of truth
GitOps flips the delivery model. Instead of a pipeline pushing kubectl apply into the cluster, a controller inside the cluster pulls the desired state from a git repo and continuously makes the cluster match it. The repo is the single source of truth; every change is a git commit (reviewed, audited, revertable); and the cluster self-heals toward git.
| Push CI/CD (CD5) | GitOps (pull) | |
|---|---|---|
| Who applies | Pipeline runs kubectl apply | In-cluster controller pulls from git |
| Source of truth | Whatever last ran | The git repo, always |
| Cluster credentials | Held by the CI system | Stay inside the cluster |
| Drift | Silent until next run | Detected continuously; can auto-revert |
| Audit / rollback | Pipeline logs | git log / git revert |
5 · Argo CD Applications, sync & drift
Argo CD is the leading GitOps controller. You define an Application that points at a repo path and a target cluster/namespace. Argo CD compares the rendered manifests in git to what's live and reports Synced or OutOfSync; it can auto-sync, and with self-heal it reverts manual kubectl changes back to git.
application.yamlapiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: web
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/example/deploy.git
targetRevision: main
path: web-chart
helm:
valueFiles: [values-prod.yaml]
destination:
server: https://kubernetes.default.svc
namespace: web
syncPolicy:
automated:
prune: true # delete resources removed from git
selfHeal: true # revert manual drift back to git
selfHeal: true is set, a kubectl edit or kubectl scale you run by hand is drift — Argo CD will revert it to match git within moments. That's the feature, not a bug: the only way to change the cluster is to change git. Teams learning GitOps are surprised the first time their manual fix vanishes. Verify current Argo CD Application CRD fields against its docs, which evolve.6 · Closing the loop with CI (CD5 → here → ch08)
Put it together and you get the full delivery pipeline this course has been building toward: CI (CD5) builds and tests the image and pushes it to a registry, then commits the new tag to the deploy repo; Argo CD sees the commit and syncs the cluster. Your ch08 DevOps agent can even be the thing that opens that PR. CI's job ends at git; delivery is the cluster pulling from git.
git revert. This is the delivery half of the ch08 capstone's story.✓ Checkpoint — you can move on when you can…
- Explain why raw per-environment YAML drifts, and how a chart + values files fixes it.
- Write a templated Helm Deployment and a prod values override.
- Install, upgrade, and roll back a Helm release from the CLI.
- Explain GitOps (git as truth, pull-based sync, drift detection) vs push CI/CD.
- Define an Argo CD Application and say what auto-sync + self-heal do.
Under Argo CD with selfHeal: true, an on-call engineer runs kubectl scale deploy/web --replicas=10 to handle a spike. Minutes later replicas are back to 6. Why, and what should they have done?
Show answer
replicaCount (or the HPA bounds) in values-prod.yaml, commit, and let Argo CD sync it. Under GitOps the cluster is a projection of git; you change git, not the cluster. (For genuine emergencies teams keep a documented break-glass procedure.)A team uses Helm but deploys to prod with helm upgrade --set image.tag=... from engineers' laptops. What are the two main risks, and how does GitOps address them?
Show answer
git revert.🪜 Practice ladder beginner → industry
Six graded exercises, easy to real-world. Try each before opening its solution.
Context: Templating is the first step off copy-pasted YAML; a parameterized Deployment is the canonical starter chart.
Your task: Turn a hardcoded Deployment into a Helm template driven by a values file for replica count, image, and port.
Requirements:
- Parameterize replicas, image repo+tag, and container port
- Provide sensible defaults in values.yaml
- Show the install command
💡 Hint: Reference values with {{ .Values.x }} and the release name with {{ .Release.Name }}.
Show solution
templates/deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata: { name: {{ .Release.Name }}-web }
spec:
replicas: {{ .Values.replicaCount }}
selector: { matchLabels: { app: {{ .Release.Name }}-web } }
template:
metadata: { labels: { app: {{ .Release.Name }}-web } }
spec:
containers:
- name: app
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
ports: [ { containerPort: {{ .Values.containerPort }} } ]values.yaml:
replicaCount: 2
image: { repository: ghcr.io/example/web, tag: "1.2.0" }
containerPort: 8000Install it: helm install web ./web-chart. Helm renders the template against the values into plain Kubernetes YAML and applies it as the release web.
Context: The payoff of Helm is running the same chart in dev and prod with only a values diff.
Your task: Show how to deploy the same chart to dev and prod, overriding only what differs, and give the two install commands.
Requirements:
- Keep defaults for dev in values.yaml
- Put only the differences in values-prod.yaml
- Give both install/upgrade commands
- State why this beats maintaining two manifest copies
💡 Hint: Override files should contain only the deltas, not a full copy.
Show solution
values-prod.yaml contains only deltas:
replicaCount: 6 # dev default was 2
image: { tag: "1.2.0" } # the promoted, tested taghelm install web ./web-chart # dev (defaults)
helm install web-prod ./web-chart -f values-prod.yaml # prod (overrides)Why it beats two copies: the base shape (probes, labels, ports, strategy) is defined once in the chart. A change to that shape is one edit that both environments inherit, and the only thing that can differ is what's explicitly in an override file — so environments can't silently drift apart in structure. Two hand-maintained manifest sets inevitably diverge.
Context: Treating a deployment as a versioned release with history is what makes upgrades and rollbacks safe.
Your task: Walk through installing a release, upgrading its image, inspecting history, and rolling back — and note the one habit that keeps this clean.
Requirements:
- Install, then upgrade the image tag
- Show helm history and interpret the revisions
- Roll back to a known-good revision
- State why --set from a laptop is a bad prod habit
💡 Hint: Each helm operation is a numbered revision you can return to.
Show solution
helm install web ./web-chart
helm upgrade web ./web-chart --set image.tag=1.3.0
helm history web # revision 1 superseded, revision 2 deployed
helm rollback web 1 # instantly return to revision 1Interpreting history: each install/upgrade is a numbered, immutable revision; helm rollback web 1 re-applies revision 1's rendered manifests, so recovery from a bad upgrade is one command with no rebuild.
The clean habit: in production, don't drive --set from a laptop — that state lives nowhere reviewable and drifts. Keep values in git and let a pipeline / Argo CD apply them, so helm history and git log tell the same story.
Context: Moving from pipeline-push to pull-based GitOps is the delivery upgrade most teams make; you should be able to define the Application and explain the shift.
Your task: Write an Argo CD Application that deploys a Helm chart from git with auto-sync and self-heal, and explain how this changes the deploy workflow.
Requirements:
- Point the Application at a repo path and values file
- Enable prune + selfHeal and explain each
- Describe how a deploy now happens (what the engineer does)
- Note the credential/security benefit vs push CI/CD
💡 Hint: Under GitOps the engineer commits; the controller deploys.
Show solution
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata: { name: web, namespace: argocd }
spec:
project: default
source:
repoURL: https://github.com/example/deploy.git
targetRevision: main
path: web-chart
helm: { valueFiles: [values-prod.yaml] }
destination: { server: https://kubernetes.default.svc, namespace: web }
syncPolicy: { automated: { prune: true, selfHeal: true } }prune deletes cluster resources that were removed from git (git is authoritative); selfHeal reverts any manual drift back to git.
New workflow: to deploy, an engineer opens a PR that bumps the image tag / values in the deploy repo; on merge, Argo CD sees the commit and syncs the cluster. Nobody runs kubectl apply.
Security benefit: the CI system no longer needs cluster credentials — the in-cluster Argo CD pulls from git, shrinking the attack surface and making every deploy a reviewed, auditable commit. Verify the Application CRD fields against current Argo CD docs.
Context: The professional artifact is the whole delivery pipeline: CI builds and proposes, GitOps delivers — with a clean credential and rollback story.
Your task: Design the end-to-end pipeline from a code push to a running change, splitting responsibilities cleanly between CI and GitOps.
Requirements:
- Define what CI does and where its job ends
- Define what GitOps does and its source of truth
- Explain the promotion path dev → staging → prod
- State the rollback story and the credential boundary
💡 Hint: CI's job ends at a git commit; GitOps takes it from there.
Show solution
CI (extends CD5): on a code push, build the image, run tests, scan it, push to the registry (ECR/GHCR) with an immutable tag, then commit that tag into the deploy repo (e.g. bump image.tag in values-staging.yaml). CI's responsibility ends at the commit — it never touches the cluster.
GitOps (this lesson): Argo CD watches the deploy repo (source of truth) and syncs each environment's Application to its values file. The cluster continuously converges on git and self-heals drift.
Promotion path: the same chart flows dev → staging → prod as promotions between values files/branches — a merge that copies the tested tag from values-staging.yaml to values-prod.yaml. Each promotion is a reviewed PR.
Rollback & credentials: rollback is git revert of the promotion commit — Argo CD syncs back automatically. The credential boundary is clean: CI holds registry and git write access but not cluster credentials; only the in-cluster controller can change the cluster. This is the delivery backbone of the ch08 DevOps capstone.
Context: Representative scenario: you're asked to make a service's deployment reproducible and self-healing across three environments, replacing ad-hoc kubectl/helm-from-laptop.
Your task: Produce the full setup: a chart, per-environment values, Argo CD Applications for each environment, and the workflow the team follows to ship and roll back.
Requirements:
- Chart with parameterized image, replicas, resources, probes
- values-dev / values-staging / values-prod overrides
- An Argo CD Application per environment with appropriate sync policy
- The ship + rollback runbook the team uses
- State what stops working the old way (and why that's good)
💡 Hint: Consider a looser sync policy in dev, strict self-heal in prod.
Show solution
1 · One chart parameterizes image repo/tag, replica count, resources, and probes in values.yaml — the base shape all environments share.
2 · Three values files hold only deltas: values-dev.yaml (1 replica, latest built tag, auto-sync no self-heal for fast iteration), values-staging.yaml (prod-like, self-heal on), values-prod.yaml (higher replicas/HPA bounds, self-heal on, manual sync window if desired).
3 · An Argo CD Application per environment points at the chart path with the matching values file and target namespace/cluster. Prod uses automated: { prune: true, selfHeal: true }; dev may relax self-heal so engineers can experiment.
4 · Ship runbook: CI builds+tests+pushes the image and commits the new tag to values-dev.yaml; Argo CD deploys dev. Promotion to staging then prod is a reviewed PR copying the tested tag forward. Nobody runs kubectl apply or helm --set against prod.
5 · Rollback runbook: git revert the offending promotion commit; Argo CD syncs the previous state back automatically (and helm rollback remains a break-glass fallback).
What stops working — deliberately: hand-editing prod with kubectl or helm --set no longer sticks (self-heal reverts it). That's the goal: the only path to change prod is a reviewed git commit, so the running state is always exactly what's in the repo. Verify Helm and Argo CD syntax against their current docs.