0x55aa
← Back to Blog

GitOps: When Git Becomes Your Entire Infrastructure (And Why That's Genius) šŸš€šŸ“¦

•16 min read

GitOps: When Git Becomes Your Entire Infrastructure (And Why That's Genius) šŸš€šŸ“¦

Real confession: Three years into my career as a Technical Lead, I got the dreaded 3 AM call. "Production is down!" I SSH'd into the server, found a config that didn't match our repo, and realized someone had "quickly fixed" something directly in prod without committing it. Two weeks earlier. The fix got overwritten during our last deploy. Users were locked out. No one knew what the old config was. 😱

Boss: "How do we prevent this from happening again?"

Me: "We could... uh... write it down somewhere?"

Senior DevOps Engineer: "Or we could use GitOps and make Git the single source of truth."

Me: "You mean... everything in Git?"

Her: "EVERYTHING. Config, infrastructure, deployments - all in Git. If it's not in Git, it doesn't exist."

Me: 🤯

Welcome to GitOps - where your Git repository isn't just where your code lives, it's your entire deployment truth!

What's GitOps Anyway? šŸ¤”

Traditional deployment (Chaos Mode):

# The old nightmare
Developer: git push to main
Jenkins: *triggers build*
Someone: SSH into server
Someone: Run some kubectl commands
Someone else: Updates a ConfigMap manually
Another person: Changes environment variable
Mystery person: "Quick fix" in production
6 months later: No one knows what's actually running
Production: *breaks mysteriously*
Everyone: ĀÆ\_(惄)_/ĀÆ

GitOps deployment (Zen Mode):

# The new way
Developer: git push to main
Git: "This is the desired state"
ArgoCD/Flux: "Got it! Making production match..."
ArgoCD/Flux: *automatically syncs everything*
Production: *matches Git exactly*
Anyone: "What's in prod?"
Git: "Here's the exact state! (with history!)"
Time travel: git log shows every change ever
Rollback: git revert = instant production rollback

Translation: Git becomes the control center. Want to know what's in production? Check Git. Want to deploy? Update Git. Want to rollback? Revert in Git. Everything else happens automatically! šŸŽÆ

The "Which Version Is Running?" Horror Story šŸ‘»

After deploying Laravel applications to AWS for 3 years, I thought I had deployments figured out. Then this happened:

The Setup:

  • E-commerce API serving 100k+ requests/day
  • Three environments: dev, staging, production
  • Manual kubectl commands for "quick fixes"
  • Config spread across Jenkins, AWS Systems Manager, and various bash scripts

The Disaster:

Monday 9 AM: "Why is the payment gateway failing?"
Me: *checks Git* "Code looks fine..."
Me: *checks production* "Wait, the API key is different!"
Me: "Who changed it?"
Team: *crickets*
Me: *checks AWS SSM* "This doesn't match either!"
Me: *checks Jenkins* "Three different API keys?!"
Boss: "WHICH ONE IS CORRECT?!"
Me: "I... I don't know..."

The aftermath:

  • 4 hours of downtime
  • Lost revenue: ~$15,000
  • Customer complaints: 247
  • Trust in our deployment process: šŸ’€

The root cause: No single source of truth. Changes happened everywhere. Git had one version, production had another, and reality had a third!

Enter GitOps: The Philosophy That Saved My Sanity 🧘

After that disaster, I spent a weekend learning about GitOps. The principles are beautifully simple:

The Four GitOps Principles šŸ“œ

1. Declarative Configuration

# Instead of: "Run these commands to deploy"
kubectl create deployment app
kubectl set image deployment/app app=v2
kubectl scale deployment/app --replicas=3

# GitOps way: "This is what production should look like"
apiVersion: apps/v1
kind: Deployment
metadata:
  name: app
spec:
  replicas: 3
  template:
    spec:
      containers:
      - name: app
        image: myapp:v2
# Git stores this. Tools make it reality.

2. Version Controlled and Immutable

# Every change is a Git commit
git log --oneline production/

abc1234 Scale API to 5 replicas (traffic spike!)
def5678 Update database password (security rotation)
ghi9012 Deploy v2.3.1 (bug fix for checkout)
jkl3456 Rollback to v2.2.0 (v2.3.0 had issues)

# Want to see what changed? Git knows!
# Want to rollback? Revert the commit!
# Want to know who changed it? Git blame!

3. Automatically Applied

# You don't run commands anymore
# You: Push to Git
# GitOps tool: "New commit detected! Syncing..."
# Production: *automatically updates*

# No SSH needed
# No kubectl commands
# No Jenkins jobs
# Just Git!

4. Continuously Reconciled

# Someone makes manual change in production
Production: *differs from Git*
GitOps tool: "Wait, this doesn't match Git!"
GitOps tool: *reverts the manual change*
GitOps tool: "Production synced with Git āœ…"

# Git is the boss. Production obeys!

The beauty: These four principles solve the "which version is running" problem forever! šŸŽ‰

How I Implemented GitOps (The Real Journey) šŸ›¤ļø

Before GitOps: The Deployment Disaster šŸ’€

Our setup:

# Code repo
github.com/company/api-code

# Deploy scripts (scattered everywhere!)
- Jenkins jobs (no version control)
- kubectl commands in Slack (seriously!)
- "Production runbook" Google Doc (outdated)
- AWS CLI commands (in someone's .bash_history)
- ConfigMaps created manually (not in Git)

# Deployment process:
1. Merge PR to main
2. Jenkins builds Docker image
3. Someone runs kubectl commands
4. Someone else updates ConfigMap
5. Hope everything worked
6. Debug when it didn't

# Knowledge location: Senior dev's brain
# Backup: None
# Bus factor: 1 person 😱

After GitOps: The Beautiful New World ✨

New structure:

# Code repo (same)
github.com/company/api-code

# NEW: GitOps repo (the source of truth!)
github.com/company/infrastructure
ā”œā”€ā”€ base/
│   ā”œā”€ā”€ deployment.yaml
│   ā”œā”€ā”€ service.yaml
│   └── configmap.yaml
ā”œā”€ā”€ environments/
│   ā”œā”€ā”€ dev/
│   │   ā”œā”€ā”€ kustomization.yaml
│   │   └── values.yaml
│   ā”œā”€ā”€ staging/
│   │   ā”œā”€ā”€ kustomization.yaml
│   │   └── values.yaml
│   └── production/
│       ā”œā”€ā”€ kustomization.yaml
│       └── values.yaml
└── apps/
    ā”œā”€ā”€ api/
    ā”œā”€ā”€ frontend/
    └── workers/

# Deployment process:
1. Update YAML in GitOps repo
2. Commit and push
3. ArgoCD sees the change
4. ArgoCD deploys automatically
5. Done! āœ…

# Knowledge location: Git repository
# Backup: Git history
# Bus factor: Entire team! šŸŽ‰

Setting Up GitOps with ArgoCD (Real Configuration) āš™ļø

After countless attempts, here's what actually works in production:

Step 1: Install ArgoCD

# Create namespace
kubectl create namespace argocd

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

# Wait for it to be ready
kubectl wait --for=condition=available --timeout=300s \
  deployment/argocd-server -n argocd

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

# Port forward to access UI
kubectl port-forward svc/argocd-server -n argocd 8080:443
# Visit: https://localhost:8080
# Login: admin / <password from above>

Real talk: This took me 10 minutes. Setting up Jenkins took me 10 hours! šŸ˜…

Step 2: Create Your GitOps Repo

# Create a new repo
mkdir infrastructure
cd infrastructure

# Directory structure
mkdir -p {base,environments/{dev,staging,production},apps}

# Base deployment (shared by all environments)
cat > base/deployment.yaml <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
spec:
  replicas: 2  # Default, environments can override
  selector:
    matchLabels:
      app: api
  template:
    metadata:
      labels:
        app: api
    spec:
      containers:
      - name: api
        image: myregistry/api:latest  # Will be overridden
        ports:
        - containerPort: 3000
        env:
        - name: NODE_ENV
          value: production
        - name: DB_HOST
          valueFrom:
            secretKeyRef:
              name: db-credentials
              key: host
EOF

# Production overrides
cat > environments/production/kustomization.yaml <<EOF
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

bases:
  - ../../base

replicas:
  - name: api
    count: 5  # Scale up in production!

images:
  - name: myregistry/api
    newTag: v1.2.3  # Specific version for prod

namespace: production
EOF

# Commit everything
git init
git add .
git commit -m "Initial GitOps setup"
git remote add origin [email protected]:company/infrastructure.git
git push -u origin main

Step 3: Create ArgoCD Application

# application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: api-production
  namespace: argocd
spec:
  # Where to deploy
  destination:
    namespace: production
    server: https://kubernetes.default.svc

  # Where is the config?
  source:
    repoURL: https://github.com/company/infrastructure
    targetRevision: main
    path: environments/production

  # Sync policy
  syncPolicy:
    automated:
      prune: true      # Remove resources deleted from Git
      selfHeal: true   # Revert manual changes
      allowEmpty: false
    syncOptions:
      - CreateNamespace=true
    retry:
      limit: 5
      backoff:
        duration: 5s
        factor: 2
        maxDuration: 3m

Deploy it:

kubectl apply -f application.yaml

Watch the magic:

# ArgoCD immediately syncs
# No manual kubectl commands needed!
# Production now matches Git!

# Check the UI
# You'll see your app syncing automatically
# Every change to Git = automatic deployment

Real GitOps Workflows That Changed My Life šŸŽÆ

Workflow #1: Deploy New Version

Before GitOps (20 steps, 15 minutes):

# Build image
docker build -t api:v1.2.3 .
docker push api:v1.2.3

# SSH into bastion
ssh bastion.prod.company.com

# Update deployment
kubectl set image deployment/api api=api:v1.2.3

# Check if it worked
kubectl rollout status deployment/api

# Update ConfigMap (forgot this twice!)
kubectl edit configmap api-config

# Restart deployment (because ConfigMap doesn't auto-reload)
kubectl rollout restart deployment/api

# Check logs
kubectl logs -f deployment/api

# Something broke
kubectl rollback deployment/api

# Try again...

After GitOps (1 step, 30 seconds):

# Update image tag in Git
cd infrastructure/environments/production
sed -i 's/newTag: v1.2.2/newTag: v1.2.3/' kustomization.yaml

# Commit
git add kustomization.yaml
git commit -m "Deploy API v1.2.3 to production"
git push

# ArgoCD does everything automatically
# - Detects change
# - Applies to cluster
# - Shows status in UI
# - Notifies Slack on success/failure

# Done! āœ…

Time saved: 90%. Stress reduced: 99%. Mistakes prevented: All of them! šŸŽ‰

Workflow #2: The 3-Second Rollback

The scenario: New version breaks in production!

Before GitOps:

# Panic!
# Find the old version number
# kubectl set image... (was it v1.2.1 or v1.2.0?)
# Wait for rollout
# Did the rollout work?
# Check logs frantically
# 15 minutes of chaos

After GitOps:

# Calm rollback
git revert HEAD
git push

# ArgoCD: "Reverting production to previous state..."
# 30 seconds later: Back to working version
# All automatic!

Real story: We had a payment bug in production. I reverted the Git commit, pushed, and production was fixed before my manager finished asking "Can we roll back?" šŸ˜Ž

Workflow #3: Environment Promotion

The goal: Test in dev, promote to staging, then production!

Before GitOps:

# Deploy to dev
kubectl apply -f dev-config.yaml

# Looks good? Deploy to staging
# Copy dev-config.yaml to staging-config.yaml
# Edit staging-config.yaml (manually! errors!)
# kubectl apply -f staging-config.yaml

# Staging good? Deploy to production
# Copy staging-config.yaml to prod-config.yaml
# Edit prod-config.yaml (more manual changes!)
# kubectl apply -f prod-config.yaml
# Cross fingers!

After GitOps:

# Deploy to dev (commit to dev branch)
git checkout dev
echo "newTag: v1.2.3" >> environments/dev/kustomization.yaml
git commit -m "Deploy v1.2.3 to dev"
git push

# ArgoCD syncs dev automatically
# Test in dev environment...

# Looks good? Promote to staging
git checkout staging
git merge dev
git push

# ArgoCD syncs staging automatically
# Test in staging...

# Staging good? Promote to production
git checkout main
git merge staging
git push

# ArgoCD syncs production
# Done! Same config across all environments!

The beauty: You're not copying configs. You're promoting Git commits. Same YAML, same configuration, just different environment overlays! šŸŽÆ

GitOps Patterns I Wish I Knew Earlier šŸ’”

Pattern #1: Separate App Code and Config

Wrong way (what I did first):

# Everything in one repo
github.com/company/api
ā”œā”€ā”€ src/           # Application code
ā”œā”€ā”€ Dockerfile
└── k8s/           # Kubernetes configs
    └── deployment.yaml

# Problems:
# - Every code change triggers infrastructure sync
# - Can't deploy app without changing infrastructure
# - Different teams need different permissions

Right way:

# App repo (developers work here)
github.com/company/api
ā”œā”€ā”€ src/
ā”œā”€ā”€ Dockerfile
└── .github/workflows/
    └── build.yml   # Builds image, updates GitOps repo

# GitOps repo (operations truth)
github.com/company/infrastructure
ā”œā”€ā”€ apps/
│   └── api/
│       ā”œā”€ā”€ base/
│       └── environments/

Why it's better:

  • Developers push code → CI builds image
  • CI updates GitOps repo with new image tag
  • ArgoCD deploys automatically
  • Clean separation of concerns!

Pattern #2: Environment-Specific Overlays with Kustomize

# Base configuration (shared)
base/
ā”œā”€ā”€ deployment.yaml      # 2 replicas, generic config
ā”œā”€ā”€ service.yaml
└── kustomization.yaml

# Dev overrides
environments/dev/
└── kustomization.yaml
    bases: [../../base]
    replicas: 1          # Small footprint
    resources:
      limits: {cpu: 100m, memory: 128Mi}

# Production overrides
environments/production/
└── kustomization.yaml
    bases: [../../base]
    replicas: 5          # High availability
    resources:
      limits: {cpu: 1000m, memory: 2Gi}
    HPA: {min: 5, max: 50}  # Auto-scaling

The magic: One base config, multiple environments, no duplication! šŸŽÆ

Pattern #3: Progressive Delivery with ArgoCD Rollouts

# Instead of: All-at-once deployment
# Use: Canary releases

apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: api
spec:
  replicas: 10
  strategy:
    canary:
      steps:
      - setWeight: 20      # 20% traffic to new version
      - pause: {duration: 5m}
      - setWeight: 50      # 50% traffic
      - pause: {duration: 5m}
      - setWeight: 100     # Full rollout
  template:
    spec:
      containers:
      - name: api
        image: api:v1.2.3

What this does:

  1. Deploys new version to 20% of pods
  2. Waits 5 minutes (checks metrics)
  3. If no errors, increases to 50%
  4. Waits again
  5. Full rollout if all good
  6. Auto-rollback if errors detected!

Real impact: Caught a memory leak that only appeared under load. Canary deployment auto-rolled back before 80% of users were affected! šŸš€

Common GitOps Mistakes (I Made Them All) 🚨

Mistake #1: Storing Secrets in Git

What I did:

# DON'T DO THIS!
apiVersion: v1
kind: Secret
metadata:
  name: db-credentials
data:
  password: cGFzc3dvcmQxMjM=  # Base64 is NOT encryption!
# Pushed to Git... security audit found it... 😱

What you should do:

# Use Sealed Secrets or External Secrets Operator
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
  name: db-credentials
spec:
  encryptedData:
    password: AgBX5F2G3k...  # Actually encrypted!
    # Can only be decrypted by cluster
    # Safe to commit to Git!

Or better:

# External Secrets (pull from AWS Secrets Manager)
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: db-credentials
spec:
  secretStoreRef:
    name: aws-secrets-manager
  target:
    name: db-credentials
  data:
  - secretKey: password
    remoteRef:
      key: production/database
      property: password
# Secrets stay in AWS, not Git!

Mistake #2: Auto-Sync Everything (Even Production)

My first ArgoCD config:

syncPolicy:
  automated:
    selfHeal: true   # Auto-fix drift
    prune: true      # Auto-delete
# Applied to PRODUCTION immediately!
# No review process!
# Accidentally deleted database StatefulSet!
# 😱😱😱

Better approach:

# Production: Manual sync required
spec:
  syncPolicy:
    # automated: false  # Require manual approval!
    syncOptions:
      - CreateNamespace=true
      - PruneLast=true  # Delete resources last
      - RespectIgnoreDifferences=true

# Dev/Staging: Auto-sync is fine!

Lesson: Production changes should require human approval! Gate the merge to main with pull request reviews!

Mistake #3: Not Using App of Apps Pattern

What I did first:

# Created 47 Application resources manually
kubectl apply -f api-prod.yaml
kubectl apply -f frontend-prod.yaml
kubectl apply -f worker-prod.yaml
# ... 44 more times
# Nightmare to manage!

App of Apps pattern:

# One "root" app that manages all other apps!
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: root-production
spec:
  source:
    repoURL: https://github.com/company/infrastructure
    path: apps/production
  destination:
    server: https://kubernetes.default.svc
  syncPolicy:
    automated: {}

# apps/production/ contains all app definitions
# Add new app? Just commit a new YAML file!
# ArgoCD auto-discovers and deploys!

The Bottom Line: Why GitOps Changed My Career šŸ’”

Before GitOps:

  • 3 AM production incidents: Monthly
  • "What's actually running in prod?": Unknown
  • Deployment confidence: Low
  • Time spent on deploys: ~8 hours/week
  • Manual SSH commands: Daily
  • Production drift: Constant
  • Rollback time: 15-20 minutes
  • My stress level: šŸ“ˆšŸ“ˆšŸ“ˆ

After GitOps:

  • 3 AM incidents: Rare (and easy to fix!)
  • "What's running in prod?": git log production/
  • Deployment confidence: High
  • Time spent on deploys: ~1 hour/week
  • Manual SSH commands: Never
  • Production drift: Impossible (ArgoCD prevents it!)
  • Rollback time: 30 seconds (git revert)
  • My stress level: šŸ“‰šŸ“‰šŸ“‰

What you learned today:

  1. GitOps = Git as single source of truth
  2. Declarative configs > imperative commands
  3. Automatic sync > manual deploys
  4. Git history = production history
  5. Rollback = git revert (30 seconds!)
  6. No more "what's running in prod?" mystery
  7. Sleep better knowing Git controls everything!

Most importantly: You'll never again wonder what version is running in production. Git knows. Git is always right. Git is the truth! šŸŽÆ

Your GitOps Action Plan šŸš€

This weekend:

  1. Create infrastructure repo
  2. Install ArgoCD in a test cluster
  3. Deploy your first app via Git
  4. Make a change, watch auto-sync
  5. Do a rollback with git revert
  6. Feel the power! ⚔

Next week:

  1. Move dev environment to GitOps
  2. Learn Kustomize for environment overrides
  3. Set up CI to update GitOps repo
  4. Use Sealed Secrets for credentials
  5. Never run kubectl manually again!

Next month:

  1. GitOps for all environments
  2. Implement App of Apps pattern
  3. Add progressive delivery (canaries!)
  4. Set up automatic image updates
  5. Become the GitOps expert on your team! šŸŽ“

Resources to Get Started šŸ“š

Tools:

Learning:

My setup:

  • ArgoCD for GitOps orchestration
  • Kustomize for environment overlays
  • Sealed Secrets for credentials
  • GitHub Actions to update GitOps repo
  • Slack notifications for sync status

Final Thoughts šŸ’­

The stat that convinced me:

"Organizations using GitOps deploy 30x more frequently, have 2x lower change failure rate, and recover 24x faster from incidents."

— Accelerate: State of DevOps Report

Translation: GitOps makes you OBJECTIVELY better at deployments!

Before GitOps:

  • "What's in prod?" → No idea
  • "Who changed it?" → Mystery
  • "When did it break?" → Unknown
  • "How do we rollback?" → Panic
  • "Can we deploy safely?" → Probably not
  • Confidence level: šŸ“‰

After GitOps:

  • "What's in prod?" → git log
  • "Who changed it?" → git blame
  • "When did it break?" → git log --since="2 hours ago"
  • "How do we rollback?" → git revert HEAD && git push
  • "Can we deploy safely?" → Yes! (auto-tested in dev/staging first)
  • Confidence level: šŸ“ˆ

The truth: GitOps isn't just about automation. It's about:

  • Confidence in every deploy
  • Knowing exactly what's running
  • Instant rollbacks when needed
  • Auditable history forever
  • Sleeping peacefully at night! 😓

After 7 years of production deployments, countless incidents, and many sleepless nights debugging "what changed?", GitOps is the workflow I wish I had discovered on day one!

Your infrastructure is code. Treat it like code. Version it. Review it. Test it. Deploy it from Git. That's GitOps! šŸŽÆ


Ready to GitOps all the things? Connect with me on LinkedIn - share your GitOps journey!

Want to see my GitOps setup? Check out my GitHub - infrastructure as code everywhere!

Now go make Git your single source of truth and never wonder "what's running in prod?" again! šŸš€šŸ“¦āœØ