Gendi Notes

GitOps

The cluster pulls its desired state from Git instead of a pipeline pushing into the cluster — which turns "what is deployed?" from an investigation into a `git log`.

Level 3 concept gitopscdkubernetesargocd

What is it?#

GitOps means:

  1. The entire desired state of the system is declared in Git.
  2. An agent inside the cluster continuously compares Git to reality and closes the gap.
  3. Nothing else has write access to the cluster.
PUSH-BASED CD (traditional) PULL-BASED CD (GitOps) ─────────────────────────── ────────────────────── git push │ git push ↓ │ CI pipeline ↓ │ holds cluster Git repo ← the only input │ CREDENTIALS │ ↓ │ agent POLLS / is notified kubectl apply ──→ cluster ↓ Argo CD / Flux (inside the cluster) credentials live OUTSIDE │ the cluster, in CI ↓ reconciles, forever cluster no external credentials needed

Why it exists#

Push-based CD works, and then accumulates four specific problems:

ProblemWhy it happensWhat GitOps changes
CI holds production credentialsIt must, to kubectl applyCredentials never leave the cluster
Nobody knows what is deployedThe cluster is the only source of truthGit is the source of truth
Manual changes persist silentlyNothing compares against intentDrift is detected and reverted
Rollback means re-running a pipelineThe old artefact may not rebuildgit revert, and the agent converges

What it is made of#

PieceJob
Config repositoryThe desired state. Often separate from application source
AgentArgo CD or Flux, running in the cluster
SyncApply Git state to the cluster
Drift detectionNotice reality differs from Git
Self-healRevert that difference automatically
Health assessmentDecide whether the applied resources are actually working

The two repositories#

This split confuses people at first, and it is the important structural choice:

APP REPO CONFIG REPO ──────── ─────────── src/ apps/web/deployment.yaml Dockerfile apps/web/service.yaml .github/workflows/ci.yml apps/web/kustomization.yaml │ ↑ │ CI: test, build, push image │ │ │ └──→ registry: web:abc123 ────────┘ then bump the tag in the config repo │ ↓ agent watches THIS repo cluster

How to use it — Argo CD#

Install and reach it
kubectl create namespace argocd
kubectl apply -n argocd -f \
  https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

kubectl -n argocd get secret argocd-initial-admin-secret \
  -o jsonpath='{.data.password}' | base64 -d; echo

kubectl port-forward -n argocd svc/argocd-server 8080:443
An Application — the unit Argo CD manages
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: web
  namespace: argocd
spec:
  project: default

  source:
    repoURL: https://github.com/you/config-repo
    targetRevision: main            # a branch, tag or commit
    path: apps/web                  # directory inside the repo

  destination:
    server: https://kubernetes.default.svc
    namespace: production

  syncPolicy:
    automated:
      prune: true                   # delete resources removed from Git
      selfHeal: true                # revert manual cluster changes
    syncOptions:
      - CreateNamespace=true
    retry:
      limit: 5
      backoff: { duration: 5s, factor: 2, maxDuration: 3m }

The app-of-apps pattern#

Managing 50 Applications by hand defeats the purpose. Instead, one Application whose contents are other Applications:

root Application (points at clusters/prod/) │ ├──→ Application: monitoring → charts/kube-prometheus-stack ├──→ Application: ingress → charts/ingress-nginx ├──→ Application: web → apps/web └──→ Application: api → apps/api Adding a service = one file in the config repo. Nothing else.

Secrets — the problem GitOps creates#

"Everything in Git" collides with "never commit secrets". Four answers, worst to best:

ApproachHow it worksVerdict
Plain Secret in Gitbase64 is not encryptionNever
Sealed SecretsEncrypt with a cluster public key; only the in-cluster controller can decryptFine. Ciphertext is per-cluster, so rotation is awkward
SOPS (+ age/KMS)Encrypt values in the YAML itself; Flux/Argo decrypt at applyGood. Readable diffs, keys in a KMS
External Secrets OperatorGit holds only a reference; the operator fetches from Vault/ASM/GSMBest for teams. Rotation happens outside Git entirely

What goes wrong#

SymptomCauseFix
OutOfSync foreverA controller or webhook mutates the object after applyignoreDifferences for that field
Sync loop, endlessly re-applyingSame, usually a defaulted or generated fieldSame; find the field with argocd app diff
Unknown healthCustom resource Argo CD cannot assessWrite a health check Lua script, or accept it
Sync succeeded, app brokenResources applied but not ReadyHealth assessment plus --wait-style gates
Manual fix reverted mid-incidentselfHeal: true doing its jobDisable auto-sync for the app deliberately, then fix Git
Image updated but nothing deployedNobody bumped the tag in the config repoImage Updater, or a CI step that commits the bump

Interview corner · الأسئلة اللي بتتسأل#

What is GitOps, and how is it different from just having a CD pipeline?

The difference is the direction of the arrow and where credentials live.

Push CDPull CD (GitOps)
Who appliesCI, from outsideAn agent, from inside
Cluster credentialsIn CINever leave the cluster
Source of truthThe clusterThe Git repo
Manual driftPersists silentlyDetected, and reverted if self-heal is on
"What is deployed?"Inspect the clustergit log
RollbackRe-run a pipelinegit revert

Three claims worth making explicitly:

  1. Security. CI runs untrusted code from pull requests; giving it cluster-admin is a large blast radius. GitOps reduces CI's permission to "write a commit".
  2. Auditability. Every change is a reviewed, attributed, reversible commit — you get change management without a change-management tool.
  3. Convergence. Because the agent reconciles continuously, the cluster is correct even after someone changes it by hand.
An engineer fixes production with kubectl edit during an incident. What happens under GitOps, and what should they have done?

With selfHeal: true, the agent reverts the fix — typically within the reconcile interval, often under three minutes. The engineer's change is gone and production breaks again.

This is correct behaviour: Git said otherwise, and the whole model is that Git wins. But it is a genuine operational hazard, and "it works as designed" is not a sufficient answer.

What should happen, in order of preference:

  1. Commit the fix to Git. The agent applies it, and cluster and repo never disagree. Slower by the length of a PR — which is why teams keep an emergency path with post-hoc review.
  2. Deliberately suspend sync for that app, fix, then reconcile Git and re-enable. The suspension must be a tracked task or it will be forgotten, and an app silently out of sync for weeks is worse than no GitOps at all.
argocd app set web --sync-policy none    # suspend
# fix, then bring Git in line, then:
argocd app set web --sync-policy automated --self-heal --auto-prune
How do you handle secrets when everything is supposed to be in Git?

You never put the secret itself in Git. Four options, and you should be able to compare them:

OptionWhere the plaintext livesTrade-off
Sealed SecretsNowhere; ciphertext in Git, private key in-clusterCiphertext is bound to one cluster's key — DR and multi-cluster are awkward
SOPS + age/KMSNowhere; encrypted values in GitReadable diffs, key management is yours
External Secrets OperatorIn Vault / AWS SM / GCP SMGit holds only a reference; rotation happens outside Git entirely
Plain base64 SecretIn Git, in plaintextNever do this

For a team, External Secrets is usually right, because rotation is the requirement people forget. With Sealed Secrets or SOPS, rotating a credential means a commit; with an external store, it means updating the store and the operator picks it up.

Key takeaways#

  • GitOps is pull, not push. An agent inside the cluster reconciles against Git; CI never holds cluster credentials.
  • The security argument is the strongest one. CI runs untrusted PR code — reducing its permission to "write a commit" is a large win.
  • prune and selfHeal are off by default and are the whole point. Turn them on, after a diff.
  • Git becomes the answer to "what is deployed?" — that is the day-to-day benefit you actually feel.
  • Self-heal will fight you during an incident. Decide the emergency path in advance and write it in the runbook.
  • Secrets need a real answer — External Secrets, SOPS or Sealed Secrets. base64 is not encryption.
  • GitOps suits declarative state and not imperative steps. Migrations and one-off jobs still need somewhere to live.

Last updated 2026-09-13