Arc 6 Quest A5

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

  1. Declarative: the desired state of the system is described in files (YAML, JSON, HCL), not in imperative scripts
  2. Versioned: everything is in Git - the complete history of every infrastructure change
  3. Automatic: an agent automatically applies changes when Git is modified
  4. Self-reconciling: if someone manually modifies the infrastructure, the agent restores it to the state described by Git

GitOps transforms Git into a control panel for your entire infrastructure. No need for SSH access to servers, no need for manual deployment scripts. Git is the only entry point.

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

With GitOps, git log becomes your complete audit journal. Every infrastructure change is tracked, attributed to an author, and can be undone. This is exactly what security auditors love.

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:

  1. Create the base/ + overlays/ structure (dev, staging, production)
  2. Write the base manifests (deployment, service)
  3. Customize each environment (replicas, resources)
  4. Simulate a promotion from dev to production
  5. 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."