GitOps and ArgoCD
Why this matters
Everything so far has been push-based: a pipeline holds cluster
credentials and reaches into AKS to apply changes (track 07 module 08's
kubectl set image, module 04's rollouts). That works, but it has real
downsides — the pipeline needs standing cluster credentials, there's no
single record of what's supposed to be running, and if someone
kubectl edits the cluster by hand, nothing notices or corrects it. GitOps
inverts the model: a controller inside the cluster continuously pulls the
desired state from a Git repo and makes the cluster match it. Git becomes
the single source of truth, every change is a reviewable commit, and the
cluster self-heals back toward Git if it drifts. This module installs
ArgoCD on your AKS cluster and makes it deploy your app from a Git repo —
the pivot from push to pull that the whole back half of this track builds on.
Concepts
Push vs. pull, made concrete
Recall module 00's two models. Push (what you've done): the CI runner,
sitting outside the cluster, authenticates in and runs kubectl/helm.
The cluster is passive; the pipeline is the actor and holds the keys.
Pull (GitOps): an agent running inside the cluster watches a Git repo,
notices when the committed manifests differ from what's running, and applies
the difference itself. The pipeline's responsibility now ends at git push — it commits the desired manifests and stops; it never touches the
cluster. Consequences: no external system needs cluster-admin credentials
(smaller attack surface — a security win developed in module 07); the Git
repo is a complete, audited, revertible record of every deploy; and the
cluster can reconcile — continuously drive itself toward the declared
state — which is the property that enables self-healing.
Git as the single source of truth
In GitOps, the desired state of the cluster lives entirely in Git as
declarative manifests (plain YAML, Kustomize, or Helm). "What is running in
production?" is answered by git log on that repo, not by querying the
cluster and hoping no one changed it by hand. This gives you: auditability
(every change is a commit with an author and a diff), rollback by
git revert (revert the commit, the controller rolls the cluster back —
no special deploy tooling), and review (a manifest change goes through
the same PR + required-checks workflow from track 08 and module 02 as
application code). The mental shift from track 09 is natural: Terraform made
infrastructure declarative and version-controlled; GitOps does the same
for application deployments, and uses a continuously-reconciling agent
rather than a human running apply.
ArgoCD's architecture and the Application object
ArgoCD is a Kubernetes controller (a set of pods you install into the
cluster, usually in an argocd namespace) plus an API/UI/CLI. Its central
custom resource is the Application: a declarative object that says "the
desired state lives at this Git repo, this path, this revision, and should
be deployed into this cluster, this namespace." ArgoCD continuously
compares that Git source against the live cluster state and reports a
sync status — Synced (cluster matches Git) or OutOfSync (they
differ) — and a health status (Healthy/Progressing/Degraded,
derived from the underlying resources' readiness, e.g. a Deployment's
rollout — connecting to track 03 and module 04). You manage apps through the
argocd CLI or UI, but the Application itself is just YAML you can (and
should) also keep in Git — "App of Apps" and declarative setup.
Sync policies: manual vs. automated, prune, self-heal
An Application's sync policy decides how differences between Git and the cluster are resolved:
- Manual sync: ArgoCD detects drift and marks the app
OutOfSync, but waits for you to click Sync (orargocd app sync) to apply. Safer for learning and for production changes you want a human to trigger. - Automated sync: ArgoCD applies committed changes automatically as soon
as it sees them in Git — the fully pull-based continuous deployment path.
Two important sub-options:
- prune: also delete cluster resources that were removed from Git. Without prune, deleting a manifest from Git leaves the orphaned resource running in the cluster. With prune, Git is authoritative for deletions too. (Powerful and slightly dangerous — a bad commit can delete real resources.)
- selfHeal: if the cluster drifts from Git (someone
kubectl edits a live resource), ArgoCD reverts it back to the Git state automatically. This is the self-healing property; without it, manual drift persists asOutOfSyncuntil someone syncs.
Drift detection and self-healing
Drift is any difference between the Git-declared state and the live
cluster. ArgoCD detects it continuously (a periodic reconcile plus watch
events). What it does about drift depends on the policy above: with
selfHeal: true, out-of-band changes are automatically reverted to match
Git — so kubectl scale deployment/x --replicas=99 on a self-healing app
gets undone within seconds, because Git said 3. With self-heal off, ArgoCD
just shows the drift as OutOfSync and leaves it for a human. This is the
single biggest operational difference from push-based CD: in push-based, the
cluster stays however anyone last left it; in GitOps with self-heal, the
cluster is continuously pulled back to what Git says, making manual hotfixes
either impossible or immediately reverted — which is the point (all changes
go through Git) but a real behavior change to internalize.
Where the CI pipeline fits now: commit the image tag to Git
GitOps changes what your CI pipeline's last step is. Instead of
kubectl set image (push), the pipeline — after building and pushing the
SHA-tagged image (module 03) — commits the new image tag into the
deployment manifest in Git (e.g. updates image: .../demo:<sha> in a
Kustomize/Helm values file and pushes). ArgoCD sees the commit and syncs the
new image into the cluster. So the flow becomes: CI builds+pushes the image
and writes the tag to Git; CD (ArgoCD) pulls it from Git into the cluster.
A common structure is two repos: an app repo (source code + CI that
builds the image and bumps the tag) and a config/GitOps repo (the
manifests ArgoCD watches) — keeping application code separate from
deployment state. The capstone (module 08) wires exactly this.
Command reference
Installing ArgoCD and driving it with the argocd CLI and kubectl.
| Command | What it does | Notes |
|---|---|---|
kubectl create namespace argocd |
Creates the namespace ArgoCD installs into | Convention |
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml |
Installs ArgoCD's controllers/API/UI | The standard non-HA install |
kubectl -n argocd get pods |
Confirms ArgoCD's own pods are running | Wait for all Running/Ready |
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath='{.data.password}' | base64 -d |
Retrieves the initial admin password | First-login credential |
kubectl -n argocd port-forward svc/argocd-server 8080:443 |
Exposes the UI/API locally | Or expose via a LoadBalancer/Ingress (track 07 module 04) |
argocd login localhost:8080 |
Authenticates the CLI to the ArgoCD API | --insecure for the self-signed port-forward cert |
argocd app create <name> --repo <git-url> --path <dir> --dest-server https://kubernetes.default.svc --dest-namespace <ns> |
Creates an Application pointing at a Git path | The core "deploy from Git" command |
argocd app list |
Lists Applications and their sync/health status | Quick overview |
argocd app get <name> |
Detailed status: sync, health, resource tree, diff | Primary troubleshooting view |
argocd app sync <name> |
Manually applies the Git state to the cluster | Needed when sync policy is manual |
argocd app diff <name> |
Shows the difference between Git and the live cluster | Why an app is OutOfSync |
argocd app set <name> --sync-policy automated |
Turns on automated sync | Add --auto-prune / --self-heal |
argocd app set <name> --sync-policy automated --auto-prune --self-heal |
Full automated GitOps: apply, delete-removed, revert-drift | The pull-based continuous-deployment setup |
argocd app history <name> |
Lists sync revisions (deploy history) | Rollback target list |
argocd app rollback <name> <id> |
Rolls the app back to a prior synced revision | GitOps rollback (or git revert the commit) |
argocd app delete <name> |
Removes the Application (and, if configured, its resources) | Cleanup |
Hands-on exercises
Use your AKS cluster (track 07). You need a Git repo of manifests ArgoCD
can read — a public GitHub repo with a k8s/ directory containing a
Deployment + Service for your demo app (referencing a SHA-tagged image
from module 03). The exercises install ArgoCD and deploy that repo.
Install ArgoCD and reach the UI. Create the
argocdnamespace and apply the install manifest. Wait for allargocdpods to beRunning. Retrieve the initial admin password, port-forwardargocd-server, and log in with theargocdCLI (argocd login localhost:8080 --insecure). Confirmargocd app listreturns (an empty list is fine).Create your first Application (manual sync). Point an Application at your manifests repo/path with a manual sync policy. Run
argocd app get <name>and observe it reportsOutOfSync(Git says deploy these objects; the cluster has nothing yet). Note ArgoCD detected the desired state but did not apply it.Sync and verify. Run
argocd app sync <name>. Watch the app becomeSyncedandHealthy, then confirm from the cluster side withkubectl get deployment,svc -n <ns>that your app is running. You just deployed via pull — you never rankubectl applyagainst the cluster yourself; ArgoCD did, from Git.Change Git, watch it go OutOfSync, sync again. Edit the Deployment's replica count in Git (e.g. 2 → 4) and commit/push. Within a minute
argocd app getshowsOutOfSync; runargocd app diff <name>to see exactly the replica difference. Sync and confirm the cluster now runs 4 replicas. This is the core GitOps loop: change Git → reconcile.Turn on automated sync.
argocd app set <name> --sync-policy automated. Now push another Git change (replicas 4 → 3) and do not sync manually — confirm ArgoCD applies it on its own within a minute. You've moved from "GitOps detects" to "GitOps deploys" — pull-based continuous deployment.Demonstrate self-heal (drift correction). Enable self-heal (
argocd app set <name> --sync-policy automated --self-heal). Now drift the cluster by hand:kubectl scale deployment/demo -n <ns> --replicas=9. Watch (kubectl get pods -wandargocd app get) ArgoCD revert it back to the Git-declared count within seconds. Contrast: with self-heal off, that manual change would persist and just show asOutOfSync. This is the behavior that makes Git authoritative.Prune: deletion through Git. Add
--auto-prune. Delete one manifest from the Git repo (e.g. remove the Service) and push. Confirm ArgoCD deletes the corresponding cluster resource. Then re-add it in Git and confirm it comes back. Note the danger: with prune on, a bad commit that removes a manifest deletes a live resource — Git is authoritative for deletions too.Rollback via Git. Make a bad change in Git (e.g. point the image at a nonexistent tag) and push; watch the app go
Progressing/Degradedas the pods fail to pull. Roll back withgit revert+ push (orargocd app rollback) and confirm the app returns toHealthy. Note you rolled back a deploy with an ordinary Git operation — no special deploy tooling.Diagnose and fix: an Application stuck OutOfSync. You'll hit the most common ArgoCD confusion. An app refuses to become
Syncedeven after you sync. Reproduce one of these: (a) a manifest in Git has a YAML/schema error so the sync operation fails (visible inargocd app get→OutOfSync, and the sync result shows the apply error); or (b) something outside ArgoCD's management keeps mutating the resource (e.g. an HPA from track 07 module 05 changing replica count while Git pins a fixedreplicas, so it flapsOutOfSyncforever). Investigate withargocd app get <name>(health/sync + conditions),argocd app diff(what differs), and the resource tree in the UI. Fix (a) by correcting the manifest in Git and re-syncing; fix (b) by removing the conflictingreplicasfield from Git so the HPA can own it (the "who owns this field" lesson). Write one sentence on why "I clicked Sync and it's still OutOfSync" is usually a real conflict, not an ArgoCD bug.
Independent challenge
No commands given — assemble it from track 07 (the cluster), module 03 (SHA-
tagged images), module 04 (why a controlled rollout matters), and this
module's ArgoCD concepts. Set up a genuine two-repo GitOps deployment for
your demo app and prove Git is the source of truth. Keep application source
(and the CI that builds/pushes a SHA-tagged image) in one repo, and the
Kubernetes manifests in a separate config repo that an ArgoCD Application
watches with automated sync, prune, and self-heal all on. Demonstrate four
properties, each with evidence: a normal deploy happens by committing a new
image tag to the config repo and letting ArgoCD pull it (you never run
kubectl apply); an out-of-band kubectl edit to a live resource is
reverted automatically to match Git; deleting a manifest from Git removes
the resource from the cluster; and a bad deploy is rolled back purely by
git revert. Then answer, in a short paragraph, the security question this
raises versus track 07 module 08's push pipeline: which system now holds
cluster-admin-level credentials, and why is that a smaller attack surface —
a point module 07 develops.
Stuck? One hint
The mental unlock is that you never deploy to the cluster anymore — you
only ever commit to Git, and ArgoCD is the only thing that touches the
cluster. So build the loop entirely around Git operations: to deploy, commit
a manifest change; to roll back, git revert; to scale, edit and commit the
replica count. If you find yourself reaching for kubectl apply to make a
change, that's the push-based habit — stop and make the change in Git
instead. The two-repo split matters because the app repo's CI writes to
the config repo (bumping the image tag), which means the CI needs push
access to the config repo (a token/deploy key), not to the cluster — that
credential shift is exactly the security point.
Common mistakes & troubleshooting
- Still running
kubectl applyagainst a GitOps-managed app. With self-heal on, ArgoCD reverts your manual change; with it off, you create drift ArgoCD flags forever. In GitOps, change Git, not the cluster. - "I synced and it's still OutOfSync." Usually a real conflict: the sync
apply failed (bad manifest — see
argocd app get's operation result), or something outside ArgoCD keeps mutating the resource (an HPA vs. a pinnedreplicasfield). It's rarely an ArgoCD bug; read the diff and the conditions. - Pinning a field another controller owns. If Git hard-codes
replicasbut an HPA (track 07 module 05) manages it, the app flapsOutOfSyncendlessly. Let the HPA ownreplicas— remove it from the Git manifest. - Enabling auto-prune without understanding it. A commit that removes or mis-paths a manifest will delete the live resource. Prune makes Git authoritative for deletions — powerful, but review deletion commits carefully.
- Treating self-heal as optional decoration. Self-heal is what enforces
"Git is the truth" — without it, anyone's
kubectl editsilently persists. But it also means emergency manual hotfixes get reverted; the fix belongs in Git. - Committing secrets in plaintext to the GitOps repo. Manifests in Git are readable by anyone with repo access. Kubernetes Secrets are only base64, not encrypted — use sealed-secrets/SOPS/external secret operators (previewed here, deepened in track 11/16) rather than committing raw secrets.
- CI pipeline still pushing to the cluster in a GitOps setup. If your
pipeline both bumps the Git tag and runs
kubectl set image, you have two systems fighting over the cluster. Pick one: in GitOps, CI writes to Git and stops.
Checkpoint quiz
Write down your answer to each question before expanding it — checking without attempting first is the single easiest way to fool yourself into thinking you've learned this.
- In GitOps, where does the deploy logic run, and what is the CI pipeline's last step (contrast with track 07 module 08's last step)?
- What is an ArgoCD Application, and what two status axes does it report?
- What is the difference between
OutOfSyncandDegraded? - What do
automated,prune, andselfHealeach add to a sync policy? - You
kubectl scalea self-healing ArgoCD app to 9 replicas. What happens, and why? - Why does GitOps reduce the credential/attack surface compared to a push-based pipeline (preview of module 07)?
- An app is stuck
OutOfSynceven after syncing, and the replica count keeps flapping. What's the likely cause and the fix? - How do you roll back a deployment in a GitOps world, and why is no special deploy tooling needed?
Show answers
- The deploy logic runs inside the cluster in the ArgoCD controller,
which pulls from Git. The CI pipeline's last step is to commit the new
image tag/manifest to Git and stop — it never touches the cluster.
Track 07 module 08's last step was
kubectl set imagedirectly against the cluster (push). - An Application is a declarative object pointing at a Git repo/path/
revision as the desired state and a target cluster/namespace. It reports
sync status (
Synced/OutOfSync— does the cluster match Git?) and health status (Healthy/Progressing/Degraded— are the resources actually working?). OutOfSyncmeans the live cluster differs from what Git declares (a reconciliation state).Degradedmeans the deployed resources exist but are unhealthy (e.g. pods crash-looping). An app can beSyncedbutDegraded(matches Git, but the Git-declared thing is broken).automatedapplies Git changes to the cluster without a manual sync;prunealso deletes cluster resources removed from Git (Git authoritative for deletions);selfHealreverts out-of-band cluster changes back to the Git state (drift correction).- ArgoCD detects the drift (cluster has 9, Git says N) and, because self-heal is on, reverts the Deployment back to the Git-declared count within seconds. Git is authoritative, so the manual change is undone.
- In push, the external pipeline holds standing cluster-admin credentials. In pull, no external system holds cluster credentials — the in-cluster controller pulls from Git, so a compromised CI system can at most commit to Git (reviewable, revertible) rather than directly command the cluster.
- Something outside ArgoCD keeps changing the resource — typically an HPA
managing
replicaswhile the Git manifest pins a fixedreplicas, so they conflict forever. Fix: removereplicasfrom the Git manifest so the HPA owns that field. git revertthe offending commit (orargocd app rollbackto a prior synced revision) and push; ArgoCD reconciles the cluster back to that state. No special tooling is needed because the deploy state is Git history, so ordinary Git operations are the rollback mechanism.
Cumulative review
Closed-book. Don't reopen modules 00-04 while attempting these — find out what actually stuck across the whole first half of the track.
- Trace one code change from a developer's commit all the way to running in the cluster, for both a push-based pipeline (track 07 module 08) and a pull-based GitOps setup (module 05). Name the tool doing each step and where cluster credentials live in each.
- You tag every image
latest. Name three distinct things this breaks, pulling from modules 00, 03, and 04, and say which is a rollout problem, which a traceability problem, and which a rollback problem. - A PR with a failing test can still be merged to
main. Give the module-02 cause. Separately, a merged PR that passed CI still brokemain. Give a different module-02 cause. Are these the same fix? - Map each strategy from module 04 (rolling, blue/green, canary) to how you'd roll it back, and to whether ArgoCD self-heal (module 05) would help or interfere with each.
- Your CI job
buildpushes an image and jobdeploy(in a GitOps setup) commits the tag to the config repo, butdeploycommits the wrong tag. Give the module-01/03 cause (how the tag should have flowed) and the fix. - Explain how "build once, deploy many" (module 00), SHA tagging (module 03), and Git-as-source-of-truth (module 05) reinforce each other to make a deploy both traceable and revertible.
- A self-healing ArgoCD app flaps
OutOfSyncforever. Independently, a replica-ratio canary (module 04) keeps drifting its traffic split. What single track-07-module-05 concept explains both problems? - For true continuous deployment to production (module 00), list the capabilities you'd need from modules 02, 04, and 05, one line each on why.
- Someone runs
kubectl editon a production Deployment to hotfix it. What happens under push-based CD versus under GitOps with self-heal, and which is "correct"? - Why is committing a raw Kubernetes Secret to a GitOps repo a mistake even though the repo is private, and what does base64 encoding not give you?
Show answers
- Push: developer commits → CI runner (GitHub Actions) builds+pushes
image (module 03) → same runner runs
kubectl set image/helmagainst AKS. Cluster credentials live in the CI system. Pull: developer commits → CI builds+pushes image and commits the new tag to a Git config repo → ArgoCD (in-cluster) pulls the change and applies it. Cluster credentials live only inside the cluster; CI holds only Git push access. - (a) Breaks
kubectl set imagerollouts — the pod spec text never changes (rollout problem, modules 03/04/track 07 module 08). (b) Destroys traceability — you can't map the image to a commit (traceability problem, modules 00/03). (c) Makes rollback impossible — no stable prior reference to return to (rollback problem, modules 03/04). Same root cause (a mutable, non-unique tag), three symptoms. - Mergeable-despite-failing: the check isn't required (branch protection not enforcing it). Merged-but-broke-main: "require up to date before merge" wasn't set, so a combination bug slipped through. Different fixes — one enables a required check, the other enables the up-to-date requirement.
- Rolling →
kubectl rollout undo; blue/green → flip the Service selector back to blue; canary → re-weight (or scale) traffic to 0% on the new version. Self-heal helps keep the declared state enforced but would interfere with an out-of-band manual rollback (it'd revert your manual flip back to what Git says) — in GitOps you roll back via Git, not by hand. - The tag should have been computed once in
buildand passed via a job output (needs.build.outputs..., modules 01/03);deployrecomputed it independently and diverged. Fix: expose the tag as a job output and havedeployconsume only that. - Build-once produces a single immutable artifact; the SHA tag gives that
artifact a unique, traceable identity mapping to an exact commit; storing
that tag in Git makes the deployed state an audited, revertible record.
Together: what ran is provably what you built, you can name it exactly,
and you can revert to any prior one with
git revert. - A controller you didn't account for owning a field — the HPA (track 07
module 05) manages
replicas. It fights ArgoCD's pinnedreplicas(endlessOutOfSync) and silently changes the pod counts that are the canary's traffic split. Both are "another controller owns the field you're trying to control." - Module 02: a strong required test suite (the only backstop with no human gate). Module 04: a canary/blue-green strategy to limit blast radius. Module 05: GitOps with automated sync (and, for safety, self-heal + auditable rollback) so deploys are declarative and instantly revertible.
- Push-based: the manual edit persists — the cluster stays however it was last left. GitOps with self-heal: ArgoCD reverts the edit back to Git within seconds. GitOps is "correct" for the model — the fix belongs in Git (commit it) so the change is reviewed and durable, not a snowflake edit that disappears on the next sync.
- Anyone with repo access can read it, and base64 is encoding, not encryption — it provides no confidentiality at all. A private repo limits who can read, but the secret is still stored in plaintext- equivalent form in Git history forever. Use sealed-secrets/SOPS/an external secrets operator instead.
Next
06-progressive-delivery-canary-and-blue-green — automate the canary and blue/green strategies from module 04 with Argo Rollouts, gated on real metrics.