The Currents of Destiny
GitOps, ArgoCD, Flux and infrastructure as code
At the top of the Citadel's tallest tower, there is a secret room that few archivists know about: the Chamber of Currents. Here, the Masters of Currents observe the invisible forces that govern the realm's infrastructure - every bridge, every road, every fortress is shaped by these currents.
The Master of Currents invites you to observe the great animated map that covers an entire wall. Lines of light traverse the realm, connecting the Citadel's servers to the most remote outposts. "Every change you see is a current - a change described in a scroll, that automatically shapes reality. This is what we call GitOps. And it's the most powerful way to govern an infrastructure."
The GitOps philosophy
GitOps is a philosophy where Git is the single source of truth for all infrastructure and application deployments. The idea is simple but revolutionary:
- All infrastructure is described in files versioned in Git
- Modifying infrastructure = making a commit
- Deploying = merging a branch
- Rolling back a deployment = git revert
The term was coined by Weaveworks in 2017. Since then, it has become a de facto standard in the Kubernetes world and beyond.
The four fundamental principles
- Declarative: the desired state of the system is described in files (YAML, JSON, HCL), not in imperative scripts
- Versioned: everything is in Git - the complete history of every infrastructure change
- Automatic: an agent automatically applies changes when Git is modified
- Self-reconciling: if someone manually modifies the infrastructure, the agent restores it to the state described by Git
Pull vs Push deployment
There are two models for applying infrastructure changes. Understanding the difference is fundamental.
The Push model (traditional)
In the push model, an external system (CI/CD) pushes changes to the cluster:
# PUSH model - the CI/CD pipeline deploys directly
# .github/workflows/deploy.yml
# 1. The developer pushes code
# 2. CI/CD builds the Docker image
# 3. CI/CD applies Kubernetes manifests
# kubectl apply -f k8s/deployment.yaml
# 4. CI/CD needs direct access to the cluster (credentials) Problems with the push model:
- The CI/CD pipeline needs cluster access credentials - significant attack surface
- If someone manually modifies the cluster, CI/CD doesn't know (drift)
- No automatic reconciliation
The Pull model (GitOps)
In the pull model, an agent installed inside the cluster watches Git and applies changes:
# PULL model - an agent in the cluster watches Git
# 1. The developer pushes code and manifests to Git
# 2. The GitOps agent (ArgoCD, Flux) detects the change
# 3. The agent PULLS the manifests from Git
# 4. The agent applies the changes in the cluster
# 5. If someone manually modifies, the agent corrects (auto-healing) Advantages of the pull model:
- No external credentials: the agent is already in the cluster
- Auto-healing: any manual modification is automatically corrected
- Complete audit: every change is a Git commit
- Simple rollback: git revert undoes a deployment
| Aspect | Push (classic CI/CD) | Pull (GitOps) |
|---|---|---|
| Who deploys | The CI/CD pipeline | An agent in the cluster |
| Credentials | CI/CD has cluster access | Agent has Git access (read-only) |
| Drift | Not detected | Automatically corrected |
| Rollback | Re-deploy an old version | git revert |
| Audit | CI/CD logs | git log (complete, permanent) |
ArgoCD - the cluster guardian
ArgoCD is the most popular GitOps tool for Kubernetes. It installs in your cluster and watches one or more Git repositories. As soon as a change is detected, it applies it.
Key concepts
- Application: an ArgoCD resource that links a Git repository to a Kubernetes namespace
- Sync: the process of aligning the Git state with the cluster state
- Health: the health status of deployed resources
- Diff: the difference between the desired state (Git) and the current state (cluster)
The Application CRD
You declare an ArgoCD Application in YAML. It's the central resource that says: "watch this Git repo and deploy to this namespace":
# argocd-application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-app
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/my-org/my-app-gitops.git
targetRevision: main
path: overlays/production # Folder containing the manifests
destination:
server: https://kubernetes.default.svc
namespace: my-app
syncPolicy:
automated: # Automatic sync enabled
prune: true # Delete resources absent from Git
selfHeal: true # Correct manual modifications
syncOptions:
- CreateNamespace=true # Create namespace if needed Sync policies
- Manual sync: you trigger deployment manually from the UI or CLI
- Automated sync: ArgoCD deploys automatically as soon as it detects a change in Git
- Self-heal: if someone does a manual
kubectl edit, ArgoCD restores the Git state - Prune: if you delete a file from Git, ArgoCD deletes the corresponding resource from the cluster
# ArgoCD CLI - common commands
# Log in
argocd login argocd.my-domain.com
# List applications
argocd app list
# See application status
argocd app get my-app
# Manually synchronize
argocd app sync my-app
# See deployment history
argocd app history my-app
# Rollback to a previous version
argocd app rollback my-app 3 Tip: ArgoCD offers a beautiful web interface that displays the status of each Kubernetes resource as a visual tree. Teams often use the UI to monitor their daily deployments.
Flux - the CNCF alternative
Flux (formerly Flux v2) is the other major GitOps tool, maintained by the CNCF (Cloud Native Computing Foundation). Unlike ArgoCD, Flux doesn't have a web interface by default - it's entirely driven by Kubernetes resources.
Key Flux concepts
- GitRepository: points to a Git repository to watch
- Kustomization: describes what to deploy from the repository and how
- HelmRelease: deploys Helm charts from Git or a registry
- ImageUpdateAutomation: automatically updates image tags in Git
# Installing Flux in a cluster
flux bootstrap github \
--owner=my-org \
--repository=flux-config \
--path=clusters/production \
--personal
# This creates a Git repository and installs Flux in the cluster
# Flux manages itself via GitOps! Flux configuration example
# source.yaml - Define the Git source
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
name: my-app
namespace: flux-system
spec:
interval: 1m # Check Git every minute
url: https://github.com/my-org/my-app-gitops
ref:
branch: main
---
# kustomization.yaml - Define what to deploy
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: my-app
namespace: flux-system
spec:
interval: 5m
path: ./overlays/production # Path in the repository
prune: true # Delete resources absent from Git
sourceRef:
kind: GitRepository
name: my-app
healthChecks: # Check health after deployment
- apiVersion: apps/v1
kind: Deployment
name: my-app
namespace: my-app ArgoCD vs Flux - how to choose?
| Aspect | ArgoCD | Flux |
|---|---|---|
| Web interface | Yes, rich and intuitive | No (CLI + third-party extensions) |
| Configuration | Application CRD + UI | 100% Kubernetes resources |
| Multi-cluster | Native (one ArgoCD manages N clusters) | One Flux per cluster |
| Helm | Built-in support | HelmRelease CRD |
| Philosophy | Centralized tool with UI | Distributed components, K8s-native |
| Learning curve | Easier (UI helps) | Steeper (everything in YAML) |
Structure of a GitOps repo
The GitOps repository organization is crucial. The most widespread structure uses Kustomize with overlays per environment:
my-app-gitops/
βββ base/ # Configuration shared across all envs
β βββ kustomization.yaml # List of base resources
β βββ deployment.yaml # Application deployment
β βββ service.yaml # Kubernetes service
β βββ configmap.yaml # Non-sensitive configuration
β βββ hpa.yaml # Auto-scaling
βββ overlays/ # Customizations per environment
β βββ dev/
β β βββ kustomization.yaml # Inherits from base/ + dev patches
β β βββ replicas-patch.yaml # 1 replica in dev
β β βββ resources-patch.yaml # Less CPU/RAM
β βββ staging/
β β βββ kustomization.yaml
β β βββ replicas-patch.yaml # 2 replicas in staging
β β βββ ingress-patch.yaml # Staging domain
β βββ production/
β βββ kustomization.yaml
β βββ replicas-patch.yaml # 5 replicas in prod
β βββ resources-patch.yaml # More CPU/RAM
β βββ ingress-patch.yaml # Production domain
βββ apps/ # Other deployed applications
βββ monitoring/
β βββ kustomization.yaml
β βββ prometheus.yaml
βββ ingress/
βββ kustomization.yaml
βββ nginx-ingress.yaml Kustomize in a nutshell
Kustomize is a tool integrated into kubectl that lets you customize YAML manifests without duplicating them. The principle: a shared base and overlays that modify it.
# base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
- configmap.yaml
---
# overlays/production/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base # Inherits from the base
patches:
- path: replicas-patch.yaml # Applies prod patches
- path: resources-patch.yaml
namespace: my-app-prod # Specific namespace
---
# overlays/production/replicas-patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
replicas: 5 # 5 replicas in production # Preview what Kustomize generates for prod
kubectl kustomize overlays/production/
# Apply directly (outside GitOps, for testing)
kubectl apply -k overlays/production/ Managing secrets in GitOps
The biggest challenge of GitOps: how do you version secrets (passwords, API keys, certificates) in Git without exposing them in plain text?
Sealed Secrets (Bitnami)
Sealed Secrets encrypts your secrets with a public key. Only the controller installed in the cluster can decrypt them:
# Create a standard Kubernetes Secret (DO NOT commit!)
kubectl create secret generic my-secret \
--from-literal=db-password=SuperSecretPassword \
--dry-run=client -o yaml > secret.yaml
# Encrypt with kubeseal (cluster's public key)
kubeseal --format yaml < secret.yaml > sealed-secret.yaml
# The SealedSecret is safe for Git - it can only be decrypted
# by the cluster's controller
cat sealed-secret.yaml
# apiVersion: bitnami.com/v1alpha1
# kind: SealedSecret
# spec:
# encryptedData:
# db-password: AgBy3i4OJSWK+... (encrypted, unreadable)
# Commit the SealedSecret (encrypted) to Git
git add sealed-secret.yaml
git commit -m "Add database secret (encrypted)" SOPS + age
SOPS (by Mozilla) encrypts the values in YAML/JSON files while keeping the keys readable. Combined with age (modern encryption), it's an elegant solution:
# Install SOPS and age
# (varies by OS)
# Generate an age key
age-keygen -o key.txt
# Public key: age1ql3z7hjy...
# Configure SOPS for the project
cat > .sops.yaml << 'EOF'
creation_rules:
- path_regex: .*secrets.*\.yaml$
age: age1ql3z7hjy... # Public key
EOF
# Encrypt a file
sops --encrypt secrets.yaml > secrets.enc.yaml
# The encrypted file keeps keys readable:
# db:
# password: ENC[AES256_GCM,data:abc123...,type:str]
# host: ENC[AES256_GCM,data:def456...,type:str]
# Decrypt (requires the private key)
sops --decrypt secrets.enc.yaml External Secrets Operator
External Secrets Operator (ESO) doesn't store secrets in Git at all. It retrieves them from an external secrets manager (AWS Secrets Manager, HashiCorp Vault, Azure Key Vault):
# external-secret.yaml - Reference to an external secret
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: my-secret
spec:
refreshInterval: 1h
secretStoreRef:
name: vault-backend
kind: ClusterSecretStore
target:
name: my-secret # Name of the created K8s Secret
data:
- secretKey: db-password
remoteRef:
key: production/database # Path in Vault
property: password | Solution | Principle | Ideal for |
|---|---|---|
| Sealed Secrets | Asymmetric encryption, decryption in the cluster | Small teams, simple setup |
| SOPS + age | Value encryption in YAML files | Flexibility, multi-cloud |
| External Secrets | Reference to an external secrets manager | Enterprises with existing Vault/AWS SM |
The power of the model - rollback and audit
This is where GitOps truly shines: operations that are complex in a traditional deployment become trivial.
Rollback = git revert
# A deployment caused a problem in production?
# Don't panic. It's a simple git revert.
# See deployment history
git log --oneline
# a1b2c3d Update image v2.3.0
# d4e5f6g Add environment variable LOG_LEVEL
# h7i8j9k Update image v2.2.0
# Undo the last deployment
git revert HEAD
# [main k1l2m3n] Revert "Update image v2.3.0"
# Push - the GitOps agent detects the change and rolls back automatically
git push
# Within seconds, prod is back to v2.2.0
# No need to find the right kubectl command
# No need to search for the right Docker image
# Git has ALL the history Audit trail = git log
# Who changed what, when, and why?
git log --format="%h %an %ad %s" --date=short
# a1b2c3d Alice 2026-03-09 Update image v2.3.0
# d4e5f6g Bob 2026-03-08 Add variable LOG_LEVEL=debug
# h7i8j9k Alice 2026-03-07 Update image v2.2.0
# l4m5n6o Carol 2026-03-05 Increase replicas 3 -> 5
# Who touched the production config?
git log --oneline -- overlays/production/
# What exactly changed in this deployment?
git diff d4e5f6g..a1b2c3d Multi-environment - branches vs folders
How do you manage dev, staging and production in a GitOps repo? Two approaches exist:
Branch approach (not recommended)
# dev branch -> deploys to dev
# staging branch -> deploys to staging
# main branch -> deploys to production
# Problem: promotions are done via merge/cherry-pick
# between branches. It's fragile and error-prone.
# Branches diverge easily. Folder approach (recommended)
# A single branch (main), folders per environment
# overlays/dev/ -> deploys to dev
# overlays/staging/ -> deploys to staging
# overlays/production/ -> deploys to production
# Promotion: modify the image tag in the next folder
# 1. Test in dev : modify overlays/dev/kustomization.yaml
# 2. Promote : copy the same tag to overlays/staging/
# 3. Ship to prod : copy the tag to overlays/production/
# Advantage: a single git log shows the full history
# of all environments. No branch divergence. Tip: The GitOps community strongly recommends the folder approach. Branches per environment seem intuitive at first, but quickly become a maintenance nightmare when environments diverge.
Practical exercise - Structure a GitOps repo
Create a GitOps repository for a fictional web application called "tavern-api" (the realm's Tavern API) with Kustomize and three environments:
- Create the base/ + overlays/ structure (dev, staging, production)
- Write the base manifests (deployment, service)
- Customize each environment (replicas, resources)
- Simulate a promotion from dev to production
- Simulate a rollback with git revert
Step 1 - Create the structure
# Create the repository
mkdir tavern-gitops
cd tavern-gitops
git init -b main
# Create the directory tree
mkdir -p base
mkdir -p overlays/dev
mkdir -p overlays/staging
mkdir -p overlays/production Step 2 - The base manifests
# base/deployment.yaml
cat > base/deployment.yaml << 'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: tavern-api
spec:
replicas: 1
selector:
matchLabels:
app: tavern-api
template:
metadata:
labels:
app: tavern-api
spec:
containers:
- name: tavern-api
image: tavern-api:v1.0.0
ports:
- containerPort: 8080
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 200m
memory: 256Mi
EOF
# base/service.yaml
cat > base/service.yaml << 'EOF'
apiVersion: v1
kind: Service
metadata:
name: tavern-api
spec:
selector:
app: tavern-api
ports:
- port: 80
targetPort: 8080
EOF
# base/kustomization.yaml
cat > base/kustomization.yaml << 'EOF'
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
EOF
git add base/
git commit -m "Add tavern-api base manifests" Step 3 - Per-environment overlays
# overlays/dev/kustomization.yaml
cat > overlays/dev/kustomization.yaml << 'EOF'
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
namespace: tavern-dev
images:
- name: tavern-api
newTag: v1.0.0
EOF
# overlays/staging/kustomization.yaml
cat > overlays/staging/kustomization.yaml << 'EOF'
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
namespace: tavern-staging
images:
- name: tavern-api
newTag: v1.0.0
patches:
- path: replicas-patch.yaml
EOF
cat > overlays/staging/replicas-patch.yaml << 'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: tavern-api
spec:
replicas: 2
EOF
# overlays/production/kustomization.yaml
cat > overlays/production/kustomization.yaml << 'EOF'
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
namespace: tavern-prod
images:
- name: tavern-api
newTag: v1.0.0
patches:
- path: replicas-patch.yaml
- path: resources-patch.yaml
EOF
cat > overlays/production/replicas-patch.yaml << 'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: tavern-api
spec:
replicas: 5
EOF
cat > overlays/production/resources-patch.yaml << 'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: tavern-api
spec:
template:
spec:
containers:
- name: tavern-api
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: "1"
memory: 1Gi
EOF
git add overlays/
git commit -m "Add dev, staging and production overlays" Step 4 - Simulate a promotion
# New version tested in dev
sed -i 's/newTag: v1.0.0/newTag: v1.1.0/' overlays/dev/kustomization.yaml
git add overlays/dev/
git commit -m "Deploy tavern-api v1.1.0 to dev"
# Promote to staging
sed -i 's/newTag: v1.0.0/newTag: v1.1.0/' overlays/staging/kustomization.yaml
git add overlays/staging/
git commit -m "Promote tavern-api v1.1.0 to staging"
# Promote to production
sed -i 's/newTag: v1.0.0/newTag: v1.1.0/' overlays/production/kustomization.yaml
git add overlays/production/
git commit -m "Promote tavern-api v1.1.0 to production" Step 5 - Simulate a rollback
# Bug in prod! Undo the last deployment
git revert HEAD --no-edit
# [main abc1234] Revert "Promote tavern-api v1.1.0 to production"
# Verify that prod is back to v1.0.0
grep newTag overlays/production/kustomization.yaml
# newTag: v1.0.0
# The complete history shows everything
git log --oneline
# abc1234 Revert "Promote tavern-api v1.1.0 to production"
# def5678 Promote tavern-api v1.1.0 to production
# ghi9012 Promote tavern-api v1.1.0 to staging
# jkl3456 Deploy tavern-api v1.1.0 to dev
# mno7890 Add dev, staging and production overlays
# pqr1234 Add tavern-api base manifests Concept summary
| Concept | Description |
|---|---|
| GitOps | Philosophy where Git is the source of truth for infrastructure |
| Pull model | An agent in the cluster watches Git and applies changes |
| Push model | CI/CD pushes changes to the cluster (traditional) |
| ArgoCD | Popular GitOps tool with rich web interface |
| Flux | CNCF GitOps tool, 100% Kubernetes-native |
| Kustomize | YAML manifest customization tool (base + overlays) |
| Sealed Secrets | Asymmetric encryption of secrets for Git |
| SOPS + age | Value encryption in YAML files |
| External Secrets | Retrieving secrets from an external manager |
| Self-healing | Automatic correction of manual modifications |
| Folders vs branches | Folders per environment are preferred over branches |
The Master of Currents dims the great animated map with a wave of his hand. The room returns to calm. "You understand now why they're called the Currents of Destiny. Every commit in your GitOps repository is a current that shapes the realm's infrastructure. No dark magic, no commands frantically typed on a burning server. Just versioned scrolls that become reality."
"The most beautiful part is that everything is in the history. Who changed what, when, why. If something breaks, you follow the current back. A simple git revert, and the realm regains its stability. That's the peace of mind that only a Master of Currents can offer."
He hands you a medallion engraved with a tree whose branches transform into luminous streams. "You've learned to govern infrastructure through the Currents. Use this power wisely."