Skip to content

3. The Application resource

Everything Argo CD does is described by one custom resource. Learning it properly is most of learning Argo CD.

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: checkout
namespace: argocd # MUST be Argo CD's namespace
finalizers:
- resources-finalizer.argocd.argoproj.io # cascade delete
spec:
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: 3m

That is a complete, production-shaped Application. Every field below is one of those, explained.

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.

Where the manifests come from.

source:
repoURL: https://github.com/acme/manifests.git
targetRevision: main
path: apps/checkout

repoURL — 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).

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 above

This 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.

Where the manifests go.

destination:
server: https://kubernetes.default.svc
namespace: shop

server 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 OPS

If 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.

The field that decides whether Argo CD acts or merely reports.

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.

syncPolicy:
automated:
prune: true
selfHeal: true
allowEmpty: false

Three 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.

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.

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-manager

The 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.

metadata:
finalizers:
- resources-finalizer.argocd.argoproj.io

Without 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.

This is not obvious and it explains several confusing situations.

Argo CD marks every resource it creates with a tracking annotation or label:

# default: label
metadata:
labels:
app.kubernetes.io/instance: checkout
# recommended: annotation
metadata:
annotations:
argocd.argoproj.io/tracking-id: checkout:apps/Deployment:shop/checkout

The 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 ConfigMap
data:
application.resourceTrackingMethod: annotation

The 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.

Terminal window
argocd app get checkout
Name: checkout
Project: shop
Server: https://kubernetes.default.svc
Namespace: shop
Repo: https://github.com/acme/manifests.git
Target: main
Path: apps/checkout
SyncWindow: Sync Allowed
Sync Policy: Automated (Prune, SelfHeal)
Sync Status: Synced to main (9f8c3ab)
Health Status: Healthy

Sync 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.

  • 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.namespace is a default, not an override — manifests with their own namespace win. Use Kustomize to force one.
  • prune and selfHeal are both off by default. Prune off means deleted manifests leave running resources; self-heal off means kubectl edit is reported but not reverted. allowEmpty off protects you from a mis-rendered chart deleting everything — leave it off.
  • Turn on PruneLast=true generally and ApplyOutOfSyncOnly=true on large Applications. Set retry — transient sync failures are normal.
  • ignoreDifferences stops Argo CD fighting an HPA over spec.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.

Written by Sepuri Sai Krishna

© 2026CC BY 4.0Source