Skip to content

GitOps

First PublishedByAtif Alam

GitOps is a deployment practice where Git is the single source of truth for your infrastructure and application state. Instead of running kubectl apply or helm install from a CI pipeline, a GitOps operator running in the cluster watches a Git repository and automatically synchronizes the cluster to match the desired state in Git.

Traditional CI/CD DeployGitOps
CI pipeline pushes to the clusterCluster pulls from Git
Pipeline needs cluster credentialsOnly the in-cluster operator needs access
”What’s running?” → check the cluster”What’s running?” → check Git
Drift goes unnoticedDrift is auto-corrected
Rollback = rerun old pipelineRollback = git revert
Audit trail in CI logsAudit trail in Git history
Push-based (traditional CI/CD):
Developer ──► Git ──► CI Pipeline ──► kubectl apply ──► Cluster
Pull-based (GitOps):
Developer ──► Git ◄── GitOps Operator (in cluster) ──► reconcile ──► Cluster
Push-BasedPull-Based (GitOps)
Who deploysCI pipelineIn-cluster operator
Cluster credentialsStored in CI (secret)Only the operator has access
Drift detectionManual or noneContinuous (operator watches)
SecurityCI needs write access to clusterCluster pulls (no inbound access)
ToolsGitHub Actions, GitLab CI, JenkinsArgoCD, Flux

ArgoCD is the most popular GitOps tool for Kubernetes. It’s a CNCF graduated project that runs as a controller in your cluster and continuously syncs applications from Git.

Git Repository (desired state)
│ ArgoCD watches for changes
ArgoCD Controller (in cluster)
│ Compares desired state (Git) vs live state (cluster)
├── In Sync ──► nothing to do
└── Out of Sync ──► reconcile (apply changes to cluster)
ConceptWhat It Is
ApplicationA CRD that maps a Git repo path to a Kubernetes namespace
ProjectGroups applications with shared access policies and allowed destinations
SyncApply the desired state from Git to the cluster
HealthWhether the deployed resources are running correctly
RefreshCheck Git for changes (automatic or manual)
Terminal window
# Install ArgoCD in the cluster
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
# Install the CLI
brew install argocd
# Get the initial admin password
argocd admin initial-password -n argocd
# Login
argocd login localhost:8080
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: myapp
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/myorg/k8s-config.git
targetRevision: main
path: apps/myapp/overlays/production # Path in the repo
destination:
server: https://kubernetes.default.svc
namespace: myapp
syncPolicy:
automated:
prune: true # Delete resources removed from Git
selfHeal: true # Auto-correct manual changes (drift)
syncOptions:
- CreateNamespace=true
retry:
limit: 3
backoff:
duration: 5s
factor: 2
maxDuration: 3m
PolicyWhat It Does
Manual syncUser clicks “Sync” in the UI or CLI
Auto syncArgoCD syncs whenever Git changes
Self-healAuto-correct drift (someone kubectl edit-ed a resource)
PruneDelete resources that were removed from Git
Terminal window
# List applications
argocd app list
# Sync an application (deploy)
argocd app sync myapp
# Get application status
argocd app get myapp
# View diff (what would change)
argocd app diff myapp
# Rollback to a previous Git revision
argocd app rollback myapp --revision 3
# Hard refresh (re-read from Git)
argocd app get myapp --hard-refresh

Manage many applications with a single “parent” Application that points to a directory of Application manifests:

k8s-config/
├── apps/
│ ├── myapp/
│ ├── api-service/
│ └── worker/
└── argocd/
└── apps.yaml # App-of-apps: points to apps/ directory
# argocd/apps.yaml — the "parent" Application
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: all-apps
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/myorg/k8s-config.git
targetRevision: main
path: apps # Directory containing Application manifests
destination:
server: https://kubernetes.default.svc
namespace: argocd
syncPolicy:
automated:
prune: true
selfHeal: true

When you add a new Application YAML to the apps/ directory and push to Git, ArgoCD automatically picks it up.

For generating many similar Applications from a template:

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: cluster-apps
namespace: argocd
spec:
generators:
- list:
elements:
- cluster: staging
url: https://staging-k8s.example.com
- cluster: production
url: https://prod-k8s.example.com
template:
metadata:
name: 'myapp-{{cluster}}'
spec:
project: default
source:
repoURL: https://github.com/myorg/k8s-config.git
path: 'apps/myapp/overlays/{{cluster}}'
destination:
server: '{{url}}'
namespace: myapp

Flux is another CNCF GitOps tool for Kubernetes. It’s a set of controllers that watch Git repositories, Helm charts, and OCI artifacts and reconcile them to the cluster.

Git Repository
Source Controller ──► fetches manifests from Git, Helm repos, OCI
Kustomize Controller ──► applies Kustomize overlays to the cluster
Helm Controller ──► installs/upgrades Helm releases
Notification Controller ──► sends alerts (Slack, Teams, webhook)
Image Automation Controller ──► updates image tags in Git
Terminal window
# Install Flux CLI
brew install fluxcd/tap/flux
# Bootstrap Flux in the cluster (also creates the Git repo structure)
flux bootstrap github \
--owner=myorg \
--repository=k8s-config \
--branch=main \
--path=./clusters/production \
--personal
# GitRepository — tells Flux where to fetch manifests
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
name: myapp
namespace: flux-system
spec:
interval: 1m # Check for changes every minute
url: https://github.com/myorg/k8s-config.git
ref:
branch: main
---
# Kustomization — tells Flux what to deploy from the repo
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: myapp
namespace: flux-system
spec:
interval: 5m
path: ./apps/myapp
prune: true # Delete removed resources
sourceRef:
kind: GitRepository
name: myapp
healthChecks:
- apiVersion: apps/v1
kind: Deployment
name: myapp
namespace: myapp
# HelmRepository — where to find charts
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: bitnami
namespace: flux-system
spec:
interval: 1h
url: https://charts.bitnami.com/bitnami
---
# HelmRelease — what to install
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: redis
namespace: flux-system
spec:
interval: 10m
chart:
spec:
chart: redis
version: "18.x"
sourceRef:
kind: HelmRepository
name: bitnami
values:
architecture: standalone
auth:
enabled: false
FeatureArgoCDFlux
UIRich web UI + CLICLI only (use Weave GitOps for UI)
ArchitectureMonolithic (single controller)Modular (multiple controllers)
Multi-clusterBuilt-in (single ArgoCD manages many clusters)One Flux per cluster (or remote apply)
Helm supportRenders Helm templates, applies as plain YAMLNative HelmRelease CRD (proper Helm lifecycle)
KustomizeSupportedNative Kustomization CRD
Image automationNot built-in (use Argo Image Updater)Built-in Image Automation Controller
NotificationsBuilt-inNotification Controller
RBACBuilt-in with SSO integrationUses Kubernetes RBAC
App-of-appsYes (Application/ApplicationSet)Yes (Kustomization dependencies)
Learning curveLower (great UI)Higher (CRD-based, CLI-focused)
CNCF statusGraduatedGraduated
ScenarioChoose
Want a web UI for visibilityArgoCD
Prefer Helm lifecycle (install/upgrade/rollback)Flux
Managing many clusters from one placeArgoCD
Want modular, composable controllersFlux
Team already uses Kustomize heavilyEither (both support Kustomize)
Need image auto-update (tag in Git)Flux (built-in)

GitOps tools work with your existing manifest management:

Both ArgoCD and Flux can deploy Helm charts from:

  • A Helm repository (e.g. Bitnami, your private chart repo).
  • A chart stored in a Git repository.
  • An OCI registry.

The values can be stored in Git alongside the chart reference, making the full configuration auditable.

A common pattern — base manifests with per-environment overlays:

apps/myapp/
├── base/
│ ├── deployment.yaml
│ ├── service.yaml
│ └── kustomization.yaml
├── overlays/
│ ├── staging/
│ │ ├── kustomization.yaml # Patches: 1 replica, staging ingress
│ │ └── ingress-patch.yaml
│ └── production/
│ ├── kustomization.yaml # Patches: 3 replicas, prod ingress, HPA
│ ├── ingress-patch.yaml
│ └── hpa.yaml

ArgoCD and Flux both detect kustomization.yaml and apply the overlay automatically.

For more on Helm and Kustomize with Kubernetes, see Helm and Helm Templating.

Pattern 1: Monorepo (App + Config Together)

Section titled “Pattern 1: Monorepo (App + Config Together)”
myapp/
├── src/ # Application code
├── Dockerfile
├── .github/workflows/ # CI pipeline
└── k8s/ # Kubernetes manifests (GitOps watches here)
├── base/
└── overlays/

Pros: Everything in one place, simple. Cons: App code changes trigger GitOps reconciliation (noisy), tight coupling.

Section titled “Pattern 2: Separate Config Repo (Recommended)”
# Repo 1: myorg/myapp (application code)
myapp/
├── src/
├── Dockerfile
└── .github/workflows/ci.yml # CI: build, test, push image
# Repo 2: myorg/k8s-config (deployment config)
k8s-config/
├── apps/
│ ├── myapp/
│ │ ├── base/
│ │ └── overlays/
│ ├── api-service/
│ └── worker/
└── infrastructure/
├── cert-manager/
└── ingress-nginx/

Pros: Clear separation of concerns, CI doesn’t need cluster access, config changes are auditable. Cons: Extra repository to manage, need to update image tags across repos.

# Repo per environment
myorg/k8s-staging/
└── apps/
└── myapp/
myorg/k8s-production/
└── apps/
└── myapp/

Pros: Strongest isolation between environments, different access controls. Cons: Duplication, harder to promote changes.

The separate config repo is the most common pattern. The CI pipeline (in the app repo) builds and pushes the image, then updates the image tag in the config repo:

App repo (CI):
push ──► build ──► test ──► docker build + push ──► update image tag in config repo
Config repo (GitOps):
ArgoCD/Flux detects tag change ──► sync to cluster
BenefitWhy
Git as audit trailEvery deployment is a Git commit — who, what, when
DeclarativeDesired state in Git, not imperative scripts
Drift detectionOperator auto-corrects manual changes
Easy rollbackgit revert = rollback
SecurityCI pipeline doesn’t need cluster credentials
ConsistencySame tool manages all environments
Trade-OffConsideration
Secret managementSecrets can’t live in Git as plain text — use Sealed Secrets, SOPS, or External Secrets Operator
Learning curveRequires understanding ArgoCD/Flux CRDs and Git workflows
Multi-cluster complexityManaging many clusters and environments adds repo structure complexity
DebuggingWhen sync fails, you troubleshoot via the operator logs, not your usual CI pipeline
Database migrationsNeed a separate mechanism (Jobs, init containers) — not purely declarative
  • GitOps uses Git as the single source of truth — the cluster continuously reconciles to match Git.
  • Pull-based (GitOps) is more secure than push-based (CI deploys) because the cluster pulls from Git instead of CI pushing to the cluster.
  • ArgoCD is best for teams that want a rich UI, multi-cluster management, and app-of-apps patterns.
  • Flux is best for teams that prefer modular controllers, native Helm lifecycle, and built-in image automation.
  • Use a separate config repo for deployment manifests — decouples app code from deployment config.
  • Drift detection and self-healing are key GitOps benefits — manual changes are auto-corrected.
  • Handle secrets with Sealed Secrets, SOPS, or External Secrets Operator — never store plain-text secrets in Git.
  • AIOps — LLM-assisted runbooks, RAG over operational knowledge, and evaluating AI suggestions alongside Git-defined desired state.