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 stateChanges happen through Git (PRs, not kubectl apply)A controller watches Git and reconciles cluster stateDrift 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:
C o d e → C I → B u i l d → P u s h I m a g e → k u b e c t l a p p l y
GitOps inverts control:
C o d e → C I → B u i l d → P u s h I m a g e → G i U t p O d p a s C t l e C u o s G ↓ n ↓ t i t e t r r o m l a l n e i r f e ← s t W s a t c h e s G i t
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# f ├ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └ l ─ ─ e ─ ─ e t a ├ │ │ │ │ │ │ │ └ c ├ │ └ - p ─ ─ l ─ ─ r p ─ ─ u ─ ─ e s s p b ├ │ │ │ └ ├ │ │ └ t s └ p └ a ─ ─ v ─ ─ e t ─ r ─ s ─ ─ e ─ ─ r a ─ o ─ e r s g d / a ├ ├ └ w ├ └ l s ├ └ p ├ └ / i k u k p ─ ─ ─ e ─ ─ a t ─ ─ r ─ ─ n u c u i ─ ─ ─ b ─ ─ y a ─ ─ o ─ ─ g s t s / / s g d / t i t d s k d k / i k p u k p o o o e e u e u n u a c u a m n m p r s p s g s t t s t i / i l v t l t / t c i t c z z o i o o o o h o o h a a y c m y m m e n m e t t m e i m i i s i s i i e . z e z z z / o o n y a n a a a n n t a t t t t t . . . m i . i i i y y y l o y o o o a a a n a n n n m m m . m . . . l l l y l y y y a a a a m m m m l l l l 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# 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# 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.