GitOps Patterns
Core Principle
Git is the single source of truth.
- Desired state lives in Git (manifests, Helm values, Kustomize overlays)
- A controller continuously reconciles cluster state to match Git
- No direct kubectl apply in production — all changes via PR
- Audit trail = Git history
ArgoCD — App of Apps Pattern
# Root app manages all other apps
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: root
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/myorg/gitops
targetRevision: main
path: apps/
destination:
server: https://kubernetes.default.svc
namespace: argocd
syncPolicy:
automated:
prune: true
selfHeal: true
# apps/myapp.yaml — child app
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: myapp-prod
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: prod
source:
repoURL: https://github.com/myorg/gitops
targetRevision: main
path: environments/prod/myapp
destination:
server: https://kubernetes.default.svc
namespace: prod
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- PrunePropagationPolicy=foreground
ignoreDifferences:
- group: apps
kind: Deployment
jsonPointers:
- /spec/replicas # managed by HPA
Repo Structure
gitops/
apps/ # ArgoCD app definitions (App of Apps)
myapp.yaml
database.yaml
environments/
prod/
myapp/
kustomization.yaml
deployment-patch.yaml
values.yaml
staging/
myapp/
kustomization.yaml
values.yaml
base/
myapp/
deployment.yaml
service.yaml
kustomization.yaml
Kustomize Overlay
# environments/prod/myapp/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../../base/myapp
patches:
- path: deployment-patch.yaml
images:
- name: ghcr.io/myorg/myapp
newTag: 1.2.3
configMapGenerator:
- name: myapp-config
literals:
- LOG_LEVEL=info
- REPLICAS=3
Secrets with SOPS + age
# Generate age key
age-keygen -o ~/.config/sops/age/keys.txt
# .sops.yaml (in repo root)
cat > .sops.yaml <<EOF
creation_rules:
- path_regex: secrets/.*\.yaml$
age: age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
EOF
# Encrypt secret
sops --encrypt --in-place secrets/prod/db-secret.yaml
# ArgoCD reads SOPS via ksops plugin or ArgoCD-vault-plugin
Flux CD (alternative to ArgoCD)
# GitRepository source
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
name: gitops
namespace: flux-system
spec:
interval: 1m
url: https://github.com/myorg/gitops
ref:
branch: main
secretRef:
name: git-credentials
---
# Kustomization (reconciler)
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: myapp-prod
namespace: flux-system
spec:
interval: 5m
path: ./environments/prod/myapp
prune: true
sourceRef:
kind: GitRepository
name: gitops
healthChecks:
- apiVersion: apps/v1
kind: Deployment
name: myapp
namespace: prod
timeout: 5m
Image Update Automation (Flux)
# Auto-update image tag in Git when new image is pushed
apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImageRepository
metadata:
name: myapp
spec:
image: ghcr.io/myorg/myapp
interval: 1m
---
apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImagePolicy
metadata:
name: myapp
spec:
imageRepositoryRef:
name: myapp
policy:
semver:
range: ">=1.0.0"
---
apiVersion: image.toolkit.fluxcd.io/v1beta1
kind: ImageUpdateAutomation
metadata:
name: myapp
spec:
interval: 5m
sourceRef:
kind: GitRepository
name: gitops
git:
push:
branch: main
commit:
author:
name: fluxbot
email: flux@example.com
messageTemplate: "chore: update myapp to {{range .Updated.Images}}{{.}}{{end}}"
update:
path: ./environments/prod
strategy: Setters
Key Rules
- Never
kubectl apply directly to production — all changes via PR + GitOps sync
- Use
selfHeal: true to auto-revert manual changes (drift correction)
- Separate app config repo from source code repo — different change cadence
- Use branch protection + required reviews on the GitOps repo
- Monitor sync status: alert if ArgoCD/Flux shows
OutOfSync for > 10 minutes