ArgoCD GitOps Deployment Validation: Testing Sync, Health Checks, and Progressive Delivery
ArgoCD manages Kubernetes deployments from Git. When it works, it's invisible — changes merge, ArgoCD syncs, applications update. When it breaks, it can be subtle: a sync that appears healthy but isn't applying changes, a health check that's too permissive, or a rollout that pauses indefinitely without alerting anyone.
Validating ArgoCD deployments means testing more than "did the sync complete?" — it means verifying the deployed application actually works after the sync.
What ArgoCD Deployment Validation Covers
Testing an ArgoCD-managed deployment has several distinct layers:
- Manifest validation: Does the Git repository contain valid Kubernetes YAML?
- Sync status validation: Did ArgoCD sync successfully? Are all resources in sync?
- Health status validation: Are all resources healthy (not just synced)?
- Application behavior validation: Does the deployed application work correctly?
- Progressive delivery validation: Did canary/blue-green rollouts complete correctly?
Most teams only check layer 2. Layers 3-5 are where production incidents actually come from.
Layer 1: Manifest Validation Before ArgoCD Sees It
Catch manifest errors in CI before they reach ArgoCD:
# .github/workflows/validate-manifests.yml
name: Validate Kubernetes Manifests
on:
pull_request:
paths:
- 'k8s/**'
- 'charts/**'
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install kubeval
run: |
wget https://github.com/instrumenta/kubeval/releases/latest/download/kubeval-linux-amd64.tar.gz
tar xf kubeval-linux-amd64.tar.gz && sudo mv kubeval /usr/local/bin/
- name: Validate manifests
run: |
find k8s/ -name "*.yaml" -exec kubeval --strict {} \;
- name: Run kubeconform (faster, more up-to-date schemas)
uses: docker://ghcr.io/yannh/kubeconform:latest
with:
args: "-strict -summary k8s/"
- name: Run kustomize build (if using kustomize)
run: |
kubectl kustomize k8s/overlays/staging | \
kubeconform -strict -summary -
- name: Render and validate Helm charts
run: |
helm template myapp charts/myapp -f charts/myapp/ci/production-values.yaml | \
kubeconform -strict -summary -Layer 2: ArgoCD Sync Status Validation
After a deployment, verify ArgoCD reports everything in sync:
#!/bin/bash
# validate-argocd-sync.sh
APP_NAME=$1
TIMEOUT=300 # 5 minutes
echo "Waiting for ArgoCD app $APP_NAME to sync..."
# Wait for sync to complete
argocd app wait "$APP_NAME" \
--sync \
--health \
--timeout "$TIMEOUT"
if [ $? -ne 0 ]; then
echo "FAIL: ArgoCD sync timed out or failed"
argocd app get "$APP_NAME" --refresh
argocd app history "$APP_NAME"
exit 1
fi
# Check sync status explicitly
SYNC_STATUS=$(argocd app get "$APP_NAME" -o json | jq -r '.status.sync.status')
HEALTH_STATUS=$(argocd app get "$APP_NAME" -o json | jq -r '.status.health.status')
echo "Sync status: $SYNC_STATUS"
echo "Health status: $HEALTH_STATUS"
if [ "$SYNC_STATUS" != "Synced" ]; then
echo "FAIL: App is not synced (status: $SYNC_STATUS)"
exit 1
fi
if [ "$HEALTH_STATUS" != "Healthy" ]; then
echo "FAIL: App is not healthy (status: $HEALTH_STATUS)"
argocd app get "$APP_NAME" --refresh
exit 1
fi
echo "PASS: App is synced and healthy"Layer 3: Resource Health Validation
ArgoCD's "Healthy" status depends on health checks for each resource type. Understand what these checks actually verify:
- Deployment: Desired replicas == Ready replicas
- StatefulSet: Desired replicas == Ready replicas
- DaemonSet: Desired == Available across all nodes
- Service: Has endpoints (for ClusterIP/NodePort)
- Ingress: Has at least one load balancer address (for cloud providers)
- CronJob: Last schedule time within expected window
A "Healthy" deployment means pods are running — not that they're doing the right thing. Test beyond the health check:
# validate_deployment.py
from kubernetes import client, config
import sys
import time
def validate_deployment_health(namespace: str, deployment_name: str) -> bool:
config.load_kube_config()
apps_v1 = client.AppsV1Api()
dep = apps_v1.read_namespaced_deployment(deployment_name, namespace)
desired = dep.spec.replicas
ready = dep.status.ready_replicas or 0
if ready != desired:
print(f"FAIL: {desired} desired, {ready} ready")
return False
# Check for OOMKilled containers (not visible in ArgoCD health)
core_v1 = client.CoreV1Api()
pods = core_v1.list_namespaced_pod(
namespace,
label_selector=f"app={deployment_name}"
)
for pod in pods.items:
if pod.status.container_statuses:
for cs in pod.status.container_statuses:
if cs.last_state and cs.last_state.terminated:
reason = cs.last_state.terminated.reason
if reason == "OOMKilled":
print(f"WARN: Container {cs.name} was OOMKilled")
print(f"PASS: {ready}/{desired} replicas ready")
return True
if __name__ == "__main__":
success = validate_deployment_health(sys.argv[1], sys.argv[2])
sys.exit(0 if success else 1)Layer 4: Application Behavior Validation After Sync
The most important validation: does the application actually work after ArgoCD syncs?
#!/bin/bash
# post-sync-validation.sh
# Called by ArgoCD PostSync hook
SERVICE_URL="https://myapp.staging.example.com"
echo "Running post-sync behavioral validation..."
# Test 1: Health endpoint responds
STATUS=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "$SERVICE_URL/health")
if [ "$STATUS" != "200" ]; then
echo "FAIL: Health endpoint returned $STATUS (expected 200)"
exit 1
fi
echo "PASS: Health check"
# Test 2: API version matches expected
VERSION=$(curl -s "$SERVICE_URL/version" | jq -r '.version')
EXPECTED_VERSION=$(cat VERSION)
if [ "$VERSION" != "$EXPECTED_VERSION" ]; then
echo "FAIL: Version mismatch. Got $VERSION, expected $EXPECTED_VERSION"
exit 1
fi
echo "PASS: Version check ($VERSION)"
# Test 3: Database connection (via app health endpoint)
DB_STATUS=$(curl -s "$SERVICE_URL/health/ready" | jq -r '.database')
if [ "$DB_STATUS" != "ok" ]; then
echo "FAIL: Database connection is $DB_STATUS"
exit 1
fi
echo "PASS: Database connection"
echo "All post-sync validations passed."Register this as an ArgoCD PostSync hook:
# k8s/post-sync-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: post-sync-validation
annotations:
argocd.argoproj.io/hook: PostSync
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
spec:
template:
spec:
containers:
- name: validator
image: curlimages/curl:latest
command: ["/bin/sh", "/scripts/post-sync-validation.sh"]
env:
- name: SERVICE_URL
valueFrom:
configMapKeyRef:
name: app-config
key: service-url
volumeMounts:
- name: scripts
mountPath: /scripts
volumes:
- name: scripts
configMap:
name: validation-scripts
restartPolicy: NeverLayer 5: Progressive Delivery Validation With Argo Rollouts
If you use Argo Rollouts for canary or blue-green deployments, validate that the analysis step works:
# k8s/rollout.yaml
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: myapp
spec:
replicas: 4
strategy:
canary:
steps:
- setWeight: 25 # Send 25% traffic to new version
- pause: {duration: 30s}
- analysis:
templates:
- templateName: success-rate
- setWeight: 75
- pause: {duration: 30s}
- analysis:
templates:
- templateName: success-rate
canaryService: myapp-canary
stableService: myapp-stable
---
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
name: success-rate
spec:
metrics:
- name: success-rate
interval: 30s
successCondition: result[0] >= 0.99
failureLimit: 3
provider:
prometheus:
address: http://prometheus:9090
query: |
sum(rate(http_requests_total{job="myapp",status!~"5.."}[2m]))
/
sum(rate(http_requests_total{job="myapp"}[2m]))Validate rollout behavior in CI:
#!/bin/bash
# validate-rollout.sh
ROLLOUT_NAME=$1
TIMEOUT=600 # 10 minutes for full rollout
# Trigger rollout
kubectl argo rollouts set image "$ROLLOUT_NAME" "*=myapp:$NEW_VERSION"
# Watch progress
kubectl argo rollouts status "$ROLLOUT_NAME" --timeout "$TIMEOUT"
if [ $? -ne 0 ]; then
echo "FAIL: Rollout did not complete"
# Get analysis results
kubectl argo rollouts get rollout "$ROLLOUT_NAME" --watch=false
# Check for aborted rollout
STATUS=$(kubectl argo rollouts get rollout "$ROLLOUT_NAME" -o json | \
jq -r '.status.phase')
if [ "$STATUS" == "Degraded" ]; then
echo "Rollout was aborted — analysis failed"
kubectl argo rollouts get rollout "$ROLLOUT_NAME" -o json | \
jq '.status.currentStepAnalysisRunStatus'
fi
exit 1
fi
echo "PASS: Rollout completed successfully"
kubectl argo rollouts get rollout "$ROLLOUT_NAME"Testing ArgoCD App of Apps
For teams using App of Apps pattern, validate parent and child app sync:
# test_app_of_apps.py
import subprocess
import json
import sys
def get_argocd_apps(parent_app: str) -> list[dict]:
result = subprocess.run(
["argocd", "app", "list", "-o", "json", "--app-namespace", "argocd"],
capture_output=True, text=True
)
apps = json.loads(result.stdout)
# Filter apps that are children of parent_app
return [a for a in apps if
a.get("metadata", {}).get("labels", {}).get("app.kubernetes.io/part-of") == parent_app]
def validate_all_apps_healthy(parent_app: str) -> bool:
apps = get_argocd_apps(parent_app)
all_healthy = True
for app in apps:
name = app["metadata"]["name"]
sync_status = app["status"]["sync"]["status"]
health_status = app["status"]["health"]["status"]
if sync_status != "Synced" or health_status != "Healthy":
print(f"FAIL: {name} - sync={sync_status}, health={health_status}")
all_healthy = False
else:
print(f"PASS: {name} - synced and healthy")
return all_healthy
if __name__ == "__main__":
parent = sys.argv[1]
success = validate_all_apps_healthy(parent)
sys.exit(0 if success else 1)CI/CD Pipeline Integration
# .github/workflows/argocd-validation.yml
name: ArgoCD Deployment Validation
on:
push:
branches: [main]
paths:
- 'k8s/**'
- 'charts/**'
jobs:
deploy-and-validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install ArgoCD CLI
run: |
curl -sSL -o argocd https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-amd64
chmod +x argocd && sudo mv argocd /usr/local/bin/
- name: Login to ArgoCD
run: |
argocd login ${{ secrets.ARGOCD_SERVER }} \
--username admin \
--password ${{ secrets.ARGOCD_PASSWORD }} \
--insecure
- name: Trigger sync
run: |
argocd app sync myapp --prune --force
- name: Wait for sync and validate
run: bash scripts/validate-argocd-sync.sh myapp
- name: Run behavioral tests
run: |
/usr/local/bin/await 'curl -sf ${{ vars.APP_URL }}/health'
pytest tests/smoke/ --base-url=${{ vars.APP_URL }} -v
- name: Alert on failure
if: failure()
run: |
argocd app get myapp --refresh
argocd app history myappArgoCD tells you what deployed. Behavioral tests tell you what works. Both are necessary for a reliable GitOps pipeline.
HelpMeTest can run automated behavioral tests as part of your ArgoCD PostSync hooks, giving you continuous verification that every GitOps deployment produces working software. Start free →