3. The Application resource
Argo CD · 3. The Application resourceEverything Argo CD does is described by one custom resource. Learning it properly is most of learning Argo CD.
The whole thing
Section titled “The whole thing”apiVersion: argoproj.io/v1alpha1kind: Applicationmetadata: name: checkout namespace: argocd # MUST be Argo CD's namespace finalizers: - resources-finalizer.argocd.argoproj.io # cascade deletespec: project: shop # an AppProject — access boundary
source: repoURL: https://github.com/acme/manifests.git targetRevision: main path: apps/checkout
destination: server: https://kubernetes.default.svc # or: name: in-cluster namespace: shop
syncPolicy: automated: prune: true selfHeal: true allowEmpty: false syncOptions: - CreateNamespace=true - PruneLast=true retry: limit: 5 backoff: duration: 5s factor: 2 maxDuration: 3mThat is a complete, production-shaped Application. Every field below is one of those, explained.
metadata.namespace — a real constraint
Section titled “metadata.namespace — a real constraint”An Application must live in Argo CD’s own namespace (usually argocd) unless you have explicitly enabled apps-in-any-namespace. This surprises people who expect to put the Application next to the workload it deploys.
The reason is authorisation: the Application resource is the permission to deploy. If anyone could create one in their own namespace, anyone with write access to a namespace could deploy anything anywhere. Keeping them in one namespace makes Kubernetes RBAC on that namespace the outer security boundary, with Argo CD’s own project RBAC inside it.
Apps-in-any-namespace exists for large multi-tenant installations and requires you to allow specific namespaces explicitly. Chapter 10 covers when it is worth the complexity.
source
Section titled “source”Where the manifests come from.
source: repoURL: https://github.com/acme/manifests.git targetRevision: main path: apps/checkoutrepoURL — git over HTTPS or SSH, or a Helm repository URL (https://charts.example.com), or an OCI registry (oci://ghcr.io/acme/charts). Credentials are configured separately as Secrets; chapter 9.
targetRevision is more flexible than it looks and the choice has real consequences:
| Value | Behaviour | Verdict |
|---|---|---|
main, develop |
tracks the branch head | common; fine for non-production |
v1.4.2 (tag) |
pinned to a tag | good, unless the tag is moved |
9f8c3ab... (SHA) |
fully immutable | the safest thing you can do |
HEAD |
default branch | works; be explicit instead |
1.2.* (Helm) |
semver range | resolves on every refresh — the manifest can change with no commit |
That last row deserves emphasis. Helm’s chapter 6 makes the same argument about Chart.lock. A Helm semver range means your desired state can change without anyone changing anything. A new chart version is published, Argo CD resolves the range differently, and auto-sync deploys it. That is convenient in a sandbox and a genuinely bad idea in production. Pin the version.
path is a directory within the repository. Argo CD renders everything under it recursively (for plain YAML, directory.recurse: true controls this).
Multiple sources
Section titled “Multiple sources”An Application can have several sources — commonly a public Helm chart plus your own values file:
sources: - repoURL: https://charts.bitnami.com/bitnami chart: redis targetRevision: 20.1.0 helm: valueFiles: - $values/envs/prod/redis-values.yaml - repoURL: https://github.com/acme/manifests.git targetRevision: main ref: values # referenced as $values aboveThis solves a real problem — keeping values in your own repository while consuming an upstream chart — without vendoring the chart or maintaining a wrapper chart. It is one of the more useful features and one of the least known.
destination
Section titled “destination”Where the manifests go.
destination: server: https://kubernetes.default.svc namespace: shopserver is the cluster’s API URL as registered with Argo CD; https://kubernetes.default.svc is the special value meaning the cluster Argo CD runs in. name is an alternative and is generally better — it survives an API endpoint changing, which does happen.
Use one or the other. Setting both is an error.
namespace is the default namespace for resources that do not specify one. It does not override resources that do. This catches people constantly:
destination.namespace: shop
a manifest with NO namespace → created in shop a manifest with namespace: ops → created in OPSIf you want to force everything into one namespace, that is Kustomize’s job (namespace: in kustomization.yaml), not Argo CD’s.
Cluster-scoped resources — ClusterRoles, CRDs, namespaces — ignore this field entirely and are governed by the AppProject’s clusterResourceWhitelist.
syncPolicy
Section titled “syncPolicy”The field that decides whether Argo CD acts or merely reports.
Manual (the default)
Section titled “Manual (the default)”With no automated block, Argo CD detects drift and shows OutOfSync, and does nothing until a human clicks Sync or runs argocd app sync. This is the right starting point for production while you build trust.
Automated
Section titled “Automated”syncPolicy: automated: prune: true selfHeal: true allowEmpty: falseThree independent switches, and each one changes behaviour meaningfully.
prune — delete resources that were removed from git. Off by default, which surprises people: without it, deleting a manifest from git leaves the resource running forever. Argo CD reports it as out of sync and refuses to remove it, on the grounds that deleting things automatically is how you lose a PersistentVolumeClaim. Turn it on deliberately, and read chapter 7 before you do.
selfHeal — revert manual changes to the live cluster. Off by default. Without it, someone’s kubectl edit is reported and left alone; with it, the change is undone within minutes.
This is the switch that decides what GitOps means at your organisation. On, git is genuinely the only way to change the cluster. Off, git is a strong default that people can override — which is sometimes what you want during an incident, and chapter 14 covers the specific failure of self-heal fighting an emergency fix.
allowEmpty — permit a sync that results in zero resources. Off by default, and leave it off. It exists to stop a mis-rendered chart (a values file that failed to load, a bad if block) from deleting an entire application. The default has saved a lot of people.
syncOptions
Section titled “syncOptions”The list of behavioural tweaks. The ones that matter:
| Option | Effect |
|---|---|
CreateNamespace=true |
create the destination namespace if missing |
PruneLast=true |
delete removed resources after everything else syncs |
ApplyOutOfSyncOnly=true |
only send changed resources — big speedup on large apps |
ServerSideApply=true |
use server-side apply — fixes large CRDs; chapter 6 |
Validate=false |
skip kubectl schema validation |
RespectIgnoreDifferences=true |
do not sync fields listed in ignoreDifferences |
Replace=true |
kubectl replace instead of apply — last resort |
FailOnSharedResource=true |
refuse if another Application already manages a resource |
PruneLast=true is worth turning on by default. Without it, deletions can happen before the replacements are ready, producing an unnecessary gap in service.
ApplyOutOfSyncOnly=true is the single easiest performance win on an Application with hundreds of resources.
retry: limit: 5 backoff: { duration: 5s, factor: 2, maxDuration: 3m }Without this, a failed sync stays failed until something changes. With it, transient failures — a webhook not yet ready, an admission controller restarting, a CRD racing its controller — resolve themselves. Set it. Five attempts with exponential backoff is a sensible default.
ignoreDifferences
Section titled “ignoreDifferences”The escape hatch for fields that legitimately change outside git:
spec: ignoreDifferences: - group: apps kind: Deployment jsonPointers: - /spec/replicas # HPA owns this - group: "" kind: Service jqPathExpressions: - '.spec.ports[] | select(.name == "http") | .nodePort' - group: apps kind: Deployment managedFieldsManagers: - kube-controller-managerThe canonical case is the first one: an HPA changes spec.replicas, git says 3, and the Application shows OutOfSync forever. Worse, with self-heal on, Argo CD and the HPA fight each other in a loop. Ignoring the field fixes it properly.
managedFieldsManagers is the newer and often better approach — instead of naming fields, name the controller allowed to own them, and Argo CD uses server-side apply’s field ownership to work out the rest.
Use this sparingly. Every ignored field is a place where git no longer describes reality, which is the property the whole tool exists to provide. Chapter 6 covers the difference between a legitimate ignore and papering over a real problem.
The finalizer
Section titled “The finalizer”metadata: finalizers: - resources-finalizer.argocd.argoproj.ioWithout it, deleting an Application deletes only the Application — the workloads keep running, unmanaged and unnoticed. With it, deletion cascades to everything the Application created.
Both behaviours are legitimate and the failure mode of each is real: without the finalizer you accumulate orphaned resources nobody knows about; with it, kubectl delete application checkout deletes production. Chapter 14 revisits this, because it is one of the sharper edges in Argo CD.
How Argo CD knows what it owns
Section titled “How Argo CD knows what it owns”This is not obvious and it explains several confusing situations.
Argo CD marks every resource it creates with a tracking annotation or label:
# default: labelmetadata: labels: app.kubernetes.io/instance: checkout
# recommended: annotationmetadata: annotations: argocd.argoproj.io/tracking-id: checkout:apps/Deployment:shop/checkoutThe default label-based tracking has two real problems. The label is only 63 characters, so long Application names are truncated and can collide. And app.kubernetes.io/instance is a standard Kubernetes label that Helm charts also set — so a chart setting it can make Argo CD believe a resource belongs to a different Application entirely.
Switch to annotation-based tracking:
# argocd-cm ConfigMapdata: application.resourceTrackingMethod: annotationThe annotation encodes the full group, kind, namespace and name, so it cannot collide or be truncated. It is not the default only for backwards compatibility. On a new installation, set it on day one; on an existing one, changing it causes one large resync and is still worth doing.
Status: what to read
Section titled “Status: what to read”argocd app get checkoutName: checkoutProject: shopServer: https://kubernetes.default.svcNamespace: shopRepo: https://github.com/acme/manifests.gitTarget: mainPath: apps/checkoutSyncWindow: Sync AllowedSync Policy: Automated (Prune, SelfHeal)Sync Status: Synced to main (9f8c3ab)Health Status: HealthySync Status and Health Status are independent, and this is the single most important thing to internalise about Argo CD:
Synced + Healthy → everything is as it should be Synced + Degraded → git was applied, and the result is broken (bad image, failing probe — Argo CD did its job; your manifest is wrong) OutOfSync + Healthy → running fine, but not what git says (drift, or a sync not yet run) OutOfSync + Degraded → broken and not what git says Synced + Progressing→ applied, still rolling out“Synced” does not mean “working”. It means the cluster matches git. If git describes something broken, Argo CD will faithfully deliver something broken and report Synced. Chapters 6 and 8 take each half of this apart.
What to take from this chapter
Section titled “What to take from this chapter”- An Application must live in Argo CD’s namespace — that namespace’s RBAC is the outer security boundary.
targetRevision: a commit SHA is safest; a Helm semver range means your desired state can change with no commit and does not belong in production.- Multiple sources let you use an upstream chart with your own values file, without vendoring or a wrapper chart.
destination.namespaceis a default, not an override — manifests with their own namespace win. Use Kustomize to force one.pruneandselfHealare both off by default. Prune off means deleted manifests leave running resources; self-heal off meanskubectl editis reported but not reverted.allowEmptyoff protects you from a mis-rendered chart deleting everything — leave it off.- Turn on
PruneLast=truegenerally andApplyOutOfSyncOnly=trueon large Applications. Setretry— transient sync failures are normal. ignoreDifferencesstops Argo CD fighting an HPA overspec.replicas. Use it sparingly; every entry is a place git no longer describes reality.- The finalizer decides whether deleting an Application deletes its workloads. Both behaviours bite; pick deliberately.
- Switch resource tracking to
annotation. The default label is truncated at 63 characters and collides with a standard Helm label. - Sync status and health are independent. “Synced” means the cluster matches git — not that anything works.
Next: manifest generation — Helm, Kustomize, plugins, and what actually happens inside the repo server.