ArgoCD GitOps Deployment Error: Sync Failures and Resource Drift
Debugging ArgoCD sync failures caused by resource drift, permission issues, and incorrect application definitions in GitOps workflows.
Introduction
GitOps with ArgoCD is an elegant concept: your Git repository is the single source of truth for your Kubernetes deployments, and ArgoCD ensures that the cluster state matches what is defined in Git. Push a commit, and the deployment updates automatically. It sounds perfect, but in practice, there are many ways the sync can fail or produce unexpected results.
I recently set up ArgoCD for a Kubernetes cluster running my trading platform microservices. The initial deployment worked, but subsequent updates were failing with cryptic sync errors. The ArgoCD UI showed the applications as "OutOfSync" but did not clearly explain what was different. After extensive debugging, I found three separate issues contributing to the sync failures.
Environment
- ArgoCD version: 2.9.3
- Kubernetes: v1.28.4 (k3s on 3 nodes)
- Git repository: Private GitHub repo with Kustomize overlays
- Applications: 8 microservices with separate ArgoCD applications
- Sync method: Automatic sync with self-heal enabled
Problem
After deploying all applications, I noticed that two of them were stuck in "OutOfSync" status:
argocd app list
# NAME STATUS HEALTH SYNCPOLICY CONDITIONS
# api-gateway Synced Healthy Auto-Create
# user-service OutOfSync Degraded Auto-Create ComparisonError
# order-service OutOfSync Healthy Auto-Create
# payment-svc Synced Healthy Auto-Create The user-service was both OutOfSync and Degraded, while order-service was OutOfSync but Healthy. I checked the details:
argocd app get user-service
# ...
# Conditions:
# - Type: ComparisonError
# Message: 'rpc error: code = Unknown desc = unknown error:
# unable to recognize "": no matches for kind "Deployment"
# in version "apps/v1"'The error mentioned a missing Deployment kind in apps/v1. This was strange because the Deployment manifest was clearly in the Git repository with the correct apiVersion.
For order-service, the situation was different:
argocd app get order-service
# ...
# Status: OutOfSync
# Diff:
# - name: IMAGE_TAG
# value: v1.2.3 (live) vs v1.3.0 (desired)The application was OutOfSync because the live deployment was running image tag v1.2.3 while Git specified v1.3.0. But automatic sync was not updating it β the sync policy was set to Auto-Create which only creates new resources, it does not update existing ones.
Analysis
Issue 1: Missing CRD for user-service
The user-service deployment used a custom resource definition (CRD) that was not installed in the cluster. The CRD defined a ServiceMonitor resource for Prometheus metrics:
# This CRD was not installed
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: user-serviceArgoCD was trying to apply the entire Kustomize output, including the ServiceMonitor, but the CRD did not exist in the cluster. The "unable to recognize" error was ArgoCD saying it could not find the ServiceMonitor resource type.
The root cause was that the Prometheus Operator CRDs were installed separately, and the user-service Kustomize overlay included a ServiceMonitor that required the CRD to be present first.
Issue 2: Sync policy mismatch for order-service
The order-service had syncPolicy.auto.create but not auto.prune or auto.selfHeal. The create policy only applies to resources that do not exist in the cluster. For existing resources, ArgoCD detects drift but does not automatically fix it unless selfHeal is enabled.
Issue 3: Resource ordering
ArgoCD applies resources in a specific order (Namespaces, Services, Deployments, etc.), but Kustomize does not guarantee that CRDs are applied before resources that depend on them.
Solution
I fixed all three issues:
1. Ensure CRDs are installed before dependent resources:
I restructured the Git repository to separate CRDs from application manifests:
gitops/
βββ base/
β βββ crds/
β β βββ service-monitor-crd.yaml
β β βββ prometheus-rules-crd.yaml
β βββ apps/
β βββ api-gateway/
β βββ user-service/
β βββ order-service/
βββ clusters/
β βββ production/
β βββ staging/
βββ argocd/
βββ root-application.yaml
βββ app-of-apps.yamlI created a root Application that deploys CRDs first:
# argocd/root-application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: cluster-resources
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/myorg/gitops.git
targetRevision: main
path: base/crds
destination:
server: https://kubernetes.default.svc
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=trueThen each application depends on the root:
# argocd/app-of-apps.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: user-service
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "1"
spec:
project: default
source:
repoURL: https://github.com/myorg/gitops.git
targetRevision: main
path: base/apps/user-service
destination:
server: https://kubernetes.default.svc
namespace: user-service
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- ServerSideApply=true2. Enable full auto-sync with self-heal:
syncPolicy:
automated:
prune: true # Remove resources not in Git
selfHeal: true # Fix drift automatically
allowEmpty: false # Prevent deleting all resources
syncOptions:
- CreateNamespace=true
- ServerSideApply=true
- ApplyOutOfSyncOnly=true
retry:
limit: 5
backoff:
duration: 5s
factor: 2
maxDuration: 3m3. Handle application hooks for initialization:
For resources that need specific ordering, I used ArgoCD hooks:
# Pre-sync hook to install CRDs
apiVersion: batch/v1
kind: Job
metadata:
name: install-crds
annotations:
argocd.argoproj.io/hook: PreSync
argocd.argoproj.io/hook-delete-policy: HookSucceeded
spec:
template:
spec:
containers:
- name: crd-installer
image: bitnami/kubectl:1.28
command:
- /bin/sh
- -c
- |
kubectl apply -f https://raw.githubusercontent.com/prometheus-operator/prometheus-operator/main/bundle.yaml
kubectl wait --for condition=Established crd/servicemonitors.monitoring.coreos.com --timeout=60s
restartPolicy: Never
backoffLimit: 3After applying the fixes:
argocd app sync cluster-resources
# Application spec is synced
argocd app sync user-service
# STATUS: Synced
# HEALTH: Healthy
argocd app sync order-service
# STATUS: Synced
# HEALTH: HealthyAll applications were now syncing correctly. I verified the sync was working with a test commit:
# Update image tag in Git
git commit -am "chore: bump user-service to v1.4.0"
git push origin main
# ArgoCD auto-syncs within 3 minutes (polling interval)
argocd app get user-service
# STATUS: Synced
# IMAGE: myregistry/user-service:v1.4.0Lessons Learned
- CRDs must be installed before dependent resources β ArgoCD cannot apply custom resources if the CRD is not in the cluster. Use PreSync hooks or a root application for CRD management.
- Enable selfHeal for GitOps β Without
selfHeal: true, ArgoCD detects drift but does not fix it. For true GitOps, you need automatic remediation. - Use sync waves for ordering β ArgoCD annotations like
argocd.argoproj.io/sync-wavecontrol the order of resource deployment. - Server-Side Apply prevents conflicts β Enable
ServerSideApplyto avoid conflicts when multiple controllers modify the same resources. - Monitor sync status β Set up ArgoCD notifications to alert when applications go OutOfSync or fail to sync.
This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.