TT Lab
Get started
Learn Learning paths Courses

GitOps and Argo CD

Application — Making the Deployment Itself an Object

Continue in TT Lab

In one sentence

ArgoCD's Application is not a deployment script but a declaration that "this path in this repository must be that namespace in that cluster", and that declaration is itself an object inside the cluster.

Why this was needed

If you build a deployment pipeline as a CI script, the deployment configuration is trapped inside the pipeline definition. To know which app goes to which namespace and who can deploy that app, you have to read the script, and that script is outside the cluster. So you cannot ask the cluster for "the list of apps deployed to this cluster".

ArgoCD removes this problem by making the deployment relationship itself a custom resource. With the single line kubectl get application -n argocd, everything comes out about what this cluster receives, from where, and under what policy. Once a deployment becomes an object, RBAC, audit logs, and even GitOps itself (the YAML that defines the Application is also committed to the repository) attach naturally.

How it works

The skeleton of an Application has four chunks.

Field What it decides
spec.source Where to read from — repoURL, path, targetRevision
spec.destination Where to put it — server (or name), namespace
spec.project Within what boundary it operates — the AppProject name
spec.syncPolicy How to reconcile — automation, pruning, retry

syncPolicy.automated has two switches. prune means "what was deleted from the repository is deleted from the cluster too", and selfHeal means "if the cluster diverges from the repository, revert it toward the repository". If you turn both off, ArgoCD becomes a dashboard that only shows the difference on the screen.

syncOptions are the details of how to apply. CreateNamespace=true creates the destination namespace for you, and ServerSideApply=true makes the server track field ownership, reducing conflicts when several controllers touch the same object. PruneLast=true makes deletion happen last after everything else is reconciled, reducing the blast radius of a deletion accident.

retry is how to retry a failure. You build exponential backoff with limit and, under backoff, duration, factor, and maxDuration. With duration: 10s and factor: 2, the gaps widen to 10 seconds, 20 seconds, and 40 seconds and stop at maxDuration. Retrying forever at the same interval turns into a load generator hammering the API server.

There are two layers of ordering control. Sync waves group resources by the number in the argocd.argoproj.io/sync-wave annotation and apply from the smallest value, and they do not move on to the next until the current wave becomes Healthy. Within the same wave, the default order by resource kind (Namespace, then ConfigMap/Secret, then RBAC, then CRD, then PV/PVC, then Service, then workloads, then Ingress) applies. On top of that, hooks divide the phases themselves — PreSync, then Sync, then PostSync, and if it fails, SyncFail runs. A hook is usually a Job, specified with the argocd.argoproj.io/hook annotation, and hook-delete-policy decides when to clean up. The default is BeforeHookCreation, so a hook resource remains even after success and is deleted just before the next sync — that is why you can look at the log of a failed migration Job after the fact, thanks to this default.

What you see in the field

First, an AppProject is a fence that sets the incident radius. With sourceRepos, destinations, clusterResourceWhitelist, and namespaceResourceBlacklist, you nail down "from which repositories, to which namespaces, and up to which kinds" deployment is allowed. If you set sourceRepos: ["*"], the point of splitting projects disappears — a single typo makes it possible to deploy someone else's repository to your own cluster.

Second, infinite synchronization. If the HPA changes spec.replicas and ArgoCD sees that as drift and reverts it, a loop arises in which the HPA changes it again and ArgoCD reverts it again. It stops only when, with ignoreDifferences, you remove /spec/replicas from the comparison. This is the place that explicitly writes down the principle that fields owned by another controller are not owned by GitOps.

Third, if you split the waves wrongly, the deployment stops right there. If you put a resource that never becomes Healthy in an early wave, the later waves never come. It is safer to use waves only for "what must exist first" and to keep their number small.

What you will do in the next lab

You apply ArgoCD's CRDs from the offline bundle to register the Application and AppProject types in the cluster, and, in the argocd namespace, write an Application web yourself. You fill in the automatic synchronization, pruning, and self-healing switches and the syncOptions and retry backoff, and attach sync waves and a PreSync hook to the repository manifests. Then you draw a boundary with an AppProject platform and make the app belong to it, and finally build a configuration report that matches the list of Applications that actually exist in the cluster. This environment has no ArgoCD controller, so an Application does not become Synced by itself — what is graded is the accuracy of the declaration.