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