Push to Git, watch your cluster update. That’s the GitOps promise. Here’s how to actually implement it.

What GitOps Is

GitOps means:

  • Git is the source of truth for infrastructure and application state
  • Changes happen through Git (PRs, not kubectl apply)
  • A controller watches Git and reconciles cluster state
  • Drift is automatically corrected

The cluster converges to match what’s in Git, continuously.

Why GitOps

Over kubectl apply

1
2
3
4
5
6
# Bad: Who ran this? When? From where?
kubectl apply -f deployment.yaml

# Good: PR reviewed, approved, merged, tracked forever
git commit -m "Scale API to 5 replicas"
git push

Over CI-Push

Traditional CI/CD pushes to the cluster:

CodeCIBuildPushImagekubectlapply

GitOps inverts control:

CodeCIBuildPushImageGiUtpOdpasCtleCuosGntitetrromlalneirfestWsatchesGit

Benefits:

  • Cluster credentials never leave the cluster
  • CI doesn’t need cluster access
  • Rollback = git revert
  • Audit trail is the Git log

ArgoCD Setup

Install

1
2
3
4
5
6
7
8
9
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

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

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

Create an Application

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# argocd-app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: my-app
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/myorg/my-app-manifests
    targetRevision: main
    path: kubernetes
  destination:
    server: https://kubernetes.default.svc
    namespace: production
  syncPolicy:
    automated:
      prune: true        # Delete resources removed from Git
      selfHeal: true     # Fix drift automatically
    syncOptions:
      - CreateNamespace=true
1
kubectl apply -f argocd-app.yaml

Now any push to main triggers a sync.

Flux Setup

Install

1
2
3
4
5
6
flux bootstrap github \
  --owner=myorg \
  --repository=fleet-infra \
  --branch=main \
  --path=./clusters/production \
  --personal

Define Sources and Kustomizations

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 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/my-app-manifests
  ref:
    branch: main
---
# 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: ./kubernetes
  prune: true
  sourceRef:
    kind: GitRepository
    name: my-app
  targetNamespace: production

Repository Structure

App-of-Apps Pattern

fleetac-plrpuesspbtspavetrseraoersgd/awlsp/ikukpeatrnucuibyaogsts//sgd/titdskdk/ikpukpoooeeueunuacuamnmprspsgsttsti/ilvtlt/tcitczzoioooohoohaaycmymmenmettmeimiisisiie.zezzz/oonyanaaanntattttt...mi.iiiyyyloyoooaaanannnmmm.m...lllylyyyaaaammmmllll

Kustomize Overlay

1
2
3
4
5
6
7
8
9
# apps/overlays/production/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: production
resources:
  - ../../base/api
  - ../../base/web
patches:
  - path: patches/api-replicas.yaml
1
2
3
4
5
6
7
# apps/overlays/production/patches/api-replicas.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
spec:
  replicas: 5

Image Automation

Update image tags automatically when new versions are pushed.

Flux Image Automation

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# Image repository to watch
apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImageRepository
metadata:
  name: my-app
  namespace: flux-system
spec:
  image: myregistry/my-app
  interval: 5m
---
# Policy for which tags to use
apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImagePolicy
metadata:
  name: my-app
  namespace: flux-system
spec:
  imageRepositoryRef:
    name: my-app
  policy:
    semver:
      range: ">=1.0.0"
---
# Auto-update manifests
apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImageUpdateAutomation
metadata:
  name: my-app
  namespace: flux-system
spec:
  interval: 5m
  sourceRef:
    kind: GitRepository
    name: my-app
  git:
    checkout:
      ref:
        branch: main
    commit:
      author:
        email: flux@example.com
        name: Flux
      messageTemplate: 'Update image to {{.NewTag}}'
    push:
      branch: main
  update:
    path: ./kubernetes
    strategy: Setters

In your deployment, mark the image:

1
2
3
4
spec:
  containers:
    - name: app
      image: myregistry/my-app:1.2.3 # {"$imagepolicy": "flux-system:my-app"}

Flux updates the tag and commits to Git.

Secrets Management

Don’t put secrets in Git. Options:

Sealed Secrets

1
2
3
4
5
# Install
kubectl apply -f https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.24.0/controller.yaml

# Create sealed secret
kubeseal --format yaml < secret.yaml > sealed-secret.yaml

The sealed secret can safely go in Git.

External Secrets

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: my-secret
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: aws-secrets-manager
    kind: SecretStore
  target:
    name: my-secret
  data:
    - secretKey: password
      remoteRef:
        key: production/my-app
        property: password

SOPS

Encrypt secrets in Git with SOPS:

1
2
3
4
# Encrypt
sops --encrypt --in-place secrets.yaml

# Flux decrypts automatically with configured key

Deployment Strategies

Progressive Delivery with Argo Rollouts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: my-app
spec:
  replicas: 5
  strategy:
    canary:
      steps:
        - setWeight: 20
        - pause: {duration: 5m}
        - setWeight: 40
        - pause: {duration: 5m}
        - setWeight: 60
        - pause: {duration: 5m}
        - setWeight: 80
        - pause: {duration: 5m}
  selector:
    matchLabels:
      app: my-app
  template:
    # Pod template

Blue-Green

1
2
3
4
5
strategy:
  blueGreen:
    activeService: my-app-active
    previewService: my-app-preview
    autoPromotionEnabled: false

Common Patterns

Environment Promotion

1
2
3
4
# Staging tested, promote to production
git checkout main
git merge staging
git push

ArgoCD syncs production automatically.

Rollback

1
2
3
# Revert the bad commit
git revert HEAD
git push

Cluster rolls back to previous state.

Drift Detection

1
2
3
4
5
# ArgoCD
argocd app diff my-app

# Flux
flux diff kustomization my-app

The GitOps Checklist

  • All manifests in Git (no manual kubectl)
  • GitOps controller installed (ArgoCD or Flux)
  • Automated sync enabled
  • Self-heal enabled (drift correction)
  • Secrets managed securely (Sealed/External/SOPS)
  • Image automation configured
  • PR workflow for changes
  • Rollback tested

Git becomes your deployment interface. Everything else follows.


GitOps isn’t just a deployment method — it’s a philosophy: if it’s not in Git, it doesn’t exist. Make your cluster a function of your repository.