The Ops Atlas xOps School

Delivery & Platform Established term Sources verified

GitOps

GitOps is a way of operating software systems in which the desired state is declared in a versioned, immutable store such as Git, and software agents continuously pull it and reconcile the running system to match.

Key takeaways

  • GitOps is defined by four OpenGitOps principles: declarative, versioned and immutable, pulled automatically, continuously reconciled. A CI job that runs kubectl apply meets two of them at most.
  • Git is the usual state store, not a requirement: an OCI registry or bucket with immutable versions, access control and audit also qualifies.
  • Pull keeps deploy credentials inside the cluster, but Argo CD and Flux reconcile as cluster-admin by default. Narrow them with projects and impersonated service accounts.
  • Whoever can change what the reconciler reads can deploy. Protect branches, tags, remote bases and chart sources as production credentials.
  • Set a drift policy per resource (revert, tolerate or alert) and keep a break-glass path, or self-heal will fight your incident responders.
  • GitOps recreates configuration, not data. A rebuild from Git still needs backups for databases and volumes.

GitOps is a way of operating software systems in which you write down the state you want (declaratively), keep every version of that declaration in a store that cannot be silently rewritten (usually Git), and let a software agent fetch it and keep the running system matching it. People change production by changing the declaration through a reviewed pull request. They do not log in to production and change it by hand.

The mental picture is a thermostat. You set the temperature you want, and a control loop keeps measuring the room and switching the heating on or off until the two match. In GitOps the setting is a commit, the room is your cluster, and the thermostat is a reconciler such as Argo CD or Flux. When someone changes the room directly (a kubectl edit at 2 a.m.), the loop notices the difference, called drift, and either reverts it or raises the alarm.

Who this page is for. Beginners (the core idea and a test for spotting it), engineers (repositories, promotion, secrets, drift), architects (pull or push, hub or per-cluster agents) and leaders (governance, evidence, risk). Last verified: 2026-10-17.

Goal Production changes only through reviewed, versioned declarations, with drift that is detected and either corrected or justified
Main users Platform teams (run the reconcilers), application teams (own their manifests), release engineers, security and audit
Scope The four principles, pull versus push, reconciliation and drift, repository layout, promotion, secrets, reconciler security, GitOps beyond Kubernetes
Prerequisites Git and pull-request review; declarative configuration (Kubernetes manifests, Kustomize or Helm); CI basics from DevOps
Key outputs A configuration repository per scope, a reconciler per cluster or fleet, a promotion procedure, a drift policy, a break-glass procedure
Term status Established (OpenGitOps v1.0.0; two CNCF graduated reconcilers); the label is used loosely by vendors
Linked, not taught here Canaries and feature flags (ReleaseOps), IaC fundamentals (InfraOps), cluster operations (KubernetesOps), GitLab specifics (GitLabOps), API configuration pipelines (API Ops)

Without GitOps, most teams deploy with a pipeline that ends in a command such as kubectl apply or helm upgrade, plus occasional manual fixes. Four problems follow.

  1. Nobody can say what production should look like. The last pipeline run and every manual edit since then together define it. Weaveworks, which coined the term, put it this way in 2017: small ad-hoc changes accumulate until “you can no longer distinguish between how the system should be and how it is”[10].
  2. Drift is invisible. A hotfix applied by hand survives until the next deploy silently removes it, or the next deploy fails because of it.
  3. CI holds the keys to production. A push pipeline needs credentials for every environment it deploys to. NIST’s DevSecOps guidance names this as the security downside of the push model: credentials can be exposed “outside of the deployment environment”[5].
  4. Rebuilding is archaeology. After losing a cluster you must work out which pipelines, in which order, with which parameters, recreate it.

GitOps answers each with a mechanism, and because the desired state is “sufficient to recreate the system”[11], a new cluster can be pointed at the same declarations:

Before After
“What is running?” means querying the cluster and pipeline logs The commit the reconciler reports as applied, plus a list of tolerated exceptions
Hotfixes by hand, forgotten until the next deploy Hand edits are reverted or alerted on within minutes, so fixes go through a pull request
CI has write credentials for every cluster CI writes to Git or a registry; the in-cluster agent applies
Rollback means re-running an old pipeline Rollback means reverting a commit
Rebuild means reconstructing pipeline history Rebuild means bootstrapping a reconciler at the same path and restoring data from backups

What the evidence does and does not show. The benefits above are mechanisms, not measured outcomes: no independent study was found (October 2026) measuring, say, change failure rate before and after adoption. CNCF’s 2025 survey reports that 58% of “cloud native innovators” use GitOps principles extensively, against 23% of “adopters”[12], which is correlation with organisations mature in other ways, not proof that GitOps causes the maturity.

  • Not “we keep YAML in Git”. Storing manifests in Git satisfies at most the first two principles. If a pipeline pushes them and nothing watches the cluster afterwards, there is no reconciliation.
  • Not a replacement for CI. CI still builds, tests, scans and publishes artefacts. GitOps replaces the deploy step: the pipeline ends with a commit or a published artefact, not with a write to the cluster[5].
  • Not tied to Git. The principles call for a state store with immutable, versioned history, which “should provide access control and auditing”. Git is “the canonical example”, but “any other system that meets these criteria may be used” [11]. Flux calls registry-based delivery “Gitless GitOps”[13].
  • Not Kubernetes-only. Kubernetes made GitOps practical because its API is declarative and built from control loops [14], so most tooling lives there. The same pattern is used for cloud resources through Kubernetes-hosted controllers (Crossplane; tofu-controller, a community OpenTofu/Terraform controller for Flux maintained in the separate flux-iac organisation, formerly Weave TF-Controller[15]).
  • Not backup or data recovery. The OpenGitOps desired state “generally does not include persistent application data, eg. database contents”[11]. See BackupOps and DisasterRecoveryOps.
  • Not a release strategy. Canaries, blue-green and feature flags decide how much traffic sees a change; that belongs to ReleaseOps. GitOps decides how the declared state reaches the cluster.
  • Not infrastructure as code. IaC is the declarative language and lifecycle (InfraOps); GitOps is one operating model for applying it continuously.
When Milestone Why it matters
2017-08-07 Alexis Richardson (Weaveworks) publishes “GitOps - Operations by Pull Request”: ““GitOps” is our name for how we use developer tooling to drive operations.”[10] Names the practice; see the 2017-versus-2021 note below
2017 Argo created at Applatix, later acquired by Intuit[16] The origin of Argo CD
2019-07-15 Flux, the open-source project behind Weaveworks’ tooling[10], accepted by CNCF[3] A vendor-neutral home for Weaveworks’ GitOps operator
2021 OpenGitOps becomes a CNCF Sandbox project (2021-01-26)[17] and releases Principles and Glossary v1.0.0 (2021-10-08)[1] A vendor-neutral definition; “pulled” and “continuously reconciled” become part of it
March 2022 NIST SP 800-204C describes the pull-based GitOps workflow model[5] A standards body recommends pull for credential reasons
2022-11-30 / 2022-12-06 Flux and Argo graduate in CNCF[3][4] Both reconcilers reach the foundation’s top maturity level
February 2024 Weaveworks shuts down; Flux continues under CNCF with maintainers employed elsewhere[18][19] A foundation project can outlive its sponsor; vendor health still matters
2025 CNCF restructures its TAGs; TAG App Delivery is spun down[8] The definition now lives with OpenGitOps
2025-11 AWS launches a managed Argo CD capability for EKS[20]; Crossplane graduates in CNCF[21] Reconcilers become managed services; declarative cloud control planes mature
2026 Argo CD 3.5 marks its rendered-manifest and least-privilege sync features as beta[22][23] The frontier moves from “can we reconcile?” to “can we review and constrain what is reconciled?”

The shift between 2017 and 2021 matters. The 2017 post described “diff tools” that “detect any divergence and notify us via Slack alerts”, with separate sync tools for convergence[10]. The 2021 principles require agents that pull on their own and “attempt to apply the desired state”[1]. Many arguments about whether a setup “is GitOps” come down to which of the two definitions someone has in mind.

The mental model is the control loop that Kubernetes itself is built from: controllers “watch the state of your cluster, then make or request changes where needed”[14]. A GitOps reconciler is one more controller whose desired state comes from outside the cluster.

Figure 1. The GitOps reconciliation loop
NoYesnext interval or webhookcreates driftReviewed pull requestmergedDesired state: versioneddeclarations in the state storeAgent pulls the declarationsAgent observes the actualstateActual matches desired?Act: apply, prune or roll backReport: status, events,commit statusChange made outside thestate store
Diagram source (Mermaid)
flowchart TB
  PR["Reviewed pull request merged"]:::neutral --> DS["Desired state: versioned declarations in the state store"]:::core
  DS --> PULL["Agent pulls the declarations"]:::core
  PULL --> OBS["Agent observes the actual state"]:::core
  OBS --> CMP{"Actual matches desired?"}:::decision
  CMP -- "No" --> ACT["Act: apply, prune or roll back"]:::core
  CMP -- "Yes" --> REP["Report: status, events, commit status"]:::neutral
  ACT --> REP
  REP -- "next interval or webhook" --> PULL
  DRIFT["Change made outside the state store"]:::risk -. "creates drift" .-> OBS
  classDef core fill:#e8f3f2,stroke:#16756f,color:#122029
  classDef risk fill:#fbeceb,stroke:#b4332b,color:#122029
  classDef decision fill:#fdf3e3,stroke:#c9800f,color:#122029
  classDef neutral fill:#f1f4f6,stroke:#5b7a87,color:#122029

Text equivalent: A merged pull request updates the desired state in the state store. The agent pulls the declarations, observes the actual state and compares the two. If they differ, it acts by applying, pruning or rolling back, then reports; if they match, it reports. The loop repeats on the next interval or when a webhook asks for a refresh. A change made outside the state store creates drift, which the next observation detects.

Term Meaning Example
Desired state “The aggregate of all configuration data that is sufficient to recreate the system”[11] Manifests for every Deployment, Service, policy and add-on in a cluster
Actual (live) state What the API server currently holds and runs The Deployment as kubectl get -o yaml shows it
State store Keeps immutable versions of the desired state, and “should provide access control and auditing”[11] A Git repository with protected branches; an OCI registry
Reconciler (agent) Controller that pulls desired state and drives actual state towards it Argo CD application controller; Flux kustomize-controller
Sync / reconcile One apply pass from desired to actual state Argo CD “sync”; a Flux Kustomization run
Drift Actual state “has moved or is in the process of moving away from the desired state”[11] A replica count changed by hand
Self-heal Re-applying desired state when the live state drifts, not only on new commits Argo CD selfHeal: true[24]
Prune Deleting live objects that are no longer declared Argo CD prune: true; Flux .spec.prune[25]
Health Whether applied objects became ready, as distinct from “applied” Deployment rollout complete; Flux wait/healthChecks
Base and overlay A shared configuration and its per-environment variants[26] apps/base plus apps/production
Kustomization (Flux) vs kustomization.yaml Two things, one word: the Flux object (kustomize.toolkit.fluxcd.io) says which path to apply, how often and as whom[25]; Kustomize’s file lists one directory’s resources and patches[26] The Flux tenants Kustomization applies tenants/production/, whose kustomization.yaml lists shop.yaml
Reconciler objects Argo CD: Application (source to destination), AppProject (the tenant fence), ApplicationSet (generates Applications)[27][28]. Flux: GitRepository or OCIRepository (sources), Kustomization and HelmRelease (what to apply)[29] An ApplicationSet that creates one Application per cluster
Promotion Moving a verified version to the next environment by changing that environment’s declaration A pull request that sets the production image digest
Rendered manifests The final YAML after Helm or Kustomize, committed for review Argo CD Source Hydrator (beta)[22]
Break-glass A recorded, time-boxed exception that suspends reconciliation for an emergency change Suspend one Kustomization, fix, merge the fix, resume
Figure 2. Where GitOps sits among neighbouring Atlas pages
DevOps: pipeline and cultureInfraOps: IaC lifecycleDevSecOps: signing andpolicyPlatformOps: golden pathsGitOps: reconcile declaredstateKubernetesOps: the clusterReleaseOps: canaries,exposureGitLabOps: one platformAPI Ops: API configuration
Diagram source (Mermaid)
flowchart LR
  DEV["DevOps: pipeline and culture"]:::neutral
  INF["InfraOps: IaC lifecycle"]:::neutral
  SEC["DevSecOps: signing and policy"]:::neutral
  PLAT["PlatformOps: golden paths"]:::neutral
  G["GitOps: reconcile declared state"]:::core
  K8S["KubernetesOps: the cluster"]:::neutral
  REL["ReleaseOps: canaries, exposure"]:::neutral
  GL["GitLabOps: one platform"]:::neutral
  API["API Ops: API configuration"]:::neutral
  DEV --> G
  INF --> G
  SEC --> G
  PLAT --> G
  G --> K8S
  G --> REL
  G --> GL
  G --> API
  classDef core fill:#e8f3f2,stroke:#16756f,color:#122029
  classDef neutral fill:#f1f4f6,stroke:#5b7a87,color:#122029

Text equivalent: GitOps sits in the middle. DevOps, InfraOps, DevSecOps and PlatformOps feed it; KubernetesOps, ReleaseOps, GitLabOps and API Ops use or specialise it. The table below gives each relationship.

Neighbour Shared ground This page’s focus Go there for
DevOps Pipelines, trunk-based work, delivery metrics What happens after CI: pull and reconcile CI/CD design, DORA’s five metrics
InfraOps Declarative definitions, drift Continuous reconciliation by an agent Writing IaC, state files, modules
KubernetesOps The Kubernetes API, controllers Delivering declared state to clusters Cluster upgrades, capacity, troubleshooting
ReleaseOps Promotion between environments The mechanism that moves declarations Canary, blue-green, feature flags
PlatformOps Self-service deployment The reconciler and repository contract Running the platform as a product
DevSecOps Supply-chain controls Who can change what the reconciler reads Scanning and gate policy
GitLabOps Flux, merge requests, environments Host-neutral principles GitLab agent and Flux wiring
API Ops Declarative config from Git Kubernetes-centred reconcilers API gateway and contract pipelines

The OpenGitOps document says the desired state of a GitOps-managed system must be[1]:

  1. Declarative. “A system managed by GitOps must have its desired state expressed declaratively.”
  2. Versioned and Immutable. “Desired state is stored in a way that enforces immutability, versioning and retains a complete version history.”
  3. Pulled Automatically. “Software agents automatically pull the desired state declarations from the source.”
  4. Continuously Reconciled. “Software agents continuously observe actual system state and attempt to apply the desired state.”

The glossary carries much of the meaning:

  • Why pull? Because the agents “must be able to access the desired state from the state store at any time, not only when there is an intentional change in the state store triggering a push event”, so reconciliation can be triggered “whenever there is a divergence”[11].
  • How continuous? “Continuous” means “reconciliation continues to happen, not that it must be instantaneous”[11]. A three-minute polling interval qualifies.
  • Immutable how? A branch is a mutable pointer to immutable commits. The principle is broken by references inside the declarations, such as an image tag like :latest, a remote Kustomize base on HEAD or an unpinned chart range: Argo CD warns that such manifests “change their meaning from one day to the next” without any commit in your repository[30].
  • Policies are part of the system. The glossary’s “software system” includes “Policies for controlling access and management of repositories, deployments, runtimes”[11].

The “Is it GitOps?” test (this Atlas’s checklist)

Link to section “The “Is it GitOps?” test (this Atlas’s checklist)”

Ask four questions, one per principle:

  1. Could you recreate the system’s configuration from the declarations alone, apart from persistent data?
  2. Is every version immutable, retained, access-controlled and audited, including what the declarations reference (images, charts, bases)?
  3. Does an agent fetch the declarations by itself, whenever it needs them, without a pipeline pushing them?
  4. Does the agent keep comparing actual with desired state, and act or alert when they diverge, not only when a new commit arrives?
Setup 1 2 3 4 Verdict
CI job runs kubectl apply -f k8s/ after merge Yes Partly (tags often mutable) No No Declarative CD with push, not GitOps
Argo CD, manual sync only Yes Depends on references Yes Observes and reports drift; applies only when a person syncs Partial: detection without automatic correction
Argo CD automated sync, selfHeal off Yes Depends Yes Applies new commits; live edits “will not trigger automated sync”[24] Partial: reconciles commits, tolerates live drift
Argo CD automated sync with selfHeal, or a Flux Kustomization Yes Depends Yes Yes; Flux corrects drift every .spec.interval[25] GitOps
CI job runs terraform apply after merge Yes Yes if modules and providers are pinned No No IaC with pull-request automation
OpenTofu/Terraform controller for Flux Yes Yes Yes Yes, or drift detection only, by configuration[15] GitOps for cloud resources
ansible-pull from cron on each host Mostly (playbooks can be imperative) Yes Yes: it “inverts the default push architecture”[31] Re-applies on schedule; no drift report unless you build one Close, depending on playbook design

“Partial” is not an insult: manual sync is a sensible first step and a defensible permanent choice for some production systems.

This Atlas’s proposed charter and invariants

Link to section “This Atlas’s proposed charter and invariants”

The following charter is this Atlas’s proposal, not a published standard. Adapt the numbers to your risk.

  • Mission. Every change to a managed system is declared, reviewed and versioned; the system converges to it automatically; every difference between declaration and reality is either corrected or recorded as an exception.
  • Invariant 1 — one path to production. A production change has a merged commit or a time-boxed break-glass record that is closed by a commit.
  • Invariant 2 — no mutable references in production. Images by digest, charts and remote bases by exact version or commit.
  • Invariant 3 — least-privilege reconcilers. A reconciler can change only what its owning team may change.
  • Invariant 4 — guarded deletion. Pruning is enabled, and the objects whose loss is catastrophic (namespaces, persistent volumes, CRDs) are protected from it.
  • Invariant 5 — data is not in Git. Backups and restore tests exist for everything the declarations cannot recreate.
  • Measurable goals. Reconciliation lag p90 within a target your users accept; zero unticketed out-of-band changes per month; a successful rebuild drill each quarter (see metrics).
  • Non-goals. Deciding canary percentages, owning CI build quality, or replacing change management where regulation requires it.
Building block Job Examples (as of October 2026) What breaks without it
State store Immutable, versioned, access-controlled declarations Git with protected branches; OCI registry No reliable record of intent
Artefact registry Stores images and charts by digest Any OCI registry “Version” references that change meaning
Renderer Turns Kustomize or Helm into plain manifests Argo CD repo-server; Flux kustomize- and helm-controllers Reviewers approve templates, not what will run
Reconciler Pulls, diffs, applies, prunes, reports health Argo CD application controller[32]; Flux controllers[29] No loop: back to push
Tenancy and policy Limits what each team’s declarations may touch Argo CD AppProjects[27]; Flux serviceAccountName with the lockdown flags[33] Any repository writer can change anything
Secrets mechanism Keeps secret values out of plain Git SOPS, Sealed Secrets, External Secrets Operator Credentials in Git history forever
Feedback Tells humans what was applied and what failed Commit statuses, events, chat alerts, metrics Silent failure; stale clusters
Promotion mechanism Moves verified versions between environments Pull requests per environment; image automation; Kargo Ad-hoc copying between folders

Typical roles: the platform team runs the reconcilers and owns the repository contract; application teams own their manifests and overlays; code owners approve production paths; security owns signing, secret keys and policy; SRE/on-call handles drift alerts and break-glass; change or compliance consumes the evidence. Small organisations combine roles but keep approvals separate.

R = responsible, A = accountable, C = consulted, I = informed.

Activity Platform team App team Code owners Security On-call / SRE
Install and upgrade reconcilers A/R I I C C
Repository layout and tenancy rules A/R C C C I
Onboard an application A R C C I
Change an application’s staging overlay I A/R I I I
Promote to production I R A I C
Rotate secret-encryption keys R I I A I
Triage a drift alert C R I I A
Break-glass change I C I I A/R
Quarterly rebuild drill A/R C I I R
  • Day 0 — design. Choose the state store and repository layout, the reconciler, hub or per-cluster topology, tenancy model, secrets pattern, drift policy and the break-glass procedure. Decide what stays out of GitOps (data, operator-generated objects).
  • Day 1 — bootstrap and migrate. Install the reconciler so that it manages itself: Flux’s bootstrap commits its own manifests and creates the objects that reconcile them[13]. In Argo CD a parent owns the Applications: an ApplicationSet (Argo CD’s recommendation) or an “app of apps”, which its documentation calls “an admin-only tool”, so only admins should push to the parent’s source[34]. A parent with self-heal restores each child’s spec, including a syncPolicy changed during an incident (break-glass); one with the deletion finalizer deletes its children with it. Migrate existing workloads one namespace at a time, with automated sync off or prune disabled until the diffs are understood.
  • Day 2 — operate. Upgrade the reconciler (it is now production-critical), rotate keys, review exceptions, tune intervals and webhooks, watch reconciliation lag and failures, and rehearse rebuilds.
  • Approval is a pull-request review. Use branch protection on the paths reconcilers read: required reviews (with code-owner review for production paths), required status checks, optionally required signed commits, and no bypass for administrators [35]. Other Git hosts offer equivalents.
  • Evidence is a chain, not a commit. Git shows what was approved. Argo CD’s documentation notes that this audit log “only applies to what happened in Git and does not necessarily correlate one-to-one with events that happen in a cluster”[36]. Keep the reconciler’s events, which name the revision applied, alongside the pull request.
  • Freeze windows are declarative too. Argo CD sync windows allow or deny syncs on a cron schedule; deny overrides allow [37].
  • Break-glass is designed in advance: scope, time limit, record and the route back to Git (template).
  • Exceptions are visible. Every ignoreDifferences rule or Flux ignore annotation is tolerated drift: record it (template) and review it.

The reference architecture below is vendor-neutral. Argo CD packs the source fetcher and renderer into its repo-server and the applier into its application controller[32]; Flux splits them into source-, kustomize- and helm-controllers[29].

Figure 3. Reference architecture with trust boundaries
Inside the cluster trust boundaryState storesOutside the cluster: change and buildpublishes digestcheck resultspulledimages pulled by kubeletpulledEngineer opens a pull requestCI: build, test, scan, signCode owners review andmergeConfig repository withprotected pathsOCI registry: images andcharts by digestExternal secret managerSource fetcher: clone andverifyRenderer: Kustomize or HelmApplier: diff, apply, prune as ascoped accountKubernetes API serverNodes: kubelet runs PodsSecrets operatorNotifier: events, commitstatus, alerts
Diagram source (Mermaid)
flowchart TB
  subgraph OUTSIDE["Outside the cluster: change and build"]
    ENG["Engineer opens a pull request"]:::neutral
    CI["CI: build, test, scan, sign"]:::neutral
    REV{"Code owners review and merge"}:::decision
  end
  subgraph STORES["State stores"]
    CFG["Config repository with protected paths"]:::core
    REG["OCI registry: images and charts by digest"]:::core
    VAULT["External secret manager"]:::neutral
  end
  subgraph CLUSTER["Inside the cluster trust boundary"]
    SRC["Source fetcher: clone and verify"]:::core
    RND["Renderer: Kustomize or Helm"]:::core
    APL["Applier: diff, apply, prune as a scoped account"]:::core
    KAPI["Kubernetes API server"]:::neutral
    NODES["Nodes: kubelet runs Pods"]:::neutral
    SECOP["Secrets operator"]:::neutral
    NOTIF["Notifier: events, commit status, alerts"]:::neutral
  end
  ENG --> REV
  ENG --> CI
  CI -- "publishes digest" --> REG
  CI -- "check results" --> REV
  REV --> CFG
  CFG -. "pulled" .-> SRC
  SRC --> RND
  RND --> APL
  APL --> KAPI
  KAPI --> NODES
  REG -. "images pulled by kubelet" .-> NODES
  VAULT -. "pulled" .-> SECOP
  SECOP --> KAPI
  APL --> NOTIF
  classDef core fill:#e8f3f2,stroke:#16756f,color:#122029
  classDef decision fill:#fdf3e3,stroke:#c9800f,color:#122029
  classDef neutral fill:#f1f4f6,stroke:#5b7a87,color:#122029

Text equivalent: Three zones. Outside the cluster, an engineer opens a pull request; CI builds, tests, scans and signs, publishes images by digest to the registry and reports check results; code owners review and merge. The state stores are the configuration repository with protected paths, the OCI registry and an external secret manager. Inside the cluster, a source fetcher pulls and verifies the configuration, a renderer runs Kustomize or Helm, and an applier diffs, applies and prunes through the API server as a scoped account; nodes run the Pods, and their kubelet pulls images from the registry; a secrets operator pulls values from the secret manager; a notifier reports events, commit statuses and alerts. Every arrow into the cluster is a pull; an optional webhook (not drawn) is the one inbound call.

Component Trust notes
Config repository Write access to a watched path is deploy access[36]
CI Registry push and, at most, the right to open pull requests or commit to non-production paths; no cluster credentials
Registry Reference by digest; restrict who can push or move tags
Source fetcher Read-only credentials; Flux can verify Git commit signatures[38]
Renderer Argo CD runs helm template and kustomize build; its security guide warns that malicious charts or kustomizations can read files outside their tree, so treat repository content as untrusted input[36][39]
Applier Cluster-admin by default; narrow it (see push versus pull)
Secrets operator Keeps values out of Git and out of the reconciler[40]
Notifier Flux posts commit statuses to GitHub, GitLab, Bitbucket, Azure DevOps and Gitea[41]

Push versus pull: where the credentials live

Link to section “Push versus pull: where the credentials live”
Figure 4. Push and pull deployment: who holds the cluster credential
Push modeluses a stored clustercredentialMerge to mainCI runner outside the clusterKubernetes API: hand editspersist unnoticedPull modelread-only pullin-cluster identityMerge to mainConfig repo or registryAgent inside the clusterKubernetes API: hand editsreverted
Diagram source (Mermaid)
flowchart TB
  subgraph PULL["Pull model"]
    direction TB
    Q1["Merge to main"]:::neutral --> Q2["Config repo or registry"]:::core
    Q2 -- "read-only pull" --> Q3["Agent inside the cluster"]:::core
    Q3 -- "in-cluster identity" --> Q4["Kubernetes API: hand edits reverted"]:::core
  end
  subgraph PUSH["Push model"]
    direction TB
    P1["Merge to main"]:::neutral --> P2["CI runner outside the cluster"]:::risk
    P2 -- "uses a stored cluster credential" --> P3["Kubernetes API: hand edits persist unnoticed"]:::risk
  end
  classDef core fill:#e8f3f2,stroke:#16756f,color:#122029
  classDef risk fill:#fbeceb,stroke:#b4332b,color:#122029
  classDef neutral fill:#f1f4f6,stroke:#5b7a87,color:#122029

Text equivalent: Two columns, push on the left and pull on the right. In the push model a merge to main triggers a CI runner outside the cluster, which uses a stored cluster credential to write to the Kubernetes API; hand edits made directly in the cluster persist unnoticed. In the pull model a merge updates the configuration repository or registry; an agent inside the cluster pulls it with read-only access and applies it with its own in-cluster identity; hand edits are detected and reverted.

NIST SP 800-204C states the case for pull bluntly: “Since CD tools have the keys to production systems, push-based models are rendered insecure.” With pull, “the deployment environment credentials (e.g., cluster credentials) are not exposed outside of the production environment”[5]. Microsoft’s Flux documentation makes the same point: “No access to clusters is needed by the source repo or by any other cluster”[42].

Two counterweights keep this honest:

  1. The agent is powerful. By default Argo CD’s controller uses a cluster-admin-level role[36], and Flux’s controllers run under a service account that “has the cluster-admin role bound to it. This is convenient for a deployment in which all users are trusted”[33]. Pull moves the credential inside the boundary; it does not make it small (see security).
  2. The repository becomes the credential. Argo CD’s security guide warns that unauthorised write access to a trusted repository lets an attacker deploy malicious images or delete resources through pruning[36]. Pull removes one place where production credentials sit; it does not protect what is merged.

A webhook does not make a setup push-based. Argo CD polls every two to three minutes by default (120 seconds plus up to 60 seconds of jitter[43]) and accepts webhooks from Git hosts and registries to skip the wait[44]; Flux’s Receiver does the same[45]. The webhook only says “look now”; the agent still pulls and applies. The price is an inbound endpoint, usually an Ingress to the Argo CD API server or Flux’s webhook-receiver Service. Argo CD’s /api/webhook “currently lacks rate limiting protection”, so set a shared secret and a payload-size limit; a Flux Receiver checks a token. Expose only that path.

Topology How it works Strengths Costs and risks
Central instance (hub) pushing to remote clusters One Argo CD reconciles many clusters; it stores each cluster’s credentials as Secrets in its own namespace[36] One view and one RBAC model for the fleet; ApplicationSet templates many Applications[28] The hub holds credentials for every cluster and needs network reach to each API server; a hub outage pauses delivery everywhere
Agent per cluster Flux (or Argo CD) installed in each cluster, each pulling its own path No inbound access to clusters unless webhooks are enabled; “each cluster’s control plane is logically separated”[42] N installations to upgrade and monitor; fleet view needs aggregation
Hub with pulling agents Lightweight agents connect out to a central control plane; “Control plane never needs direct cluster access”[46] Central view without central credentials; suits edge and unreliable networks argocd-agent is pre-1.0 (v0.10.0, August 2026); check maturity before production
Managed reconciler The EKS Capability for Argo CD runs “in AWS-owned infrastructure separate from your clusters”[20]; the AKS/Arc Flux extension runs in each cluster, latest three versions supported[42] Fewer upgrades to engineer Provider feature set and cadence; lock-in
Figure 5. One change from pull request to healthy rollout, with the exception path
ClusterAgentGit host and CIEngineeralt[Healthy][Unhealthy or failed]Open pull requestCI renders, diffs, checks policyApprove and mergeWebhook (optional)Pull new revisionDry-run apply and diffApply and pruneRollout statusCommit status: successCommit status: failure, alertMerge a revertReconcile previous revision
Diagram source (Mermaid)
sequenceDiagram
  participant E as Engineer
  participant G as Git host and CI
  participant A as Agent
  participant K as Cluster
  E->>G: Open pull request
  G->>G: CI renders, diffs, checks policy
  E->>G: Approve and merge
  G-->>A: Webhook (optional)
  A->>G: Pull new revision
  A->>K: Dry-run apply and diff
  A->>K: Apply and prune
  K-->>A: Rollout status
  alt Healthy
    A-->>G: Commit status: success
  else Unhealthy or failed
    A-->>G: Commit status: failure, alert
    E->>G: Merge a revert
    A->>K: Reconcile previous revision
  end

Text equivalent: The engineer opens a pull request; CI renders it, diffs it and checks policy; the engineer approves and merges, and the Git host may send a webhook. The agent pulls the revision, dry-runs and diffs, applies and prunes, and reads the rollout status. It then sets a success commit status, or a failure status with an alert, after which the engineer merges a revert and the agent reconciles the previous revision.

Step Trigger Actor Decision System Output and feedback
Propose Need for change Engineer or bot What to change, where Git host Pull request; CI checks
Verify Pull request opened CI Does it render, pass policy, use digests? CI, renderer Rendered diff and pass/fail, posted on the pull request
Approve Checks green Code owners Is the risk acceptable now? Git host Merge commit; audit trail
Reconcile New revision or interval Agent Is anything different? Reconciler, API server Applied objects; events, metrics
Assess Apply finished Agent Did objects become healthy in time? Health checks Commit status, alert
Recover Failure or alert Engineer, on-call Revert, fix forward or break glass? Git host Revert commit or exception record; incident review

Core practices: repository layout and environment promotion

Link to section “Core practices: repository layout and environment promotion”

Separate configuration from application source. Argo CD recommends a separate configuration repository: a replica change should not trigger a build, the audit history stays readable, access can differ, and CI pushing manifests to the source repository can cause build loops[30]. Flux documents four layouts: a monorepo (apps/, infrastructure/, clusters/, environments as directories on one branch), a repository per environment when production access must be restricted (Git cannot restrict reads per file), a repository per team and a repository per app[47].

Model environments as directories, not branches. Kustomize overlays exist to create “development, QA, staging and production environment variants” of a shared base[26]. Kostis Kapelonis, writing on a GitOps vendor’s blog, calls branch-per- environment “an anti-pattern”: merges carry unintended changes and the wrong order of changes, cherry-picks do not scale, and hand edits in upstream branches are never back-ported[48]. Directories make the difference between staging and production a reviewable file diff.

Promote by changing one value in the next environment. CI records an immutable digest. Promotion is a pull request that sets that digest in the next overlay. Automation can open the request: Flux image automation scans a registry, picks a tag by policy, updates manifests and commits, optionally to a separate branch so a human merges it[49]. Kargo, an open-source project from Akuity, orchestrates multi-stage promotion on top of GitOps[50].

Figure 6. Promotion of one image digest through three environments
healthy, tests passfailschecks and review passfailscode owners approve,window openno approval or window closedunhealthyCI builds the image andrecords its digestAutomation updates the devoverlayBot opens PR: stagingoverlay to the same digestFix forward or revertPR: production overlay to thesame digestProduction reconciles; healthcheckedWait for the window orescalateRevert the production commit
Diagram source (Mermaid)
flowchart TB
  B["CI builds the image and records its digest"]:::neutral --> D["Automation updates the dev overlay"]:::core
  D -- "healthy, tests pass" --> SP["Bot opens PR: staging overlay to the same digest"]:::core
  D -. "fails" .-> FIX["Fix forward or revert"]:::risk
  SP -- "checks and review pass" --> PP["PR: production overlay to the same digest"]:::core
  SP -. "fails" .-> FIX
  PP -- "code owners approve, window open" --> REC["Production reconciles; health checked"]:::core
  PP -. "no approval or window closed" .-> WAIT["Wait for the window or escalate"]:::neutral
  REC -. "unhealthy" .-> RV["Revert the production commit"]:::risk
  classDef core fill:#e8f3f2,stroke:#16756f,color:#122029
  classDef risk fill:#fbeceb,stroke:#b4332b,color:#122029
  classDef neutral fill:#f1f4f6,stroke:#5b7a87,color:#122029

Text equivalent: CI builds the image and records its digest; automation updates the development overlay. If development is healthy and tests pass, a bot opens a pull request setting the staging overlay to the same digest. If staging checks and review pass, a pull request sets the production overlay to the same digest. If code owners approve and the deployment window is open, production reconciles and health is checked; otherwise the change waits or is escalated. A failure in development or staging leads to a fix forward or a revert; an unhealthy production rollout leads to reverting the production commit.

Review what will run, not only the template. A one-line change to Helm values can change hundreds of rendered lines. Either render in CI and post the diff on the pull request, or commit rendered manifests. Argo CD’s Source Hydrator (beta since 3.5.0) implements the rendered manifest pattern by pushing hydrated manifests to Git before syncing them[22].

Never commit plain Kubernetes Secrets: their values are only base64-encoded, and even inside the cluster Secrets are, by default, “stored unencrypted in the API server’s underlying data store (etcd)”[51].

Pattern How it works Strengths Weaknesses
Encrypted in Git SOPS “encrypts configuration files while keeping the structure visible”[52]; Flux decrypts in-cluster[53]. Sealed Secrets can be decrypted “only by the controller running in the target cluster”[54] Secrets follow the same review and history as everything else Key management and rotation; old ciphertext stays in history, so rotate values after key exposure
Reference in Git, value elsewhere External Secrets Operator reads a secret manager and “automatically injects the values into a Kubernetes Secret”[55]; refreshInterval controls re-reads[56] Values never touch Git; rotation happens in the manager Another dependency at runtime; secret changes bypass pull-request review
Injected at render time A plugin fetches values while the reconciler generates manifests Familiar to Vault users Argo CD “strongly caution[s] against” it: the reconciler needs secret access and caches generated manifests in plaintext in Redis[40]

Argo CD recommends populating secrets on the destination cluster (Sealed Secrets, External Secrets Operator, Secrets Store CSI Driver) so the reconciler never sees them[40]. For SOPS, Flux recommends age over OpenPGP where possible and encrypting only data and stringData[53]; who can read the decryption key is covered under the reference implementation.

“Correct all drift” sounds right and fails in practice, because other controllers legitimately write to the objects you declare.

Situation Policy Mechanism
Hand edits to Deployments, Services, RBAC Revert Argo CD selfHeal: true[24]; Flux re-applies every .spec.interval and reverts kubectl edit/patch/delete[13]
replicas managed by an autoscaler Tolerate Leave replicas out of Git[30]; or Argo CD ignoreDifferences with RespectIgnoreDifferences=true[57][58]; Flux HelmRelease drift-detection ignore rules[59]
Fields filled in by mutating webhooks or other controllers Tolerate narrowly Argo CD diffing customization[57]; Flux kustomize.toolkit.fluxcd.io/ssa: IfNotPresent or Merge[25]
Namespaces, persistent volume claims, CRDs Protect from pruning Argo CD Prune=false or Prune=confirm[58]; Flux kustomize.toolkit.fluxcd.io/prune: disabled[25]
Helm releases Decide explicitly Flux HelmRelease drift detection is opt-in (warn or enabled)[59]; Argo CD inflates charts with helm template, so drift is handled like any manifest[39]
Emergency change during an incident Suspend, then reconcile back Argo CD sync windows or disabling automation; Flux suspend[37][25]; preconditions in runbook step 1

Declarations arrive all at once; clusters need order. Custom resource definitions must exist before custom resources, and a controller must be ready before objects that depend on its webhooks. Flux expresses order between Kustomizations with dependsOn and gates on readiness with wait or healthChecks[25]. Argo CD orders within one sync by phases (PreSync, Sync, PostSync) and by argocd.argoproj.io/sync-wave annotations, and prunes in reverse wave order[63]. “Healthy” means Kubernetes reports the objects ready, not that the service is correct; add a smoke test (an Argo CD PostSync hook, or a Job checked by Flux health checks) when readiness probes are weak.

Source Type What it requires or recommends Binding?
OpenGitOps Principles and Glossary v1.0.0[1][11] Community specification (CNCF Sandbox) The four principles; meaning of desired state, state store, drift, pull Voluntary; the reference for “is it GitOps”
NIST SP 800-204C[5] US federal guidance Pull-based GitOps workflow for microservices delivery; operators limited by platform authorisation Guidance, not regulation

No law or certification standard requires GitOps. Regulated organisations can use its pull-request and reconciliation evidence for their own change-control requirements; mapping those belongs to ComplianceOps.

Pick by job first, then by product. Versions and features below are as of October 2026.

Job Representative tools (verified) Notes
Kubernetes reconciler Argo CD (CNCF graduated; v3.5.5 on 2026-10-12)[4][64]; Flux (CNCF graduated; v2.9.6 on 2026-10-01)[3][65] Both satisfy all four principles when automated sync and drift correction are on
Managed or bundled reconciler Flux extension for AKS and Azure Arc[42]; EKS Capability for Argo CD[20]; GitLab agent with Flux[9] Fewer upgrades to run; feature set set by the provider
Multi-cluster templating Argo CD ApplicationSet[28]; Flux per-cluster paths[47] ApplicationSet deletions cascade by default; see failures
Promotion Flux image automation[49]; Kargo[50] Automate lower environments; keep a review step where risk warrants
Secrets SOPS (CNCF Sandbox)[52]; Sealed Secrets[54]; External Secrets Operator[55] See the patterns table above
Cloud and infrastructure resources Crossplane (CNCF graduated 2025-11-06)[21]; tofu-controller, a community controller for Flux (flux-iac)[15] GitOps beyond Kubernetes workloads; IaC fundamentals are on InfraOps
Progressive delivery Flagger (Flux project)[13]; Argo Rollouts (Argo project)[16] Strategy belongs to ReleaseOps

Argo CD and Flux compared on documented behaviour

Link to section “Argo CD and Flux compared on documented behaviour”
Criterion (as of October 2026) Argo CD 3.5 Flux 2.9
Shape API server, repo-server and application controller; web UI and CLI consume the API[32] Separate controllers for sources, Kustomize, Helm, notifications and image automation[29]
Sources Git, Helm repositories and OCI, with webhook refresh[44] Git, OCI, Helm repositories and S3 buckets[29][13]
Helm Charts inflated with helm template; Argo CD owns the lifecycle, so helm ls shows nothing[39] Helm releases with history, opt-in drift detection, and configurable remediation of failed upgrades (strategy rollback by default)[59]
Drift correction Optional selfHeal; automated sync and pruning are off until enabled[24] Every Kustomization re-applies on its interval; prune is a required field[25]
Tenancy AppProjects restrict sources, destinations and kinds; syncs run with control-plane privileges unless the beta impersonation feature is enabled[27][23] Kubernetes RBAC through impersonated service accounts; a fence only with the lockdown flags --default-service-account and --no-cross-namespace-refs[33][25]
Source verification Source Integrity in AppProjects: GnuPG signatures on Git sources only, not Helm or OCI; the legacy signatureKeys setting is deprecated[66] GitRepository verifies PGP or SSH commit and tag signatures[38]
Multi-cluster Typically one hub with stored cluster credentials[36] Typically one installation per cluster[42]
Tested Kubernetes versions 1.33 to 1.36[67] 1.34 to 1.36[65]

Neither column is “better”: a shared UI and a central fleet view favour Argo CD; per-cluster autonomy and Kubernetes-native RBAC favour Flux. CNCF’s 2025 survey of Argo CD users found 42% running more than 500 applications per instance and 25% connecting more than 20 clusters[68]; as a survey of its own users, it says nothing about market share.

Automate the mechanical steps (pull-request checks, development image updates, promotion pull requests, statuses); keep human judgement for high-risk production promotion, pruning protected kinds and break-glass.

AI assistants can draft manifests, summarise rendered diffs or explain why an application is out of sync. An assistant or agent that can push to a watched path is a deployer: give it the same scoped, reviewable access as a CI bot, behind the same review and policy checks. No evidence was found (as of October 2026) that AI-specific tooling improves GitOps outcomes.

Assumptions. Two Kubernetes 1.36 clusters (staging and production), Flux 2.9 installed by bootstrap in each with Flux’s multi-tenancy lockdown, one configuration repository fleet-config on a Git host with branch protection, SOPS with one age key per tenant and environment, and a tenant team “shop” that may change only its own namespace (hostnames are placeholders). An Argo CD equivalent follows.

Example status: layout only (text).

fleet-config/
├── clusters/
│   ├── staging/
│   │   ├── flux-system/          # written by flux bootstrap, plus the lockdown patch
│   │   ├── infrastructure.yaml   # platform add-ons, reconciled as cluster admin
│   │   └── tenants.yaml          # tenant onboarding, depends on infrastructure
│   └── production/               # same files, production paths
├── infrastructure/
│   ├── base/                     # ingress, cert-manager, policy controller
│   ├── staging/
│   └── production/
├── tenants/
│   ├── staging/shop.yaml
│   └── production/shop.yaml      # namespace, service account, Flux objects for "shop"
├── apps/
│   ├── base/shop/                # Deployment, Service, ServiceAccount
│   ├── staging/shop/             # image digest, replicas, encrypted secrets
│   └── production/shop/
└── .sops.yaml                    # one age recipient per tenant and environment

Lock Flux down first. By default a Kustomization that names no service account is applied as cluster-admin, and Flux grants the built-in admin role rights over Flux objects[25][33]. So a tenant holding admin in its namespace could commit its own Kustomization without serviceAccountName and have Flux apply it as cluster-admin. Flux’s documented lockdown closes this. Such objects fall back to their namespace’s default account, which has no permissions; cross-namespace source references and Kustomize remote bases are refused (principle 2); flux-system keeps the controller’s account[33].

Example status: YAML syntax-checked; patches as in Flux’s multi-tenancy guide[33], except that source-watcher is left out of the --default-service-account patch, because the guide says it accepts that flag only from Flux 2.10.0; not applied.

# clusters/production/flux-system/kustomization.yaml: bootstrap + lockdown (Flux 2.9)
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - gotk-components.yaml
  - gotk-sync.yaml
patches:
  - patch: |
      - op: add
        path: /spec/template/spec/containers/0/args/-
        value: --no-cross-namespace-refs=true
    target:
      kind: Deployment
      name: "(kustomize-controller|helm-controller|notification-controller)"
  - patch: |
      - op: add
        path: /spec/template/spec/containers/0/args/-
        value: --no-cross-namespace-refs=true
    target:
      kind: Deployment
      name: "(image-reflector-controller|image-automation-controller|source-watcher)"
  - patch: |
      - op: add
        path: /spec/template/spec/containers/0/args/-
        value: --no-remote-bases=true
    target:
      kind: Deployment
      name: kustomize-controller
  - patch: |
      - op: add
        path: /spec/template/spec/containers/0/args/-
        value: --default-service-account=default
    target:
      kind: Deployment
      name: "(kustomize-controller|helm-controller)"   # add source-watcher from Flux 2.10
  - patch: |
      - op: add
        path: /spec/serviceAccountName
        value: kustomize-controller
    target:
      kind: Kustomization
      name: flux-system

The platform’s own Kustomizations install add-ons and tenant namespaces, so they name the controller’s account explicitly:

Example status: YAML syntax-checked by the validator; fields follow the Flux v1 Kustomization API[25]; not applied to a cluster.

# clusters/production/infrastructure.yaml and tenants.yaml
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: infrastructure
  namespace: flux-system
spec:
  serviceAccountName: kustomize-controller   # platform scope: cluster-admin, on purpose
  interval: 30m          # drift check; new commits are picked up when the source changes
  retryInterval: 2m
  timeout: 5m
  sourceRef:
    kind: GitRepository
    name: flux-system
  path: ./infrastructure/production
  prune: true
  wait: true             # health-check everything this applies
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: tenants
  namespace: flux-system
spec:
  serviceAccountName: kustomize-controller
  dependsOn:
    - name: infrastructure
  interval: 30m
  timeout: 3m
  sourceRef:
    kind: GitRepository
    name: flux-system
  path: ./tenants/production
  prune: true

The tenant file gives “shop” a namespace, a service account that Flux impersonates, and its own source. With the lockdown set, the Kustomization below can do only what shop-reconciler may do in shop[33][25]. Because that account holds admin, commits under apps/production/shop/ can also create Flux objects in shop; under the lockdown those run only as accounts in shop too.

Example status: YAML syntax-checked; not applied. The trusted-signers, shop-config-auth and sops-age Secrets are created out of band during cluster bootstrap and are never committed.

# tenants/production/shop.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: shop
  labels:
    kustomize.toolkit.fluxcd.io/prune: disabled   # never garbage-collect the namespace
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: shop-reconciler
  namespace: shop
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: shop-reconciler-admin
  namespace: shop
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: admin                # namespace-scoped through the RoleBinding
subjects:
  - kind: ServiceAccount
    name: shop-reconciler
    namespace: shop
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
  name: shop-config
  namespace: shop
spec:
  interval: 1m
  url: ssh://git@git.example.com/platform/fleet-config.git
  ref:
    branch: main
  secretRef:
    name: shop-config-auth     # read-only deploy key
  verify:
    mode: HEAD                 # the HEAD commit must carry a trusted signature
    secretRef:
      name: trusted-signers    # .asc (PGP) or .sshpub (SSH) public keys
  ignore: |                    # trims the artefact; NOT access control
    /*
    !/apps/production/shop/
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: shop
  namespace: shop
spec:
  serviceAccountName: shop-reconciler
  interval: 10m
  retryInterval: 1m
  timeout: 3m
  sourceRef:
    kind: GitRepository
    name: shop-config
  path: ./apps/production/shop
  targetNamespace: shop
  prune: true
  wait: true
  decryption:
    provider: sops
    secretRef:
      name: sops-age

Who can read these credentials. ignore only trims the artefact Flux builds. The deploy key in shop-config-auth reads all of fleet-config: in a monorepo every tenant can read every tenant’s configuration, because Git “is not designed to restrict access to certain files”[47]. Both Secrets sit in namespace shop, and “anyone who is authorized to create a Pod in a namespace can use that access to read any Secret in that namespace”[51]. That covers the shop team, shop-reconciler (admin allows “read/write access to most resources in a namespace”[69], Pods included), and so any commit merged under apps/production/shop/, plus cluster administrators. So the age key must decrypt nothing but shop’s production Secrets: one key per tenant and environment (see Lab 3), or a per-tenant cloud KMS key reached through Flux’s object-level workload identity (behind a feature gate in Flux 2.9[25]). If tenants must not read each other’s configuration, use Flux’s repository-per-team layout with a deploy key per tenant repository[47].

What a signature proves. When merges happen on the Git host, the HEAD of main is a commit the host created. GitHub.com signs web-interface commits with its own key (https://github.com/web-flow.gpg); GitHub Enterprise Server does so only if a site administrator has enabled web commit signing, with the key at https://HOSTNAME/web-flow.gpg. Rebase-and-merge adds commits “without commit signature verification”[70]. So trusted-signers must include the host’s key where the host signs, or a release bot must sign merges or tags (mode: Tag or TagAndHEAD)[38]. The signature then proves the change came through the host or the bot; who approved it comes from branch protection.

The same intent in Argo CD uses an AppProject to fence the tenant and an Application with automated sync. Cluster-scoped kinds are allow-listed in a project, so leaving the list out permits none[27]. Without a deletion finalizer, kubectl delete of the Application does not cascade to the workloads, but argocd app delete cascades by default[71].

Example status: YAML syntax-checked; fields follow the Argo CD 3.5 documentation[27][24][37]; not applied.

apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  name: shop
  namespace: argocd
spec:
  description: Shop team applications
  sourceRepos:
    - https://git.example.com/platform/fleet-config.git
  destinations:
    - server: https://kubernetes.default.svc
      namespace: shop
  syncWindows:
    - kind: deny                 # weekend freeze; on-call may still sync manually
      schedule: "0 18 * * 5"
      timeZone: "Europe/Amsterdam"
      duration: 62h
      applications:
        - "*"
      manualSync: true
---
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: shop-production
  namespace: argocd
spec:
  project: shop
  source:
    repoURL: https://git.example.com/platform/fleet-config.git
    targetRevision: main
    path: apps/production/shop
  destination:
    server: https://kubernetes.default.svc
    namespace: shop
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    retry:
      limit: 5
      backoff:
        duration: 10s
        factor: 2
        maxDuration: 3m

Pull-request checks need no cluster credentials. Render every directory that has a kustomization.yaml, including tenants/ and clusters/; run the image guard from Lab 3 on application and infrastructure renders; and refuse cluster-scoped RBAC or CRDs in tenant paths. The lockdown would make such an apply fail; this check makes the pull request fail first.

Example status: bash -n syntax-checked; logic executed on 2026-10-16 in a throw-away repository with a stub kustomize that concatenates the resources each kustomization.yaml lists; not run with real kustomize.

#!/usr/bin/env bash
# ci/validate.sh: run on every pull request (needs kustomize and python3; no cluster)
set -euo pipefail
status=0
rendered="$(mktemp)"
trap 'rm -f "$rendered"' EXIT
mapfile -t dirs < <(find . -name kustomization.yaml -not -path './.git/*' \
  -exec dirname {} \; | sort)
for dir in "${dirs[@]}"; do
  echo "== ${dir}"
  if ! kustomize build "$dir" > "$rendered"; then
    echo "render failed: ${dir}"
    status=1
    continue
  fi
  case "$dir" in
    ./apps/*)
      python3 ci/check_images.py "$rendered" || status=1
      if grep -Eq '^kind: (ClusterRole(Binding)?|CustomResourceDefinition)$' "$rendered"
      then
        echo "cluster-scoped RBAC or CRD in a tenant path: ${dir}"
        status=1
      fi ;;
    ./infrastructure/*) python3 ci/check_images.py "$rendered" || status=1 ;;
  esac
done
exit "$status"

Labs 1–3 were not executed here (no container runtime or Kubernetes tools); their commands follow the cited documentation and pass syntax checks. The Python scripts in Labs 3 and 4 were executed (2026-10-16, Python 3.8.10); their outputs are real.

Lab 1 (beginner): watch Argo CD revert drift

Link to section “Lab 1 (beginner): watch Argo CD revert drift”

Objective. See the reconciliation loop: declare an application, change it by hand, watch it come back, then turn self-heal off and see drift persist. Prerequisites. Docker; kind v0.33.0[72]; kubectl 1.35 or later; about 4 GB of free memory; internet access to GitHub and public registries. Cost and risk. Free and local. The cluster is disposable.

Example status: bash -n and YAML syntax-checked; not executed. Pinned to Argo CD v3.5.5, which is tested on Kubernetes 1.33–1.36 [67], hence the 1.36 node image.

  1. Create the cluster and install Argo CD with server-side apply, which its CRDs need[73]. Run these commands.
kind create cluster --name gitops-lab \
  --image kindest/node:v1.36.4@sha256:099e049362a1526b2db71494e1947aae99bd16290d7c895f2b7ea312e3cbfaed
kubectl create namespace argocd
kubectl apply -n argocd --server-side --force-conflicts \
  -f https://raw.githubusercontent.com/argoproj/argo-cd/v3.5.5/manifests/install.yaml
kubectl -n argocd rollout status statefulset/argocd-application-controller --timeout=300s
kubectl -n argocd rollout status deployment/argocd-repo-server --timeout=300s
  1. Save as guestbook-app.yaml and apply it with kubectl apply -f guestbook-app.yaml. The revision is a commit SHA, so the manifests cannot change under you; the image tag they name (gb-frontend:v5) still can, and the Lab 3 guard would flag it:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: guestbook
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/argoproj/argocd-example-apps.git
    targetRevision: 8088f4c0d970abb09e250248cc97e35623447cb5
    path: guestbook
  destination:
    server: https://kubernetes.default.svc
    namespace: guestbook
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true
  1. Check status, create drift, and watch it disappear:
kubectl -n argocd get application guestbook \
  -o jsonpath='{.status.sync.status}{" "}{.status.health.status}{"\n"}'
kubectl -n guestbook scale deployment guestbook-ui --replicas=3
sleep 30
kubectl -n guestbook get deployment guestbook-ui -o jsonpath='{.spec.replicas}{"\n"}'
kubectl -n argocd get events --field-selector involvedObject.name=guestbook
  1. Turn self-heal off, repeat the scale, and compare:
kubectl -n argocd patch application guestbook --type merge \
  -p '{"spec":{"syncPolicy":{"automated":{"prune":true,"selfHeal":false}}}}'
kubectl -n guestbook scale deployment guestbook-ui --replicas=3
sleep 30
kubectl -n argocd get application guestbook -o jsonpath='{.status.sync.status}{"\n"}'

Expected output. After step 2, within a few minutes: Synced Healthy. In step 3 the replica count returns to 1 (self-heal retries after a 5-second default timeout[24]) and the events list sync operations. In step 4 the count stays at 3 and the status is OutOfSync: Argo CD sees the drift but, with self-heal off, live changes “will not trigger automated sync”[24]. Validation. You can explain why steps 3 and 4 differ using principle 4. Troubleshooting. CRD errors about annotation size mean --server-side was missing; Unknown sync status usually means the repo-server cannot reach GitHub; ImagePullBackOff means the node cannot reach the registry. Teardown. kind delete cluster --name gitops-lab.

Lab 2 (intermediate): Flux reconciliation, break-glass and a new revision

Link to section “Lab 2 (intermediate): Flux reconciliation, break-glass and a new revision”

Objective. Reconcile a pinned release with Flux, see a kubectl set image reverted, practise suspend and resume as a break-glass step, and change the desired state. Prerequisites. As Lab 1 (Flux 2.9 supports Kubernetes 1.34–1.36[65]). Cost and risk. Free and local; this uses Flux’s “dev install”, which Flux documents for testing only[74].

Example status: bash -n and YAML syntax-checked; not executed.

kind create cluster --name flux-lab \
  --image kindest/node:v1.36.4@sha256:099e049362a1526b2db71494e1947aae99bd16290d7c895f2b7ea312e3cbfaed
kubectl apply -f https://github.com/fluxcd/flux2/releases/download/v2.9.6/install.yaml
kubectl -n flux-system rollout status deployment/source-controller --timeout=180s
kubectl -n flux-system rollout status deployment/kustomize-controller --timeout=180s

Save as podinfo.yaml and apply it. The source is pinned to tag 6.15.0 of the podinfo demo application; its kustomize/ directory holds a Deployment, a Service and a HorizontalPodAutoscaler. Because the autoscaler owns replicas, this lab creates drift on the image instead.

apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
  name: podinfo
  namespace: flux-system
spec:
  interval: 1m
  url: https://github.com/stefanprodan/podinfo
  ref:
    tag: "6.15.0"
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: podinfo
  namespace: flux-system
spec:
  interval: 2m
  timeout: 2m
  sourceRef:
    kind: GitRepository
    name: podinfo
  path: ./kustomize
  targetNamespace: default
  prune: true
  healthChecks:
    - apiVersion: apps/v1
      kind: Deployment
      name: podinfo
      namespace: default
kubectl apply -f podinfo.yaml
kubectl -n flux-system wait kustomization/podinfo --for=condition=ready --timeout=3m
img() {
  kubectl -n default get deployment podinfo \
    -o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}'
}
reconcile() {
  kubectl -n flux-system annotate --field-manager=flux-client-side-apply --overwrite \
    kustomization/podinfo reconcile.fluxcd.io/requestedAt="$(date +%s)"
}
set_suspend() {  # $1 = true or false
  kubectl -n flux-system patch kustomization podinfo \
    --field-manager=flux-client-side-apply --type merge -p "{\"spec\":{\"suspend\":$1}}"
}
OLD=ghcr.io/stefanprodan/podinfo:6.14.1
img                                                    # 1. desired image
kubectl -n default set image deployment/podinfo podinfod="$OLD"
reconcile; sleep 20; img                               # 2. drift reverted
set_suspend true
kubectl -n default set image deployment/podinfo podinfod="$OLD"
sleep 20; img                                          # 3. break-glass: fix survives
set_suspend false
reconcile; sleep 20; img                               # 4. resume: declared image
kubectl -n flux-system patch gitrepository podinfo --type merge \
  -p '{"spec":{"ref":{"tag":"6.14.1"}}}'
sleep 90; img                                          # 5. new desired state

Expected output. Lines 1, 2 and 4 print ghcr.io/stefanprodan/podinfo:6.15.0; line 3 prints …:6.14.1, because suspension pauses drift correction[25]; line 5 prints …:6.14.1 again, this time as the declared state. Validation. kubectl -n flux-system get kustomization podinfo shows READY True and the applied revision. In a real setup step 5 is a merged pull request, because the GitRepository itself lives in Git after bootstrap[13]; GitLabOps shows that loop through GitLab’s agent for Kubernetes. Troubleshooting. kubectl -n flux-system describe kustomization podinfo and kubectl -n flux-system get events show build, apply and health-check errors; a health-check timeout usually means the image could not be pulled. Teardown. kind delete cluster --name flux-lab.

Lab 3 (intermediate, no cluster): environments, an encrypted secret and a merge guard

Link to section “Lab 3 (intermediate, no cluster): environments, an encrypted secret and a merge guard”

Objective. Build a base-and-overlay layout, encrypt a Secret with SOPS and age, and block mutable images and plain Secrets before merge. Prerequisites. kustomize 5.8, sops 3.13, age 1.3 (current releases in October 2026), Python 3.8 or later. Cost and risk. Free and local, but the .agekey files are private keys: keep them out of Git. If one is ever committed, rotate it and every value it encrypted, because Git history keeps the old ciphertext.

Example status: shell steps bash -n syntax-checked and not executed (tools not installed here); check_images.py executed on 2026-10-16 on the sample described in step 3.

  1. Create a base and a production overlay, a variant of the base[26]: save each document below as the file its first comment names, then copy the two overlay files to apps/staging/shop/ with another count and digest. The base pins a digest too, because the reference implementation’s ci/validate.sh runs the guard on every render under apps/; Kustomize’s images field replaces it by image name, and replicas sets the count[75].

Example status: YAML syntax-checked; not rendered (kustomize not installed here). The digests are placeholders.

# apps/base/shop/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - deployment.yaml
  - service.yaml
images:
  - name: registry.example.com/shop/web
    digest: sha256:1111111111111111111111111111111111111111111111111111111111111111
---
# apps/base/shop/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 1
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - name: web
          image: registry.example.com/shop/web   # the kustomization pins the digest
          ports:
            - containerPort: 8080
---
# apps/base/shop/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: web
spec:
  selector:
    app: web
  ports:
    - port: 80
      targetPort: 8080
---
# apps/production/shop/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: shop
resources:
  - ../../base/shop
  - secret-db.yaml
replicas:
  - name: web
    count: 3
images:
  - name: registry.example.com/shop/web
    digest: sha256:2222222222222222222222222222222222222222222222222222222222222222
---
# apps/production/shop/secret-db.yaml: plain until step 2 encrypts it
apiVersion: v1
kind: Secret
metadata:
  name: db
type: Opaque
stringData:
  password: lab-placeholder-not-a-real-secret
  1. Create one key per environment for the shop tenant and a SOPS rule per path, then encrypt each secret-db.yaml so only the stringData values are encrypted[53]. A file that matches no rule is refused, so nothing is encrypted to the wrong key:
for env in staging production; do
  age-keygen -o "shop-${env}.agekey"   # private key: outside Git, backed up offline
done
echo "*.agekey" >> .gitignore
pub_stg="$(age-keygen -y shop-staging.agekey)"
pub_prd="$(age-keygen -y shop-production.agekey)"
cat > .sops.yaml <<EOF
creation_rules:                             # one recipient per tenant and environment
  - path_regex: apps/staging/shop/secret.*\.yaml$
    encrypted_regex: ^(data|stringData)$
    age: ${pub_stg}
  - path_regex: apps/production/shop/secret.*\.yaml$
    encrypted_regex: ^(data|stringData)$
    age: ${pub_prd}
EOF
for env in staging production; do
  sops --encrypt --in-place "apps/${env}/shop/secret-db.yaml"
done
grep -c 'ENC\[' apps/production/shop/secret-db.yaml   # expect 1 or more
kustomize build apps/production/shop > rendered-production.yaml
  1. Save the guard as ci/check_images.py and run it on your render: it should print 0 finding(s). The executed run below used a sample render that adds the mistakes the guard exists for: beside an encrypted Secret, a plain Secret, a Deployment with one pinned and one unpinned container, and a CronJob on :latest.
"""Pre-merge guard for a GitOps repository: refuse mutable images and unencrypted Secrets.

Usage: kustomize build overlays/production | python3 check_images.py
   or: python3 check_images.py rendered.yaml
Reads rendered Kubernetes YAML (documents separated by ---). Standard library only and
line-based on purpose: it inspects renderer output, not hand-written YAML, so it needs no
YAML parser. Exit code 1 on any finding.
"""
import re
import sys

IMAGE = re.compile(r"^\s*-?\s*image:\s*['\"]?([^'\"\s]+)")
KIND = re.compile(r"^kind:\s*(\S+)")
NAME = re.compile(r"^\s{2}name:\s*(\S+)")


def documents(text):
    """Split a multi-document YAML stream on lines that are exactly '---'."""
    doc = []
    for line in text.splitlines():
        if line.strip() == "---":
            if doc:
                yield doc
            doc = []
        else:
            doc.append(line)
    if doc:
        yield doc


def check(text):
    findings = []
    for lines in documents(text):
        kind = next((m.group(1) for m in map(KIND.match, lines) if m), "?")
        name = next((m.group(1) for m in map(NAME.match, lines) if m), "?")
        where = "%s/%s" % (kind, name)
        if kind == "Secret" and not any(l.startswith("sops:") for l in lines):
            findings.append("%s: Secret is not SOPS-encrypted (no top-level sops: block)"
                            % where)
        for line in lines:
            m = IMAGE.match(line)
            if not m:
                continue
            ref = m.group(1)
            if "@sha256:" in ref:
                continue
            tag = ref.rsplit(":", 1)[1] if ":" in ref.rsplit("/", 1)[-1] else "(none)"
            reason = ("mutable tag 'latest'" if tag == "latest"
                      else "tag %s without digest" % tag)
            findings.append("%s: image %s uses %s" % (where, ref, reason))
    return findings


def main():
    if len(sys.argv) > 1:
        text = open(sys.argv[1], encoding="utf-8").read()
    else:
        text = sys.stdin.read()
    findings = check(text)
    for f in findings:
        print("FAIL " + f)
    print("%d finding(s)" % len(findings))
    return 1 if findings else 0


if __name__ == "__main__":
    sys.exit(main())

Expected output (actual output of python3 check_images.py rendered-production.yaml on the sample; exit code 1):

FAIL Secret/legacy-api-key: Secret is not SOPS-encrypted (no top-level sops: block)
FAIL Deployment/checkout: image registry.example.com/infra/log-shipper:2.4.1 uses tag 2.4.1 without digest
FAIL CronJob/nightly-report: image registry.example.com/shop/report:latest uses mutable tag 'latest'
3 finding(s)

Validation. SOPS_AGE_KEY_FILE=shop-production.agekey sops --decrypt apps/production/shop/secret-db.yaml shows the plain values, and the staging key fails on the same file, so the keys are separated (SOPS reads age keys from SOPS_AGE_KEY_FILE[76]). On the sample, pinning both images by digest and encrypting the second Secret brings the output to 0 finding(s), exit 0. Troubleshooting. sops reporting “no matching creation rules” means the file path does not match path_regex; a Secret with encrypted metadata will fail in Flux, which supports encrypting only data or stringData[53]. Teardown. Delete the lab directory and the two practice .agekey files.

Lab 4 (advanced, executed): measure a week of GitOps operations

Link to section “Lab 4 (advanced, executed): measure a week of GitOps operations”

Objective. Compute the metrics of this page from one week of events for one cluster; in production, take the inputs from the data sources listed in the metrics table. Prerequisites. Python 3.8 or later. Cost and risk. None.

Example status: executed on 2026-10-16 with Python 3.8.10; JSON and Python syntax-checked by the validator. The data is invented for the exercise.

Save as gitops_events.json:

{
  "cluster": "prod-eu-1",
  "window": {"start": "2026-09-07T00:00:00Z", "end": "2026-09-14T00:00:00Z"},
  "managed_apps": 12,
  "changes": [
    {"pr": 101, "merged_at": "2026-09-07T09:02:00Z",
     "healthy_at": "2026-09-07T09:06:30Z", "outcome": "healthy"},
    {"pr": 102, "merged_at": "2026-09-07T13:40:00Z",
     "healthy_at": "2026-09-07T13:43:10Z", "outcome": "healthy"},
    {"pr": 103, "merged_at": "2026-09-08T10:15:00Z",
     "healthy_at": null, "outcome": "failed", "fixed_by_pr": 104},
    {"pr": 104, "merged_at": "2026-09-08T10:41:00Z",
     "healthy_at": "2026-09-08T10:45:00Z", "outcome": "healthy"},
    {"pr": 105, "merged_at": "2026-09-09T08:55:00Z",
     "healthy_at": "2026-09-09T09:03:40Z", "outcome": "healthy"},
    {"pr": 106, "merged_at": "2026-09-09T16:20:00Z",
     "healthy_at": "2026-09-09T16:22:50Z", "outcome": "healthy"},
    {"pr": 107, "merged_at": "2026-09-10T11:05:00Z",
     "healthy_at": "2026-09-10T11:31:00Z", "outcome": "healthy"},
    {"pr": 108, "merged_at": "2026-09-11T09:30:00Z",
     "healthy_at": "2026-09-11T09:33:20Z", "outcome": "healthy"},
    {"pr": 109, "merged_at": "2026-09-11T15:10:00Z",
     "healthy_at": null, "outcome": "failed", "fixed_by_pr": 110},
    {"pr": 110, "merged_at": "2026-09-11T15:52:00Z",
     "healthy_at": "2026-09-11T15:55:30Z", "outcome": "healthy"}
  ],
  "drift": [
    {"object": "Deployment/checkout", "cause": "human", "ticket": null,
     "detected_at": "2026-09-07T22:10:00Z", "resolved_at": "2026-09-07T22:10:20Z",
     "resolution": "reverted"},
    {"object": "ConfigMap/feature-flags", "cause": "controller", "ticket": null,
     "detected_at": "2026-09-08T03:00:00Z", "resolved_at": "2026-09-08T11:40:00Z",
     "resolution": "ignore-rule-added"},
    {"object": "Deployment/search", "cause": "human", "ticket": "INC-2291",
     "detected_at": "2026-09-09T19:45:00Z", "resolved_at": "2026-09-10T08:30:00Z",
     "resolution": "adopted-into-git"},
    {"object": "Service/payments", "cause": "human", "ticket": null,
     "detected_at": "2026-09-10T02:12:00Z", "resolved_at": "2026-09-10T02:12:30Z",
     "resolution": "reverted"},
    {"object": "HorizontalPodAutoscaler/web", "cause": "human", "ticket": null,
     "detected_at": "2026-09-12T14:00:00Z", "resolved_at": "2026-09-12T14:00:10Z",
     "resolution": "reverted"},
    {"object": "Deployment/search", "cause": "human", "ticket": null,
     "detected_at": "2026-09-13T21:05:00Z", "resolved_at": "2026-09-13T21:05:20Z",
     "resolution": "reverted"}
  ],
  "out_of_sync": [
    {"app": "web", "from": "2026-09-08T03:00:00Z", "to": "2026-09-08T11:40:00Z"},
    {"app": "search", "from": "2026-09-09T19:45:00Z", "to": "2026-09-10T08:30:00Z"},
    {"app": "checkout", "from": "2026-09-08T10:15:00Z", "to": "2026-09-08T10:45:00Z"},
    {"app": "web", "from": "2026-09-11T15:10:00Z", "to": "2026-09-11T15:55:30Z"}
  ]
}

Save as gitops_metrics.py and run python3 gitops_metrics.py gitops_events.json:

"""Compute GitOps operating metrics for ONE cluster from a weekly event log.

Usage: python3 gitops_metrics.py gitops_events.json
Metric definitions are this Atlas's (see the Metrics and KPIs section).
Standard library only; Python 3.8+.
"""
import json
import statistics
import sys
from datetime import datetime


def ts(value):
    """Parse an ISO-8601 UTC timestamp such as 2026-09-07T09:02:00Z."""
    return datetime.fromisoformat(value.replace("Z", "+00:00"))


def minutes(start, end):
    return (ts(end) - ts(start)).total_seconds() / 60


def p90(values):
    """Nearest-rank 90th percentile (small samples: report it, do not over-read it)."""
    ordered = sorted(values)
    rank = max(1, -(-9 * len(ordered) // 10))  # ceil(0.9 * n)
    return ordered[rank - 1]


def main(path):
    with open(path, encoding="utf-8") as fh:
        data = json.load(fh)
    start, end = data["window"]["start"], data["window"]["end"]
    window_min = minutes(start, end)

    changes = data["changes"]
    lags = [minutes(c["merged_at"], c["healthy_at"])
            for c in changes if c["outcome"] == "healthy"]
    failed = [c for c in changes if c["outcome"] == "failed"]
    print("Cluster %s, %s to %s" % (data["cluster"], start[:10], end[:10]))
    print("Changes merged: %d, reached healthy: %d, failed: %d"
          % (len(changes), len(lags), len(failed)))
    print("Reconciliation lag (merge -> healthy): "
          "median %.1f min, p90 %.1f min, max %.1f min"
          % (statistics.median(lags), p90(lags), max(lags)))
    print("Apply failure rate: %d/%d = %.0f%%"
          % (len(failed), len(changes), 100.0 * len(failed) / len(changes)))

    drift = data["drift"]
    human = [d for d in drift if d["cause"] == "human"]
    unticketed = [d for d in human if not d["ticket"]]
    resolve = [minutes(d["detected_at"], d["resolved_at"]) for d in drift]
    mix = {}
    for d in drift:
        mix[d["resolution"]] = mix.get(d["resolution"], 0) + 1
    print("Drift events: %d (human %d, controller %d); median time to resolve %.1f min"
          % (len(drift), len(human), len(drift) - len(human), statistics.median(resolve)))
    print("Resolution mix: "
          + ", ".join("%s %d" % (k, v) for k, v in sorted(mix.items())))
    print("Out-of-band human changes: %d, of which without a break-glass ticket: %d"
          % (len(human), len(unticketed)))

    oos = sum(minutes(o["from"], o["to"]) for o in data["out_of_sync"])
    share = 100.0 * oos / (data["managed_apps"] * window_min)
    print("Time out of sync: %.0f app-minutes of %.0f = %.2f%%"
          % (oos, data["managed_apps"] * window_min, share))


if __name__ == "__main__":
    if len(sys.argv) != 2:
        sys.exit("usage: python3 gitops_metrics.py gitops_events.json")
    main(sys.argv[1])

Expected output (actual output of the executed run):

Cluster prod-eu-1, 2026-09-07 to 2026-09-14
Changes merged: 10, reached healthy: 8, failed: 2
Reconciliation lag (merge -> healthy): median 3.8 min, p90 26.0 min, max 26.0 min
Apply failure rate: 2/10 = 20%
Drift events: 6 (human 5, controller 1); median time to resolve 0.4 min
Resolution mix: adopted-into-git 1, ignore-rule-added 1, reverted 4
Out-of-band human changes: 5, of which without a break-glass ticket: 4
Time out of sync: 1360 app-minutes of 120960 = 1.12%

Validation. Recompute one number by hand (the worked calculation in Metrics and KPIs does this). Questions. Why does the p90 equal the maximum? Why does a median time-to-resolve of 0.4 minutes hide the most important drift events? Troubleshooting. ValueError from fromisoformat means a timestamp is not ISO-8601; Python older than 3.8 is not supported. Teardown. None.

All five scenarios are illustrative: they combine documented tool behaviour into realistic situations, not reports from named organisations.

Scenario Context and problem Method Trade-offs and pitfalls Success measure
1. Platform add-ons across a fleet A platform team runs 40 clusters; ingress, certificate and policy controllers differ by cluster Shared infrastructure/base with per-cluster paths (Flux) or an ApplicationSet cluster generator (Argo CD)[47][28]; two canary clusters first One bad base change reaches every cluster; ApplicationSet deletions cascade by default (guards and their preconditions under failures)[77] Number of distinct add-on versions across the fleet; time to roll a security patch to all clusters
2. Change evidence in a regulated firm Auditors sample production changes: who approved, what applied, when Protected production paths with code-owner review and signed commits, the host’s key trusted[35]; reconciler events kept with pull requests; sync windows for freezes[37] Merge and apply are separate events[36]; break-glass must be just as traceable Share of sampled changes with a complete evidence chain; unticketed out-of-band changes per month
3. Rebuild a cluster or region A cluster is lost or a new region is needed Bootstrap a reconciler at the same path, restore data from backups, reconcile in dependency order (dependsOn, sync waves)[25][63] Desired state excludes data[11]; decryption keys and bootstrap credentials must be recoverable outside the cluster Rebuild time from empty cluster to all applications healthy, measured in a quarterly drill
4. Disconnected edge sites Hundreds of shops with outbound-only, unreliable links One agent per site pulling small, signed OCI artefacts (“Gitless GitOps”)[13][29]; site overlays only for true differences Large artefacts over thin links; overlay sprawl; a hub cannot reach sites behind NAT Share of sites on the current revision within 24 hours; sites stale for more than 7 days
5. Cloud resources beside the workload Teams click databases and buckets into existence; production differs from staging Declare cloud resources with Crossplane[21] or tofu-controller, starting in drift-detection-only mode[15] A field change can force replacement of a stateful resource; keep deletion protection and approval for destructive plans Cloud resources created outside Git per month; drift detected versus corrected

GitOps changes how changes reach production, so measure the mechanism here and leave delivery outcomes to DORA’s metrics on DevOps. The definitions below are this Atlas’s; no standard defines GitOps metrics.

KPI Definition and formula Data source Pitfall Review
Reconciliation lag Time from merge (or artefact publication) to every object healthy at that revision; report median and p90 per cluster Merge time from the Git host; applied revision and health from reconciler status or events Polling and health timeouts dominate; webhooks cut polling[44][45] Weekly
Apply failure rate Merged changes that did not reach healthy without a further commit ÷ merged changes Commit statuses, reconciler events, argocd_app_sync_total{phase}[78] Automatic retries hide flapping; count per revision, not per attempt Weekly
Drift rate Drift events per managed application per week, split by cause (human, controller) and resolution (reverted, adopted into Git, ignore rule added) Self-heal events, drift alerts Ignore rules hide drift from the count; review the rule list too Weekly
Out-of-band changes Write requests by human identities to managed objects; target zero without a break-glass ticket Kubernetes audit logs (who, what, when, on which object)[79] Shared admin accounts make “who” useless Weekly, monthly trend
Time out of sync Σ out-of-sync durations ÷ (managed applications × window) argocd_app_info{sync_status}[78]; Flux gotk_resource_info{ready}[80] Suspended applications may report ready; count suspension separately Monthly
Rebuild time Time from an empty cluster to all applications healthy in a drill, including data restore Drill records A drill without data restore measures the easy half Quarterly
Reconciler health Reconcile duration, errors, controller restarts, source fetch failures gotk_reconcile_duration_seconds[80]; argocd_app_reconcile[78] A stopped reconciler causes stale clusters, not outages, so nobody notices Alert continuously

Take the Lab 4 week. Eight of ten merged changes became healthy. Their lags, in minutes, sorted: 2.8, 3.2, 3.3, 3.5, 4.0, 4.5, 8.7, 26.0.

  • Median lag = (3.5 + 4.0) ÷ 2 = 3.75 min (printed as 3.8).
  • p90 by nearest rank = value number ⌈0.9 × 8⌉ = 8 = 26.0 min. With eight samples the p90 is the maximum; report it, but do not set targets on a week of data.
  • Apply failure rate = 2 ÷ 10 = 20%. Both failures were fixed by follow-up pull requests merged 26 and 42 minutes later.
  • Time out of sync = 520 + 765 + 30 + 45.5 = 1,360.5 app-minutes, over 12 applications × 10,080 minutes = 120,960 app-minutes, which is 1.12%.
  • The story behind the numbers. Two drift events account for (520 + 765) ÷ 1,360.5 = 94% of the out-of-sync time: a controller rewriting feature-flags, tolerated only after an ignore rule was reviewed and merged 8 hours 40 minutes later, and the hotfix on search, adopted into Git the next morning. Yet the median time to resolve drift is 0.4 minutes, because self-heal reverted the four other events within seconds. The median says “drift is a non-issue”; the distribution says “two exceptions ran for hours outside Git”. Review the tail, and review the four unticketed human changes.

Targets. No published benchmarks exist for these measures. Measure four weeks, then set targets from your own baseline and from what users of the platform accept (for example, “p90 lag under 10 minutes for application changes”).

Vanity metrics. “Percentage of services on GitOps”, “syncs per day” and “number of Applications” grow with effort, not with control.

Example status: PromQL written for this page and not executed; metric and label names are from the Argo CD and Flux documentation [78][80]. The Flux resource metric requires the kube-state-metrics configuration from Flux’s monitoring example.

# Argo CD: applications currently out of sync, per project
count by (project) (argocd_app_info{sync_status="OutOfSync"})

# Argo CD: share of sync operations that ended in Error or Failed over 7 days
sum(increase(argocd_app_sync_total{phase=~"Error|Failed"}[7d]))
  / sum(increase(argocd_app_sync_total[7d]))

# Flux: Kustomizations that are not ready, per namespace
count by (exported_namespace) (
  gotk_resource_info{customresource_kind="Kustomization", ready="False"})

# Flux: p90 reconcile duration by kind over the last hour
histogram_quantile(0.9,
  sum by (le, kind) (rate(gotk_reconcile_duration_seconds_bucket[1h])))

Security and compliance: who can really deploy

Link to section “Security and compliance: who can really deploy”

The most useful security question in GitOps is not “is the cluster locked down?” but “who can change what the reconciler reads?”. Each row below is a path to production.

What the reconciler reads or trusts Who can change it Control
The watched branch and paths Anyone who can push or merge Required reviews with code owners, required checks, signed commits, no administrator bypass[35]; Flux commit verification[38]; Argo CD source integrity[66]. A signature shows who merged, not who approved (what a signature proves)
The reconciler’s own objects (Applications, AppProjects, Kustomizations, GitRepositories) Whoever can edit them in Git or in the cluster Keep them in a platform-owned path; restrict Kubernetes RBAC on these kinds
Images, charts and remote bases referenced by the manifests Registry pushers and upstream maintainers Digests and exact versions; Argo CD’s repository allow-list “only restricts the initial repository which is cloned”, not remote bases or chart dependencies[36]
Secret-decryption keys Key holders, and anyone who can create Pods where the key Secret lives[51] One key per tenant and environment (this Atlas’s recommendation; Flux’s guide scopes a .sops.yaml to a cluster or namespace directory[53]); cloud KMS or offline age keys; rotation
Cluster credentials held by a central instance Hub administrators Narrow the argocd-manager role on each cluster; Argo CD needs cluster-wide read but not cluster-wide write[36]
Suspend, sync and rollback permissions Users with reconciler roles or UI access SSO groups and project roles[27]; audit these actions like deployments

Shrink the reconciler. In Flux, impersonate tenant service accounts under the lockdown shown in the reference implementation[33]. In Argo CD, use dedicated AppProjects (the default project permits any source, destination and kind[27]), limit the controller’s write rights by namespace and kind[36], and evaluate the beta impersonation feature, whose documentation warns that without it a compromised Argo CD gives attackers cluster-admin access[23].

Keep the renderer narrow. Disable unused config-management tools[36] and inject no secrets at render time (secrets).

Evidence and retention. Keep the evidence chain for your audit period, with audit-log entries for any out-of-band action[79]; Argo CD’s guide recommends persisting its Kubernetes Events with an exporter if you need them for longer[36]. Retention periods, and whether pull-request approval satisfies segregation-of-duties rules, depend on your regulator; map them with ComplianceOps.

Supply-chain controls that act before a change reaches the state store (scanning, signing images, admission policies) belong to DevSecOps; cluster hardening belongs to KubernetesSecOps.

  • Reconciler capacity. Argo CD’s repo-server forks Helm or Kustomize per render (--parallelismlimit avoids out-of-memory kills), the application controller can be sharded, and the HA manifests need at least three nodes[81]. Flux’s cost grows with the number of objects and each interval[25]; shorter intervals buy faster drift correction with more API and Git traffic.
  • Git host and registry load. Every source is polled on its interval; with hundreds of applications, prefer webhooks for speed and longer intervals for drift checks (Flux’s recommended production example uses one minute for the source and an hour for drift detection)[25].
  • Monitoring cost. Exposing Application labels as metrics multiplies series; Argo CD warns about cardinality[78].
  • People cost. Review time is the real price of “every change is a pull request”; automate low-risk promotions.
  • Sustainability. This page found no evidence that GitOps changes energy use materially. Carbon and cost optimisation of workloads belong to GreenOps and FinOps.

A GitOps setup can look healthy while quietly failing. Five checks keep it honest:

  1. Is the loop running? Alert when a source or Kustomization is not ready, or when an application has not reconciled within several intervals; a dead reconciler leaves clusters stale, not down.
  2. Is Git the only path? Review out-of-band human writes from audit logs weekly; every one needs a break-glass record.
  3. Are exceptions shrinking? Review every ignore rule and every prune: disabled annotation quarterly; remove the stale ones.
  4. Can we rebuild? Run a quarterly drill: new cluster, bootstrap, restore data, measure rebuild time.
  5. Does the evidence chain hold? Sample ten production changes a quarter and trace each from pull request to approval to applied revision to healthy status.
Symptom Likely cause Diagnosis Remediation Prevention
Application OutOfSync right after a successful sync A mutating webhook or controller changes fields; templates with random values; HPA reordering metrics[57] Diff live against desired; find the field that differs Fix the manifest, or ignore that one field Avoid random values in templates; keep ignore rules narrow
Replica count flaps; endless syncs replicas is in Git and an autoscaler also sets it Events show repeated syncs of one Deployment Remove replicas from Git or ignore it[30] Lint for replicas on autoscaled workloads
Kustomization never becomes ready Image pull failure, failing probes, or a custom resource applied before its CRD kubectl describe the Kustomization; read its events Fix the workload; add dependsOn or sync waves Health checks with realistic timeouts[25][63]
Secret decryption fails The age key Secret is missing or rotated; encrypted metadata Reconciler events name the decryption error Restore the key Secret; re-encrypt with current recipients Key-rotation runbook; encrypt only data/stringData[53]
Merged changes take minutes to appear Polling interval (up to three minutes by default in Argo CD) Compare merge time with the last fetched revision Configure webhooks or a Flux Receiver[44][45] Make lag a tracked KPI
Nothing deploys and nobody notices Expired Git credential or a crashed controller Source objects not ready; reconcile metrics stop Renew the credential; restart and investigate Alert on staleness; track credential expiry
Apps vanish from many clusters at once An ApplicationSet generator stopped matching clusters; the default sync policy deletes Applications and the finalizer cascades[77] ApplicationSet events and history Restore the generator input; resync Controller-wide --policy create-update, or per-ApplicationSet applicationsSync: create-update, which is ignored unless applicationsetcontroller.enable.policy.override is "true" (default false); plus preserveResourcesOnDeletion: true, which also protects workloads when the ApplicationSet itself is deleted[77]
Apply fails on an immutable field A Deployment selector or Job template changed Apply error names the field Set Flux force once, or the per-object force annotation, then remove it[25] Review changes to immutable fields explicitly

Worked runbook: a refactor that prunes production

Link to section “Worked runbook: a refactor that prunes production”
Figure 7. Runbook for an accidental prune
Yes, in parallelYes or noYesNoAlert: applications missing ornamespace terminatingStep 1: freeze reconciliationfor the affected scopeStep 2: data-bearing objectsdeleted?Step 4: restore data frombackupStep 3: find the commit andrevert it by pull requestStep 5: resume reconciliationand watch healthAll healthy and data verified?Step 6: prune protection, CIdeletion checkKeep the freeze; repeat steps3 and 5
Diagram source (Mermaid)
flowchart TB
  A["Alert: applications missing or namespace terminating"]:::risk --> B["Step 1: freeze reconciliation for the affected scope"]:::core
  B --> C{"Step 2: data-bearing objects deleted?"}:::decision
  C -- "Yes, in parallel" --> D["Step 4: restore data from backup"]:::risk
  C -- "Yes or no" --> E["Step 3: find the commit and revert it by pull request"]:::core
  E --> G["Step 5: resume reconciliation and watch health"]:::core
  G --> H{"All healthy and data verified?"}:::decision
  D --> H
  H -- "Yes" --> I["Step 6: prune protection, CI deletion check"]:::neutral
  H -- "No" --> R["Keep the freeze; repeat steps 3 and 5"]:::risk
  classDef core fill:#e8f3f2,stroke:#16756f,color:#122029
  classDef risk fill:#fbeceb,stroke:#b4332b,color:#122029
  classDef decision fill:#fdf3e3,stroke:#c9800f,color:#122029
  classDef neutral fill:#f1f4f6,stroke:#5b7a87,color:#122029

Text equivalent: Step numbers match the runbook below. After the alert, step 1 freezes reconciliation for the affected scope and step 2 checks whether data-bearing objects were deleted. Either way, step 3 finds and reverts the removing commit by pull request; if data was deleted, step 4 restores it from backup in parallel. Step 5 resumes reconciliation and watches health. If anything is unhealthy or the data is unverified, keep the freeze and repeat steps 3 and 5; otherwise step 6 adds prune protection and a CI deletion check.

1. Freeze (minutes 0–5). Stop the reconciler from acting on the bad revision. A Flux suspension pauses new revisions and drift correction; it holds because suspend is not declared in Git (a declared suspend: false “does not allow for ‘hot patching’” [25]). Turning off Argo CD’s automated sync holds only if nothing restores it: a parent application with self-heal, or an ApplicationSet, puts the child’s syncPolicy back, and self-heal then reverts your live fix. Prepare the exemption in advance: in the parent, ignoreDifferences on /spec/syncPolicy/automated with RespectIgnoreDifferences=true[34]; in an ApplicationSet, ignoreApplicationDifferences on /spec/syncPolicy, documented for “temporarily toggling auto-sync” [77]. A deny sync window blocks all syncs without editing the Application[37], but its AppProject usually lives in Git too.

Example status: bash -n syntax-checked; not executed. Flux names match the reference implementation; the Argo CD application name is illustrative and assumes the exemption above.

# Flux: pause the platform's tenants Kustomization
kubectl -n flux-system patch kustomization tenants \
  --field-manager=flux-client-side-apply --type merge -p '{"spec":{"suspend":true}}'
# Argo CD equivalent: stop automated sync for the application that manages tenants
argocd app set tenants-production --sync-policy none

2. Assess (minutes 5–15). List what was deleted and whether it held data. Reconciler events and Argo CD’s sync history name the revision and the pruned objects; Git shows the change.

argocd app history tenants-production                        # Argo CD: revisions synced
# Flux: what the tenants Kustomization did
kubectl -n flux-system get events --field-selector involvedObject.name=tenants
git log --oneline -5 -- tenants/production
SUSPECT_SHA=abc1234   # placeholder: the commit that dropped payments.yaml
git show --stat "$SUSPECT_SHA" -- tenants/production
kubectl get namespace payments -o jsonpath='{.status.phase}{"\n"}'

3. Restore configuration (minutes 15–30). Revert the offending commit through a pull request (an emergency approval path is fine; skipping Git is not). If the namespace is still Terminating, wait for deletion to finish before resuming, because a terminating namespace “cannot be used for new objects”[82].

git revert --no-edit "$SUSPECT_SHA"
git push origin HEAD:revert-tenants-prune   # then merge it via the emergency path

4. Restore data (in parallel). Configuration comes back from Git; data does not[11]. Restore the ledger volume from the latest backup and reconcile any transactions since, following DisasterRecoveryOps.

5. Resume and verify. Resume reconciliation (suspend: false, or re-enable automated sync), then confirm every application is Synced and healthy and the restored data passes application-level checks.

6. Prevent. Mark namespaces and claims as not prunable (Argo CD Prune=false or Prune=confirm[58]; Flux kustomize.toolkit.fluxcd.io/prune: disabled[25]); use a Retain reclaim policy for critical data classes[83]; and add a CI check that renders every directory with a kustomization.yaml (including tenants/, where this incident happened) and fails when a pull request removes a protected kind:

Example status: bash -n syntax-checked; logic executed on 2026-10-16 in a throw-away repository shaped like the reference layout, with a stub kustomize that concatenates the resources each kustomization.yaml lists; not run with real kustomize.

#!/usr/bin/env bash
# ci/deletion-check.sh: fail if a PR removes Namespaces, PVCs or CRDs from any render.
# Usage: ci/deletion-check.sh origin/main   (run from the repo root; needs git, kustomize)
set -euo pipefail
base_ref="${1:-origin/main}"
work="$(mktemp -d)"
trap 'git worktree remove --force "$work/base" >/dev/null 2>&1 || true
      rm -rf "$work"' EXIT
git worktree add --quiet --detach "$work/base" "$base_ref"

dirs() {  # every directory with a kustomization.yaml, tenants/ and clusters/ included
  find . -name kustomization.yaml -not -path './.git/*' -exec dirname {} \;
}
render() {  # $1 = directory, $2 = output; empty if the directory is absent on that side
  : > "$2"
  [ -f "$1/kustomization.yaml" ] || return 0
  kustomize build "$1" \
    | awk '/^kind:/ {k=$2; next} k && /^  name:/ {print k "/" $2; k=""}' | sort -u > "$2"
}

all="$( { (cd "$work/base" && dirs); dirs; } | sort -u)"
status=0
for dir in $all; do
  if ! render "$work/base/$dir" "$work/before" || ! render "$dir" "$work/after"; then
    echo "render failed: ${dir}"   # a broken render must not pass as "nothing removed"
    status=1
    continue
  fi
  removed="$(comm -23 "$work/before" "$work/after" \
    | grep -E '^(Namespace|PersistentVolumeClaim|CustomResourceDefinition)/' || true)"
  if [ -n "$removed" ]; then
    printf 'Protected objects removed from %s:\n%s\n' "$dir" "$removed"
    status=1
  fi
done
exit "$status"

On the test repository, the incident’s change (the payments.yaml line dropped from tenants/production/kustomization.yaml) printed Protected objects removed from ./tenants/production: and Namespace/payments, and exited 1. Removing apps/production/shop/ reported PersistentVolumeClaim/orders-db; a broken render on either side printed render failed and exited 1; no change exited 0. Because a broken base render fails too, a pull request that repairs a broken main needs a code owner’s override. The awk step assumes renderer output with kind before metadata, which kustomize produces; it is not a YAML parser.

Each has a home section above; this list is the short form.

  • “We use Git, so we do GitOps.” Without a pulling, reconciling agent it is versioned push deployment (four-question test).
  • Branch per environment. Merges drag unrelated changes along[48]; use directories (layout).
  • Mutable references in production. Production changes without a commit[30]; pin digests and versions, and check in CI.
  • Self-heal without break-glass. Responders fight the reconciler, or turn GitOps off for good (judgement call).
  • Myth: pull is automatically secure. The repository becomes the credential and the reconciler is highly privileged (push versus pull).

Good fit. The target exposes a declarative API with a reconciler you can run (Kubernetes above all; cloud resources through Kubernetes-hosted controllers). Several people or teams change the same environments. You need evidence of who approved what. Drift has hurt you. You run more than a handful of clusters or environments that should stay alike.

Poor fit, or only partly.

  • Imperative or one-off work (schema migrations, data fixes, certificate ceremonies): GitOps assumes repeatable convergence; design these as runbooks, triggered from declared Jobs if you must.
  • Objects created by other controllers: operators, autoscalers and admission webhooks own parts of the state. Do not declare what they manage.
  • Targets with no agent: SaaS settings or appliances without an API a reconciler can drive. Push from CI with scoped credentials and an audit log, and say honestly that it is push.
  • A single small service on one host: the reconciler and review discipline may cost more than they save.
  • Teams without review capacity: pull requests that nobody reads add delay without adding control.

Hidden responsibilities. Running and upgrading reconcilers, key management, the exception list, break-glass drills, and on-call for the delivery system itself.

Figure 8. Should this component be managed with GitOps?
NoYesNoYesNoYesNoYesA component you want tomanageConfiguration exposedthrough a declarative API?Do not force it: scripts orpush, with an audit logCan an agent run near it andreach the store?Push from CI with scopedcredentials, or ship artefactsto a local agentConfiguration, not data orone-off actions?Keep data and migrationsout: runbooks and backupsCan the team review changesand own break-glass?Start with manual sync anddrift alertsAdopt: automated sync,self-heal and guarded prune
Diagram source (Mermaid)
flowchart TB
  S["A component you want to manage"]:::neutral --> Q1{"Configuration exposed through a declarative API?"}:::decision
  Q1 -- "No" --> N1["Do not force it: scripts or push, with an audit log"]:::risk
  Q1 -- "Yes" --> Q2{"Can an agent run near it and reach the store?"}:::decision
  Q2 -- "No" --> N2["Push from CI with scoped credentials, or ship artefacts to a local agent"]:::neutral
  Q2 -- "Yes" --> Q3{"Configuration, not data or one-off actions?"}:::decision
  Q3 -- "No" --> N3["Keep data and migrations out: runbooks and backups"]:::risk
  Q3 -- "Yes" --> Q4{"Can the team review changes and own break-glass?"}:::decision
  Q4 -- "No" --> N4["Start with manual sync and drift alerts"]:::neutral
  Q4 -- "Yes" --> Y["Adopt: automated sync, self-heal and guarded prune"]:::core
  classDef core fill:#e8f3f2,stroke:#16756f,color:#122029
  classDef risk fill:#fbeceb,stroke:#b4332b,color:#122029
  classDef decision fill:#fdf3e3,stroke:#c9800f,color:#122029
  classDef neutral fill:#f1f4f6,stroke:#5b7a87,color:#122029

Text equivalent: Four questions in sequence. No declarative API for the configuration: do not force GitOps; use scripts or push automation with an audit log. No agent near it that can reach the state store: push from CI with scoped credentials, or ship artefacts to a local agent. Data or one-off actions rather than configuration: keep them out; use runbooks and backups. A team that cannot review changes or own break-glass: start with manual sync and drift alerts. Otherwise adopt automated sync, self-heal and guarded pruning.

Weighted comparison (illustrative). Scores 1 (poor) to 5 (good) are this Atlas’s judgement for the stated assumptions, not measurements; change the weights to fit your situation.

Assumptions: a multi-team Kubernetes platform with ten clusters, regulated change evidence, and a 24/7 on-call rotation.

Criterion Weight Push CD from CI Pull, manual sync Pull, automated with self-heal
Credential exposure 25 2 4 4
Drift control 20 1 3 5
Audit evidence 20 3 4 5
Incident flexibility 15 4 4 3
Operational overhead (higher = lighter) 10 4 3 3
Learning curve (higher = easier) 10 5 4 3
Weighted score (÷ 100) 2.80 3.70 4.05

For a four-person start-up with one cluster, weights might be 10, 15, 5, 20, 25 and 25. The scores become 3.55, 3.60 and 3.50: a near tie. At that size, choose by what you expect to grow into.

This Atlas’s model (no official GitOps maturity model exists). Move up only when the exit criteria hold for a full quarter.

Level Observable behaviour Evidence Exit criteria
0 Scripted Deploys by scripts or pipelines; hand fixes common Pipeline logs All production configuration exists as declarations
1 Declared Manifests in Git and reviewed; CI pushes them Pull requests Every overlay renders in CI; mutable tags flagged
2 Reconciled Agents pull; drift is visible; production may use manual sync Reconciler status, drift alerts CI holds no cluster credentials for managed environments
3 Governed Automated sync with self-heal; least-privilege tenants; digests; secrets pattern; prune guards; break-glass procedure Projects or impersonation config; exception list; break-glass records Zero unticketed out-of-band changes; no tenant reconciles as cluster-admin
4 Assured Metrics reviewed; rebuild drills pass; evidence sampled; promotion automated with policy Dashboards, drill reports, sampling results Rebuild time within target; sampling finds complete evidence chains
Phase Actions Owner Milestone and metric
Days 1–30 Write the repository-and-promotion ADR; pick one non-critical service and the staging cluster; bootstrap the reconciler; manual sync; CI rendering and the guard script; drift alerts Platform team with one app team The staging service changes only through pull requests; reconciliation-lag baseline recorded
Days 31–60 Automated sync and self-heal in staging; choose the secrets pattern; AppProjects or the Flux lockdown for tenants; onboard the service in production with manual sync; write and rehearse break-glass; remove CI’s cluster credentials for that service Platform, security, on-call First production change through GitOps; break-glass rehearsal done
Days 61–90 Automated sync in production for low-risk services; prune guards and the deletion check; promotion automation for development and staging; metrics dashboard; first staging rebuild drill; plan the next wave Platform, app teams, SRE Three or more services managed; 30 days without unticketed out-of-band changes; measured rebuild time

Quick win: drift alerts alone (manual sync) show hand changes nobody knew about. Dependencies: branch protection, an artefact registry with digests, and an owner for the reconciler on-call.

Quick start

  • Write down what is in scope (clusters, namespaces, kinds) and what stays out (data, operator-owned objects)
  • Choose a state store and layout; record it in an ADR
  • Install the reconciler so it manages itself (bootstrap)
  • Render every overlay in CI and block mutable images and plain Secrets
  • Turn on drift alerts before automatic correction

Application onboarding preflight

  • Images referenced by digest; charts and remote bases pinned
  • No replicas for autoscaled workloads; known mutated fields listed as narrow ignore rules
  • Secrets follow the chosen pattern; no plain Secret manifests
  • Health checks or readiness probes reflect real readiness
  • Namespaces and claims carry prune protection
  • Owner, code owners and on-call contact recorded

Reconciler production readiness

  • Version pinned and within the supported Kubernetes range[67][65]
  • Tenants fenced: AppProjects, not the default project[27], or the Flux lockdown flags, so no tenant Kustomization or HelmRelease runs as cluster-admin[33]
  • Webhooks configured; intervals chosen deliberately
  • Metrics scraped; alerts for not-ready sources and stale reconciliation
  • Credential expiry dates tracked; decryption keys backed up outside the cluster
  • Upgrade procedure tested in staging

Security review

  • Branch protection on watched paths, with no administrator bypass[35]
  • Commit or source verification on production sources[38][66]
  • CI cannot write to production paths without review; bots scoped to one repository and path
  • Remote bases refused (Flux --no-remote-bases) or pinned by commit[33][36]
  • Unused config-management tools disabled on the renderer[36]
  • No render-time secret injection[40]

Incident and break-glass

  • Who may suspend reconciliation, for which scope, for how long
  • Suspension cannot be undone by Git or a parent (preconditions in runbook step 1)
  • Suspension creates a ticket and an alert if it exceeds the time limit
  • The fix reaches Git before reconciliation resumes
  • Revert-by-pull-request rehearsed; emergency approval path known

Monthly review

  • Reconciliation lag, apply failures, drift and out-of-band changes reviewed
  • Exception list (ignore rules, prune-disabled objects) reviewed and pruned
  • Reconciler and controller versions checked against support windows

1. Architecture decision record: repository and promotion strategy

ADR-NNN: GitOps repository and promotion strategy
Status: proposed | accepted | superseded by ADR-NNN        Date: YYYY-MM-DD
Context: number of clusters, teams, environments; regulatory evidence needs;
  Git host; registry.
Decision:
  State store: Git repository <name> (or OCI artefacts in <registry>)
  Layout: monorepo with apps/, infrastructure/, clusters/ | repo per environment
          | repo per team
  Environments: directories with Kustomize overlays (no branch per environment)
  Promotion: PR that sets the image digest in the next overlay;
             automation allowed up to <environment>
  Reconciler: <Argo CD | Flux> <version>; topology: hub | per cluster | managed
  Tenancy: <AppProjects | impersonated service accounts>; platform paths owned by <team>
  Secrets: <SOPS+age | External Secrets Operator | Sealed Secrets>
  Drift policy: revert by default; tolerated fields listed in <file>
Consequences: what gets easier, what gets harder, what we must operate.
Review date: YYYY-MM-DD

2. Break-glass record

Break-glass record BG-YYYY-NNN
Incident: INC-NNNN            Opened by: <name>            Approved by: <on-call lead>
Scope suspended: <cluster>/<namespace>/<Kustomization or Application>  (smallest possible)
Suspended at: YYYY-MM-DD HH:MM UTC        Time limit: <hours>   Alert if exceeded: yes
Manual changes made (exact commands or diff):
Pull request that brings the change into Git: <link>          Merged at:
Reconciliation resumed at:                 Post-resume state: Synced/Healthy (yes/no)
Follow-up actions:

3. Application onboarding request

Application: <name>        Team: <team>        Code owners: <group>
Environments and clusters: dev / staging / production on <clusters>
Source of manifests: <path in config repo> | <app repo path or chart + version>
Images: registry path; digest pinning confirmed (yes/no)
Secrets: pattern used; keys or secret-manager paths
Autoscaled or operator-managed fields: <list>
Data-bearing objects: <PVCs, databases>; backup owner: <team>
Health: readiness probes; extra smoke test (yes/no)
Promotion: automated up to <environment>; production approval by <group>
Risk class: low | medium | high        On-call contact: <rota>

4. Drift exception record

Exception DX-NNN    Object: <kind>/<namespace>/<name>    Field(s): <JSON pointer>
Why it differs: <controller, webhook, autoscaler, vendor operator>
Mechanism: ignoreDifferences | ssa annotation | omitted from Git | prune disabled
Owner: <team>    Created: YYYY-MM-DD    Review by: YYYY-MM-DD
Removal condition: <what must change for this exception to go away>

GitOps against push CD is covered in push versus pull and the weighted comparison. Three other approaches compete with it:

Approach Who applies changes Drift handling Credentials Best for
IaC with pull-request automation A CI job running plan and apply Detected at the next plan Cloud credentials in CI Cloud infrastructure with careful plan review (InfraOps)
Configuration-management agents An agent on each host pulling playbooks or manifests[31] Re-applied on schedule; reporting varies On the host Virtual machines and bare metal
Platform-managed deploys A PaaS or managed service The provider’s model Provider IAM Teams that want the platform to own delivery (PlatformOps, NoOps)

Do I need Kubernetes to do GitOps? No, but it helps. The principles are tool-neutral[1]. Outside Kubernetes you need a reconciler for the target: Crossplane or the OpenTofu/Terraform controller for cloud resources, or pull agents such as ansible-pull for hosts, with less mature drift reporting[21][15][31].

How do I roll back? Revert the commit and let the reconciler apply it. Argo CD’s own rollback cannot be used while automated sync is enabled[24]. Flux can roll back a failed Helm upgrade automatically when remediation is configured[59]; when it does, Git still declares the failed version, so follow with a revert so that Git tells the truth.

Can CI still deploy to development? Yes: push or automated image updates in development, pull with review for production. Be explicit about which environments meet which principles.

What about database migrations? Treat them as application concerns: backward-compatible migrations run by the application or by a declared Job with ordering (sync waves or dependsOn)[63][25]. Never rely on GitOps to undo a migration; reverting a manifest does not revert data.

Does GitOps satisfy a change advisory board? It provides evidence (approvals, applied revisions), but deciding which changes need which approval is change management (ITOps): automate low-risk standard changes and keep human approval where policy demands it.

Status as of October 2026; each topic is introduced earlier on the page.

Topic What to know
Rendered manifests in Git (Argo CD Source Hydrator, beta)[22] Reviewers see exactly what will run; storage and pull-request noise grow
OCI artefacts as the state store (“Gitless GitOps”)[13] Removes the Git host from the production path; pair with signature verification
Promotion orchestration (Kargo)[50] Moves promotion logic out of ad-hoc scripts; another component to run
Pull-based multi-cluster for Argo CD (argocd-agent, pre-1.0)[46] Central view without central credentials; evaluate maturity
Declarative cloud control planes (Crossplane)[21] Extends reconciliation to cloud resources; guard destructive changes
  • Prerequisites. Git and pull requests; Kubernetes objects and kubectl; Kustomize or Helm basics; CI concepts from DevOps. KubernetesOps helps.
  • One day. Read the four principles and the glossary[1][11]; do Lab 1; run the four-question test on your current setup.
  • One week. Labs 2 and 3; design a repository layout for two environments; write the drift policy and break-glass procedure for one service.
  • Thirty days. Run the first 30 days of the roadmap on a real non-critical service; compute the metrics with Lab 4’s method from your own events.
  • Specialisations. Multi-tenancy and reconciler security (DevSecOps, KubernetesSecOps); fleet management (PlatformOps); progressive delivery (ReleaseOps); cloud resources (InfraOps); GitLab wiring (GitLabOps).
  • Portfolio projects. A public repository with overlays, SOPS-encrypted secrets, a CI guard and a documented drift policy; a written rebuild drill with measured times.
One sentence Declare the desired state in a versioned store and let agents pull and reconcile it continuously
The test Declarative · versioned and immutable · pulled automatically · continuously reconciled[1]
Decide first State store and layout; hub or per-cluster agents; tenancy; secrets pattern; drift policy; break-glass
Essential practices Directories, not branches, for environments; promote by changing one digest; pin everything; protect namespaces and claims from pruning; render and check in CI
Measure Reconciliation lag (median, p90), apply failure rate, drift by cause, unticketed out-of-band changes, time out of sync, rebuild time; delivery outcomes via DORA on DevOps
Tools (as of October 2026) Argo CD 3.5, Flux 2.9; SOPS, External Secrets Operator, Sealed Secrets; Kargo; Crossplane; managed options on AKS and EKS
Watch-outs Cluster-admin reconcilers (no lockdown); Git write equals deploy; mutable references; render-time secrets; ApplicationSet cascades; break-glass undone by a parent
Next actions Run the four-question test; write the ADR; onboard one service with drift alerts; schedule the first rebuild drill

A judgement call: self-heal during an incident

Link to section “A judgement call: self-heal during an incident”

Option A — keep self-heal on and fix only through an emergency pull request. The invariant holds, the evidence chain stays intact, and nobody has to remember to undo anything. It fails when the path to Git is the bottleneck (approver asleep, Git host degraded) or when the reconciler is part of the problem, for example by re-applying a bad revision.

Option B — suspend reconciliation for the one affected application, patch live, then land the fix in Git before resuming. Service recovers faster. The risk is the forgotten suspension: drift becomes permanent, the next merge silently overwrites the fix, or the application stays outside GitOps for weeks.

Assumptions. The emergency path usually takes about ten minutes (illustrative; measure your own); suspension is possible per application or per object (Flux supports both a suspended Kustomization and a per-object kustomize.toolkit.fluxcd.io/reconcile: disabled annotation[25]; Argo CD can switch off automation for one application or block syncs with a sync window [24][37]); nothing undoes the suspension (the preconditions in runbook step 1); suspension actions are audited.

Why the attractive choice can be wrong. “Never bypass GitOps” sounds principled, but it turns a reliability control into an outage multiplier when approvals or the Git host are slow. “Just patch it” sounds pragmatic, but as a habit it stops Git describing production.

Decision. Choose B, designed in advance: the smallest possible scope, a time limit with an alert (for example four hours), a break-glass record, a fix pull request opened during the incident, and resumption only after it merges. Count the action as an out-of-band change with a ticket in your metrics. Prefer A whenever the emergency path is reliably faster than your recovery target and the Git host is healthy.

Uncertainty. No published data compares recovery times for the two approaches; the answer depends on your approval latency. Measure both in game days and revisit the rule.

After this page you should be able to:

  • Explain the four OpenGitOps principles and why “pull” enables continuous reconciliation.
  • Classify a delivery setup with the four-question test.
  • Design a repository layout, promotion flow, secrets pattern and drift policy for several teams.
  • Operate a reconciler safely: least privilege, prune guards, break-glass, metrics.
  • Evaluate when GitOps is the wrong tool, and defend the choice to a leader or auditor.
1. “We do GitOps: our CI workflow runs kubectl apply after every merge.” Which principles does this meet?

It is declarative and probably versioned (manifests in Git), though tags may be mutable. It is not pulled automatically (CI pushes) and not continuously reconciled: after the job ends, nothing compares the cluster with Git. CI also holds cluster credentials, the push model’s security downside in NIST’s guidance[5]. An in-cluster agent with automated sync and drift correction closes both gaps.

2. Argo CD runs automated sync with selfHeal off. A colleague scales a Deployment by hand. What happens?

The new replica count stays and the application shows OutOfSync: with self-heal off, live changes “will not trigger automated sync”[24]. The next commit’s sync overwrites the drift, so it is detected but only incidentally corrected. In a multi-source application a commit to any source does this, and Argo CD warns that disabling self-heal “does not guarantee that live cluster changes in multi-source applications will persist”[24].

3. A staging overlay uses the image nginx:1.27 and a remote base with ?ref=main. Which principle is at risk, and what is the fix?

Versioned and immutable. Both references can change meaning without any commit in your repository: the tag can be re-pushed and the branch moves. Argo CD’s best practices warn about exactly this[30]. Pin the image by digest and the base to a tag or commit SHA, and enforce it with a CI check such as the Lab 3 guard.

4. Two teams share a cluster. How do you stop team A’s manifests from creating cluster-wide RBAC?

In Flux, run team A’s Kustomization under an impersonated service account bound only in its namespace, and set the lockdown flags: --default-service-account, so that no Kustomization or HelmRelease without a service account runs as cluster-admin, and --no-cross-namespace-refs, so team A cannot use another namespace’s sources[25][33]. In Argo CD, put the team’s applications in an AppProject that restricts destinations and allow-lists no cluster-scoped kinds[27]; consider the beta impersonation feature, because without it syncs run with the controller’s privileges [23]. In both, add code-owner review on the platform paths.

5. Eight changes merged this week. Seven became healthy after 2, 3, 3, 4, 5, 6 and 30 minutes; one failed. Compute the median lag, the nearest-rank p90 and the apply failure rate.

Median of seven sorted values = the 4th = 4 minutes. p90 rank = ⌈0.9 × 7⌉ = 7, so p90 = 30 minutes, which is also the maximum. Apply failure rate = 1 ÷ 8 = 12.5%. Report the p90 but do not set targets on one week; investigate the 30-minute change rather than the average.

6. After an incident you discover the reconciler’s Git token expired nine days ago. What failed, and what would have caught it?

The delivery system failed silently: the clusters were stale, not down, so no user-facing alert fired. The missing assurance checks: an alert on not-ready sources or Kustomizations (for example gotk_resource_info{ready="False"} for Flux[80], or application conditions in Argo CD[78]), an alert when an application has not reconciled within several intervals, and a calendar of credential expiry dates.

7. An auditor asks for proof that production change X was approved and is what actually ran. What do you provide, and which gap must you close?

The pull request and its approvals, the merge commit, the reconciler event naming the applied revision and its health, and audit-log entries for any manual action. The gap: Git history “does not necessarily correlate one-to-one with events that happen in a cluster”[36], so export and retain reconciler events and audit logs for your audit period.

8. The CTO proposes GitOps for the company’s single monolith running on one virtual machine. When would you advise against it?

Run the decision tree. With no declarative API for the host, one service and a small team, the reconciler, layout and review discipline may cost more than they return; a well-logged push pipeline or a configuration-management agent may be enough. Revisit when the workload moves to a declarative platform, more teams change it, or audit evidence becomes a requirement.

GitOps is a small idea with large consequences: write down what production should be, keep every version, and let a control loop close the gap. Its definition is now stable and vendor-neutral, two mature reconcilers implement it, and both standards guidance and cloud providers treat the pull model as normal.

The idea is also easy to misread. Pull removes cluster credentials from CI, but it turns the repository into the credential and puts a cluster-admin reconciler inside the cluster unless you narrow it. Self-heal stops quiet drift, but it will fight your responders unless you design break-glass in advance. Git recreates configuration, not data. Most GitOps failures are failures of these edges: mutable references, unguarded pruning, forgotten suspensions, stale reconcilers, and guarantees taken without their preconditions.

Adopt it where the target is declarative and many hands change it; start with drift alerts and manual sync; earn automation by measuring lag, failures and out-of-band changes; and say plainly when a system is better served by an honest push pipeline. GitOps is not “using Git”. It is choosing to make the declared state the only path to production, and then doing the work to keep that true.

All sources were opened on 2026-10-16 (those changed in the post-score polish again on 2026-10-17); retrieval notes, quotation sources and the gaps that could not be checked are in the evidence ledger (research/gitops-ledger.md). Version-specific facts are as of October 2026.

Further reading

Changelog

  • 2026-10-16 — First published draft (managing author): research, ledger, 13 parts, 8 diagrams, 4 labs (2 scripts executed). Awaiting independent fact-check.
  • 2026-10-16 — Revision after fact-check r1 (reviews/gitops-r1-response.md): Flux lockdown and credential-reach notes; per-tenant, per-environment SOPS keys; CI scripts render tenants/ and clusters/ (re-tested with a stub); ApplicationSet and break-glass preconditions; signing, webhook, attribution and Lab 4 fixes; repetition cut. Argo CD 3.6 was at rc3 on 2026-10-15: refresh the 3.5 lines when it ships.
  • 2026-10-17, post-score polish (reviews/gitops-polish.md): GitHub Enterprise Server signing condition; polling range; multi-source self-heal caveat; self-contained Lab 3; runbook figure with step numbers and parallel restore; shorter decision-tree labels; restatements cut. Argo CD 3.6 still rc3 on 2026-10-17.

  1. GitOps Principles v1.0.0 — OpenGitOps, released 2021-10-08 (release list). Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  2. OpenGitOps project README — OpenGitOps (CNCF Sandbox), undated. Checked 2026-10-16. ↩︎

  3. Flux project page — CNCF (accepted 2019-07-15, incubating 2021-03-12, graduated 2022-11-30). Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎

  4. Argo project page — CNCF (accepted 2020-03-26, graduated 2022-12-06). Checked 2026-10-16. ↩︎ ↩︎ ↩︎

  5. SP 800-204C, Implementation of DevSecOps for a Microservices-based Application with Service Mesh — R. Chandramouli, NIST, March 2022, §4.7–4.7.1. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  6. GitOps Working Group charter — CNCF TAG App Delivery, undated. Checked 2026-10-16. ↩︎

  7. TAG App Delivery repository README (working groups table) — CNCF, undated. Checked 2026-10-16. ↩︎

  8. CNCF Technical Advisory Groups — CNCF, 2025 structure. Checked 2026-10-16. ↩︎ ↩︎

  9. GitOps with the agent for Kubernetes and Flux — GitLab Docs, current release. Checked 2026-10-16. ↩︎ ↩︎

  10. GitOps - Operations by Pull Request (archived copy) — Alexis Richardson, Weaveworks, 2017-08-07 (Wayback Machine snapshot 2018-01-07; the original domain no longer hosts it). Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎

  11. GitOps Glossary v1.0.0 — OpenGitOps, 2021. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  12. CNCF Annual Cloud Native Survey announcement — CNCF, 2026-01-20. Checked 2026-10-16. ↩︎

  13. Core Concepts — Flux documentation (2.9). Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  14. Controllers — Kubernetes documentation. Checked 2026-10-16. ↩︎ ↩︎

  15. tofu-controller — flux-iac (community controller for Flux, previously Weave TF-Controller), v0.16.5 published 2026-08-06. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  16. The Cloud Native Computing Foundation announces Argo has graduated — CNCF, 2022-12-06. Checked 2026-10-16. ↩︎ ↩︎

  17. OpenGitOps project page — CNCF, accepted 2021-01-26. Checked 2026-10-16. ↩︎

  18. State of GitOps now in Flux as Weaveworks shuts down — SD Times, 2024-02-09. Checked 2026-10-16. ↩︎

  19. The continued support of FluxCD at GitLab — GitLab, 2024-03-05. Checked 2026-10-16. ↩︎

  20. Announcing Amazon EKS Capabilities — AWS, 2025-11-30. Checked 2026-10-16. ↩︎ ↩︎ ↩︎

  21. Cloud Native Computing Foundation announces graduation of Crossplane — CNCF, 2025-11-06. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  22. Source Hydrator (beta since v3.5.0) — Argo CD documentation. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎

  23. Application Sync using impersonation (beta since v3.5.0) — Argo CD documentation. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎

  24. Automated Sync Policy — Argo CD documentation (3.5). Checked 2026-10-17. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  25. Kustomization API — Flux documentation (kustomize.toolkit.fluxcd.io/v1). Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  26. Kustomize glossary — Kubernetes SIG CLI. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎

  27. Projects — Argo CD documentation (3.5). Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  28. Introduction to ApplicationSet controller — Argo CD documentation (3.5). Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎

  29. GitOps Toolkit components — Flux documentation (2.9). Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  30. Best Practices — Argo CD documentation (3.5). Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  31. ansible-pull — Ansible documentation. Checked 2026-10-16. ↩︎ ↩︎ ↩︎

  32. Architecture — Argo CD documentation (3.5). Checked 2026-10-16. ↩︎ ↩︎ ↩︎

  33. Flux multi-tenancy — Flux documentation (2.9). Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  34. Cluster Bootstrapping — Argo CD documentation (3.5): ApplicationSets recommended, app of apps “an admin-only tool”, ignoring differences in child applications. Checked 2026-10-16. ↩︎ ↩︎

  35. About protected branches — GitHub Docs. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎

  36. Security — Argo CD documentation (3.5). Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  37. Sync Windows — Argo CD documentation (3.5). Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  38. GitRepository API: verification — Flux documentation (source.toolkit.fluxcd.io/v1). Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  39. Helm — Argo CD documentation (3.5). Checked 2026-10-16. ↩︎ ↩︎ ↩︎

  40. Secret Management — Argo CD documentation (3.5). Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎

  41. Notification Provider API: Git commit status updates — Flux documentation. Checked 2026-10-16. ↩︎

  42. Application deployments with GitOps (Flux v2) — Microsoft Learn, updated 2026-03-24. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  43. FAQ: how often does Argo CD check for changes — Argo CD documentation (3.5). Checked 2026-10-17. ↩︎

  44. Webhook Configuration — Argo CD documentation (3.5). Checked 2026-10-17. ↩︎ ↩︎ ↩︎ ↩︎

  45. Notification Receiver API — Flux documentation. Checked 2026-10-16. ↩︎ ↩︎ ↩︎

  46. argocd-agent — argoproj-labs, v0.10.0 published 2026-08-26. Checked 2026-10-16. ↩︎ ↩︎

  47. Ways of structuring your repositories — Flux documentation (2.9). Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  48. Stop using branches for deploying to different GitOps environments — Kostis Kapelonis, Octopus Deploy blog (originally Codefresh), 2021-12-17. Checked 2026-10-16. ↩︎ ↩︎

  49. Automate image updates to Git — Flux documentation (2.9). Checked 2026-10-16. ↩︎ ↩︎

  50. Kargo — Akuity, Apache-2.0; v1.12.4 published 2026-10-12. Checked 2026-10-16. ↩︎ ↩︎ ↩︎

  51. Secrets — Kubernetes documentation. Checked 2026-10-16. ↩︎ ↩︎ ↩︎

  52. SOPS: Secrets OPerationS — SOPS project (CNCF Sandbox). Checked 2026-10-16. ↩︎ ↩︎

  53. Manage Kubernetes secrets with SOPS — Flux documentation (2.9). Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  54. Sealed Secrets — Bitnami Labs. Checked 2026-10-16. ↩︎ ↩︎

  55. External Secrets Operator — External Secrets project. Checked 2026-10-16. ↩︎ ↩︎

  56. ExternalSecret API — External Secrets project (external-secrets.io/v1). Checked 2026-10-16. ↩︎

  57. Diffing Customization — Argo CD documentation (3.5). Checked 2026-10-16. ↩︎ ↩︎ ↩︎

  58. Sync Options — Argo CD documentation (3.5). Checked 2026-10-16. ↩︎ ↩︎ ↩︎

  59. HelmRelease API: drift detection and remediation — Flux documentation (helm.toolkit.fluxcd.io/v2). Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎

  60. Helm 3 End of Life — Helm project, 2026-06-02. Checked 2026-10-16. ↩︎

  61. Helm documentation overview (Helm 4) — Helm documentation. Checked 2026-10-16. ↩︎

  62. Flux v2.8.0 release notes — Flux project, 2026-02-24. Checked 2026-10-16. ↩︎

  63. Sync Phases and Waves — Argo CD documentation (3.5). Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎

  64. Argo CD releases — Argo project, v3.5.5 published 2026-10-12. Checked 2026-10-17. ↩︎

  65. Flux v2.9.0 release notes (Kubernetes compatibility) — Flux project, 2026-06-30; latest patch v2.9.6 published 2026-10-01. Checked 2026-10-17. ↩︎ ↩︎ ↩︎ ↩︎

  66. Source Integrity Verification — Argo CD documentation (3.5); Git GnuPG verification is its only method, the legacy signatureKeys setting is deprecated. Checked 2026-10-16. ↩︎ ↩︎ ↩︎

  67. Installation: tested Kubernetes versions — Argo CD documentation (3.5). Checked 2026-10-16. ↩︎ ↩︎ ↩︎

  68. CNCF End User Survey finds Argo CD as majority adopted GitOps solution for Kubernetes — CNCF, 2025-07-24 (survey of Argo CD users). Checked 2026-10-16. ↩︎

  69. Using RBAC Authorization: user-facing roles — Kubernetes documentation. Checked 2026-10-16. ↩︎

  70. About commit signature verification and its Enterprise Server 3.22 version — GitHub Docs (web-interface signing and rebase-and-merge notes). Checked 2026-10-17. ↩︎

  71. App Deletion — Argo CD documentation (3.5). Checked 2026-10-16. ↩︎

  72. kind v0.33.0 release notes — Kubernetes SIGs, 2026-08-26. Checked 2026-10-16. ↩︎

  73. Getting Started — Argo CD documentation (3.5). Checked 2026-10-16. ↩︎

  74. Flux installation (dev install) — Flux documentation (2.9). Checked 2026-10-16. ↩︎

  75. images and replicas — Kustomize reference, Kubernetes SIG CLI. Checked 2026-10-17. ↩︎

  76. SOPS: age identities — SOPS documentation. Checked 2026-10-16. ↩︎

  77. Controlling if/when the ApplicationSet controller modifies Applications and Application Pruning & Resource Deletion — Argo CD documentation (3.5). Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎

  78. Metrics — Argo CD documentation (3.5). Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  79. Auditing — Kubernetes documentation. Checked 2026-10-16. ↩︎ ↩︎

  80. Flux Prometheus metrics — Flux documentation (2.9). Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎

  81. High Availability — Argo CD documentation (3.5). Checked 2026-10-16. ↩︎

  82. Share a Cluster with Namespaces — Kubernetes documentation. Checked 2026-10-16. ↩︎ ↩︎

  83. Persistent Volumes: reclaiming — Kubernetes documentation. Checked 2026-10-16. ↩︎ ↩︎

Related Ops

GitLabOps

GitLabOps is this Atlas's name for operating software delivery on the GitLab platform: designing .gitlab-ci.yml pipelines, running runner fleets, governing deployments and scanners, and keeping a GitLab instance healthy and current.

DevOps

DevOps is a way of organising software delivery so that the people who build a service and the people who run it share goals, tools and feedback, letting changes reach users quickly, safely and repeatedly. It is judged by delivery outcomes, not job titles.

ReleaseOps

ReleaseOps is an informal label for turning deployed code into a deliberate, reversible change for users: versioning, cadence, progressive exposure, feature flags, rollback and release records. The exact name is also a ServiceNow product.

KubernetesOps

KubernetesOps is this Atlas's name for operating Kubernetes clusters and the workloads on them: keeping the control plane, nodes and add-ons healthy and current, making workloads schedule and scale correctly, and diagnosing failures.

PlatformOps

PlatformOps is a less common name for platform engineering. This page uses it for the operating half: running an internal developer platform as a product, with service levels, support, upgrades and deprecation that keep its self-service golden paths trustworthy.

InfraOps

InfraOps is informal shorthand for infrastructure operations. This Atlas uses it for running infrastructure through code: declaring it, reviewing every change as a plan, applying it from a pipeline and keeping the tool's state in step with reality.

DevSecOps

DevSecOps is the practice of building security into the software delivery pipeline and into the teams that run it, so that every change is checked, traceable and safe to ship without a separate security phase that stalls delivery.

API Ops

API lifecycle operations: design-first with OpenAPI, API gateways, versioning and deprecation, rate limiting and quotas, API security (OAuth 2.0, keys), contract testi…

In preparation

Practise and get certified

From xOps School

Practise GitOps and get certified

The free GitOps track on xOps School has study notes, a 25-question practice quiz with explanations, and a verifiable certificate when you score 70% or more. Sign in with Google to start.

For teams

Instructor-led training

Live batches for your team, with completion certificates and reports for L&D/HR.

Source: master.md (the editorial source of this page) · built 2026-10-17 · diagrams rendered with @mermaid-js/mermaid-cli 11.17.0 (mermaid 11.17.2)