Module 5: Gitops With Argocd
5. Anatomy of an ArgoCD `Application`
Description
You have both pieces — Gitea (lesson 3) and ArgoCD (lesson 4) — running, but still unaware of each other: ArgoCD doesn't know andes-cargo-k8s exists, and Gitea has no idea something called ArgoCD is running on the same cluster. The one object still missing is an Application — ArgoCD's central resource, the same one cicd-and-gitops-on-aws-guide M7.3 showed you verified against official documentation but never applied. This lesson dissects that object field by field, with the real YAML you're going to apply in lesson 6 — and closes with literal evidence, already executed on this very lab, of what happens when selfHeal is active.
Connection to the module
This lesson is the hinge between installing the pieces (lessons 3-4) and connecting them (lesson 6). Without understanding what each Application field means, applying lesson 6's manifest would be copying YAML with no judgment — with this lesson, every line of that manifest has a reason you can explain without reading it again.
The complete resource, the same one you're going to apply in lesson 6
# application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: andes-cargo-status-api
namespace: argocd
spec:
project: default
source:
repoURL: http://gitea-http.gitea.svc.cluster.local:3000/andes-cargo/andes-cargo-k8s.git
targetRevision: main
path: .
destination:
server: https://kubernetes.default.svc
namespace: andes-cargo
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=false
This isn't a generic example — it's, literally, the file you're going to apply in lesson 6, with the exact repoURL of the repository you created in lesson 3. It's worth reading it with the same judgment you'd read a Terraform resource or a Deployment with: each top-level block answers a different question.
metadata: where the Application object itself lives
metadata:
name: andes-cargo-status-api
namespace: argocd
Here's the first detail that surprises anyone seeing this for the first time: the Application lives in the argocd namespace, not in andes-cargo. An Application is a control object — it belongs to ArgoCD, it's the instruction ArgoCD follows — distinct from the resources that Application manages (the Deployment, the Service, etc., which are going to live in andes-cargo, as destination.namespace declares below). It's the same separation you already know between a HorizontalPodAutoscaler (lives in the namespace of the workload it scales) and a ClusterRole (doesn't live in any namespace, because it's a cluster-scoped resource) — every Kubernetes object lives wherever makes sense for who manages it, not necessarily alongside what it describes.
spec.source: where the desired state comes from
source:
repoURL: http://gitea-http.gitea.svc.cluster.local:3000/andes-cargo/andes-cargo-k8s.git
targetRevision: main
path: .
repoURL— Gitea's internal DNS name (lesson 3), notlocalhost. It's the exact fix lesson 3 already foreshadowed in its "Common mistakes": ArgoCD runs inside the cluster, so it needs the address that resolves from there, not the one that resolves from your machine.targetRevision— the branch, tag, or commit ArgoCD follows.mainmeans "whatever's latest on that branch, always" — every time ArgoCD polls the repository (lesson 2), it asks again "what's onmainright now?", not "what was onmainwhen I applied thisApplication?".path— the folder, inside the repository, that holds the manifests..means "the root itself" — the nine YAML files you pushed in lesson 3 are all there, with no subfolders. A larger repository, with several applications, would normally use separate subfolders (apps/andes-cargo-status-api/,apps/some-other-service/) and oneApplicationper folder, each pointing at its ownpath.
spec.destination: where it gets applied
destination:
server: https://kubernetes.default.svc
namespace: andes-cargo
server— the Kubernetes API ArgoCD is going to apply the manifests against.https://kubernetes.default.svcis a special address: it means "the same cluster where ArgoCD is running," resolved by Kubernetes' internal DNS without you having to write any IP or any extrakubeconfig. ArgoCD can also manage clusters external to its own (a real pattern called hub-and-spoke, where a single central ArgoCD manages several clusters) — outside this guide's scope, but worth knowing the field exists for that.namespace—andes-cargo, the same namespace you declared innamespace.yamlsince Module 1. Any repository manifest that does not declare its own explicitmetadata.namespaceinherits this value; the ones that do declare it explicitly (all nine in this repository do, all withnamespace: andes-cargo) use it as-is, with no dependency on this field.
spec.syncPolicy: manual versus automated sync
This is the part that most changes ArgoCD's day-to-day behavior, and the one this lesson explains in most detail because it isn't obvious at a glance.
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=false
If the automated block didn't exist (syncPolicy: {}, or no syncPolicy at all), the Application would still exist and keep comparing — lesson 2's loop always keeps running — but ArgoCD would only report the difference (Sync Status: OutOfSync) without applying anything on its own. Someone would have to run argocd app sync andes-cargo-status-api (or click "Sync" from the UI) every time they wanted the change applied — manual sync, useful when a team wants explicit human approval before every change, even one coming from Git.
With automated present, as in this application.yaml, ArgoCD applies the change as soon as it detects it, with no command needed — automated sync, the model the rest of this module uses.
Inside automated, two boolean fields that do different things:
prune: true— if a manifest disappears from the repository (someone deleteshpa.yamland doesgit push), ArgoCD deletes the corresponding resource from the cluster too. Withoutprune: true, ArgoCD only applies what still exists in Git, but never deletes what's no longer there — an "orphaned" resource would keep running indefinitely.selfHeal: true— if someone changes something directly on the cluster (kubectl scale,kubectl edit, anything that doesn't go through Git), ArgoCD reverts that change so it matches what Git declares again. It's the more aggressive of the two properties, and the one the next section confirms with real evidence.
syncOptions: [CreateNamespace=false] is an explicit decision for this lab: the andes-cargo namespace already exists (namespace.yaml, applied since Module 1) and keeps living inside the same repository ArgoCD syncs — ArgoCD doesn't need to create it on its own before syncing everything else.
Real evidence: selfHeal in action, on this very lab
Everything that follows actually ran, against the already-synced andes-cargo-status-api Application (the same YAML above, applied as part of building this module) — lesson 6 walks you step by step to this exact point. Before the test, the Deployment had 5 replicas, per the latest recorded convergence. Now, someone — simulating a rushed engineer, bypassing Git — runs this directly against the cluster:
kubectl scale deployment andes-cargo-status-api -n andes-cargo --replicas=2
kubectl get deployment andes-cargo-status-api -n andes-cargo
What to expect (literal, executed — the manual change does apply, for an instant):
deployment.apps/andes-cargo-status-api scaled
NAME READY UP-TO-DATE AVAILABLE AGE
andes-cargo-status-api 2/5 5 2 79m
Kubernetes obeyed the manual command without protest — kubectl scale is a valid operation against any Deployment, ArgoCD has no way to "block" it the moment it happens. What happens next is the part that demonstrates selfHeal:
# waiting, without running anything else...
kubectl get deployment andes-cargo-status-api -n andes-cargo
What to expect (literal, executed — about eight seconds later, with no one fixing anything by hand):
NAME READY UP-TO-DATE AVAILABLE AGE
andes-cargo-status-api 5/5 5 5 79m
Back to 5 replicas, on its own. The namespace's own event log confirms the exact sequence:
kubectl get events -n andes-cargo --sort-by=.lastTimestamp
... Normal ScalingReplicaSet deployment/andes-cargo-status-api Scaled down replica set andes-cargo-status-api-548966dd97 from 5 to 2
... Normal ScalingReplicaSet deployment/andes-cargo-status-api Scaled up replica set andes-cargo-status-api-548966dd97 from 2 to 5
Two events, in that order: first the manual command (Scaled down ... from 5 to 2), then ArgoCD's correction (Scaled up ... from 2 to 5) — with no kubectl apply involved in the correction. argocd app history confirms it too, recording no new deployment tied to this event — because, from Git's point of view, nothing changed; the Deployment simply went back to matching what it already declared.
WHAT selfHeal: true JUST DID
Git says: replicas = 5 Someone runs: ArgoCD detects
(source of truth, unchanged) kubectl scale --replicas=2 the difference
(the cluster now says 2) and fixes it, alone
│ │ │
└────────────── continuous comparison (lesson 2) ───────────┘
│
▼
The cluster says 5 again,
with no one running
kubectl apply or git push
Common mistakes
Assuming syncPolicy.automated means "ArgoCD never lets me touch the cluster by hand" (expectation, corrected by this lesson's own evidence). What happens: someone thinks kubectl scale directly against a resource ArgoCD manages is going to fail or be blocked. How to spot it: this lesson's evidence contradicts it — the manual command did apply, for about eight seconds. How to fix it: selfHeal doesn't prevent the manual change the moment it happens — Kubernetes still accepts any valid command against any resource. What it does is revert it on the next comparison, which in this lab took about eight seconds. A very brief manual change (faster than ArgoCD's comparison cycle) would technically take effect for an instant — but it doesn't last.
Confusing prune: true with "deletes everything it doesn't recognize" (scope, matters for shared namespaces). What happens: someone worries prune: true is going to delete resources from other teams sharing the same namespace. How to spot it: if you're not clear on exactly which resources fall within the scope of "what Git declares" for this Application. How to fix it: prune only acts on resources ArgoCD already manages — the ones created as part of an earlier sync of this same Application, identified by an internal label ArgoCD adds. A resource another team created by hand, never going through this Application, never falls within prune's scope, no matter what namespace it lives in.
Thinking Application lives in the same namespace it manages, by analogy with other resources (conceptual, already explained above but easy to forget). What happens: someone looks for the Application with kubectl get application -n andes-cargo and finds nothing. How to spot it: the command returns an empty list or an error. How to fix it: Application lives in argocd (ArgoCD's namespace), not in the namespace it declares as destination — kubectl get application -n argocd is the correct command.
Exercises
Exercise 1 — Rewrite syncPolicy for manual sync, unassisted. Starting from this lesson's complete YAML, write the version of syncPolicy that would leave ArgoCD reporting differences without applying them automatically.
See solution
syncPolicy:
syncOptions:
- CreateNamespace=false
It's enough to remove the entire automated block (prune/selfHeal) — without it, ArgoCD keeps comparing (the loop never stops) but leaves Sync Status at OutOfSync when it finds a difference, instead of fixing it on its own. Someone would have to explicitly run argocd app sync andes-cargo-status-api to apply the change.
Exercise 2 — Explain why Application lives in argocd, in your own words. Without copying this lesson's text, explain to a colleague why the Application that manages andes-cargo resources doesn't itself live in the andes-cargo namespace.
See solution
A reasonable explanation: "A Kubernetes object's namespace normally reflects who manages it, not necessarily what it describes. The Application is a control object that belongs to ArgoCD — it lives alongside the rest of ArgoCD's objects (argocd), even though what it describes (a Deployment, a Service) lives in a completely different namespace (andes-cargo). It's similar to how a remote control doesn't live inside the TV it controls."
Exercise 3 — Predict the outcome if prune were false in Module 4, lesson 8's scenario. Recall Module 4's checklist: nine manifests, including two NetworkPolicy. If prune: false (instead of true) and someone deleted networkpolicy-default-deny.yaml from the repository, with git push, what would you expect to see on the cluster?
See solution
The default-deny-ingress NetworkPolicy would keep existing on the cluster, unchanged — with prune: false, ArgoCD never deletes a resource that no longer appears in Git, it only applies what's still there. Sync Status would probably show a partial difference (the file disappeared from Git, but the resource is still on the cluster), with ArgoCD never resolving it on its own — exactly why this lab uses prune: true: a Git repository that's genuinely the single source of truth needs deleting a file to have the same effect as deleting the resource.
Summary and next step
This lesson dissected the complete Application you're going to apply in lesson 6: source (where the desired state comes from — Gitea's repository, main branch, repository root), destination (where it gets applied — the cluster itself, andes-cargo namespace), and syncPolicy (how it syncs — automated, with prune and selfHeal active). You confirmed, with real, not promised, evidence, that selfHeal: true fixes a manual change in seconds, with no one running any corrective command — the GitOps property cicd-and-gitops-on-aws-guide's drift.yml never actively implemented.
Before moving on you should be able to: explain every top-level field of an Application unassisted; distinguish manual sync from automated sync with a YAML example; and predict what prune: true does versus prune: false when a file gets deleted from the repository.
Next lesson: hands-on, syncing andes-cargo-status-api from Git. There you apply, yourself, exactly this Application — and watch, step by step, the whole cluster's first real convergence.
Resources
- Argo CD — Application Specification — complete official reference for every
specfield, including ones this lesson didn't cover because they're outside this lab's scope. - Argo CD — Sync Options — documentation for
CreateNamespaceand the rest ofsyncOptions' options. - Argo CD — Automated Sync Policy — official documentation for
pruneandselfHeal, the source for this lesson's explanation. cicd-and-gitops-on-aws-guide(NIEVA), Module 7, lesson 3 — the originalApplicationYAML, shown without running, that this lesson finally applies for real.