GitOps (Argo CD, Flux) — Junior¶
At junior level, focus on this question:
Given one small application and one target cluster, can you make an Argo CD or Flux controller continuously reconcile the cluster to match a Git repository, and prove that a manual change gets detected and reverted?
Use the smallest realistic scenario that exposes the decision and its failure behavior.
GitOps is not "we keep our YAML in Git." It is a running controller inside the cluster that continuously compares what Git says should exist against what actually exists, and closes the gap on its own. This level is about standing that loop up for one service and watching it actually enforce Git as the source of truth — not just believing that it does.
Core Concept 1 — Vocabulary¶
| Term | Meaning |
|---|---|
| Desired state | What the manifests in Git say the cluster should look like right now |
| Actual (live) state | What is really running in the cluster at this moment |
| Reconciliation loop | The controller's continuous cycle: read Git, read the cluster, compute the diff, apply the diff |
| Drift | Any difference between desired and actual state — usually caused by a manual kubectl change |
| Sync | The act of applying the diff so actual state matches desired state |
| Self-heal | Automatically re-applying Git's state whenever drift is detected, without a human clicking "sync" |
| Prune | Deleting cluster resources that are no longer defined in Git |
| Application (Argo CD) | The custom resource that tells Argo CD which Git repo/path to track and which cluster/namespace to deploy it into |
| GitRepository + Kustomization (Flux) | Flux's two-part equivalent: a source object (where to pull from) and a reconciler object (what to apply and where) |
| Push-based deployment | A CI pipeline runs kubectl apply/helm upgrade itself, using credentials it holds to reach the cluster |
| Pull-based deployment (GitOps) | A controller inside the cluster pulls from Git and applies changes; nothing outside the cluster needs write access to it |
Core Concept 2 — Push vs Pull, Side by Side¶
| Push-based CI/CD | Pull-based GitOps | |
|---|---|---|
| Who initiates the deploy | The pipeline, on every commit/merge | A controller inside the cluster, on a timer or webhook |
| Where cluster credentials live | In the CI runner/pipeline secrets | Only inside the cluster's controller |
| How drift is detected | It usually isn't — nothing is watching after the deploy | Continuously — the reconciliation loop compares state every cycle |
| What "rollback" means | Re-run an old pipeline job, or kubectl apply an old manifest by hand | git revert the bad commit; the controller applies the reverted state |
| Audit trail | Pipeline logs, if kept | Git history — every change is a commit with an author and a message |
The point of this table isn't that one approach is universally better. It's that GitOps moves the "who is allowed to change the cluster" question from pipeline credentials to who can merge to a Git branch, and it adds a standing process that keeps checking the cluster even when nobody just deployed anything.
Core Concept 3 — A Repeatable Method for One Service¶
- Put the manifests in Git. Create a repo (e.g.,
platform-gitops) with a path per service, e.g.apps/hello-web/deployment.yamlandapps/hello-web/service.yaml. - Install the controller in the cluster. Argo CD via its Helm chart, or Flux via
flux bootstrap github --owner=... --repository=platform-gitops --path=clusters/dev. - Point the controller at your repo and path. Create an Argo CD
Applicationor a FluxGitRepository+Kustomizationnaming the repo URL, branch, and path to watch. - Turn on automated sync with self-heal and prune. Without this, the controller only tells you about drift — it does not correct it, and deleted manifests leave orphaned resources behind.
- Confirm live state matches Git. Check the Application/Kustomization status; it should report both "in sync" and "healthy," not just one.
Core Concept 4 — Worked Example: hello-web¶
Repository layout:
Argo CD Application manifest:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: hello-web
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/example-org/platform-gitops.git
targetRevision: main
path: apps/hello-web
destination:
server: https://kubernetes.default.svc
namespace: hello-web
syncPolicy:
automated:
prune: true
selfHeal: true
Both controllers need to know two separate things: where to pull the desired state from (the Git source), and what to do with it once pulled (apply it to the cluster). Argo CD bundles both into one Application object; Flux keeps them as two objects on purpose, so one GitRepository source can feed several Kustomization reconcilers. Neither tool watches Git in real time by magic — the interval field is a polling period (here, Argo CD checks every few minutes by default and Flux is set explicitly to 1m), and a repository webhook can shorten the gap between "I pushed" and "the controller noticed" without changing what the controller actually does once it notices.
The Flux equivalent — a GitRepository (the source) plus a Kustomization (the reconciler):
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
name: platform-gitops
namespace: flux-system
spec:
interval: 1m
url: https://github.com/example-org/platform-gitops.git
ref:
branch: main
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: hello-web
namespace: flux-system
spec:
interval: 5m
sourceRef:
kind: GitRepository
name: platform-gitops
path: ./apps/hello-web
prune: true
targetNamespace: hello-web
Once applied, argocd app get hello-web (or flux get kustomizations) reports Synced and Healthy. Now prove the loop is real, not decorative:
Run kubectl scale deployment hello-web --replicas=5 -n hello-web. Within one reconciliation interval, the controller notices the live replica count doesn't match Git's 2, and reverts it — with no human re-running anything. Then push a real change: bump the image tag to 1.3.0 in deployment.yaml, commit, and push. The controller picks it up and rolls out the new image without anyone touching kubectl.
Core Concept 5 — What "Done" Looks Like¶
For one service, the setup is complete when all of these are true:
- The Application/Kustomization status shows both Synced and Healthy — one without the other is not done.
- A manual
kubectlchange to a tracked resource is detected and reverted automatically, and you watched it happen. - Deleting a manifest from Git and pushing actually removes the matching resource from the cluster (this requires
prune: true). - The only way you changed the cluster the whole exercise was by committing to Git — never by running
kubectl applyyourself.
Common Mistakes¶
- Editing GitOps-managed resources with
kubectl. With self-heal on, the controller reverts it — which feels like "the change didn't work" but is actually the system working correctly. Without self-heal, the edit silently persists as invisible drift. - Storing plaintext secrets in the repo. A password or API key committed to Git — even in a "private" repo — is now in the Git history forever, readable by anyone with repo access, past or present.
- Leaving sync policy manual and forgetting to sync. A repo can look like "our source of truth" while the cluster has drifted for weeks because nobody clicked the sync button — manual sync only helps if someone actually does it.
- Forgetting
prune: true. Deletingservice.yamlfrom Git looks like it should remove the Service, but without pruning the old object is silently orphaned in the cluster. - Assuming "in Git" means "in the cluster." Until the controller has actually completed a sync cycle, a commit is just a commit — check the Application/Kustomization status, don't assume.
Apply it¶
- Start a local
kindcluster and install Argo CD (or Flux) into it. - Create a small Git repo with
apps/hello-web/deployment.yaml(2 replicas, a real image) andservice.yaml. - Create the Application (or GitRepository + Kustomization) pointing at that repo and path, with
automated: { prune: true, selfHeal: true }. - Confirm the app reaches Synced + Healthy, then run
kubectl scale deployment hello-web --replicas=5 -n hello-weband time how long it takes to revert on its own. - Change the image tag in Git only (no
kubectl), push, and confirm a new rollout happens without you running any cluster command yourself.
Verify your work¶
argocd app get hello-web(orflux get kustomizations) shows Synced and Healthy simultaneously.- A manual replica-count change you made with
kubectlreverted back to Git's value without you re-running anything. - Deleting
service.yamlfrom Git and pushing removed the Service object from the cluster (pruning worked). - A commit-only image tag change produced a new rollout, visible via
kubectl rollout history deployment/hello-web -n hello-web.
Review questions¶
- What is the difference between the controller detecting drift and the controller correcting drift?
- Why does pull-based GitOps mean cluster credentials never need to live in a CI pipeline?
- What two conditions must both be true for an Argo CD Application (or Flux Kustomization) to be considered fully synced?
- Why does deleting a manifest from Git only remove the matching resource from the cluster if pruning is enabled?