Skip to content

Fix: ArgoCD Not Working — OutOfSync, Sync Waves, RBAC, Helm/Kustomize, and Webhook Setup

FixDevs ·

Quick Answer

How to fix ArgoCD errors — application stuck OutOfSync, sync waves not respected, RBAC permission denied, Helm values not merged, ApplicationSet generator config, repo auth, and webhook not triggering.

The Error

You commit a change to your manifests but ArgoCD never picks it up:

NAME       SYNC STATUS   HEALTH STATUS
my-app     OutOfSync     Healthy

Or auto-sync is enabled but the diff stays:

spec:
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
# Still OutOfSync after 30 minutes.

Or sync waves don’t run in the order you expected:

metadata:
  annotations:
    argocd.argoproj.io/sync-wave: "1"
# Wave 1 resources start AFTER wave 5 finished.

Or you grant a user permission but they still get permission denied:

ERROR: code = PermissionDenied desc = permission denied: applications, sync, default/my-app

Why This Happens

ArgoCD compares the Git state to the cluster state and reports diffs. Most issues come from one of:

  • Sync polling vs webhook. By default ArgoCD polls Git every 3 minutes. A change you just pushed may not appear for that long. Webhooks bring it down to seconds.
  • Auto-sync conditions. Auto-sync runs only when ArgoCD’s sync controller is healthy and there are no manual --dry-run syncs in flight and the resource is sync-eligible. Resources excluded via ignoreDifferences or with a finalizer hold may stay OutOfSync forever.
  • Sync waves vs hooks. Sync waves group resources by sync-wave annotation; lower numbers go first. But hooks (PreSync/Sync/PostSync) cross-cut waves — a PreSync hook for wave 5 still runs before wave 1’s main resources.
  • RBAC is layered. Three levels combine: cluster-level (RoleBindings), project-level (AppProject), and global ArgoCD RBAC (configmap or OIDC group mapping). A “permission denied” can come from any layer.

Fix 1: Check Application Status in Detail

The CLI shows the full picture:

argocd app get my-app

Output includes:

  • Sync status with the exact resources OutOfSync.
  • Health status with messages for each resource.
  • Conditions explaining why sync isn’t progressing.

For diffs:

argocd app diff my-app

Or from the UI: Application → DIFF tab. Drift comes in three flavors:

  • Expected diffs (changes you intend to apply).
  • Drift in the cluster (manual kubectl edit, controllers mutating resources).
  • Ignored fields that ArgoCD treats as never-equal but doesn’t surface.

For the third, check spec.ignoreDifferences on the Application and resource.customizations.ignoreDifferences in the ArgoCD ConfigMap.

Pro Tip: Run argocd app sync my-app --dry-run to see what would change without actually applying. Useful for verifying a fix before clicking Sync.

Fix 2: Configure a Webhook Instead of Polling

Polling every 3 minutes is fine for batch updates. For tight feedback loops, set up a Git webhook:

# In GitHub → Settings → Webhooks:
URL:          https://argocd.example.com/api/webhook
Content-Type: application/json
Secret:       (your webhook secret)
Events:       Just the push event

In ArgoCD’s argocd-secret:

apiVersion: v1
kind: Secret
metadata:
  name: argocd-secret
  namespace: argocd
data:
  webhook.github.secret: <base64-secret>

After applying, pushes to Git trigger ArgoCD to re-evaluate immediately. The “sync” itself still respects auto-sync rules; webhooks just remove the poll delay.

Common Mistake: Setting the webhook to “Pull request” events. ArgoCD wants push events to pick up new commits to tracked branches. PR events fire on PR creation but not on the actual push that updated the branch.

Fix 3: Auto-Sync With prune and selfHeal

For full GitOps (cluster matches Git, period), enable auto-sync with both flags:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: my-app
spec:
  destination:
    namespace: my-app
    server: https://kubernetes.default.svc
  project: default
  source:
    repoURL: https://github.com/myorg/manifests
    path: apps/my-app
    targetRevision: main
  syncPolicy:
    automated:
      prune: true        # Delete resources removed from Git.
      selfHeal: true     # Revert manual changes in the cluster.
    syncOptions:
      - CreateNamespace=true
      - ApplyOutOfSyncOnly=true
      - PrunePropagationPolicy=foreground

Three syncOptions worth knowing:

  • CreateNamespace=true — ArgoCD creates destination.namespace if it doesn’t exist.
  • ApplyOutOfSyncOnly=true — only apply the resources that differ (faster, less churn).
  • PrunePropagationPolicy=foreground — wait for dependents (pods) to delete before removing parents (deployments).

Common Mistake: Setting selfHeal: true without realizing it kills any manual kubectl edit. If you scale a Deployment from the CLI, ArgoCD reverts it on the next sync. Either commit changes to Git, or set ignoreDifferences for spec.replicas on Deployments you want to scale manually:

spec:
  ignoreDifferences:
    - group: apps
      kind: Deployment
      jsonPointers:
        - /spec/replicas

Fix 4: Sync Waves and Hooks

Use sync-wave to order resources within a single sync:

# Database first:
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
  annotations:
    argocd.argoproj.io/sync-wave: "0"

# Then migrations:
apiVersion: batch/v1
kind: Job
metadata:
  name: db-migrate
  annotations:
    argocd.argoproj.io/sync-wave: "1"
    argocd.argoproj.io/hook: PreSync

# Then the app:
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
  annotations:
    argocd.argoproj.io/sync-wave: "2"

Hooks add lifecycle:

  • PreSync — runs before the main sync. Wait for completion before continuing.
  • Sync — runs as part of the regular sync.
  • PostSync — runs after the main sync, after all resources are healthy.
  • SyncFail — runs only when the sync fails (cleanup hook).

Combine hook and sync-wave to express dependencies:

metadata:
  annotations:
    argocd.argoproj.io/hook: PreSync
    argocd.argoproj.io/sync-wave: "0"
    argocd.argoproj.io/hook-delete-policy: HookSucceeded  # Clean up the Job after success

hook-delete-policy controls when ArgoCD removes the hook resource (HookSucceeded, HookFailed, BeforeHookCreation).

Fix 5: Helm Values Across Environments

For Helm charts, use valueFiles and values in the Application:

spec:
  source:
    repoURL: https://github.com/myorg/charts
    path: charts/my-app
    targetRevision: main
    helm:
      releaseName: my-app
      valueFiles:
        - values.yaml
        - values-prod.yaml
      values: |
        image:
          tag: v1.2.3

valueFiles are merged in order; later files override earlier. The inline values block is highest priority.

For multi-environment GitOps with a single source of truth, use ApplicationSets:

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: my-app
spec:
  generators:
    - list:
        elements:
          - env: dev
            url: https://dev.k8s.example.com
          - env: prod
            url: https://prod.k8s.example.com
  template:
    metadata:
      name: "my-app-{{env}}"
    spec:
      project: default
      source:
        repoURL: https://github.com/myorg/manifests
        path: apps/my-app
        targetRevision: main
        helm:
          valueFiles:
            - values.yaml
            - "values-{{env}}.yaml"
      destination:
        server: "{{url}}"
        namespace: my-app
      syncPolicy:
        automated: { prune: true, selfHeal: true }

One ApplicationSet generates one Application per environment, using template substitution.

Pro Tip: For dynamic discovery, use the git or cluster generators instead of list. cluster auto-creates Applications for every registered cluster matching a label.

Fix 6: Kustomize Overlays

For Kustomize-based projects:

spec:
  source:
    repoURL: https://github.com/myorg/manifests
    path: overlays/prod
    targetRevision: main
    kustomize:
      images:
        - my-app=registry.example.com/my-app:v1.2.3
      namePrefix: prod-
      commonLabels:
        env: prod

kustomize.images rewrites image tags without touching Git — useful for image-based deploy pipelines that update the tag via argocd app set:

argocd app set my-app --kustomize-image my-app=registry.example.com/my-app:v1.2.4

The change is persisted on the Application spec (not in Git), and the new tag is reflected at the next sync.

Note: Using argocd app set for image updates is the “imperative-on-top-of-declarative” pattern. Some teams prefer this for fast deploys; purists insist on PR-driven tag bumps. Both work.

Fix 7: RBAC Across Project and Global

ArgoCD has two RBAC layers:

Global RBAC (in argocd-rbac-cm ConfigMap):

apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-rbac-cm
  namespace: argocd
data:
  policy.default: role:readonly
  policy.csv: |
    p, role:devs, applications, sync, default/*, allow
    p, role:devs, applications, get, default/*, allow
    g, github-team:engineering, role:devs

Project RBAC (in AppProject):

apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  name: payments
  namespace: argocd
spec:
  sourceRepos: ["https://github.com/myorg/payments-manifests"]
  destinations:
    - namespace: payments
      server: https://kubernetes.default.svc
  clusterResourceWhitelist:
    - group: ""
      kind: Namespace
  namespaceResourceBlacklist:
    - group: ""
      kind: ResourceQuota
  roles:
    - name: developer
      policies:
        - p, proj:payments:developer, applications, sync, payments/*, allow
      groups:
        - github-team:payments

A user needs permissions at both layers. The Project layer scopes what their global role can touch (only Apps in payments, only sources from a specific repo).

Common Mistake: Defining a permissive global RBAC but a narrow Project. The Project wins — users get the intersection of permissions.

To debug, use argocd account can-i:

argocd account can-i sync applications payments/my-app
# yes / no

Fix 8: Repository Authentication

For private Git repos:

# HTTPS with PAT:
argocd repo add https://github.com/myorg/manifests \
  --username myorg \
  --password $GITHUB_TOKEN

# SSH:
argocd repo add [email protected]:myorg/manifests.git \
  --ssh-private-key-path ~/.ssh/id_ed25519

# GitHub App (multi-repo, scalable):
argocd repo add https://github.com/myorg \
  --github-app-id 12345 \
  --github-app-installation-id 67890 \
  --github-app-private-key-path ./private-key.pem

For multi-repo orgs, GitHub App auth is best — one credential for all repos under the org, with rate limits scaled by installation.

Repository creds can also be declared via Kubernetes Secrets with argocd.argoproj.io/secret-type: repo-creds:

apiVersion: v1
kind: Secret
metadata:
  name: github-org
  namespace: argocd
  labels:
    argocd.argoproj.io/secret-type: repo-creds
stringData:
  url: https://github.com/myorg
  githubAppID: "12345"
  githubAppInstallationID: "67890"
  githubAppPrivateKey: |
    -----BEGIN PRIVATE KEY-----
    ...

Apply this once, and any Application referencing a repo under myorg uses this credential.

Still Not Working?

A few less-obvious failures:

  • ComparisonError: rpc error: code = Unknown desc = Manifest generation error. Helm or Kustomize failed. Check argocd app get output for the underlying error — usually a missing values file or kustomization.yaml path.
  • OutOfSync for a resource with no diff visible. A controller is mutating a field ArgoCD doesn’t ignore. Add ignoreDifferences for that field, or annotate the resource with argocd.argoproj.io/compare-options: IgnoreExtraneous.
  • Sync succeeds but pods crash. Health check, not sync. Look at argocd app get → “Health” section for the failing resource. Custom health checks via resource.customizations.health.GROUP_KIND in the ArgoCD ConfigMap.
  • App stuck Progressing → Degraded loop. Resource has a lifecycle mismatch. Common culprits: an Ingress without DNS, a Service expecting a missing Endpoint.
  • InvalidSpecError after Application apply. The Application’s YAML is invalid (typo in repoURL, wrong project name, missing destination). kubectl describe application <name> -n argocd shows details.
  • Sync hangs on PreSync hook. The hook Job/Pod is stuck Pending or failing. kubectl get pod -n my-app -l job-name=db-migrate to inspect.
  • Application out of sync after every sync. ArgoCD applies what’s in Git, but a controller (HPA, Operator) immediately changes a tracked field. Add ignoreDifferences for that field.
  • Resources stuck in deletion (Terminating). Finalizers haven’t run. argocd app sync my-app --prune --force may not help — finalizers run when the owner is gone. Manually remove finalizers if you’re sure.

For related Kubernetes deployment and GitOps issues, see Kubernetes CrashLoopBackOff, Kubernetes ImagePullBackOff, Helm not working, and Terraform state lock.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles