GitOps Cheat Sheet
Principles and tooling for using Git as the single source of truth for declarative infrastructure and application deployments.
Core Principles
What defines a GitOps workflow.
- Declarative desired state- The entire system state is described declaratively (YAML/manifests), not as imperative steps
- Git as source of truth- A Git repository is the single, versioned, auditable record of the desired state
- Automated reconciliation- An agent (operator) continuously compares live state to Git and converges toward it
- Pull-based deployment- The cluster pulls changes from Git rather than CI pushing credentials into the cluster
- Observability & drift detection- The operator surfaces when live state diverges from Git ('drift') and can auto-correct it
- Everything via pull request- Changes are proposed, reviewed, and merged as PRs, giving a full audit trail
Argo CD Application
Declare an app that Argo CD syncs from Git.
apiVersion: argoproj.io/v1alpha1kind: Applicationmetadata: name: myapp namespace: argocdspec: project: default source: repoURL: https://github.com/org/myapp-manifests.git targetRevision: main path: overlays/production destination: server: https://kubernetes.default.svc namespace: myapp syncPolicy: automated: prune: true selfHeal: true
Flux Kustomization
Declare a Flux source and reconciliation target.
apiVersion: source.toolkit.fluxcd.io/v1kind: GitRepositorymetadata: name: myappspec: interval: 1m url: https://github.com/org/myapp-manifests ref: branch: main---apiVersion: kustomize.toolkit.fluxcd.io/v1kind: Kustomizationmetadata: name: myappspec: interval: 5m path: ./overlays/production sourceRef: kind: GitRepository name: myapp prune: true
Common Tooling
The GitOps ecosystem.
- Argo CD- Kubernetes-native GitOps operator with a UI, sync waves, and app-of-apps pattern
- Flux- Lightweight GitOps operator built around Kustomize/Helm controllers
- Kustomize- Overlay-based YAML customization without templating, often used per-environment
- Helm- Templated Kubernetes packaging, frequently combined with GitOps operators
- selfHeal- Argo CD setting that automatically reverts manual cluster changes back to match Git
Argo CD Sync Waves & Hooks
Order resource application and run one-off jobs (migrations) at specific points in the sync lifecycle.
apiVersion: v1kind: Namespacemetadata: name: myapp annotations: argocd.argoproj.io/sync-wave: "-1" # applied before wave 0 resources---apiVersion: batch/v1kind: Jobmetadata: name: db-migrate annotations: argocd.argoproj.io/hook: PreSync # runs before the main sync argocd.argoproj.io/hook-delete-policy: HookSucceededspec: template: spec: containers: - name: migrate image: myapp-migrator:1.4.0 command: ["./migrate", "up"] restartPolicy: Never
Encrypting Secrets for Git (Sealed Secrets)
Never commit plaintext secrets — encrypt them client-side so only the in-cluster controller can decrypt.
# Create the plaintext Secret locally (never commit this file)kubectl create secret generic db-creds \ --from-literal=password='s3cr3t' --dry-run=client -o yaml > secret.yaml# Encrypt it against the cluster's public key -> safe to commit to Gitkubeseal --format yaml --cert pub-cert.pem < secret.yaml > sealed-secret.yamlrm secret.yaml# In-cluster SealedSecrets controller decrypts sealed-secret.yaml into a# real Secret automatically once Argo CD/Flux applies it -- ciphertext is# only ever decryptable by the controller's private key, not by anyone reading Git.
App-of-Apps / ApplicationSet
Generate one Argo CD Application per cluster/environment from a single templated source instead of hand-maintaining N manifests.
apiVersion: argoproj.io/v1alpha1kind: ApplicationSetmetadata: name: myapp-envsspec: generators: - list: elements: - env: staging cluster: https://staging.k8s.internal - env: production cluster: https://prod.k8s.internal template: metadata: name: 'myapp-{{env}}' spec: project: default source: repoURL: https://github.com/org/myapp-manifests.git targetRevision: main path: 'overlays/{{env}}' destination: server: '{{cluster}}' namespace: myapp syncPolicy: automated: {prune: true, selfHeal: true}
AppProject: Scoping What Can Deploy Where
Restrict which repos, clusters, namespaces, and resource kinds a team's Applications are allowed to touch.
apiVersion: argoproj.io/v1alpha1kind: AppProjectmetadata: name: team-payments namespace: argocdspec: sourceRepos: - 'https://github.com/org/payments-*' destinations: - {server: https://kubernetes.default.svc, namespace: 'payments-*'} clusterResourceWhitelist: - {group: '', kind: Namespace} # no ClusterRoles, no CRDs -- team stays namespace-scoped namespaceResourceBlacklist: - {group: '', kind: ResourceQuota} # platform team owns quotas, not app teams roles: - name: deployer policies: - 'p, proj:team-payments:deployer, applications, sync, team-payments/*, allow' groups: - 'org:payments-team'
Repo Structure Patterns
How teams lay out the Git repo(s) that hold desired state.
- Mono-repo, dir-per-env- One repo, `overlays/staging` / `overlays/production` via Kustomize; simplest to review diffs across envs side by side
- Branch-per-env- `main` -> staging, `production` branch -> prod; promotion is a merge/cherry-pick, but branches drift and merge conflicts get messy
- Poly-repo (app repo + manifests repo)- CI in the app repo builds an image and opens a PR against a separate manifests repo -- keeps app code and deploy config permissions separate
- Hub-and-spoke (app-of-apps)- One root Application points at N child Application manifests, one per service/team, each independently syncable
- Environment promotion via PR- A bot bumps the image tag/commit SHA in the next environment's manifest as a PR, giving an explicit, reviewable promotion gate
Enable selfHeal/drift-correction only after your team trusts the pipeline — during early adoption, run in detect-only mode first so unexpected auto-reverts of manual hotfixes don't cause confusing incidents.