Software Engineering WikiSE Wiki

Argo CD

Diagnose Argo CD Applications that will not sync or stay healthy, and control drift, sync order, pruning and multi-tenant access.

Reviewed MarkdownEdit

On this page

Cheatsheet#

Commands target Argo CD 3.x. The argocd CLI needs argocd login first; the kubectl forms work with cluster access alone.

TaskCommand
Application state and conditionsargocd app get my-app
Live vs desired, field by fieldargocd app diff my-app
Sync nowargocd app sync my-app
Preview a syncargocd app sync my-app --dry-run
Sync one resourceargocd app sync my-app --resource apps:Deployment:my-app
Re-read Git and re-renderargocd app get my-app --hard-refresh
Wait until synced and healthyargocd app wait my-app --sync --health --timeout 600
Stop a running syncargocd app terminate-op my-app
History and roll backargocd app history my-app, then argocd app rollback my-app <id>
What Argo CD renderedargocd app manifests my-app --source git
What exists in the clusterargocd app manifests my-app --source live
Delete resources removed from Gitargocd app sync my-app --prune
Application YAMLkubectl get application my-app -n argocd -o yaml
Controller logskubectl logs -n argocd statefulset/argocd-application-controller
Render errorskubectl logs -n argocd deploy/argocd-repo-server --tail 100

An Application that will not sync#

Argo CD compares two states: desired (the manifests rendered from Git) and live (what the cluster has). Sync status (Synced, OutOfSync) is that comparison. Health (Healthy, Progressing, Degraded, Suspended, Missing, Unknown) is a separate assessment of the live objects only. An Application can be Synced and Degraded, or OutOfSync and Healthy.

argocd app get my-app                                   # sync, health, conditions, last operation
argocd app diff my-app                                  # exact fields that differ; exit 1 when there is a diff
argocd app get my-app --hard-refresh                    # bypass the rendered-manifest cache
kubectl get application my-app -n argocd -o jsonpath='{.status.conditions}' | jq
kubectl get application my-app -n argocd -o jsonpath='{.status.operationState.message}{"\n"}'
kubectl logs -n argocd deploy/argocd-repo-server --tail 100
SymptomCauseCheck or fix
ComparisonError conditionRendering failed: bad path, missing values file, Helm or Kustomize errorRead the condition message; it quotes the tool’s stderr
Unknown sync status, no resourcesRepo server cannot fetch the repository: credentials, host key, networkargocd repo list, repo-server logs
OutOfSync again straight after a syncA controller or webhook mutates the object after applyargocd app diff, then ignoreDifferences (Diffing and drift)
Sync finished, app stays ProgressingHealth check waiting for rollout, a LoadBalancer address, or a custom health scriptargocd app get my-app shows which resource is Progressing
Sync stuck RunningA hook Job never completes, or a wave never becomes healthyargocd app get my-app, then argocd app terminate-op my-app
metadata.annotations: Too longClient-side apply stores the whole object in an annotation; large CRDs exceed 262144 bytesAdd the ServerSideApply=true sync option
Resource SharedResourceWarningTwo Applications manage the same objectRemove it from one source
Deleted objects come backDeleted by hand and restored by self-heal or the next syncDelete from Git and sync with prune
Nothing happens on pushNo webhook; polling runs every 120s plus up to 60s jitterConfigure the Git webhook, or --refresh
permission denied on syncAppProject forbids the repository, destination or kindargocd proj get <project>
Rollback refusedAutomated sync is enabled on the ApplicationDisable auto-sync first, or revert in Git

Note

The application controller is a StatefulSet in current manifests. If yours runs as a Deployment, use deploy/argocd-application-controller.

Application spec#

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: my-app
  namespace: argocd
  finalizers:
    - resources-finalizer.argocd.argoproj.io      # deleting the Application deletes its resources
spec:
  project: platform
  source:
    repoURL: https://github.com/example/infra.git
    targetRevision: v1.4.2                         # branch, tag or commit SHA
    path: clusters/prod/my-app
    helm:
      valueFiles: [values.yaml, values.prod.yaml]
      parameters:
        - { name: image.tag, value: "1.4.2" }
  destination:
    server: https://kubernetes.default.svc
    namespace: my-namespace
  syncPolicy:
    automated:
      prune: true                                  # delete objects removed from Git
      selfHeal: true                               # revert manual changes in the cluster
      allowEmpty: false                            # refuse to sync to zero resources
    syncOptions:
      - CreateNamespace=true
      - ServerSideApply=true
      - PruneLast=true                             # prune after all other resources are healthy
    retry:
      limit: 5
      backoff: { duration: 15s, factor: 2, maxDuration: 5m }
  revisionHistoryLimit: 10

Pin targetRevision to a tag or SHA for production. With main, the next merge deploys itself, which is either the point of GitOps or an incident, depending on the repository.

selfHeal: true reverts a manual kubectl edit within about 5 seconds of the controller noticing it. Check for it before debugging why a change keeps vanishing. Without the resources-finalizer, deleting the Application leaves its resources running and unmanaged.

Argo CD 3.0 changed defaults that affect upgrades from 2.x: resource tracking uses the argocd.argoproj.io/tracking-id annotation instead of a label, status fields are ignored in diffs for all resources, and RBAC update and delete on an Application no longer extend to its resources. See the 3.0 upgrade notes.

Diffing and drift#

Argo CD diffs rendered manifests against live objects after normalisation. Fields written by other controllers show as permanent drift unless excluded.

spec:
  ignoreDifferences:
    - group: apps
      kind: Deployment
      jsonPointers: ["/spec/replicas"]             # an HPA owns this
    - group: ""
      kind: Secret
      name: db
      jqPathExpressions: ['.data["ca.crt"]']
    - group: admissionregistration.k8s.io
      kind: MutatingWebhookConfiguration
      managedFieldsManagers: [cert-manager-cainjector]   # ignore fields this manager owns
  syncPolicy:
    syncOptions:
      - RespectIgnoreDifferences=true              # also leave those fields alone during sync

ignoreDifferences only changes the comparison by default. During sync the full desired manifest is still applied, so an HPA’s replica count is reset on every sync. RespectIgnoreDifferences=true stops that for resources that already exist.

ServerSideApply=true hands field ownership to the API server, which is the cleaner fix when several controllers legitimately write to one object and when manifests are too large for client-side apply.

argocd app diff my-app --local ./clusters/prod/my-app   # compare a working copy against live
argocd app manifests my-app --source git                # what Argo CD rendered
argocd app manifests my-app --source live               # what exists now

App of apps and ApplicationSets#

An Application whose source directory contains more Application manifests bootstraps a whole cluster from one object. An ApplicationSet generates Applications from a template and a generator, which avoids writing one file per cluster or per team.

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata: { name: tenants, namespace: argocd }
spec:
  goTemplate: true
  goTemplateOptions: ["missingkey=error"]
  generators:
    - git:
        repoURL: https://github.com/example/infra.git
        revision: main
        directories: [{ path: tenants/* }]
  template:
    metadata: { name: '{{.path.basename}}' }
    spec:
      project: tenants
      source: { repoURL: https://github.com/example/infra.git, targetRevision: main, path: '{{.path.path}}' }
      destination: { server: https://kubernetes.default.svc, namespace: '{{.path.basename}}' }
      syncPolicy: { automated: { prune: true, selfHeal: true } }
  syncPolicy:
    applicationsSync: create-update                  # never delete generated Applications
    preserveResourcesOnDeletion: true                # keep workloads if an Application is deleted

Generators include git (directories or files), cluster, list, matrix, merge, scmProvider and pullRequest. By default (sync policy) a directory removed from Git deletes its Application, and with the resources finalizer, the tenant’s workloads. applicationsSync: create-update prevents that while a pattern is being trialled. The controller’s --policy flag overrides this field unless policy override is enabled.

Sync phases, waves and hooks#

A sync runs in phases: PreSync, Sync, PostSync, with SyncFail on failure. Within a phase, resources are ordered by wave (lowest first, default 0), then by kind (namespaces and CRDs before other resources, custom resources last), then by name. Argo CD applies a wave, waits until it is healthy, pauses 2 seconds (ARGOCD_SYNC_WAVE_DELAY), then moves to the next. Pruning runs in reverse wave order.

metadata:
  annotations:
    argocd.argoproj.io/sync-wave: "-1"             # before wave 0
metadata:
  annotations:
    argocd.argoproj.io/hook: PreSync               # PreSync, Sync, PostSync, SyncFail, Skip
    argocd.argoproj.io/hook-delete-policy: HookSucceeded

A PreSync Job that never completes blocks the sync indefinitely. Give hook Jobs backoffLimit and activeDeadlineSeconds. Hooks do not run during a selective sync (--resource). A PreSync hook runs before the Application’s own ConfigMaps and Secrets exist, so give those an earlier wave and make them hooks too if the Job needs them.

Projects and access#

An AppProject restricts which repositories, destination clusters and namespaces, and resource kinds its Applications may use. It is the boundary that stops a team’s Application from creating a ClusterRoleBinding.

apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata: { name: tenants, namespace: argocd }
spec:
  sourceRepos: ["https://github.com/example/infra.git"]
  destinations:
    - { server: https://kubernetes.default.svc, namespace: "tenant-*" }
  clusterResourceWhitelist: []                      # no cluster-scoped objects
  namespaceResourceBlacklist:
    - { group: "", kind: ResourceQuota }

The default project allows everything. Do not put tenant Applications in it.

Health checks and custom resources#

Built-in health checks cover Deployments, StatefulSets, DaemonSets, ReplicaSets, Services, Ingresses, PVCs, Jobs, CronJobs, Pods and HPAs, plus a bundled library of Lua checks for common CRDs. A custom resource with no health check contributes no health status, so a broken CR can leave an Application Healthy. The Application’s health is the worst health of its direct resources.

# argocd-cm ConfigMap; key format resource.customizations.health.<group>_<kind>
resource.customizations.health.example.com_Database: |
  hs = {}
  if obj.status ~= nil and obj.status.phase == "Ready" then
    hs.status = "Healthy"
    hs.message = "ready"
  else
    hs.status = "Progressing"
    hs.message = "waiting for status.phase=Ready"
  end
  return hs

ApplicationSet generators#

Each generator produces a list of parameter sets; the template is rendered once per set. Combining generators with matrix (cartesian product) and merge (join on a key) covers the usual “every app on every cluster” and “per-cluster overrides” cases without a second tool.

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata: { name: platform-addons, namespace: argocd }
spec:
  goTemplate: true
  goTemplateOptions: ["missingkey=error"]
  generators:
    - matrix:
        generators:
          - clusters:                                   # one set per registered cluster with these labels
              selector:
                matchLabels: { env: prod }
              values:
                region: '{{index .metadata.labels "topology.kubernetes.io/region"}}'
          - list:
              elements:
                - { addon: cert-manager, chart: cert-manager, version: v1.18.2 }
                - { addon: external-dns, chart: external-dns, version: 1.19.0 }
  template:
    metadata:
      name: '{{.addon}}-{{.name}}'                      # .name is the cluster name from the clusters generator
      labels: { addon: '{{.addon}}', cluster: '{{.name}}' }
    spec:
      project: platform
      sources:
        - repoURL: https://charts.example.com
          chart: '{{.chart}}'
          targetRevision: '{{.version}}'
          helm:
            releaseName: '{{.addon}}'
            valueFiles: ['$values/addons/{{.addon}}/values.yaml', '$values/addons/{{.addon}}/{{.name}}.yaml']
            ignoreMissingValueFiles: true
        - repoURL: https://github.com/example/infra.git
          targetRevision: main
          ref: values
      destination: { server: '{{.server}}', namespace: '{{.addon}}' }
      syncPolicy:
        automated: { prune: true, selfHeal: true }
        syncOptions: [CreateNamespace=true, ServerSideApply=true]
  templatePatch: |                                     # per-set YAML merged over the template (2.10+)
    {{- if eq .addon "cert-manager" }}
    spec:
      syncPolicy:
        syncOptions: [CreateNamespace=true, ServerSideApply=true, Replace=true]
    {{- end }}
GeneratorParameters producedTypical use
listElements written inlineSmall fixed sets, bootstrapping
clustersEvery cluster Secret matching a selector: name, server, metadata.labels, metadata.annotationsFleet-wide add-ons
git directoriesOne set per matching directory: path.path, path.basename, path.segmentsTenant or app folders
git filesOne set per matching file, with the file’s YAML or JSON keys as parametersconfig.json per environment
matrixCartesian product of two generatorsApps x clusters
mergeJoin of generators on mergeKeys, later generators override earlierBase list plus per-cluster overrides
scmProvider, pullRequestRepositories or open PRs from GitHub, GitLab, Gitea, Bitbucket, Azure DevOpsReview environments
pluginOutput of an HTTP pluginInventory from an external system
argocd appset list
argocd appset get platform-addons                      # generated Applications and conditions
argocd appset generate platform-addons.yaml            # render the Applications locally without creating anything (2.13+)
kubectl get applicationset platform-addons -n argocd -o jsonpath='{.status.conditions}' | jq
kubectl logs -n argocd deploy/argocd-applicationset-controller --tail 100 | grep -i error

The clusters generator only sees clusters registered as Secrets with the argocd.argoproj.io/secret-type: cluster label. The in-cluster destination has no such Secret by default, so it is skipped until one is created for https://kubernetes.default.svc with the labels the selector expects. Progressive syncs (spec.strategy.type: RollingSync) sync generated Applications in labelled steps and remain gated behind the ARGOCD_APPLICATIONSET_CONTROLLER_ENABLE_PROGRESSIVE_SYNCS environment variable.

App of apps in practice#

The bootstrap Application points at a directory of Application (or ApplicationSet) manifests. Deleting the root with the finalizer cascades through every child; leave the finalizer off the root and on the children so a mistake removes the pointers, not the workloads.

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: root
  namespace: argocd                       # no resources-finalizer here
spec:
  project: default                        # the root creates Applications, which are cluster-scoped from Argo CD's point of view
  source:
    repoURL: https://github.com/example/infra.git
    targetRevision: main
    path: clusters/prod/apps              # contains one Application manifest per component
  destination: { server: https://kubernetes.default.svc, namespace: argocd }
  syncPolicy:
    automated: { prune: true, selfHeal: true }
    syncOptions: [ApplyOutOfSyncOnly=true]
kubectl apply -n argocd -f clusters/prod/apps/root.yaml   # the only manual apply a cluster should ever need
argocd app sync root && argocd app wait root --health --timeout 300
argocd app list -o name | xargs -n1 -P4 argocd app wait --sync --health --timeout 900   # then every child

Give children a sync-wave when order matters (CRD-providing operators at -2, cluster add-ons at -1, workloads at 0), since the root syncs its children in wave order and waits for each Application’s health. The root itself must be allowed to create Application resources in the argocd namespace, which the default project permits and a restricted project must list under namespaceResourceWhitelist. Anything in the root’s directory that is not an Application is applied too, which is a convenient place for AppProjects.

Resource-level sync options#

Annotations on individual manifests override the Application’s sync behaviour for that object.

metadata:
  annotations:
    argocd.argoproj.io/sync-options: Prune=false                 # never prune this object (PVCs, namespaces)
    argocd.argoproj.io/sync-options: Delete=false                # keep it when the Application is deleted
    argocd.argoproj.io/sync-options: Replace=true                # kubectl replace instead of apply (immutable fields, large objects)
    argocd.argoproj.io/sync-options: Force=true,Replace=true     # delete and recreate; causes downtime
    argocd.argoproj.io/sync-options: SkipDryRunOnMissingResource=true   # CR whose CRD arrives in the same sync
    argocd.argoproj.io/sync-options: Validate=false              # skip kubectl schema validation
    argocd.argoproj.io/compare-options: IgnoreExtraneous         # exists in the cluster, not in Git, do not report OutOfSync
    argocd.argoproj.io/hook: PostSync
    argocd.argoproj.io/hook-delete-policy: BeforeHookCreation    # delete the previous hook run before creating the new one (default)

Several options go in one annotation separated by commas; repeating the key in YAML keeps only the last. Replace=true on a Deployment resets status and can trigger a rollout; Force=true deletes first and is for objects with immutable fields such as Jobs and some StatefulSet changes. IgnoreExtraneous is for objects other tools create inside a namespace Argo CD manages.

RBAC#

Access is decided by Casbin policies in the argocd-rbac-cm ConfigMap plus per-project roles in AppProjects. Users come from the local accounts in argocd-cm or from SSO, whose group claims map to roles through g lines. policy.default applies to every authenticated user that no policy matches; set it to role:readonly or '' (nothing) rather than leaving role:admin.

# argocd-rbac-cm
data:
  policy.default: role:readonly
  scopes: '[groups, email]'                         # which OIDC claims the g lines match against
  policy.csv: |
    p, role:deployer, applications, get,    platform/*, allow
    p, role:deployer, applications, sync,   platform/*, allow
    p, role:deployer, applications, action/apps/Deployment/restart, platform/*, allow
    p, role:deployer, logs, get, platform/*, allow
    p, role:deployer, exec, create, platform/*, deny
    p, role:tenant-a, applications, *, tenant-a/*, allow
    p, role:tenant-a, applications, delete/*/Pod/*/*, tenant-a/*, allow   # 3.0+: sub-resource permissions, here pod deletion only
    p, role:tenant-a, repositories, get, https://github.com/example/tenant-a.git, allow
    g, my-org:platform-team, role:deployer
    g, my-org:tenant-a-devs, role:tenant-a
    g, alice@example.com, role:admin
argocd account can-i sync applications 'platform/my-app'        # what the logged-in identity may do
argocd account can-i delete applications 'tenant-a/*' --as-user bob   # impersonate (needs admin)
argocd admin settings rbac validate --policy-file policy.csv     # syntax check before applying
argocd admin settings rbac can my-org:tenant-a-devs get applications 'tenant-a/web' --policy-file policy.csv --default-role role:readonly
argocd proj role create tenants ci-deployer                       # project-scoped role for automation
argocd proj role add-policy tenants ci-deployer -a sync -p allow -o '*'
argocd proj role create-token tenants ci-deployer -e 720h         # prints a JWT for that role; store it in the CI secret store
argocd proj role list-tokens tenants ci-deployer

Objects in policy.csv are <project>/<application>; a policy on */* spans projects. Application names are cluster-wide, so a tenant with create on tenant-a/* can still collide with names elsewhere unless AppProjects also constrain sourceNamespaces. The exec resource gates argocd app exec/terminal in the UI and should stay denied for anything but break-glass roles. Project role tokens are the right credential for CI: they can only touch the one project and expire.

CLI operations#

argocd login argocd.example.com --sso --grpc-web          # SSO through the browser; --grpc-web when behind a proxy that lacks HTTP/2
argocd login argocd.example.com --username admin --password "$ARGOCD_PASSWORD"   # local account; prefer --sso or a project token
argocd login argocd.example.com --auth-token "$ARGOCD_TOKEN"   # CI with a project role token; ARGOCD_AUTH_TOKEN and ARGOCD_SERVER also work
argocd context                                             # switch between servers
argocd version --short

argocd app create my-app --repo https://github.com/example/infra.git --path clusters/prod/my-app --dest-server https://kubernetes.default.svc --dest-namespace my-namespace --project platform --sync-policy automated --self-heal --auto-prune
argocd app set my-app --revision v1.5.0                    # change targetRevision; use with disabled auto-sync for a controlled rollout
argocd app set my-app --helm-set image.tag=1.5.0 --parameter foo=bar
argocd app unset my-app --parameter image.tag
argocd app patch my-app --type merge --patch '{"spec":{"syncPolicy":null}}'   # disable automated sync
argocd app sync my-app --async                             # return immediately; use app wait afterwards
argocd app sync my-app --force --replace                   # replace instead of apply; downtime for some kinds
argocd app sync my-app --apply-out-of-sync-only --prune
argocd app sync -l team=payments --retry-limit 3           # by label, several apps
argocd app wait my-app --operation                         # only until the current operation finishes
argocd app resources my-app                                # every managed object with sync and health status
argocd app resources my-app --orphaned                     # objects in the namespace that nothing manages (project must enable orphanedResources)
argocd app logs my-app --kind Deployment --name my-app -f --tail 100
argocd app actions list my-app --kind Deployment
argocd app actions run my-app restart --kind Deployment --resource-name my-app   # rollout restart without kubectl access
argocd app delete-resource my-app --kind Pod --resource-name my-app-7c9f-abcde   # delete one live object (RBAC: delete/*/Pod/*/*)
argocd app history my-app && argocd app rollback my-app 12
argocd app delete my-app --cascade=false                   # remove the Application, keep the workloads

argocd cluster add my-context --name prod --label env=prod  # creates a ServiceAccount and Secret; the cluster must be in the kubeconfig
argocd cluster list
argocd repo add https://github.com/example/infra.git --username git --password "$GIT_TOKEN"
argocd repo add git@github.com:example/infra.git --ssh-private-key-path ~/.ssh/argocd_deploy
argocd repocreds add https://github.com/example/ --username git --password "$GIT_TOKEN"   # credential template for a whole org
argocd proj list && argocd proj get platform
argocd admin export > argocd-backup.yaml                   # Applications, AppProjects, repos, clusters, settings (contains secrets)
argocd admin import - < argocd-backup.yaml
argocd admin app get-reconcile-results --refresh --l team=payments reconcile.yaml   # reconcile matching apps offline; writes sync and health per app to the file (note the double-dash --l)

argocd app set writes the Application spec, so the change is visible in Git-managed clusters as drift on the Application object itself if the Application is managed by an app-of-apps; use it for break-glass and put the real change in Git. argocd cluster add needs cluster-admin on the target and installs an argocd-manager ServiceAccount with cluster-admin; for a least-privilege registration, create the ServiceAccount and Secret by hand with a narrower ClusterRole.

Troubleshooting#

SymptomCauseCheck or fix
ApplicationSet creates no ApplicationsGenerator matched nothing: cluster selector, git path glob, or goTemplate render errorkubectl get applicationset X -o jsonpath='{.status.conditions}'; controller logs
ErrApplicationNotAllowedToUseProject on generated appsTemplate’s project does not permit the destination or repositoryargocd proj get; add the destination and repo
ApplicationSet deleted its Applications and their workloadsDirectory removed or generator changed with applicationsSync: sync and finalizers on childrenSet applicationsSync: create-update or preserveResourcesOnDeletion: true for tenant sets
templatePatch ignoredgoTemplate: false, or patch YAML invalidgoTemplate: true; argocd appset generate to test
Hook Job fails every second sync with AlreadyExistshook-delete-policy missing and generateName not usedBeforeHookCreation (default in recent versions) or HookSucceeded; or use generateName
error validating data ... unknown field on syncCRD not installed yet, or schema changedSkipDryRunOnMissingResource=true on the CR, CRDs in an earlier wave; or Validate=false
the object has been modified; please apply your changesAnother controller updating the object during applyRetry; ServerSideApply=true for shared objects
PermissionDenied for a user who is in the right groupscopes does not include the claim, or the group name differs (case, prefix)argocd account get-user-info shows the groups Argo CD sees
Project role token rejectedToken expired, role was recreated (new JWT issued-at), or its policy lacks the actionargocd proj role get PROJECT ROLE lists policies and token IDs; issue a new token
argocd login fails with transport: authentication handshake failedIngress terminates TLS and does not forward gRPC--grpc-web, or configure the ingress for gRPC
Controller CPU high, syncs slowToo many Applications per controller shard, or a huge clusterargocd_app_reconcile histogram on the controller’s metrics port (8082); increase controller replicas and set ARGOCD_CONTROLLER_REPLICAS for sharding
argocd app diff exit 1 in CI but the UI shows Synceddiff reports changes from local rendering or ignoreDifferences not applied to --localUse argocd app diff --server-side-generate, and compare --revision instead of --local
Application stuck DeletingFinalizer waiting on a resource that cannot be deleted (finalizers on a PVC or CR, missing CRD)kubectl get <kind> -n ns -o jsonpath='{.metadata.finalizers}'; remove those first, then the Application’s finalizer as a last resort

Oneliners#

# Every Application that is not synced or not healthy
argocd app list -o json | jq -r '.[] | select(.status.sync.status!="Synced" or .status.health.status!="Healthy") | [.metadata.name, .status.sync.status, .status.health.status] | @tsv'

# The same without the CLI
kubectl get applications -n argocd -o custom-columns='NAME:.metadata.name,SYNC:.status.sync.status,HEALTH:.status.health.status'

# Which commit each Application is running
kubectl get applications -n argocd -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.sync.revision}{"\n"}{end}'

# Sync every Application in a project, four at a time
argocd app list -p platform -o name | xargs -n1 -P4 argocd app sync

# Applications with automated sync off
kubectl get applications -n argocd -o json | jq -r '.items[] | select(.spec.syncPolicy.automated==null) | .metadata.name'

# Hard refresh every Application after a repository credential change
kubectl get applications -n argocd -o name | xargs -I{} kubectl annotate {} -n argocd argocd.argoproj.io/refresh=hard --overwrite

# Follow the controller for one Application
kubectl logs -n argocd statefulset/argocd-application-controller -f | grep -E 'my-app|error'

# Initial admin password (prints a credential; rotate it and delete the Secret after first login)
kubectl get secret argocd-initial-admin-secret -n argocd -o jsonpath='{.data.password}' | base64 -d

# Applications by project with sync and health, as a table
argocd app list -o json | jq -r '.[] | [.spec.project, .metadata.name, .status.sync.status, .status.health.status, .spec.source.targetRevision // .spec.sources[0].targetRevision] | @tsv' | column -t

# Applications whose last operation failed, with the message
kubectl get applications -A -o json | jq -r '.items[] | select(.status.operationState.phase == "Failed" or .status.operationState.phase == "Error") | "\(.metadata.name)\t\(.status.operationState.message | .[0:120])"'

# Applications that have not been reconciled in the last 10 minutes (controller stuck or sharded away)
kubectl get applications -n argocd -o json | jq -r --arg t "$(date -u -d '10 minutes ago' +%FT%TZ)" '.items[] | select(.status.reconciledAt < $t) | "\(.metadata.name) \(.status.reconciledAt)"'

# Which resources inside an Application are OutOfSync
kubectl get application my-app -n argocd -o json | jq -r '.status.resources[] | select(.status == "OutOfSync") | "\(.kind)/\(.name) \(.namespace // "-")"'

# Which resources are unhealthy, with the health message
kubectl get application my-app -n argocd -o json | jq -r '.status.resources[] | select(.health.status != null and .health.status != "Healthy") | "\(.kind)/\(.name): \(.health.status) \(.health.message // "")"'

# Applications pointing at a branch instead of a tag or SHA
kubectl get applications -A -o json | jq -r '.items[] | select((.spec.source.targetRevision // .spec.sources[0].targetRevision) | test("^(main|master|develop|HEAD)$")) | .metadata.name'

# Applications without selfHeal, or without prune
kubectl get applications -A -o json | jq -r '.items[] | select(.spec.syncPolicy.automated.selfHeal != true) | .metadata.name'

# Applications in the default project (should be none in a multi-tenant install)
kubectl get applications -A -o json | jq -r '.items[] | select(.spec.project == "default") | .metadata.name'

# Disable automated sync on every Application in a project before maintenance (re-enable from Git afterwards)
argocd app list -p platform -o name | xargs -n1 -I{} argocd app patch {} --type merge --patch '{"spec":{"syncPolicy":{"automated":null}}}'

# Refresh (soft) every Application, staggered so the repo server is not flooded
kubectl get applications -n argocd -o name | while read -r a; do kubectl annotate "$a" -n argocd argocd.argoproj.io/refresh=normal --overwrite; sleep 0.5; done

# Sync all OutOfSync Applications in a project and wait for them
argocd app list -p platform -o json | jq -r '.[] | select(.status.sync.status == "OutOfSync") | .metadata.name' | xargs -n1 -P4 -I{} sh -c 'argocd app sync {} --async && argocd app wait {} --sync --health --timeout 600'

# Diff what a Git revision would deploy against live, without touching the Application
argocd app diff my-app --revision feature-branch

# Render the manifests for a revision to a file for review
argocd app manifests my-app --revision v1.5.0 > /tmp/my-app-v1.5.0.yaml

# Applications and the clusters they target, counted per cluster
kubectl get applications -A -o json | jq -r '.items[].spec.destination | .name // .server' | sort | uniq -c | sort -rn

# Registered clusters with connection state
argocd cluster list -o json | jq -r '.[] | [.name, .server, .connectionState.status, (.info.serverVersion // "-")] | @tsv'

# Repositories whose connection failed
argocd repo list -o json | jq -r '.[] | select(.connectionState.status != "Successful") | "\(.repo) \(.connectionState.message)"'

# Who can sync what: dump the effective RBAC policy
kubectl get cm argocd-rbac-cm -n argocd -o jsonpath='{.data.policy\.csv}'

# Groups Argo CD sees for the logged-in user (SSO debugging)
argocd account get-user-info

# Roll a Deployment inside an Application without kubectl access to the cluster
argocd app actions run my-app restart --kind Deployment --resource-name my-app

# Terminate every stuck operation in a project
argocd app list -p platform -o json | jq -r '.[] | select(.status.operationState.phase == "Running") | .metadata.name' | xargs -n1 argocd app terminate-op

# Server-side rendered diff, matching what the controller computes
argocd app diff my-app --server-side-generate

# Recent sync events across all Applications
kubectl get events -n argocd --field-selector reason=OperationCompleted --sort-by=.lastTimestamp | tail -20

Scripts#

Fleet status report: every Application with sync, health, revision, age of the last reconciliation and the last operation’s result, sorted with the broken ones first. Read-only; uses kubectl only, so it works without argocd login.

#!/usr/bin/env bash
# usage: argocd-report.sh [namespace]   default argocd
set -euo pipefail

ns=${1:-argocd}
now=$(date +%s)
kubectl get applications -n "$ns" -o json | jq -r --argjson now "$now" '
  .items[]
  | {name: .metadata.name,
     project: .spec.project,
     sync: (.status.sync.status // "Unknown"),
     health: (.status.health.status // "Unknown"),
     rev: ((.status.sync.revision // "-") | .[0:8]),
     age_m: (if .status.reconciledAt then (($now - (.status.reconciledAt | sub("\\.[0-9]+"; "") | fromdate)) / 60 | floor) else -1 end),
     op: (.status.operationState.phase // "-")}
  | [(if .sync != "Synced" or .health != "Healthy" or (.op | IN("Failed", "Error")) then 0 else 1 end), .project, .name, .sync, .health, .rev, (.age_m | tostring) + "m", .op]
  | @tsv' \
| sort -k1,1n -k2,2 -k3,3 | cut -f2- \
| { printf 'PROJECT\tNAME\tSYNC\tHEALTH\tREVISION\tRECONCILED\tLAST-OP\n'; cat; } | column -t -s $'\t'

bad=$(kubectl get applications -n "$ns" -o json | jq '[.items[] | select((.status.sync.status // "") != "Synced" or (.status.health.status // "") != "Healthy")] | length')
printf '\n%s Applications need attention\n' "$bad"
(( bad == 0 ))

Controlled release: pin an Application to a new Git revision with automated sync disabled, sync it, wait for health, and restore the previous revision on failure. Suited to a CI job that holds a project role token.

#!/usr/bin/env bash
# usage: argocd-release.sh APP REVISION   (ARGOCD_SERVER and ARGOCD_AUTH_TOKEN in the environment)
set -euo pipefail

app=${1:?application} rev=${2:?revision}
timeout=${TIMEOUT:-600}
prev=$(argocd app get "$app" -o json | jq -r '.spec.source.targetRevision')
auto=$(argocd app get "$app" -o json | jq -c '.spec.syncPolicy.automated // null')

restore() {
  printf 'restoring %s to %s\n' "$app" "$prev" >&2
  argocd app set "$app" --revision "$prev"
  argocd app sync "$app" --async >/dev/null || true
  if [[ $auto != null ]]; then argocd app patch "$app" --type merge --patch "{\"spec\":{\"syncPolicy\":{\"automated\":$auto}}}" >/dev/null; fi
}

[[ $auto != null ]] && argocd app patch "$app" --type merge --patch '{"spec":{"syncPolicy":{"automated":null}}}' >/dev/null
argocd app set "$app" --revision "$rev"
argocd app diff "$app" --revision "$rev" || true          # exit 1 when there is a diff; shown for the log
if argocd app sync "$app" --timeout "$timeout" --retry-limit 2 && argocd app wait "$app" --sync --health --timeout "$timeout"; then
  [[ $auto != null ]] && argocd app patch "$app" --type merge --patch "{\"spec\":{\"syncPolicy\":{\"automated\":$auto}}}" >/dev/null
  printf 'released %s at %s\n' "$app" "$rev"
  exit 0
fi
argocd app get "$app" | sed -n '/^GROUP/,$p' >&2
restore
exit 1

Orphan and drift audit: for every Application, list live resources that are OutOfSync and, where the project enables orphaned resource monitoring, objects in the namespace that no Application manages. Read-only.

#!/usr/bin/env bash
# usage: argocd-audit.sh [project]
set -euo pipefail

proj=${1:-}
args=(-o name); [[ -n $proj ]] && args+=(-p "$proj")
rc=0
for app in $(argocd app list "${args[@]}"); do
  app=${app#argocd/}
  oos=$(argocd app resources "$app" 2>/dev/null | awk 'NR > 1 && $5 == "OutOfSync" {print $2 "/" $4}' | paste -sd, -)
  orphans=$(argocd app resources "$app" --orphaned 2>/dev/null | awk 'NR > 1 {print $2 "/" $4}' | paste -sd, -)
  if [[ -n $oos || -n $orphans ]]; then
    printf '%s\n' "$app"
    [[ -n $oos ]] && printf '  out of sync: %s\n' "$oos"
    [[ -n $orphans ]] && printf '  orphaned:    %s\n' "$orphans"
    rc=1
  fi
done
exit "$rc"

Removing the resources finalizer

kubectl patch application my-app -n argocd --type json -p '[{"op":"remove","path":"/metadata/finalizers"}]' lets a stuck Application delete without cascading. Its resources stay in the cluster, unmanaged. Use it only when the cascade itself is failing and you will clean up the resources yourself.

Further reading#