Multi-Source Applications
Why Separate Chart from Config?
The most mature GitOps pattern separates the Helm chart (owned by the application team) from the environment-specific values (owned by the platform or ops team). With a single-source Application you must store both in the same repo, which creates friction:
- Application developers can't change chart versions without access to the config repo
- Ops teams can't update environment values without touching the chart repo
- Chart publishing to an OCI registry becomes impossible if the chart and values must coexist
Multi-source Applications (Argo CD v2.6+) solve this elegantly.
Multi-Source Application Spec
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: grade-api-prod
namespace: argocd
spec:
# 'sources' replaces single 'source' for multi-source
sources:
# Source 1: Helm chart from OCI registry
- repoURL: oci://ghcr.io/your-org/charts
chart: grade-api
targetRevision: 0.2.0
helm:
releaseName: grade-api
# Source 2: Values files from separate config repo
# $values is a special ref to the second source's root
- repoURL: https://github.com/your-org/grade-api-gitops
targetRevision: HEAD
ref: values # name this source 'values' for referencing
# Back in Source 1, reference the config repo
# (This pattern requires Argo CD v2.6+)
# Use helm.valueFiles: $values/environments/production/values.yaml
# in the first source
destination:
server: https://kubernetes.default.svc
namespace: production
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
ref key names a source so it can be referenced by other sources. In Source 1's helm.valueFiles, use $values/path/to/values.yaml where $values matches the ref name you gave the config repo source. Argo CD fetches both repos and combines them during rendering.
# Complete multi-source example with ref
spec:
sources:
- repoURL: oci://ghcr.io/your-org/charts
chart: grade-api
targetRevision: 0.2.0
helm:
releaseName: grade-api
valueFiles:
- $values/environments/production/values.yaml
- $values/secrets/production-overrides.yaml
- repoURL: https://github.com/your-org/grade-api-gitops
targetRevision: main
ref: values # ← this 'values' matches $values above
Sync Windows
Sync windows control when Argo CD is allowed to sync Applications. Use them to prevent deployments during business hours, mandate change freeze periods, or restrict production deployments to specific time slots:
Allow vs Deny Windows
- Allow window: Syncs are only permitted during the window. Outside the window, auto-sync is blocked.
- Deny window: Syncs are blocked during the window. Useful for "no deployments on Friday afternoon" policies.
Window Specification
Sync windows are configured in AppProject (covered in Part 4), not directly in Applications. But you can test them via the CLI:
# AppProject with sync windows (preview — full Projects in Part 4)
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: production
namespace: argocd
spec:
syncWindows:
# Allow deployments Mon-Fri 9am-5pm UTC
- kind: allow
schedule: "0 9 * * 1-5" # cron: at 09:00, Mon-Fri
duration: 8h
applications:
- "*"
namespaces:
- production
manualSync: true # allow manual syncs outside window
# Deny deployments on Fridays 3pm-6pm (code freeze)
- kind: deny
schedule: "0 15 * * 5" # at 15:00 on Friday
duration: 3h
applications:
- "*"
# Check if sync is currently allowed for an app
argocd app get grade-api-prod | grep "Sync Window"
# SyncWindow: Sync Allowed (or "Sync Blocked")
# Force sync even when window blocks (requires --force)
argocd app sync grade-api-prod --force
Resource Hooks
Argo CD Hooks vs Helm Hooks
Both Argo CD and Helm have hooks, but they serve different purposes and are not interchangeable:
argocd.argoproj.io/hook annotation. Apply during Argo CD sync phases (PreSync, Sync, PostSync, SyncFail). Work with any source type (plain YAML, Kustomize, Helm).
Helm Hooks: Use
helm.sh/hook annotation. Apply during helm install/helm upgrade lifecycle. Only relevant when Argo CD uses a Helm source. Important: When Argo CD manages a Helm chart, Helm hooks are executed by Argo CD, not by Helm directly — so helm.sh/hook-delete-policy is ignored by Argo CD (you must use argocd.argoproj.io/hook-delete-policy instead).
PreSync Job
# Run a database migration before syncing any other resources
apiVersion: batch/v1
kind: Job
metadata:
name: grade-api-migrate-{{ now | unixEpoch }}
namespace: production
annotations:
argocd.argoproj.io/hook: PreSync
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
spec:
backoffLimit: 3
template:
spec:
restartPolicy: Never
containers:
- name: migrate
image: ghcr.io/example/grade-api:{{ .Values.image.tag }}
command: ["./migrate", "--direction=up", "--all"]
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: grade-api-db-secret
key: database-url
PostSync Job
# Send a Slack notification after successful sync
apiVersion: batch/v1
kind: Job
metadata:
name: grade-api-deploy-notify
namespace: production
annotations:
argocd.argoproj.io/hook: PostSync
argocd.argoproj.io/hook-delete-policy: HookSucceeded
spec:
template:
spec:
restartPolicy: Never
containers:
- name: notify
image: curlimages/curl:8.6.0
command:
- sh
- -c
- |
curl -X POST $SLACK_WEBHOOK_URL \
-H 'Content-Type: application/json' \
-d '{"text": "grade-api deployed to production successfully"}'
env:
- name: SLACK_WEBHOOK_URL
valueFrom:
secretKeyRef:
name: slack-webhook
key: url
# Hook phases available:
# PreSync - before any resources are applied
# Sync - during the sync (same time as other resources)
# PostSync - after all resources are Healthy
# SyncFail - only if sync fails (for alerting)
# Skip - never synced (useful for manually-managed resources)
Health Checks & Status
Built-in Health Assessors
Argo CD includes built-in health assessors for standard Kubernetes resource types:
- Deployment: Healthy when all desired replicas are available
- StatefulSet: Healthy when all replicas are updated and available
- DaemonSet: Healthy when all nodes have the desired pods ready
- Job: Healthy when complete, degraded when failed
- PVC: Healthy when Bound, degraded when Pending for too long
- Service (LoadBalancer): Healthy when external IP is assigned
- Custom CRDs: No built-in assessors — need Lua health checks
Custom Health Checks (Lua)
For CRDs (Crossplane resources, cert-manager Certificates, etc.) you write Lua scripts that Argo CD evaluates:
# argocd-cm ConfigMap — add custom health checks
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-cm
namespace: argocd
data:
# Health check for cert-manager Certificate
resource.customizations.health.cert-manager.io_Certificate: |
hs = {}
if obj.status ~= nil then
if obj.status.conditions ~= nil then
for i, condition in ipairs(obj.status.conditions) do
if condition.type == "Ready" and condition.status == "False" then
hs.status = "Degraded"
hs.message = condition.message
return hs
end
if condition.type == "Ready" and condition.status == "True" then
hs.status = "Healthy"
hs.message = "Certificate is ready"
return hs
end
end
end
end
hs.status = "Progressing"
hs.message = "Waiting for certificate"
return hs
# Health check for Crossplane Managed Resource
resource.customizations.health.database.crossplane.io_PostgreSQLInstance: |
hs = {}
if obj.status.conditions ~= nil then
for i, condition in ipairs(obj.status.conditions) do
if condition.type == "Ready" then
if condition.status == "True" then
hs.status = "Healthy"
hs.message = "Database is provisioned and ready"
return hs
else
hs.status = "Progressing"
hs.message = condition.message
return hs
end
end
end
end
hs.status = "Progressing"
hs.message = "Waiting for database to be ready"
return hs
Sync Options Deep-Dive
Sync options control low-level sync behavior. The most important ones for production:
syncPolicy:
syncOptions:
# Create the namespace if it doesn't exist (saves a kubectl create ns step)
- CreateNamespace=true
# Use Server-Side Apply instead of client-side apply
# Required for large CRDs that exceed client-side apply's 256KB limit
# Also prevents "last-applied-configuration annotation too long" errors
- ServerSideApply=true
# Validate manifests with the API server's dry-run before applying
# Catches schema validation errors early (default: true)
- Validate=true
# Control how pruned resources are deleted
# foreground: parent deleted first, then children
# background: children deleted in background (default)
# orphan: remove argocd tracking labels but don't delete
- PrunePropagationPolicy=foreground
# Only prune last sync was successful (prevents cascade deletes during failures)
- PruneLast=true
# Replace instead of patch for resources that can't be patched
# (useful for immutable fields)
- Replace=false
# Apply Out Of Sync resources only (skip Synced resources)
# Faster but may miss resources that need refresh
- ApplyOutOfSyncOnly=true
UI Walkthrough
The Argo CD UI is the primary operational interface. Key areas to understand:
argocd app diff but visual.
# Useful CLI commands that mirror UI features
# Show the diff (what's out of sync)
argocd app diff grade-api-prod
# Get detailed resource tree
argocd app resources grade-api-prod
# Show resource details (events, status)
argocd app resource grade-api-prod \
--kind Deployment \
--resource-name grade-api
# Get logs for a pod managed by the Application
argocd app logs grade-api-prod \
-c grade-api \
--tail 100
# Force hard refresh (re-fetch from Git, don't use cache)
argocd app get grade-api-prod --hard-refresh
Exercises
argocd app get shows both sources in the output.
echo "Pre-sync health check passed". Trigger a sync and watch the Job run before the Deployment is updated. Check argocd app get --watch to see the PreSync phase.
image.tag to a non-existent tag). Verify the SyncFail hook runs and the Job logs appear in the Argo CD UI.
Key Takeaways & Next Steps
- Multi-source Applications separate chart ownership from config ownership — the right model for large teams
- Sync windows enforce change freeze periods and deployment schedules declaratively
- Argo CD hooks (
argocd.argoproj.io/hook) work across all source types; Helm hooks are Helm-specific - Built-in health assessors cover standard K8s types; custom Lua scripts handle CRDs
ServerSideApply=trueis essential for CRDs and large resources- The UI's tree view and diff view are the primary operational tools for GitOps visibility
Next in This Track
In Part 3: App-of-Apps & Sync Waves, we solve the multi-Application management problem — using the App-of-Apps pattern and ApplicationSets to manage hundreds of Applications, plus sync waves for dependency ordering between Applications.