ArgoCD & GitOps
Overview
GitOps is an operational model where Git repositories serve as the single source of truth for declarative infrastructure and applications. ArgoCD is a Kubernetes-native continuous delivery tool that implements GitOps — it continuously monitors Git repositories and ensures the cluster state matches the desired state defined in Git.
Core concepts
| Concept | Description |
|---|---|
| Application | A group of Kubernetes resources defined in a Git repo. |
| Project | A logical grouping of applications with access control policies. |
| Source | The Git repository (or Helm repo) containing manifests. |
| Destination | The target cluster + namespace where resources are deployed. |
| Sync | The process of reconciling the live cluster state with the Git state. |
| Health | Whether the application is healthy (all pods running, etc.). |
Installation
Install the ArgoCD control plane into a dedicated argocd namespace, then expose and secure the API server so you can manage applications from the CLI. Run these commands once per cluster, before creating any applications:
# Create namespace
kubectl create namespace argocd
# Install ArgoCD
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
# Expose the API server
kubectl port-forward svc/argocd-server -n argocd 8080:443
# Get initial admin password
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d
# Login via CLI
argocd login localhost:8080 --username admin --password <password>
argocd account update-password
Application management
Applications are the core ArgoCD resource — each one connects a Git source to a target cluster and namespace.
Creating an application
Declare an ArgoCD Application custom resource to connect a Git source to a destination namespace. ArgoCD watches this manifest and continuously reconciles the cluster to match Git — you apply it once and ArgoCD takes over:
# argocd-app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-app
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
repoURL: https://github.com/myorg/my-app-deploy
targetRevision: main
path: overlays/production
# For Helm:
# helm:
# valueFiles:
# - values-prod.yaml
destination:
server: https://kubernetes.default.svc
namespace: production
syncPolicy:
automated:
prune: true # Delete resources removed from Git
selfHeal: true # Auto-fix drift
allowEmpty: false
syncOptions:
- CreateNamespace=true
- PrunePropagationPolicy=foreground
retry:
limit: 5
backoff:
duration: 5s
factor: 2
maxDuration: 3m
Register the application with the cluster by applying the manifest:
kubectl apply -f argocd-app.yaml
Sync status and operations
Inspect applications and drive syncs from the CLI — whether you need to see what automated sync is doing or want manual control over a deployment. These commands cover status, sync, rollback, diff, and deletion:
# Check sync status
argocd app get my-app
argocd app list
# Manual sync
argocd app sync my-app
argocd app sync my-app --resource apps:Deployment:my-app
# Rollback
argocd app rollback my-app <revision-id>
# Diff (preview changes)
argocd app diff my-app
# Delete app (and optionally all its resources)
argocd app delete my-app
argocd app delete my-app --cascade
Health checks
ArgoCD automatically detects resource health. Customize with annotations:
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
argocd.argoproj.io/sync-wave: "5" # Deploy order (lower = first)
argocd.argoproj.io/hook: PostSync # Run after sync
App of Apps pattern
A parent application manages multiple child applications:
# parent-app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: apps
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/myorg/infra
targetRevision: main
path: argocd/apps
destination:
server: https://kubernetes.default.svc
namespace: argocd
syncPolicy:
automated:
prune: true
selfHeal: true
# apps/backend-app.yaml (in the infra repo, path: argocd/apps/)
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: backend
namespace: argocd
spec:
source:
repoURL: https://github.com/myorg/backend
targetRevision: main
path: kubernetes
destination:
namespace: backend
server: https://kubernetes.default.svc
syncPolicy:
automated: {}
ApplicationSet — multi-app automation
ApplicationSet automates creating and managing multiple Applications from a single template. Instead of manually writing an Application manifest for every environment or cluster, define a generator that produces Applications automatically.
Architecture
┌──────────────────────────────────────────────┐
│ ApplicationSet │
│ │
│ ┌──────────┐ ┌──────────────────┐ │
│ │ Generator│────▶│ Template (app) │ │
│ │ (list, │ │ ↓ ↓ ↓ │ │
│ │ git, │ │ Application A │ │
│ │ cluster,│ │ Application B │ │
│ │ matrix) │ │ Application C │ │
│ └──────────┘ └──────────────────┘ │
└──────────────────────────────────────────────┘
List generator — per-environment apps
The simplest generator: a static list of values that each produce an Application.
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: myapp-environments
namespace: argocd
spec:
generators:
- list:
elements:
- env: staging
namespace: myapp-staging
replicas: "2"
- env: production
namespace: myapp-prod
replicas: "5"
- env: eu-west
namespace: myapp-eu
replicas: "3"
template:
metadata:
name: "myapp-{{env}}"
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
repoURL: https://github.com/myorg/myapp-deploy
targetRevision: main
path: "overlays/{{env}}"
destination:
server: https://kubernetes.default.svc
namespace: "{{namespace}}"
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
This single 30-line manifest generates 3 Applications (myapp-staging, myapp-production, myapp-eu-west) — one per environment.
Git generator — discover apps from repo layout
Scan directories or files in a Git repository and generate an Application per match. The most common pattern: a monorepo where each subdirectory is a deployable service.
Directory-based (one app per subdirectory):
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: cluster-services
namespace: argocd
spec:
generators:
- git:
repoURL: https://github.com/myorg/infra
revision: main
directories:
- path: apps/*
template:
metadata:
name: "{{path.basename}}"
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/myorg/infra
targetRevision: main
path: "{{path}}"
destination:
server: https://kubernetes.default.svc
namespace: "{{path.basename}}"
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
# Git repo structure this generator matches:
infra/
└── apps/
├── backend/ → Application: backend (auto-created)
├── frontend/ → Application: frontend (auto-created)
├── monitoring/ → Application: monitoring (auto-created)
└── ingress/ → Application: ingress (auto-created)
Add a new directory under apps/, push, and ArgoCD creates the corresponding Application automatically.
File-based (discover from JSON/YAML files):
generators:
- git:
repoURL: https://github.com/myorg/infra
revision: main
files:
- path: "environments/*.json"
template:
metadata:
name: "{{name}}"
spec:
source:
path: "{{path}}"
destination:
namespace: "{{namespace}}"
// environments/staging.json
{
"name": "myapp-staging",
"path": "overlays/staging",
"namespace": "myapp-staging",
"replicas": 2
}
Cluster generator — deploy to every cluster
When ArgoCD manages multiple clusters, this generator discovers them and creates an Application per cluster.
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: cluster-bootstrap
namespace: argocd
spec:
generators:
- clusters:
selector:
matchLabels:
argocd.argoproj.io/secret-type: cluster
template:
metadata:
name: "bootstrap-{{name}}"
spec:
project: default
source:
repoURL: https://github.com/myorg/infra
targetRevision: main
path: bootstrap
destination:
server: "{{server}}"
namespace: argocd
syncPolicy:
automated: {}
Every cluster registered with ArgoCD automatically gets the bootstrap-{cluster} Application — no per-cluster manual configuration needed.
Matrix generator — combine generators
Cross-product of two generators — e.g., deploy every app to every environment:
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: services-per-env
namespace: argocd
spec:
generators:
- matrix:
generators:
- list:
elements:
- env: staging
namespace: staging
- env: production
namespace: prod
- git:
repoURL: https://github.com/myorg/services
revision: main
directories:
- path: services/*
template:
metadata:
name: "{{path.basename}}-{{env}}"
spec:
source:
repoURL: https://github.com/myorg/services
path: "{{path.path}}"
destination:
namespace: "{{namespace}}"
This generates N × M Applications: every service × every environment.
Generator comparison
| Generator | Source of truth | Best for |
|---|---|---|
| List | Static YAML list | Fixed set of environments (dev/staging/prod) |
| Git — directories | Git repo structure | Monorepo with one dir per app |
| Git — files | JSON/YAML config files in Git | Declarative per-app config |
| Cluster | ArgoCD cluster secrets | Multi-cluster bootstrap |
| Matrix | Product of two generators | Every app × every env |
| Merge | Merged values from two generators | Override defaults per environment |
| SCM Provider | GitHub/GitLab/Bitbucket API | Per-repo Application generation |
| Pull Request | Open PRs in a repo | Preview environments (PR→temp namespace) |
PR generator — preview environments
Create temporary Applications for every open pull request:
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: preview-envs
namespace: argocd
spec:
generators:
- pullRequest:
github:
owner: myorg
repo: myapp
tokenRef:
secretName: github-token
key: token
requeueAfterSeconds: 300
template:
metadata:
name: "myapp-pr-{{number}}"
annotations:
notifications.argoproj.io/subscribe.on-sync-succeeded.slack: "#preview-envs"
spec:
project: default
source:
repoURL: https://github.com/myorg/myapp
targetRevision: "{{head_sha}}"
path: kubernetes
destination:
server: https://kubernetes.default.svc
namespace: "pr-{{number}}"
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
Every PR gets its own namespace (pr-42) and Application — merge the PR and the preview environment is cleaned up automatically (when the branch is deleted).
# Check which preview envs are running
argocd appset get preview-envs
# Manually trigger regeneration
argocd appset generate preview-envs
Progressive delivery (Argo Rollouts)
Replace standard Deployments with Rollout objects for blue-green and canary deployments:
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: my-app
spec:
replicas: 5
strategy:
canary:
steps:
- setWeight: 20
- pause: { duration: 60s }
- setWeight: 40
- pause: { duration: 60s }
- setWeight: 100
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
spec:
containers:
- name: app
image: myapp:stable
# Trigger a new rollout
kubectl argo rollouts set image my-app app=myapp:v2.0
# Monitor the rollout
kubectl argo rollouts get rollout my-app --watch
# Promote (skip remaining pauses)
kubectl argo rollouts promote my-app
# Abort (rollback)
kubectl argo rollouts abort my-app
Git repository structure
A well-organized GitOps repository keeps ArgoCD configuration separate from application manifests, and uses a base/overlay layout so the same app can be deployed to multiple environments. A common structure looks like this:
infra-repo/
├── argocd/
│ ├── apps/ # App of Apps child applications
│ │ ├── backend.yaml
│ │ ├── frontend.yaml
│ │ └── monitoring.yaml
│ └── projects/ # ArgoCD project definitions
│ └── default.yaml
├── apps/
│ └── backend/
│ ├── base/ # Base manifests (common across envs)
│ │ ├── deployment.yaml
│ │ ├── service.yaml
│ │ └── kustomization.yaml
│ └── overlays/ # Environment-specific overlays
│ ├── staging/
│ │ ├── deployment-patch.yaml
│ │ └── kustomization.yaml
│ └── production/
│ ├── deployment-patch.yaml
│ └── kustomization.yaml
└── clusters/
├── staging/
└── production/
GitOps workflow
The end-to-end GitOps pipeline flows from a code push through CI to a manifest update that ArgoCD continuously watches:
Developer pushes code → CI builds image → CI updates manifest repo with new tag
→ ArgoCD detects the change → ArgoCD syncs the cluster → App is updated
Example: CI updates the deployment tag
After CI builds and pushes an image, the pipeline updates the image tag in the manifest repository with kustomize and pushes the change — ArgoCD then deploys the new version automatically:
# In CI pipeline after building and pushing the image
git clone git@github.com:myorg/infra.git
cd infra/apps/backend/overlays/production
# Update the image tag using kustomize
kustomize edit set image myapp=myapp:${CI_COMMIT_SHA}
# Commit and push
git add .
git commit -m "deploy: update backend to ${CI_COMMIT_SHA}"
git push origin main
# ArgoCD automatically detects the change and syncs within 3 minutes (default polling)
Notifications
ArgoCD Notifications sends alerts on sync status, health changes, and deployment events to Slack, email, webhooks, and more.
Architecture
Application sync/health change
│
▼
┌──────────────────┐ ┌──────────────┐
│ ArgoCD │────▶│ Notifications│────▶ Slack
│ (emits events) │ │ Controller │────▶ Email
└──────────────────┘ │ (triggers + │────▶ Webhook
│ templates) │────▶ Grafana
└──────────────┘
Setup
# Install notifications controller
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj-labs/argocd-notifications/stable/manifests/install.yaml
# Install triggers and templates
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj-labs/argocd-notifications/stable/catalog/install.yaml
Slack notifications
# argocd-notifications-secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: argocd-notifications-secret
namespace: argocd
stringData:
slack-token: xoxb-your-slack-bot-token
# argocd-notifications-cm.yaml — triggers and templates
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-notifications-cm
namespace: argocd
data:
# Trigger: when to send a notification
trigger.on-sync-succeeded: |
- description: Application synced successfully
send:
- app-sync-succeeded
when: app.status.operationState.phase == 'Succeeded'
trigger.on-sync-failed: |
- description: Application sync failed
send:
- app-sync-failed
when: app.status.operationState.phase == 'Failed'
trigger.on-deployed: |
- description: Application is synced and healthy
send:
- app-deployed
when: app.status.operationState.phase == 'Succeeded' and app.status.health.status == 'Healthy'
trigger.on-health-degraded: |
- description: Application health degraded
send:
- app-health-degraded
when: app.status.health.status == 'Degraded'
# Template: what to send
template.app-sync-succeeded: |
message: |
✅ {{.app.metadata.name}} synced successfully to {{.app.spec.destination.namespace}}.
Revision: {{.app.status.sync.revision}}
{{.app.status.operationState.syncResult.revision}}
slack:
attachments: |
[{
"color": "#36a64f",
"title": "{{.app.metadata.name}}",
"title_link": "{{.context.argocdUrl}}/applications/{{.app.metadata.name}}",
"fields": [
{"title": "Sync Status", "value": "{{.app.status.sync.status}}"},
{"title": "Repository", "value": "{{.app.spec.source.repoURL}}"}
]
}]
template.app-sync-failed: |
message: |
❌ {{.app.metadata.name}} sync FAILED.
{{range .app.status.conditions}}
{{.message}}
{{end}}
slack:
attachments: |
[{
"color": "#ff0000",
"title": "{{.app.metadata.name}}",
"title_link": "{{.context.argocdUrl}}/applications/{{.app.metadata.name}}"
}]
# Subscription: which app gets which triggers — annotate the app
# metadata.annotations:
# notifications.argoproj.io/subscribe.on-sync-succeeded.slack: my-channel
Apply subscription via application annotation:
# In the Application manifest
metadata:
annotations:
notifications.argoproj.io/subscribe.on-sync-succeeded.slack: "#deployments"
notifications.argoproj.io/subscribe.on-sync-failed.slack: "#alerts"
notifications.argoproj.io/subscribe.on-deployed.slack: "#deployments"
notifications.argoproj.io/subscribe.on-health-degraded.slack: "#alerts"
RBAC — Access Control
ArgoCD's built-in RBAC controls who can do what with which applications. Policies are defined in the argocd-rbac-cm ConfigMap.
Policy structure
p, <subject>, <resource>, <action>, <object>, <permission>
Common patterns
# argocd-rbac-cm.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-rbac-cm
namespace: argocd
data:
# Default policy (applied when no specific rule matches)
policy.default: role:readonly
# CSV policy format
policy.csv: |
# Grant admin role to specific users
g, admin, role:admin
# Team-based roles via OIDC groups
g, platform-team, role:admin
g, backend-team, role:admin
# Role definitions: what each role can do
p, role:admin, *, *, *, allow
p, role:readonly, *, get, *, allow
p, role:readonly, *, sync, *, deny
# Application-level permissions
p, backend-team, applications, sync, backend/*, allow
p, backend-team, applications, get, backend/*, allow
p, backend-team, applications, delete, backend/*, deny
# Project-level permissions
p, frontend-team, applications, *, frontend/*, allow
p, frontend-team, projects, get, frontend, allow
Built-in roles
| Role | Permissions |
|---|---|
role:admin | Full access to all resources |
role:readonly | View only, no changes allowed |
SSO — OIDC Authentication
Integrate ArgoCD with your identity provider (Okta, Auth0, Google, Azure AD, Keycloak) so users log in with existing credentials and groups.
OIDC configuration
# argocd-cm.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-cm
namespace: argocd
data:
url: https://argocd.example.com
oidc.config: |
name: Okta
issuer: https://dev-123456.okta.com
clientID: 0oa1abc2def3ghi4jk5
clientSecret: $oidc.okta.clientSecret
requestedScopes:
- openid
- profile
- email
- groups
requestedIDTokenClaims:
groups:
essential: true
# Map OIDC groups to ArgoCD roles (requires RBAC above)
# In argocd-rbac-cm:
# g, platform-team, role:admin
# Create the client secret
kubectl create secret generic argocd-oidc-secret \
-n argocd \
--from-literal=okta.clientSecret=<your-client-secret>
Dex (bundled OIDC proxy)
ArgoCD includes Dex for when your provider doesn't support OIDC directly. Dex translates LDAP, SAML, GitHub, and other protocols to OIDC.
# argocd-cm.yaml — Dex with GitHub OAuth
data:
dex.config: |
connectors:
- type: github
id: github
name: GitHub
config:
clientID: $dex.github.clientID
clientSecret: $dex.github.clientSecret
orgs:
- name: myorg
teams:
- platform-team
- backend-team
ArgoCD Image Updater
Automatically updates container images in Git when new versions are pushed to a registry. No CI pipeline needed for image tag updates.
# Install Image Updater
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj-labs/argocd-image-updater/stable/manifests/install.yaml
Application annotation
# Annotate your Application to enable auto-updates
metadata:
annotations:
argocd-image-updater.argoproj.io/image-list: myapp=myorg/myapp
argocd-image-updater.argoproj.io/myapp.update-strategy: semver
argocd-image-updater.argoproj.io/myapp.allow-tags: regexp:^v?[0-9]+\.[0-9]+\.[0-9]+$
argocd-image-updater.argoproj.io/write-back-method: git
argocd-image-updater.argoproj.io/git-branch: main
# Optional: only update to patch versions (no minors)
# argocd-image-updater.argoproj.io/myapp.update-strategy: latest
# argocd-image-updater.argoproj.io/myapp.ignore-tags: "*rc*,*alpha*,*beta*"
Update strategies
| Strategy | Behavior | Example |
|---|---|---|
semver | Follows semantic versioning | v1.2.3 → v1.2.4, NOT v1.3.0 |
latest | Always update to newest tag | Uses creation timestamp |
name | Alphabetically newest tag | Uses tag name sorting |
digest | Pinned to a specific SHA digest | Immutable pinning |
Registry credentials
# argocd-image-updater pulls from private registries
# Create a secret per registry
apiVersion: v1
kind: Secret
metadata:
name: dockerhub-creds
namespace: argocd
labels:
argocd-image-updater.argoproj.io/image-list: myorg/myapp
stringData:
# Format: <registry>:<username>:<password>
registries: |
- "https://index.docker.io/v1/:myuser:mypassword"
Argo Workflows
Argo Workflows is a Kubernetes-native workflow engine for running complex jobs — CI pipelines, data processing, ML training, and infrastructure automation.
Core concepts
| Concept | Description |
|---|---|
| Workflow | A DAG (directed acyclic graph) of steps |
| Template | Reusable step definition (container, script, resource) |
| Step | A single unit of work in a workflow |
| Artifact | Files passed between steps (S3, GCS, volumes) |
| Parameter | Inputs/outputs passed between steps |
Hello World workflow
apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
generateName: hello-world-
spec:
entrypoint: main
templates:
- name: main
steps:
- - name: hello
template: greet
arguments:
parameters:
- name: message
value: "Hello from Argo Workflows!"
- name: greet
inputs:
parameters:
- name: message
container:
image: alpine:latest
command: [echo]
args: ["{{inputs.parameters.message}}"]
# Submit a workflow
argo submit hello-world.yaml
# List workflows
argo list
# Watch logs
argo logs @latest
# Get workflow status
argo get @latest
DAG workflow (parallel steps)
apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
generateName: ci-pipeline-
spec:
entrypoint: ci
templates:
- name: ci
dag:
tasks:
- name: lint
template: run-lint
- name: test
template: run-test
dependencies: [lint]
- name: build-backend
template: docker-build
arguments:
parameters:
- name: service
value: backend
dependencies: [test]
- name: build-frontend
template: docker-build
arguments:
parameters:
- name: service
value: frontend
dependencies: [test]
- name: deploy
template: argocd-sync
dependencies: [build-backend, build-frontend]
- name: run-lint
container:
image: node:22
command: [npm]
args: [run, lint]
- name: run-test
container:
image: node:22
command: [npm]
args: [test]
- name: docker-build
inputs:
parameters:
- name: service
container:
image: docker:latest
command: [docker]
args: [build, -t, "{{inputs.parameters.service}}:latest", "."]
- name: argocd-sync
container:
image: argoproj/argocd:latest
command: [argocd]
args: [app, sync, my-app, --insecure, --grpc-web]
Argo Events — event-driven triggers
GitHub push → Webhook → EventSource → Sensor → Argo Workflow / ArgoCD Sync
# EventSource: listens for GitHub webhooks
apiVersion: argoproj.io/v1alpha1
kind: EventSource
metadata:
name: github
spec:
github:
push:
repository: myorg/myapp
events:
- push
webhook:
endpoint: /push
port: "12000"
method: POST
url: https://events.example.com
---
# Sensor: triggers a workflow on push event
apiVersion: argoproj.io/v1alpha1
kind: Sensor
metadata:
name: github-push
spec:
dependencies:
- name: push
eventSourceName: github
eventName: push
triggers:
- template:
name: argo-workflow
argoWorkflow:
operation: submit
source:
resource:
apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
generateName: ci-pipeline-
spec:
entrypoint: ci
templates:
- name: ci
container:
image: alpine
command: [echo]
args: ["Build triggered by push"]
See also
- Helm Reference — package management for Kubernetes
- Observability Stack — Prometheus, Grafana, Loki, Tempo, Mimir
- OpenTelemetry — instrumentation and collection