I was debugging why a Kubernetes deployment wasn’t updating after merging a PR. The manifest was in git, but the cluster was still running the old version. Turns out ArgoCD was stuck in a sync loop. Here’s what I learned.
My GitOps workflow Link to heading
A typical ArgoCD workflow looks like this:
- Open PR with manifest changes (Helm chart, kustomize, or raw YAML)
- Merge to main
- ArgoCD detects the change (polls every 3 minutes by default)
- Auto-sync deploys to cluster (if enabled)
When auto-sync is disabled, I manually trigger syncs. This is common for production environments where you want explicit control.
Quick reference commands Link to heading
List applications:
kubectl get applications -n argocd
View application status:
kubectl get application my-app -n argocd -o yaml
Force refresh (fetch latest from git):
kubectl patch app my-app -n argocd \
-p '{"metadata": {"annotations":{"argocd.argoproj.io/refresh":"normal"}}}' \
--type merge
Get sync status as JSON:
kubectl get application my-app -n argocd -o json | \
jq '.status.resources[] | select(.name == "my-resource")'
Check what’s out of sync:
kubectl describe application my-app -n argocd | less
ArgoCD CLI (easier when you have it) Link to heading
argocd app get my-app
argocd app sync my-app
But kubectl works when you don’t have the CLI handy or when you’re debugging from a pod without the ArgoCD binary.
Handling failed syncs Link to heading
The most common sync failures I see:
1. Resource quota exceeded Link to heading
error: namespaces "my-namespace" is forbidden: exceeded quota
Fix: Increase the ResourceQuota or delete unused resources. Check what’s consuming quota:
kubectl describe resourcequota -n my-namespace
2. Immutable field changes Link to heading
error: field is immutable
Fix: Some fields can’t be changed after creation (like Service clusterIP). You need to delete and recreate:
argocd app sync my-app --force
Or manually delete the resource and let ArgoCD recreate it.
3. Webhook validation failures Link to heading
error: admission webhook denied the request
Fix: Check the webhook configuration. Sometimes webhooks are too strict or have bugs. View the full error:
kubectl get application my-app -n argocd -o yaml | grep -A 20 "status:"
4. Git repository unreachable Link to heading
error: failed to get repository
Fix: Check ArgoCD’s access to your git repo:
argocd repo list
argocd repo get https://github.com/your/repo
SSH key might have expired, or the repository moved.
My debugging workflow Link to heading
When a sync fails:
- Check the application status in the UI (I know, but it’s actually good)
- Look at the sync operation details:
argocd app get my-app - Check the actual diff:
argocd app diff my-app - Inspect the affected resources:
kubectl get/describe <resource> - Check ArgoCD controller logs:
kubectl logs -n argocd -l app.kubernetes.io/name=argocd-application-controller
The diff is crucial - it shows you exactly what ArgoCD is trying to change. Sometimes you find unexpected changes from overlapping apps or manual kubectl edits.
When to use sync vs refresh Link to heading
- Refresh: Fetches latest from git but doesn’t apply. Use this to see what would change.
- Sync: Applies the changes to the cluster.
- Hard refresh: Clears the cache and re-clones the repo. Use when git force-push happened.
# Just check for changes
argocd app get my-app --refresh
# Apply changes
argocd app sync my-app
# Nuclear option - clear cache and sync
argocd app get my-app --hard-refresh
argocd app sync my-app --force
Auto-sync philosophy Link to heading
I’m opinionated here: enable auto-sync for non-production environments, disable it for production.
Why? In dev/staging, you want fast feedback. Merge and it deploys within 3 minutes. No manual steps.
In production, you want a human to click the sync button. It’s a forcing function to:
- Verify the diff looks right
- Check if it’s a good time to deploy
- Be ready to rollback if needed
Configure it in the Application manifest:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-app
spec:
syncPolicy:
automated:
prune: true # Delete resources removed from git
selfHeal: true # Sync if someone does manual kubectl changes
syncOptions:
- CreateNamespace=true