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.
30-second recap
- Pods, Deployments & Services: A Pod runs containers, a Deployment keeps N identical Pods alive and rolls out updates, a Service gives them one stable address.
1 · Why raw YAML doesn't scale
Picture the moment this bites you. You ship a service to dev with a hand-written deployment.yaml. It works, so you copy it to a staging/ folder, bump the replica count, and copy it again to prod/ with a different image tag and hostname. Three folders, three near-identical files, all happy. Then a real change arrives — add a liveness probe, tweak a resource limit — and now you must make that same edit in three places, perfectly, forever. Miss one and prod quietly diverges from staging. That divergence has a name — configuration drift — and it's the source of a huge share of "but it worked in staging!" incidents.
Here's the analogy that makes it click: hand-copied manifests are like retyping the same letter for a hundred recipients, changing only the name each time. Nobody does that — you write one form letter with a {{ name }} blank and mail-merge the differences. Helm is mail-merge for Kubernetes manifests. You write the shape of the Deployment once, leave blanks where environments differ, and fill those blanks from a small per-environment values file. The structure lives in exactly one place, so a change to the structure is one edit that every environment inherits automatically.
The common mistake to name up front: beginners reach for copy-paste-and-tweak because it feels faster on day one — and it is, for the first deploy. The cost is deferred and compounding. Every environment you add multiplies the edit burden, and the files slowly rot apart because the only thing keeping them in sync is human discipline. The fix isn't more discipline; it's removing the duplication so there's nothing to keep in sync. You want one templated definition plus per-environment values — the base shape shared, only the genuine differences (replica counts, image tags, hostnames) called out separately. That's Helm.
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.
Compare the two halves across the dashed divider: the left is copy-paste-and-tweak, the right is Helm's one-template approach.
- The left red stack is three near-identical manifest files (dev, staging, prod). The red arrows show that a single structural change has to be re-applied to all three by hand — miss one and prod silently diverges.
- The right blue box is the one shared template (the shape), and the green boxes are the small per-environment values files that fill its blanks — so dev, staging, and prod all come from the same source.
- The payoff is the amber "one change" box on the right: a change to the shape is a single edit that every environment inherits automatically, so there is nothing to keep in sync.
In short: Left = one change, three edits, drift; right = one change, one edit, every environment inherits it.
2 · A Helm chart, templated
If Helm is mail-merge, a chart is the reusable letter template plus its default answers, bundled so anyone can run it. That bundling matters more than it first sounds: a chart isn't just "a file with blanks in it," it's a self-contained, shareable, versioned package. Someone else can grab your chart, supply their own values, and get a working install without ever seeing the templates — the same way you helm install a Postgres or an nginx-ingress chart today without hand-writing a line of their YAML.
Think of the three pieces as a fill-in-the-blank form with a cover sheet. Chart.yaml is the cover sheet — the name and version, so Helm knows what this package is. values.yaml is the default answers already written into the blanks, so the form is usable out of the box. And templates/ holds the actual forms — ordinary Kubernetes manifests, except wherever a value should go you write a {{ }} placeholder like {{ .Values.replicaCount }} instead of a hardcoded number. When you install, Helm does the merge: it walks each template, substitutes the placeholders with real values, and emits plain Kubernetes YAML — the same YAML you'd have written by hand, just generated instead of typed.
The mental model to hold — and the mistake to avoid: a template is not valid Kubernetes YAML on its own. Those {{ }} braces are Helm's Go-template syntax; the cluster has never heard of them. Rendering happens on your machine (or in Argo CD), before anything reaches the cluster — the API server only ever sees the finished, substituted manifest. Newcomers sometimes try to kubectl apply a template directly and get a parse error, or they hardcode a value "just this once" and reintroduce the very drift the chart existed to prevent. The rule of thumb: anything that legitimately differs between environments belongs in a values file as a placeholder, never baked into the template. Keep that split clean and one chart genuinely serves every environment.
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.
{{ }}.
Read it left-to-right: the three parts of a chart flow into a render step, which emits the finished YAML on the right.
- The left column is the chart's three files:
Chart.yamlis the cover sheet (name + version),values.yamlholds the default answers, andtemplates/holds the manifests with{{ }}placeholders. - The middle box is the render/merge step — it happens on your machine (or in Argo CD), substituting each placeholder with a real value. Note the label: the cluster never sees the braces.
- The right box is the payoff — plain Kubernetes YAML, the exact manifest you'd have written by hand, just generated instead of typed. That is all the API server ever receives.
In short: Template + values render into plain YAML before the cluster — which is why applying a raw template directly fails: the braces are Helm's, not Kubernetes'.
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
Here's the shift in thinking that makes Helm worth learning past templating. A raw kubectl apply is fire-and-forget: you push some YAML, the cluster changes, and nothing remembers what it looked like before. If the new version is broken, "rolling back" means finding the old YAML (hope you kept it), figuring out exactly what changed, and applying it again by hand under pressure. Helm removes that whole scramble by treating your installed app as a release — a named, versioned thing with a history you can walk backwards through.
The analogy is the package manager already on your laptop. When you apt install or brew install something, you don't think in individual files — you think "install this package," "upgrade it," "remove it," and the tool tracks what's installed and at what version. Helm brings that exact experience to a running app in Kubernetes: helm install creates release revision 1, each helm upgrade stamps a new numbered revision, helm history lists them, and helm rollback web 1 re-applies a known-good revision in a single command — no rebuild, no hunting for old manifests. Every revision is immutable, so revision 1 still means exactly what it meant when you installed it.
The trap that surprises people: because upgrades feel this easy, it's tempting to tweak a live release with an ad-hoc --set image.tag=1.3.0 from your terminal whenever something needs changing. It works, and the demo below shows it precisely because you'll see it in the wild — but reach for it and you've quietly created a problem. That flag changes what's running without changing what's written down anywhere: the new tag lives only in your shell history, so no reviewer saw it and no repo records it. Fine for a throwaway demo, genuinely dangerous for prod, and the exact seam where environments start drifting from their definitions. Hold that tension as you read — it's the precise gap the GitOps sections ahead are built to close.
Helm manages a release like a package manager: install it, upgrade it, and roll it back — with history.
helm rollback to a known-good one.
Read the top row left-to-right as time: each box is one numbered revision of the same release, created by an install or upgrade.
- The boxes are revisions —
rev 1fromhelm install, then a new numbered revision for eachhelm upgrade.helm historyis exactly this list. rev 3is coloured red to stand for a bad upgrade. The dashed green arrow curving back torev 1ishelm rollback web 1— it re-applies that revision's rendered manifests in one command.- The payoff is that every revision is immutable: rev 1 still means exactly what it meant at install time, so rollback is safe and needs no rebuild or hunting for old YAML.
In short: Install and upgrades build a numbered history; a bad revision is undone by rolling back to a known-good number — no scramble.
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
Ask a room of engineers "what's actually running in prod right now?" and watch the pause. With push-based deploys the honest answer is "whatever the last pipeline run, or the last person with kubectl, happened to apply" — and to be sure you'd have to go read the live cluster. That uncertainty is the disease GitOps cures. The core move is almost philosophical: stop treating the cluster as the thing you change, and start treating it as a projection of a text file you can read, review, and version. The cluster becomes a screen showing what git says; git becomes the thing you actually edit.
The analogy is a thermostat. You don't walk around the house adjusting each radiator by hand — you set a target temperature, and a controller continuously compares the room to that target and nudges reality until they match. GitOps works the same way: git holds the desired state (the target), and a controller running inside the cluster continuously compares the live cluster to git and reconciles any difference. Note the direction — the controller pulls from git; nothing outside the cluster pushes into it. This flips the model you built in CD5, where a pipeline reached in with kubectl apply. Now the pipeline's job ends at a git commit, and the cluster comes and fetches its own instructions.
Two consequences fall out for free, and they're the reason teams adopt this. First, everything is a git commit — reviewed in a PR, attributed to an author, and instantly revertable, so "roll back prod" becomes git revert and your deploy history is your git log. Second, the credential boundary inverts: because the controller pulls from inside, no external CI system or engineer's laptop needs cluster credentials, shrinking the attack surface dramatically. The common misconception to clear up: GitOps is not "using git to store YAML" — plenty of push pipelines already do that. The defining ingredient is the in-cluster controller that continuously reconciles, so the cluster actively self-heals toward git rather than only changing when someone runs a pipeline. Storing manifests in git is necessary; the reconciliation loop is what makes it GitOps.
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.
Read the four boxes left-to-right as one pull loop, following the blue arrows from the commit to the final state.
- The first blue box is where a human's work ends — you commit the desired state to the git repo; nothing is pushed at the cluster.
- The blue arrows chain the middle two boxes: Argo CD watches the repo from inside the cluster, then syncs the cluster to match — the controller pulls, it is not pushed to.
- The payoff is the green Cluster = git box on the right: once the loop settles, what's running is exactly what git says, so git is the single source of truth.
In short: Commit to git, and an in-cluster controller pulls and reconciles until the cluster equals git — the source of truth.
| 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
GitOps is the idea; Argo CD is the thermostat that actually runs the loop. Everything in the previous section — desired state in git, continuous reconciliation, self-healing — needs a concrete piece of software living in the cluster to do the watching and the nudging. Argo CD is the most widely used one, and the beautiful part is that you configure it the same way you configure everything else in Kubernetes: with a YAML object. You don't script the reconciliation; you declare what "in sync" means and let the controller enforce it.
That object is an Application, and reading it is just answering three questions: what do I want (a repo URL, a branch, a path, and which values file), where should it run (a target cluster and namespace), and how aggressively should Argo CD keep them matched (the sync policy). With that in hand, Argo CD renders the manifests from git, diffs them against what's actually live, and paints every resource Synced (git and cluster agree) or OutOfSync (they've diverged) — that diff is drift detection, made continuous and visible instead of discovered during an outage. Think of it as a spell-checker for your cluster: it constantly underlines the moment reality stops matching the source of truth.
Two policy switches turn that passive diff into active enforcement, and they're worth understanding precisely because they change how your whole team operates. auto-sync means Argo CD doesn't just report drift, it corrects it — a new commit lands and the cluster updates without anyone clicking "deploy." self-heal extends that to changes made on the cluster side: run a manual kubectl edit and Argo CD treats it as drift from git and reverts it. And here's the mistake that catches every team new to GitOps — someone SSHes in, hand-patches a Deployment to fix a fire, walks away satisfied, and minutes later their fix has silently vanished. That's not Argo CD malfunctioning; it's Argo CD doing its one job. The lesson to internalize: with self-heal on, the cluster is read-only to humans — the only durable way to change it is to change git. (The prune option, which you'll see below, is the matching rule for deletions: remove a resource from git and Argo CD removes it from the cluster.)
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.
Follow it left-to-right: git and the live cluster both feed the diff in the middle, which sorts every resource into Synced or OutOfSync on the right.
- The two left boxes are the two things Argo CD compares — git (the desired state, blue) and the live cluster (what's actually running, amber).
- The middle box is the continuous diff. Its output splits: matching resources go to the green Synced box; a divergence (a manual
kubectl edit) goes to the red OutOfSync box — that is drift, made visible instead of discovered in an outage. - The payoff is the dashed arrow from OutOfSync back to git: with
selfHeal: true, Argo CD doesn't just report the drift, it reverts the cluster to match git — which is why the cluster becomes read-only to humans.
In short: Argo CD constantly diffs git against the cluster; agreement is Synced, a hand-edit is OutOfSync drift, and self-heal erases that drift 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)
Every piece you've met has been half of one machine; this section bolts them together. CD5 gave you continuous integration — build the image, test it, scan it, push it to a registry. This lesson gave you continuous delivery the GitOps way — a cluster that pulls its desired state from git. The question that's been hanging in the air is the handoff: how does a passing test on a code push actually become a new version running in prod, without anyone typing kubectl? The answer is a clean seam, and knowing exactly where that seam sits is what separates people who "use CI/CD" from people who can design it.
Here's the relay-race analogy. CI runs the first leg and carries the baton only as far as a git commit: it builds and verifies the image, pushes it to the registry with an immutable tag, then commits that tag into the deploy repo — bumping image.tag in the right values file. Then it hands off. Argo CD runs the second leg: it sees the new commit and reconciles the cluster to match. The baton is the commit; the two runners never occupy the same track. This is why the earlier warning about --set from a laptop mattered — that was a runner trying to sprint the whole race alone, skipping the git handoff that makes everything auditable.
The insight worth carrying out of this whole chapter: CI and CD have genuinely different jobs, and conflating them is the common design error. CI produces artifacts and proposes desired state; CD (GitOps) makes reality match desired state. Draw that line cleanly and three good things fall out automatically — the pipeline never needs cluster credentials (only the in-cluster controller changes the cluster), every deploy is a reviewed commit you can read in git log, and rollback collapses to git revert. It also explains why your ch08 DevOps agent fits so naturally here: an agent that opens a PR to bump a tag is just another well-behaved producer of proposed desired state — it plays CI's role, and GitOps delivers its proposal exactly like any human's. That is the delivery backbone the ch08 capstone is built on.
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.
Read the row left-to-right as one relay race; the vertical divider under "git commit" marks the handoff between the two runners.
- The left segment is CI's leg: a code push triggers build/test/scan, the image is pushed with an immutable tag, and CI commits that tag into the deploy repo. CI's job ends at the amber-to-blue
git commitbox. - The git commit box is the baton — the single seam where responsibility passes. The label calls it the handoff; the two runners never share a track.
- The right segment is CD's leg: Argo CD sees the commit, pulls from git, and syncs the cluster to reach the green
prodbox. The bottom note is the payoff — CI holds registry and git access but never cluster credentials.
In short: CI produces and proposes (ending at a commit); GitOps makes reality match — so every deploy is a reviewed commit and the pipeline never touches the cluster.
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.