What GitOps is
Link to section “What GitOps is”GitOps in 30 seconds
Link to section “GitOps in 30 seconds”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.
At a glance
Link to section “At a glance”| 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) |
The problem GitOps solves
Link to section “The problem GitOps solves”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.
- 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].
- Drift is invisible. A hotfix applied by hand survives until the next deploy silently removes it, or the next deploy fails because of it.
- 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].
- 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.
What GitOps is not
Link to section “What GitOps is not”- 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.
Origins, vocabulary and neighbours
Link to section “Origins, vocabulary and neighbours”Origins and evolution
Link to section “Origins and evolution”| 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.
Glossary and mental model
Link to section “Glossary and mental model”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.
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 |
How GitOps relates to other Ops
Link to section “How GitOps relates to other Ops”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 |
Principles and operating model
Link to section “Principles and operating model”The four principles, read carefully
Link to section “The four principles, read carefully”The OpenGitOps document says the desired state of a GitOps-managed system must be[1]:
- Declarative. “A system managed by GitOps must have its desired state expressed declaratively.”
- Versioned and Immutable. “Desired state is stored in a way that enforces immutability, versioning and retains a complete version history.”
- Pulled Automatically. “Software agents automatically pull the desired state declarations from the source.”
- 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 onHEADor 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:
- Could you recreate the system’s configuration from the declarations alone, apart from persistent data?
- Is every version immutable, retained, access-controlled and audited, including what the declarations reference (images, charts, bases)?
- Does an agent fetch the declarations by itself, whenever it needs them, without a pipeline pushing them?
- 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.
Fundamentals and building blocks
Link to section “Fundamentals and building blocks”| 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 |
Roles, ownership and RACI
Link to section “Roles, ownership and RACI”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 |
Lifecycle: Day 0, Day 1, Day 2
Link to section “Lifecycle: Day 0, Day 1, Day 2”- 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
syncPolicychanged 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.
Operating model and governance
Link to section “Operating model and governance”- 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
ignoreDifferencesrule or Flux ignore annotation is tolerated drift: record it (template) and review it.
Architecture and workflow
Link to section “Architecture and workflow”Reference architecture
Link to section “Reference architecture”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].
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”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:
- 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-adminrole 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). - 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.
Hub-and-spoke or an agent per cluster
Link to section “Hub-and-spoke or an agent per cluster”| 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 |
End-to-end workflow
Link to section “End-to-end workflow”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 |
Practices, patterns and tools
Link to section “Practices, patterns and tools”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].
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].
Secrets practice: three patterns
Link to section “Secrets practice: three patterns”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.
Drift practice: revert, tolerate or alert
Link to section “Drift practice: revert, tolerate or alert”“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 |
Ordering, health and dependencies
Link to section “Ordering, health and dependencies”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.
Methods, standards and frameworks
Link to section “Methods, standards and frameworks”| 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.
Tooling landscape
Link to section “Tooling landscape”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.
Automation and AI
Link to section “Automation and AI”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.
GitOps in practice
Link to section “GitOps in practice”Reference implementation
Link to section “Reference implementation”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"
Hands-on labs
Link to section “Hands-on labs”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.
- 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
- Save as
guestbook-app.yamland apply it withkubectl 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
- 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
- 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.
- 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 anothercountand digest. The base pins a digest too, because the reference implementation’sci/validate.shruns the guard on every render underapps/; Kustomize’simagesfield replaces it by image name, andreplicassets 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
- Create one key per environment for the shop tenant and a SOPS rule per path, then encrypt each
secret-db.yamlso only thestringDatavalues 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
- Save the guard as
ci/check_images.pyand run it on your render: it should print0 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.
Use cases and scenarios
Link to section “Use cases and scenarios”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 |
Measure, secure and pay
Link to section “Measure, secure and pay”Metrics and KPIs
Link to section “Metrics and KPIs”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 |
Worked calculation
Link to section “Worked calculation”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 onsearch, 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.
Queries
Link to section “Queries”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.
Cost, capacity and sustainability
Link to section “Cost, capacity and sustainability”- Reconciler capacity. Argo CD’s repo-server forks Helm or Kustomize per render (
--parallelismlimitavoids 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 eachinterval[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.
Assurance of the operating model
Link to section “Assurance of the operating model”A GitOps setup can look healthy while quietly failing. Five checks keep it honest:
- 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.
- Is Git the only path? Review out-of-band human writes from audit logs weekly; every one needs a break-glass record.
- Are exceptions shrinking? Review every ignore rule and every
prune: disabledannotation quarterly; remove the stale ones. - Can we rebuild? Run a quarterly drill: new cluster, bootstrap, restore data, measure rebuild time.
- 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.
When things go wrong
Link to section “When things go wrong”Common failures and troubleshooting
Link to section “Common failures and troubleshooting”| 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”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.
Anti-patterns and myths
Link to section “Anti-patterns and myths”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).
Deciding and adopting
Link to section “Deciding and adopting”When to use GitOps and when not to
Link to section “When to use GitOps and when not to”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.
Decision framework
Link to section “Decision framework”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.
Maturity model
Link to section “Maturity model”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 |
Adoption roadmap: 30/60/90 days
Link to section “Adoption roadmap: 30/60/90 days”| 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.
Toolkit
Link to section “Toolkit”Checklists
Link to section “Checklists”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
replicasfor 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
defaultproject[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
Templates
Link to section “Templates”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>
Deep comparisons
Link to section “Deep comparisons”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.
Going further
Link to section “Going further”Advanced topics and emerging direction
Link to section “Advanced topics and emerging direction”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 |
Learning path
Link to section “Learning path”- 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.
Wrap-up
Link to section “Wrap-up”Executive recap and cheat sheet
Link to section “Executive recap and cheat sheet”| 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.
Learning objectives and mastery check
Link to section “Learning objectives and mastery check”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.
Conclusion
Link to section “Conclusion”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.
Sources and further reading
Link to section “Sources and further reading”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
- opengitops.dev — the principles’ home page and community links.
- flux2-kustomize-helm-example and flux2-multi-tenancy — reference repositories linked from Flux’s repository-structure guide.
- argocd-example-apps — the sample applications used in Lab 1.
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 rendertenants/andclusters/(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.
GitOps Principles v1.0.0 — OpenGitOps, released 2021-10-08 (release list). Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
OpenGitOps project README — OpenGitOps (CNCF Sandbox), undated. Checked 2026-10-16. ↩︎
Flux project page — CNCF (accepted 2019-07-15, incubating 2021-03-12, graduated 2022-11-30). Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎
Argo project page — CNCF (accepted 2020-03-26, graduated 2022-12-06). Checked 2026-10-16. ↩︎ ↩︎ ↩︎
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. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
GitOps Working Group charter — CNCF TAG App Delivery, undated. Checked 2026-10-16. ↩︎
TAG App Delivery repository README (working groups table) — CNCF, undated. Checked 2026-10-16. ↩︎
CNCF Technical Advisory Groups — CNCF, 2025 structure. Checked 2026-10-16. ↩︎ ↩︎
GitOps with the agent for Kubernetes and Flux — GitLab Docs, current release. Checked 2026-10-16. ↩︎ ↩︎
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. ↩︎ ↩︎ ↩︎ ↩︎
GitOps Glossary v1.0.0 — OpenGitOps, 2021. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
CNCF Annual Cloud Native Survey announcement — CNCF, 2026-01-20. Checked 2026-10-16. ↩︎
Core Concepts — Flux documentation (2.9). Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
Controllers — Kubernetes documentation. Checked 2026-10-16. ↩︎ ↩︎
tofu-controller — flux-iac (community controller for Flux, previously Weave TF-Controller), v0.16.5 published 2026-08-06. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
The Cloud Native Computing Foundation announces Argo has graduated — CNCF, 2022-12-06. Checked 2026-10-16. ↩︎ ↩︎
OpenGitOps project page — CNCF, accepted 2021-01-26. Checked 2026-10-16. ↩︎
State of GitOps now in Flux as Weaveworks shuts down — SD Times, 2024-02-09. Checked 2026-10-16. ↩︎
The continued support of FluxCD at GitLab — GitLab, 2024-03-05. Checked 2026-10-16. ↩︎
Announcing Amazon EKS Capabilities — AWS, 2025-11-30. Checked 2026-10-16. ↩︎ ↩︎ ↩︎
Cloud Native Computing Foundation announces graduation of Crossplane — CNCF, 2025-11-06. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
Source Hydrator (beta since v3.5.0) — Argo CD documentation. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎
Application Sync using impersonation (beta since v3.5.0) — Argo CD documentation. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎
Automated Sync Policy — Argo CD documentation (3.5). Checked 2026-10-17. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
Kustomization API — Flux documentation (kustomize.toolkit.fluxcd.io/v1). Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
Kustomize glossary — Kubernetes SIG CLI. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎
Projects — Argo CD documentation (3.5). Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
Introduction to ApplicationSet controller — Argo CD documentation (3.5). Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎
GitOps Toolkit components — Flux documentation (2.9). Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
Best Practices — Argo CD documentation (3.5). Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
ansible-pull — Ansible documentation. Checked 2026-10-16. ↩︎ ↩︎ ↩︎
Architecture — Argo CD documentation (3.5). Checked 2026-10-16. ↩︎ ↩︎ ↩︎
Flux multi-tenancy — Flux documentation (2.9). Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
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. ↩︎ ↩︎
About protected branches — GitHub Docs. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎
Security — Argo CD documentation (3.5). Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
Sync Windows — Argo CD documentation (3.5). Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
GitRepository API: verification — Flux documentation (source.toolkit.fluxcd.io/v1). Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
Helm — Argo CD documentation (3.5). Checked 2026-10-16. ↩︎ ↩︎ ↩︎
Secret Management — Argo CD documentation (3.5). Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎
Notification Provider API: Git commit status updates — Flux documentation. Checked 2026-10-16. ↩︎
Application deployments with GitOps (Flux v2) — Microsoft Learn, updated 2026-03-24. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
FAQ: how often does Argo CD check for changes — Argo CD documentation (3.5). Checked 2026-10-17. ↩︎
Webhook Configuration — Argo CD documentation (3.5). Checked 2026-10-17. ↩︎ ↩︎ ↩︎ ↩︎
Notification Receiver API — Flux documentation. Checked 2026-10-16. ↩︎ ↩︎ ↩︎
argocd-agent — argoproj-labs, v0.10.0 published 2026-08-26. Checked 2026-10-16. ↩︎ ↩︎
Ways of structuring your repositories — Flux documentation (2.9). Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
Stop using branches for deploying to different GitOps environments — Kostis Kapelonis, Octopus Deploy blog (originally Codefresh), 2021-12-17. Checked 2026-10-16. ↩︎ ↩︎
Automate image updates to Git — Flux documentation (2.9). Checked 2026-10-16. ↩︎ ↩︎
Kargo — Akuity, Apache-2.0; v1.12.4 published 2026-10-12. Checked 2026-10-16. ↩︎ ↩︎ ↩︎
Secrets — Kubernetes documentation. Checked 2026-10-16. ↩︎ ↩︎ ↩︎
SOPS: Secrets OPerationS — SOPS project (CNCF Sandbox). Checked 2026-10-16. ↩︎ ↩︎
Manage Kubernetes secrets with SOPS — Flux documentation (2.9). Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
Sealed Secrets — Bitnami Labs. Checked 2026-10-16. ↩︎ ↩︎
External Secrets Operator — External Secrets project. Checked 2026-10-16. ↩︎ ↩︎
ExternalSecret API — External Secrets project (external-secrets.io/v1). Checked 2026-10-16. ↩︎
Diffing Customization — Argo CD documentation (3.5). Checked 2026-10-16. ↩︎ ↩︎ ↩︎
Sync Options — Argo CD documentation (3.5). Checked 2026-10-16. ↩︎ ↩︎ ↩︎
HelmRelease API: drift detection and remediation — Flux documentation (helm.toolkit.fluxcd.io/v2). Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎
Helm 3 End of Life — Helm project, 2026-06-02. Checked 2026-10-16. ↩︎
Helm documentation overview (Helm 4) — Helm documentation. Checked 2026-10-16. ↩︎
Flux v2.8.0 release notes — Flux project, 2026-02-24. Checked 2026-10-16. ↩︎
Sync Phases and Waves — Argo CD documentation (3.5). Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎
Argo CD releases — Argo project, v3.5.5 published 2026-10-12. Checked 2026-10-17. ↩︎
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. ↩︎ ↩︎ ↩︎ ↩︎
Source Integrity Verification — Argo CD documentation (3.5); Git GnuPG verification is its only method, the legacy
signatureKeyssetting is deprecated. Checked 2026-10-16. ↩︎ ↩︎ ↩︎Installation: tested Kubernetes versions — Argo CD documentation (3.5). Checked 2026-10-16. ↩︎ ↩︎ ↩︎
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. ↩︎
Using RBAC Authorization: user-facing roles — Kubernetes documentation. Checked 2026-10-16. ↩︎
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. ↩︎
App Deletion — Argo CD documentation (3.5). Checked 2026-10-16. ↩︎
kind v0.33.0 release notes — Kubernetes SIGs, 2026-08-26. Checked 2026-10-16. ↩︎
Getting Started — Argo CD documentation (3.5). Checked 2026-10-16. ↩︎
Flux installation (dev install) — Flux documentation (2.9). Checked 2026-10-16. ↩︎
images and replicas — Kustomize reference, Kubernetes SIG CLI. Checked 2026-10-17. ↩︎
SOPS: age identities — SOPS documentation. Checked 2026-10-16. ↩︎
Controlling if/when the ApplicationSet controller modifies Applications and Application Pruning & Resource Deletion — Argo CD documentation (3.5). Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎
Metrics — Argo CD documentation (3.5). Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
Auditing — Kubernetes documentation. Checked 2026-10-16. ↩︎ ↩︎
Flux Prometheus metrics — Flux documentation (2.9). Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎
High Availability — Argo CD documentation (3.5). Checked 2026-10-16. ↩︎
Share a Cluster with Namespaces — Kubernetes documentation. Checked 2026-10-16. ↩︎ ↩︎
Persistent Volumes: reclaiming — Kubernetes documentation. Checked 2026-10-16. ↩︎ ↩︎