CoursesArgo CDThe Application resource

The Application resource

Source, destination, and sync.

Advanced12 min · lesson 2 of 12

A work order tacked to a job board names three things: the warehouse to pull parts from, the loading dock to deliver them to, and the manager who signed off. An Argo CD Application is that work order. Argo CD is the tool that keeps a Kubernetes cluster in step with configuration files stored in Git. Kubernetes is the system that runs your containers and picks which machine each one lands on. Git is the version-control system that records every change to a set of files. The Application is the record that ties one folder in a Git repository to one spot in your cluster, and almost everything Argo CD does hangs off it. Drift detection, health readouts, auto-healing: all of it reads this one object. So the three fields to learn first map straight onto that work order: where the files come from (source), where they land (destination), and who fenced in what the job may touch (project). Writing the order does not run it. That takes one more action, the first sync, and that is where this lesson ends up.

The Work Order Lives Where Argo CD Lives

An Application is a Kubernetes custom resource (a new object type that someone taught the cluster to understand, on top of the built-in ones like Pod and Service). Its kind is Application, and its apiVersion is argoproj.io/v1alpha1. That string is a group plus a version: argoproj.io is the API group (the family name Kubernetes uses to bundle related object types) and v1alpha1 is the version of that type's schema. Here is the part that catches people. The Application object lives in the namespace where Argo CD itself runs, conventionally one called argocd. A namespace is a named partition inside the cluster, the way a shared office fridge has a labeled shelf per team so nobody's lunch collides. The workloads the Application deploys usually land in a different namespace entirely. So the record and the things it manages sit in two separate places, and forgetting that leads to a specific class of mistakes we hit at the end.

application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: guestbook
namespace: argocd # where Argo CD runs, NOT where the app deploys
spec:
project: default # the AppProject that fences in what this app may do
source:
repoURL: https://github.com/argoproj/argocd-example-apps.git
targetRevision: HEAD # a branch, a tag, or a commit SHA
path: guestbook # folder in the repo that holds the manifests
destination:
server: https://kubernetes.default.svc # the in-cluster API endpoint
namespace: guestbook # target namespace for the workloads

Three fields carry the weight. spec.project names an AppProject, the manager who signed off: an Argo CD object that fences in what a group of Applications is allowed to touch (which repos, which clusters, which namespaces). spec.source points at the Git content. spec.destination says where the rendered result goes. And because the Application is itself a chunk of YAML (a plain-text format for structured configuration), you can commit it to Git alongside your app, so even the wiring is version-controlled. Hold that thought; it changes how you create and how you delete these objects.

Source: Pin Exactly What Ships

spec.source has three pieces. repoURL is the repository address. path is the folder inside it, relative to the repo root, and for this lesson it holds plain manifests (the YAML files that describe Kubernetes objects). Sources that render through Helm or Kustomize (two tools that build manifests from templates or layered overlays instead of storing them as-is) are their own lesson. targetRevision decides which version of that folder Argo reads. Set it to HEAD (Git's pointer to the newest commit on the default branch) and Argo tracks whatever lands on top of that branch. Set it to a tag or a commit SHA (the unique fingerprint Git stamps on every commit) and Argo reads exactly that frozen point, nothing else.

HEAD Is a Moving Target an Attacker Can Push To
targetRevision: HEAD means the live cluster follows the branch tip. Anyone who can merge to that branch can change what runs in production, and with auto-sync on they do not even need cluster access to do it: a pushed commit becomes a deployed workload. For anything you care about, pin a tag or a full commit SHA so a random or malicious push cannot move a running cluster out from under you, and lean on the AppProject to restrict which repos and paths an app may pull from. When you work an incident, the Sync Revision on the Application is the exact commit that shipped.

Destination: Which Cluster, Which Namespace

spec.destination names a cluster in one of two ways. server is the raw API endpoint URL; the special value https://kubernetes.default.svc means the same cluster Argo CD is running in, reached from inside it. name is a friendly label Argo assigns when you register an external cluster with argocd cluster add. Use server for the in-cluster shortcut, and switch to name once you run more than one cluster, because prod-eu-1 is easier to read and harder to fumble than a long API URL. Either way, namespace sets where the workloads land, which, again, is not the argocd namespace the Application lives in.

How one Application maps a repo folder to a cluster namespace
1Application object
the work order, in the argocd namespace
2Argo CD controller
reads the order, starts the pull
3Git repo + path
spec.source at the pinned revision
4Rendered manifests
plain YAML from that path
5Cluster + namespace
spec.destination, where workloads land

Two Ways to Create the Object

You can hand the fields to the command-line tool and let it build the object, or you can write the YAML yourself and apply it. The imperative form (you list the pieces on the command line and the tool assembles the object) is quick for a throwaway: argocd app create builds the same Application under the hood.

terminal
# imperative: Argo builds the Application object from these flags
argocd app create guestbook \
--repo https://github.com/argoproj/argocd-example-apps.git \
--path guestbook \
--dest-server https://kubernetes.default.svc \
--dest-namespace guestbook \
--project default
# to target a registered cluster by its friendly name instead of a URL:
# argocd app create guestbook --dest-name prod-eu-1 ...
output
application 'guestbook' created

For anything that outlives a demo, write the manifest and apply it with kubectl. This declarative form is the GitOps way (running your cluster from files kept in Git, so the repository stays the single source of truth), because the Application itself now lives in a commit you can review, revert, and audit. The wiring becomes data, not a command you typed once and forgot.

terminal
# declarative: commit application.yaml, then apply it to the argocd namespace
kubectl apply -n argocd -f application.yaml
output
application.argoproj.io/guestbook created

Creating Is Not Deploying

Creating the Application registers intent. It does not apply anything to the destination on its own. With no sync policy set (automatic sync is a later lesson), the app is born OutOfSync and waits for you to push the first apply. Read its state before you touch anything with argocd app get.

terminal
argocd app get guestbook
output
Name: argocd/guestbook
Project: default
Server: https://kubernetes.default.svc
Namespace: guestbook
URL: https://argocd.example.com/applications/guestbook
Source:
- Repo: https://github.com/argoproj/argocd-example-apps.git
Target: HEAD
Path: guestbook
SyncWindow: Sync Allowed
Sync Policy: <none>
Sync Status: OutOfSync from HEAD (53e28ff)
Health Status: Missing
GROUP KIND NAMESPACE NAME STATUS HEALTH HOOK MESSAGE
Service guestbook guestbook-ui OutOfSync Missing
apps Deployment guestbook guestbook-ui OutOfSync Missing

OutOfSync means the cluster does not yet match the source. Missing health means the objects named in the manifests do not exist in the cluster at all, which is exactly right for an app that has never been synced. Now push the opening deploy. argocd app sync tells the controller (the background loop that keeps comparing your declared intent against the live cluster and nudges reality toward it) to render the source and apply it to the destination.

terminal
argocd app sync guestbook
output
TIMESTAMP GROUP KIND NAMESPACE NAME STATUS HEALTH HOOK MESSAGE
2026-07-20T10:14:02Z Service guestbook guestbook-ui OutOfSync Missing
2026-07-20T10:14:02Z apps Deployment guestbook guestbook-ui OutOfSync Missing
2026-07-20T10:14:05Z Service guestbook guestbook-ui Synced Healthy
2026-07-20T10:14:05Z apps Deployment guestbook guestbook-ui Synced Healthy
Name: argocd/guestbook
Sync Status: Synced to HEAD (53e28ff)
Health Status: Healthy
Operation: Sync
Sync Revision: 53e28ff20cc530b9ada2173fbbd64d48338583ba
Phase: Succeeded
Start: 2026-07-20 10:14:02 +0000 UTC
Finished: 2026-07-20 10:14:05 +0000 UTC
Duration: 3s
Message: successfully synced (all tasks run)
GROUP KIND NAMESPACE NAME STATUS HEALTH HOOK MESSAGE
Service guestbook guestbook-ui Synced Healthy service/guestbook-ui created
apps Deployment guestbook guestbook-ui Synced Healthy deployment.apps/guestbook-ui created

The sync ran to Succeeded, and the two objects flipped to Synced and Healthy. The Sync Revision in that output is the commit that actually shipped; keep it, because during an incident it is the fastest honest answer to what is running right now. Confirm from the Kubernetes side, and notice that the query for the Application runs against the argocd namespace while the Pods it created live in guestbook.

terminal
kubectl get applications -n argocd
kubectl get pods -n guestbook
output
NAME SYNC STATUS HEALTH STATUS
guestbook Synced Healthy
NAME READY STATUS RESTARTS AGE
guestbook-ui-6b689df5cd-4n2xq 1/1 Running 0 41s

Deletion Is Where People Get Burned

Deleting an Application asks a question Kubernetes answers with a finalizer. A finalizer works like the confirmation an uninstaller shows before it also wipes your saved files: it is a tag on an object that says run this cleanup before you actually remove me. Add resources-finalizer.argocd.argoproj.io to the Application's metadata.finalizers, and deleting the Application performs a cascading delete: Argo tears down every workload it deployed. Leave the finalizer off, and deleting the Application removes only the record; the workloads keep running with nothing reconciling them, orphaned.

app-finalizer.yaml
metadata:
name: guestbook
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io # delete the Application -> delete its workloads
The Finalizer Cuts Both Ways, and the Namespace Hides the Blade
With the finalizer, kubectl delete application guestbook -n argocd wipes out every workload the app deployed. Good for a clean teardown, a disaster if you thought you were only telling Argo to stop watching. Without the finalizer, the opposite bites: the delete returns instantly and leaves a pile of unmanaged resources running. Decide per app, on purpose, and never rehearse deletion against a shared namespace. One more trap: the Application lives in argocd, so a kubectl delete that omits -n argocd targets your default namespace, finds nothing, and looks like it did nothing while you assume the app is gone.
terminal
# wrong namespace: no -n argocd, so kubectl looks in the default namespace
kubectl delete application guestbook
output
Error from server (NotFound): applications.argoproj.io "guestbook" not found
Quick check
01You delete an Application that has no finalizer with kubectl delete application guestbook -n argocd. What happens to the Deployment and Service it had synced into the guestbook namespace?
Incorrect — That is the finalizer's job. Without resources-finalizer.argocd.argoproj.io, the managed resources are not cleaned up.
Correct — No finalizer means only the Application record is removed; the workloads are orphaned and drift is no longer corrected.
Incorrect — Sync status does not block deletion of the Application object.
Incorrect — The Application is gone, so the controller has nothing left to reconcile them against.
02The lesson warns that spec.source.targetRevision: HEAD is 'a moving target an attacker can push to.' Why, and what does it recommend instead?
Incorrect — HEAD is not pinned; it always points at the newest commit on the default branch.
Correct — With auto-sync on, a pushed commit becomes a deployed workload, so pin an immutable revision for anything you care about.
Incorrect — targetRevision selects a Git revision; it has nothing to do with Helm versus Kustomize rendering.
Incorrect — targetRevision does not affect AppProject scoping; those are separate controls.
03Right after argocd app create on an app with no sync policy, argocd app get shows Sync Status: OutOfSync and Health Status: Missing. What does this mean and what is the next step?
Incorrect — Create succeeded; the Application object exists, but nothing has been applied to the destination yet.
Incorrect — Missing only means the declared objects do not exist in the cluster yet, not that they are invalid.
Correct — Creating the Application does not apply anything on its own; the first sync renders the source and applies it.
Incorrect — Missing health means the workloads were never created, so there is nothing live that could have drifted.

Before you ever run a delete on a shared cluster, print the object first: kubectl get application guestbook -n argocd -o yaml and read the finalizers list. If resources-finalizer.argocd.argoproj.io is there, deleting the Application takes its workloads with it; if it is not, you will be cleaning up orphans by hand. Check the list, then decide.

Try this

Run kubectl apply -n argocd -f application.yaml on a scratch host or disposable cluster and read the output against what this lesson described. Then change one input so it fails, and re-run: the error you get is the one you will meet in production.

Takeaway

The trap worth remembering here: hEAD Is a Moving Target an Attacker Can Push To. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.

Related