Skip to main content
Navigation
HomeTechnical ReferenceJournalGitHubGitHub
Sidebar — toggle document categories via the logo
Categories

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​

ConceptDescription
ApplicationA group of Kubernetes resources defined in a Git repo.
ProjectA logical grouping of applications with access control policies.
SourceThe Git repository (or Helm repo) containing manifests.
DestinationThe target cluster + namespace where resources are deployed.
SyncThe process of reconciling the live cluster state with the Git state.
HealthWhether 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​

GeneratorSource of truthBest for
ListStatic YAML listFixed set of environments (dev/staging/prod)
Git — directoriesGit repo structureMonorepo with one dir per app
Git — filesJSON/YAML config files in GitDeclarative per-app config
ClusterArgoCD cluster secretsMulti-cluster bootstrap
MatrixProduct of two generatorsEvery app × every env
MergeMerged values from two generatorsOverride defaults per environment
SCM ProviderGitHub/GitLab/Bitbucket APIPer-repo Application generation
Pull RequestOpen PRs in a repoPreview 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​

RolePermissions
role:adminFull access to all resources
role:readonlyView 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​

StrategyBehaviorExample
semverFollows semantic versioningv1.2.3 → v1.2.4, NOT v1.3.0
latestAlways update to newest tagUses creation timestamp
nameAlphabetically newest tagUses tag name sorting
digestPinned to a specific SHA digestImmutable 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​

ConceptDescription
WorkflowA DAG (directed acyclic graph) of steps
TemplateReusable step definition (container, script, resource)
StepA single unit of work in a workflow
ArtifactFiles passed between steps (S3, GCS, volumes)
ParameterInputs/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​