GitOps Delivery with Argo CD: Sync, Drift & Self-Heal
Deploy by changing Git, then break the cluster by hand and watch Argo CD put it back.
- Time
- 45 min
- Level
- Intermediate
- Objectives
- 6 objectives
- Cost
- Low cost
Where this fits in the platform
Already built
This lab adds
- A cluster that follows Git, and reverts anything applied by hand
Which lets you
—
Before you start
You will need
- The EKS cluster and ECR repository from the earlier labs
- kubectl, and access to the Git repository holding your manifests
- Argo CD, installed in step 1
You do not need these already — the lab environment below provides them.
You will be able to
- Deploy by changing desired state rather than by running commands against a cluster
- Read an Argo CD diff and say which side is wrong
- Recognise drift, and know which direction reconciliation resolves it
Cost — Low cost
Assumes the EKS cluster from the earlier labs is already running — this adds only Argo CD itself, which is a handful of Pods on nodes you are already paying for. If the cluster is not up, that is the ~$0.30/hour, not this.
Nothing to pay in the browser. Open the terminal runs this against a simulated cloud — the same API calls and the same commands, with no account and no bill. The figure above applies only if you build it in your own.
What this lab is for#
Every lab up to here ended with you applying something. This one takes that away. From now on the cluster is not something you change — it is something that follows a change you make somewhere else.
The pipeline you built already stops one step short of deploying: its last stages update a manifest and push a commit. This lab is what happens next.
Jenkins
| 8 Update manifest (the image tag in kustomization.yaml)
| 9 Push manifest
v
Git repository <-- the desired state
|
v
Argo CD <-- notices, compares, applies
|
v
KubernetesHands-on environment
Run this lab in a real terminal, free and in your browser. The environment is temporary and yours alone — break it as much as you like.
Open the terminalOpens in Killercoda, in a new tab — keep this page open for the steps.
Run it on your own machine
Run this lab on your own machine. One command starts the environment, with everything the lab needs already installed:
Argo CD reconciles a kind cluster exactly as it does EKS. Point the Application at your own repository and use a locally built image in the kustomization instead of the ECR reference.
You will need:
- docker
- kubectl
- kind
git clone https://github.com/Waleeddarwesh/EgyKode-lab.git
cd EgyKode-lab
./egykode start k8s
./egykode shellYou need Docker and Git installed. Everything else runs inside the environment. The first start downloads it and takes a few minutes; later starts are seconds.
Not sure what you already have? Run: npm run doctor — it checks and changes nothing.
Run it on AWS
This lab builds real cloud infrastructure, so it needs your own AWS account. Follow the cost and cleanup notes above — the resources are yours, and so is the bill.
Anything you tick here is your own record. EgyKode cannot see inside that terminal, so the success criteria stay self-assessed even when the environment checks your work for you.
Install Argo CD and look at what it is
Step 1 of 7
What you are proving: You can install a reconciler and describe what it watches and compares
This step settles no success criterion on its own.
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
kubectl -n argocd rollout status deploy/argocd-server --timeout=300sGet the initial password and port-forward the UI — the CLI works too, but the diff view is worth seeing at least once:
kubectl -n argocd get secret argocd-initial-admin-secret \
-o jsonpath='{.data.password}' | base64 -d; echo
kubectl -n argocd port-forward svc/argocd-server 8080:443Argo CD is a controller. It has no knowledge of your pipeline and no webhook into it; it watches a Git path and a cluster, and works to make the second look like the first.
What you are proving: You can point an Application at a repository path, and know which namespace it must live in
Marking this settles success criterion 1.
Create the Application. This is the whole configuration — everything else is consequence:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: ivolve-app
namespace: argocd # MUST be Argo CD's own namespace
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
repoURL: https://github.com/<you>/CloudDevOpsProject.git
targetRevision: HEAD
path: 04-Kubernetes/manifests
destination:
server: https://kubernetes.default.svc
namespace: ivolve
syncPolicy:
automated:
prune: true # objects removed from Git are removed from the cluster
selfHeal: true # changes made to the cluster are reverted
allowEmpty: false
syncOptions:
- CreateNamespace=trueTwo details that cause silent failures:
metadata.namespacemust beargocd. An Application created anywhere else is ignored — no error, no event, it simply never syncs.- The finalizer makes deletion cascade. Without it,
kubectl delete applicationremoves the record and orphans everything it created.
kubectl apply -f argocd-application.yamlWhat you are proving: You can tell a synced cluster from a healthy one, and say why those answer different questions
This step settles no success criterion on its own.
kubectl -n argocd get application ivolve-appWait for SYNC STATUS: Synced and HEALTH STATUS: Healthy. Those answer two
different questions — does the cluster match Git, and are the workloads
actually working — and you will see them disagree later.
Watch the first sync happen in order:
kubectl -n ivolve get pods -wThe database comes up first, then the two backends, then the frontend, then the
ingress. That is not luck. It is argocd.argoproj.io/sync-wave on the
manifests — 1 for the database, 2 for the backends, 3 for the frontend,
4 for the ingress. Without it everything applies at once and the services that
need the database crash-loop until it happens to be ready.
What you are proving: You can deploy by committing, without running a command against the cluster
Marking this settles success criterion 2.
This is the part that replaces every kubectl apply you have run so far.
In 04-Kubernetes/manifests/kustomization.yaml, find the images: block —
this is exactly what stage 8 of the Jenkins pipeline rewrites:
images:
- name: ivolve-frontend
newName: <account>.dkr.ecr.us-east-1.amazonaws.com/ivolve-frontend
newTag: 9-c1713b9 # <- change this to another tag you have pushedChange newTag to a different tag that exists in ECR, then:
git commit -am "deploy frontend 10-abc1234"
git pushNow do nothing. Argo CD polls roughly every three minutes:
kubectl -n argocd get application ivolve-app -wSynced → OutOfSync → Synced. You did not touch the cluster.
What you are proving: You can verify a deployment against the running Pod and the sync history rather than against your own commit
Marking this settles success criteria 3 and 4.
The manifest says what you asked for. The Pod says what happened:
kubectl -n ivolve get pods -l app.kubernetes.io/name=ivolve-frontend \
-o jsonpath='{.items[*].spec.containers[*].image}'; echoThat must show the tag you committed. Checking the manifest instead would only prove you can read your own commit.
kubectl -n argocd get application ivolve-app \
-o jsonpath='{.status.history[-1].revision}'; echoThe sync history records which commit produced the running state. This is the deployment record — there is no separate one, and it cannot drift from reality because it is the thing that caused reality.
What you are proving: You can change the cluster by hand and watch reconciliation put it back
Marking this settles success criterion 5.
Everything so far shows the happy path. The interesting behaviour is what happens when the cluster and Git disagree.
kubectl -n ivolve scale deployment ivolve-frontend --replicas=1
kubectl -n ivolve get deploy ivolve-frontend -wWatch the replica count. It goes to 1 — and then comes back.
Git Cluster
replicas: 3 replicas: 3
| |
| kubectl scale --replicas=1
| v
replicas: 3 <-- drift --> replicas: 1
|
| Argo CD compares, finds a difference,
| and applies Git's version
v
replicas: 3 replicas: 3That is selfHeal: true. Your change was not rejected — the API server accepted
it happily. It was reverted, because a controller is continuously making the
cluster match Git, and your edit was simply the next difference it found.
Look at what it recorded:
kubectl -n argocd get application ivolve-app -o jsonpath='{.status.operationState.phase}'; echoThis is the lesson of the whole phase. In a push-based world, that
kubectl scale would have stayed — and six weeks later nobody would know why
production had one replica while the repository said three. Here the repository
is not documentation of the system. It is the system.
What you are proving: You can state what self-heal costs you, and how a rollback works once it is on
Marking this settles success criterion 6.
Self-heal has a cost, and pretending otherwise is how people get surprised at 3am:
- You cannot hotfix by hand. An emergency
kubectl editis reverted within minutes. The fix is a commit, and your ability to recover is now bounded by how fast you can merge. - A bad commit deploys itself. Automation applies what Git says, including a mistake. The protection is branch review, not the cluster.
prune: truedeletes. Remove a manifest from Git and the object is removed from the cluster. That is the point, and it is also how a bad rebase deletes a database.
Rolling back is therefore git revert, and it goes through the same path as
everything else:
git revert --no-edit HEAD
git pushThree failures you will hit, and the reasoning that separates them. All three look like "Argo CD is broken"; none of them are.
Nothing happens at all#
No sync, no error, no event. The Application sits there.
kubectl -n argocd get application ivolve-app -o jsonpath='{.status.sync.status}'; echo
kubectl -n argocd describe application ivolve-app | tail -20The controller is working perfectly and looking somewhere you are not. Check
spec.source.path against what is actually in the repository, and check the
Application's own namespace — an Application created outside argocd is
ignored silently, with no error to find.
Production lesson: a reconciler that finds nothing to do is indistinguishable from a reconciler that is broken, unless you look at what it believes its desired state is.
It reports Synced while the application is down#
kubectl -n argocd get application ivolve-app
kubectl -n ivolve get podsSYNC STATUS: Synced and HEALTH STATUS: Degraded together are not a
contradiction. Sync answers does the cluster match Git; health answers do
the workloads work. A manifest with a typo in the image tag syncs perfectly
and never becomes healthy.
Production lesson: alert on health, not on sync. A green sync status tells you your pipeline worked, not that your users can reach anything.
The Pod runs an image you did not expect#
kubectl -n ivolve get pod -l app.kubernetes.io/name=ivolve-frontend \
-o jsonpath='{.items[*].spec.containers[*].image}'; echo
kubectl -n ivolve describe pod -l app.kubernetes.io/name=ivolve-frontend | grep -A5 EventsImagePullBackOff here means the tag in the manifest does not exist in ECR —
the commit was valid, the sync succeeded, and the tag was still wrong. Argo CD
applied exactly what you asked for.
Production lesson: GitOps guarantees the cluster matches the repository. It guarantees nothing about whether the repository is correct, which is why the review on that commit is the actual control.
Clean up#
Deleting the Application is what removes the workloads — and only because of the finalizer you set in step 2. Without it you delete the record and leave everything it created running, which is a genuinely expensive mistake on a cluster you are paying for.
Destructive — This removes real resources. Check which environment you are in first.
kubectl delete application ivolve-app -n argocd # cascades via the finalizer
kubectl get all -n ivolve # must be empty — verify, do not assume
helm uninstall argocd -n argocd && kubectl delete ns argocdIf kubectl get all -n ivolve still shows Pods, the cascade did not happen.
Check the finalizer was present before the delete; recreating the Application
and deleting it again is the tidiest recovery.
The EKS cluster itself is untouched by this lab — destroy it with
terraform destroy when you finish for the day, as always.
What you added to the platform#
Before this lab, something still had to run kubectl — and that something
needed cluster credentials. Now nothing does. The pipeline's authority ends at
a commit, and the authority to change the cluster lives inside the cluster.
That is a security property as much as a workflow one, and it is the last piece of the delivery chain: an image built once, scanned, pushed immutably, referenced by a manifest, and deployed by reconciliation.
Maintained by others, on Killercoda. Useful for extra repetition on one tool — it does not complete this lab or settle any criterion above.
- Argo scenariosArgo CD, Rollouts and Workflows
Success criteria
0 of 6
The concept behind it
Phase complete · 09 GitOps delivery
You can now: A version deploys because Git changed, and a change you make by hand does not survive.
Next phase
Lab 48 of 59 on the project path