GitOps with ArgoCD & Flux

ArgoCD Applications & Sync

18 min Lesson 4 of 30

ArgoCD Applications & Sync

An Application is ArgoCD's fundamental unit of work. It binds a Git source (a repo + path + target revision) to a Kubernetes destination (a cluster + namespace) and continuously compares what is in Git against what is running in the cluster. Understanding exactly how that comparison loop works — and how to tune it — is the difference between a GitOps workflow that teams trust and one that generates noise and manual toil.

The Application Manifest

You define an Application as a Kubernetes custom resource (kind Application, API group argoproj.io/v1alpha1) and commit it to Git, or create it via the CLI or UI. The key fields are spec.source (where the desired state lives), spec.destination (where to apply it), and spec.syncPolicy (how and when to reconcile).

# apps/my-api.yaml — committed to your GitOps repo apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: my-api namespace: argocd # ArgoCD control-plane namespace finalizers: - resources-finalizer.argocd.argoproj.io # cascade-delete on removal spec: project: default source: repoURL: https://github.com/acme/k8s-config.git targetRevision: HEAD # track main; or pin to a tag/SHA path: apps/my-api/overlays/production destination: server: https://kubernetes.default.svc # in-cluster namespace: my-api-prod syncPolicy: automated: prune: true # delete resources removed from Git selfHeal: true # revert out-of-band changes syncOptions: - CreateNamespace=true # create dest namespace if absent - PrunePropagationPolicy=foreground - ApplyOutOfSyncOnly=true retry: limit: 5 backoff: duration: 5s factor: 2 maxDuration: 3m
Core concept: ArgoCD is a level-triggered controller, not an event-driven one. It runs a reconciliation loop (default poll every 3 minutes, or instantly on a webhook trigger) that always tries to converge cluster state toward the Git state. The sync policy controls how that convergence happens — automatically or requiring human approval.

Sync Policies: Manual vs Automated

Without syncPolicy.automated, an Application stays in OutOfSync status when drift is detected, but does nothing on its own. An engineer must click "Sync" in the UI or run argocd app sync my-api. This is the right default for production environments where changes need a human gate — automated sync in prod is a team trust and maturity decision, not a technical one.

With automated enabled, ArgoCD immediately syncs whenever the live state diverges from Git. The two sub-fields that matter most:

  • prune: true — Resources that exist in the cluster but no longer exist in Git are deleted. Without this, deleted manifests leave orphaned Deployments, Services, and ConfigMaps silently running forever — a common source of security and cost surprises. Enable it; pair it with PrunePropagationPolicy=foreground so dependents are deleted before owners.
  • selfHeal: true — If someone runs kubectl apply directly on the cluster (breaking the GitOps contract), ArgoCD detects the drift within the poll interval and reverts the live state back to Git within seconds. This is what enforces the single-source-of-truth guarantee.
Production pitfall: Do not enable automated + prune on day one in production without first auditing what resources exist in the cluster versus what is tracked in Git. ArgoCD will faithfully delete everything it cannot find in the target path — including resources that belong to other systems or were created manually. Identify and codify orphans first.

Sync Waves: Ordering Deployments

Kubernetes applies all resources in a sync operation concurrently by default. That creates problems when ordering matters: a Deployment that depends on a ConfigMap being present, a Job that must complete before an application starts, or a CRD that must exist before its instances. ArgoCD solves this with sync waves.

Assign a wave number to any resource via the annotation argocd.argoproj.io/sync-wave. Resources in wave -1 are applied first, then wave 0 (the default), then wave 1, and so on. ArgoCD waits for all resources in wave N to be healthy before proceeding to wave N+1.

# 1. Namespace and RBAC first (wave -2) apiVersion: v1 kind: Namespace metadata: name: my-api-prod annotations: argocd.argoproj.io/sync-wave: "-2" --- # 2. Secrets / ConfigMaps before Pods (wave -1) apiVersion: v1 kind: ConfigMap metadata: name: my-api-config namespace: my-api-prod annotations: argocd.argoproj.io/sync-wave: "-1" data: LOG_LEVEL: "info" DB_HOST: "postgres.my-api-prod.svc.cluster.local" --- # 3. Database migration Job before the Deployment (wave 0) apiVersion: batch/v1 kind: Job metadata: name: my-api-migrate namespace: my-api-prod annotations: argocd.argoproj.io/sync-wave: "0" argocd.argoproj.io/hook: Sync argocd.argoproj.io/hook-delete-policy: BeforeHookCreation spec: template: spec: restartPolicy: Never containers: - name: migrate image: acme/my-api:1.4.2 command: ["./migrate", "--run"] --- # 4. The main Deployment comes last (wave 1) apiVersion: apps/v1 kind: Deployment metadata: name: my-api namespace: my-api-prod annotations: argocd.argoproj.io/sync-wave: "1" spec: replicas: 3 selector: matchLabels: app: my-api template: metadata: labels: app: my-api spec: containers: - name: my-api image: acme/my-api:1.4.2 ports: - containerPort: 8080
ArgoCD Sync Wave Execution Order Wave -2 Namespace RBAC healthy? ✓ proceed Wave -1 ConfigMap Secret healthy? ✓ proceed Wave 0 DB Migrate Job job complete? ✓ proceed Wave 1 Deployment Service rollout complete? ✓ synced Sync Wave Execution — ordered, health-gated
ArgoCD applies waves sequentially, gating each on health checks before advancing to the next wave.

Controlling the Sync from the CLI

The argocd CLI is essential for scripting sync operations in CI pipelines and for incident response. Key commands:

# Check the current state of an application argocd app get my-api # Trigger a manual sync (respects current syncPolicy) argocd app sync my-api # Sync and wait until the application is healthy argocd app sync my-api --timeout 180 # Force a sync even if the app is already Synced (re-applies all resources) argocd app sync my-api --force # Sync only specific resources (useful for targeted rollouts) argocd app sync my-api --resource apps:Deployment:my-api # Sync to a specific revision (pin a release without changing the Application manifest) argocd app sync my-api --revision v1.4.2 # Manually trigger a hard refresh (re-clones Git, bypasses the manifest cache) argocd app get my-api --hard-refresh # Roll back to a previous revision (picks from ArgoCD history, not Git) argocd app history my-api argocd app rollback my-api 3 # 3 is the history ID from the above command
Production practice: In CI/CD pipelines, use argocd app sync --timeout combined with argocd app wait --health as a post-deploy gate. This blocks the pipeline until the rollout reaches Healthy status — giving you a real signal that the new version is serving traffic before the pipeline marks the deploy green. Pair it with alerting on Degraded transitions in your monitoring stack.

Sync Options Worth Knowing

The spec.syncOptions list lets you tune behaviour per Application:

  • ApplyOutOfSyncOnly=true — Skips applying resources that are already in sync. Dramatically speeds up syncs for large Applications with hundreds of resources. Enable for all applications beyond ~20 resources.
  • ServerSideApply=true — Uses kubectl apply --server-side instead of client-side apply. Eliminates the "too long annotation" error on large CRDs and ConfigMaps, and correctly handles field ownership when multiple controllers manage the same resource.
  • RespectIgnoreDifferences=true — Tells the sync engine to honour the spec.ignoreDifferences rules when deciding whether to apply a resource, not just when calculating diff status. Prevents unnecessary re-applies on resources managed by admission webhooks.
  • Replace=true (per-resource annotation) — Forces a kubectl replace instead of apply for immutable fields like spec.selector on a Job. Use with care — it deletes and recreates the resource.
ignoreDifferences: Webhooks and controllers often mutate resources after apply (adding annotations, setting defaults). ArgoCD detects these changes as drift and triggers constant reconciliation. Use spec.ignoreDifferences to exclude specific JSON pointer paths — for example, the caBundle field injected by cert-manager into MutatingWebhookConfiguration objects — so ArgoCD stops reporting phantom drift on resources it does not fully own.

The Self-Heal Feedback Loop

With selfHeal: true, the reconciliation loop runs continuously. When a cluster operator runs kubectl scale deployment my-api --replicas=1 during an incident, ArgoCD detects the replica count divergence within the next poll cycle (up to 3 minutes by default, or sooner if webhook delivery is fast) and reverts the deployment back to the replica count declared in Git. This is intentional — the source of truth is Git, not the live cluster. If you need a temporary override, the correct procedure is to commit the override to Git, let ArgoCD apply it, and revert the commit when the incident is resolved. This leaves an audit trail; a bare kubectl command leaves none.

ES
Edrees Salih
1 hour ago

We are still cooking the magic in the way!