Skip to main content

GitOps — ArgoCD, Flux, and Declarative Infrastructure

· 7 min read
Goel Academy
DevOps & Cloud Learning Hub

Your Kubernetes manifests live in a Git repository. Your CI pipeline builds images and updates tags. But who applies those manifests to the cluster? If the answer is "someone runs kubectl apply" or "the CI pipeline has cluster credentials," you have a deployment process that is fragile, hard to audit, and impossible to roll back cleanly. GitOps fixes all of this by making Git the single source of truth for your entire deployment state.

GitOps Principles

GitOps is a set of practices for managing infrastructure and application deployments using Git as the source of truth. The term was coined by Weaveworks in 2017, but the underlying ideas — version control, code review, and automation — are as old as software engineering itself.

The four core principles:

PrincipleWhat It Means
DeclarativeThe entire system is described declaratively (YAML manifests, Helm charts, Kustomize overlays)
Versioned and ImmutableThe desired state is stored in Git, giving you a complete audit trail and the ability to revert
Pulled AutomaticallyAn agent in the cluster continuously pulls the desired state from Git and applies it
Continuously ReconciledThe agent detects drift between the actual state and the desired state and corrects it automatically

Push vs Pull Deployment

Traditional CI/CD (Push):
Developer → Git Push → CI Pipeline → kubectl apply → Cluster

CI has cluster credentials
(security risk)

GitOps (Pull):
Developer → Git Push → Git Repository ← Agent polls → Cluster

Agent runs INSIDE the cluster
(no external credentials needed)

The key difference: in push-based deployment, your CI pipeline needs cluster credentials. In pull-based GitOps, the agent runs inside the cluster and pulls changes from Git. The cluster is never exposed to external systems.

ArgoCD Installation

ArgoCD is the most popular GitOps tool for Kubernetes. It provides a web UI, CLI, and rich sync policies.

# Install ArgoCD into the argocd namespace
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

# Wait for all pods to be ready
kubectl wait --for=condition=available deployment -l app.kubernetes.io/part-of=argocd -n argocd --timeout=300s

# Get the initial admin password
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d

# Port-forward the ArgoCD UI
kubectl port-forward svc/argocd-server -n argocd 8080:443

# Access the UI at https://localhost:8080
# Username: admin
# Password: (from the command above)

# Install the ArgoCD CLI
curl -sSL -o argocd https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-amd64
chmod +x argocd && sudo mv argocd /usr/local/bin/

# Login via CLI
argocd login localhost:8080 --username admin --password <password> --insecure

ArgoCD Application CRD

ArgoCD uses a custom resource called Application to define what to deploy and where:

# argocd/applications/my-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/k8s-manifests.git
targetRevision: main
path: apps/my-app/overlays/production

destination:
server: https://kubernetes.default.svc
namespace: my-app

syncPolicy:
automated:
prune: true # Delete resources removed from Git
selfHeal: true # Revert manual changes in the cluster
allowEmpty: false # Don't sync if source is empty
syncOptions:
- CreateNamespace=true
- PrunePropagationPolicy=foreground
- PruneLast=true
retry:
limit: 5
backoff:
duration: 5s
factor: 2
maxDuration: 3m

ArgoCD Sync Policies

# Manual sync — you decide when to deploy
syncPolicy: {}

# Automated sync — deploy on every Git commit
syncPolicy:
automated:
prune: false # Do NOT delete resources removed from Git
selfHeal: false # Do NOT revert manual kubectl changes

# Automated sync with full reconciliation
syncPolicy:
automated:
prune: true # Delete resources removed from Git
selfHeal: true # Revert manual kubectl changes (drift correction)

# Sync with specific options
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- Validate=true # Validate manifests before applying
- CreateNamespace=true # Create namespace if it doesn't exist
- ServerSideApply=true # Use server-side apply
- RespectIgnoreDifferences=true # Ignore specified field differences

Flux Installation

Flux is the CNCF-graduated alternative to ArgoCD. It is lighter, more composable, and uses native Kubernetes controllers.

# Install Flux CLI
curl -s https://fluxcd.io/install.sh | sudo bash

# Bootstrap Flux into a cluster with a GitHub repository
flux bootstrap github \
--owner=myorg \
--repository=fleet-infra \
--branch=main \
--path=clusters/production \
--personal

# This command:
# 1. Creates the GitHub repo if it doesn't exist
# 2. Installs Flux controllers in the cluster
# 3. Configures Flux to sync from the specified path

# Verify installation
flux check

Flux GitRepository and Kustomization

# clusters/production/my-app-source.yaml
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
name: my-app
namespace: flux-system
spec:
interval: 1m
url: https://github.com/myorg/k8s-manifests.git
ref:
branch: main
secretRef:
name: github-credentials

---
# clusters/production/my-app-kustomization.yaml
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: my-app
namespace: flux-system
spec:
interval: 5m
path: ./apps/my-app/overlays/production
prune: true
sourceRef:
kind: GitRepository
name: my-app
healthChecks:
- apiVersion: apps/v1
kind: Deployment
name: my-app
namespace: my-app
timeout: 3m
retryInterval: 1m

ArgoCD vs Flux Comparison

FeatureArgoCDFlux
Web UIBuilt-in (rich, feature-complete)None (use Weave GitOps for UI)
CLIYesYes
Multi-clusterYes (centralized)Yes (per-cluster agents)
Helm SupportYes (native)Yes (HelmRelease CRD)
KustomizeYes (native)Yes (native)
RBACBuilt-in, SSO integrationKubernetes native RBAC
NotificationsYes (Slack, webhook, etc.)Yes (alert/provider CRDs)
Image AutomationArgoCD Image UpdaterBuilt-in (ImagePolicy CRDs)
CNCF StatusGraduatedGraduated
Learning CurveMediumMedium
Best ForTeams wanting a UI and centralized controlTeams wanting lightweight, composable controllers

App of Apps Pattern

For managing many applications, ArgoCD supports the App of Apps pattern — a parent Application that manages child Applications:

# argocd/app-of-apps.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: app-of-apps
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/myorg/k8s-manifests.git
targetRevision: main
path: argocd/applications # Directory containing Application YAMLs
destination:
server: https://kubernetes.default.svc
namespace: argocd
syncPolicy:
automated:
prune: true
selfHeal: true
Git Repository Structure:
argocd/
app-of-apps.yaml ← Parent Application
applications/
frontend.yaml ← Child Application
backend-api.yaml ← Child Application
redis.yaml ← Child Application
monitoring.yaml ← Child Application
apps/
frontend/
base/
overlays/production/
backend-api/
base/
overlays/production/

Handling Secrets in GitOps

Secrets cannot be stored as plain text in Git. Here are two common solutions:

# Option 1: Sealed Secrets (by Bitnami)
# Encrypts secrets so only the cluster can decrypt them

# Install Sealed Secrets controller
kubectl apply -f https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.24.0/controller.yaml

# Install kubeseal CLI
brew install kubeseal

# Create a normal secret, then seal it
kubectl create secret generic db-credentials \
--from-literal=username=admin \
--from-literal=password=supersecret \
--dry-run=client -o yaml | kubeseal --format yaml > sealed-secret.yaml

# The sealed-secret.yaml is safe to commit to Git
# Only the Sealed Secrets controller in the cluster can decrypt it
# Option 2: SOPS with age encryption
# .sops.yaml — configure SOPS for this repository
creation_rules:
- path_regex: .*\.enc\.yaml$
age: "age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

# Encrypt a secret file
# sops --encrypt --in-place secrets.enc.yaml

# Flux can decrypt SOPS-encrypted files natively:
# clusters/production/my-app-kustomization.yaml
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: my-app
namespace: flux-system
spec:
interval: 5m
path: ./apps/my-app
prune: true
sourceRef:
kind: GitRepository
name: my-app
decryption:
provider: sops
secretRef:
name: sops-age # Kubernetes secret containing the age private key

Common Pitfalls

  1. Storing secrets in plain text — Use Sealed Secrets, SOPS, or an external secrets operator. Never commit unencrypted secrets to Git.
  2. Using latest image tags — GitOps relies on immutable tags. If your image tag does not change, the GitOps agent sees no diff and does not redeploy.
  3. Mixing CI and CD credentials — Your CI pipeline should push images to a registry and update manifests in Git. It should never talk directly to the cluster.
  4. Ignoring drift — If selfHeal is disabled, manual kubectl changes will not be reverted. Enable it in production.
  5. Monorepo sprawl — Keep application code and deployment manifests in separate repositories. This separates developer access from deployment access.

Your deployments are now declarative, versioned, and self-healing. But how do you measure whether your systems are actually reliable enough? In the next post, we will cover SRE principles — SLOs, error budgets, and toil reduction — the framework Google built to keep services running at scale.