ResourcesInfrastructure

What Is GitOps? Principles, Tools, and GitOps for Workflows

GitOps runs systems from a desired state declared in Git, with automated agents that pull that state and reconcile reality to it. This guide covers the four OpenGitOps principles, pull vs push, GitOps vs CI/CD and IaC, the tools, the failure modes, and how to apply GitOps to workflows and operations, not only Kubernetes deployments.

GitOps is a way of operating systems in which the desired state of everything you run is declared in files stored in Git, and automated agents pull that state and continuously reconcile the live system to match it. Git becomes the single source of truth: the only way to change production is to change the repository, and the only way the repository reaches production is through an agent that applies it and corrects drift.

Most GitOps guides stop at Kubernetes deployments. That is where the practice was born and where Argo CD and Flux live. But the same discipline applies to the things platform teams change most often and govern least: the pipelines, scheduled automations, and infrastructure runbooks that sit around the clusters. This guide covers the definition and principles, the trade-offs, and then the practitioner part: how to run workflow orchestration the GitOps way.

TL;DR — GitOps = declarative desired state in Git + agents that pull and continuously reconcile it. It is a deployment model, not a synonym for “CI/CD with Git”. It handles configuration well and runtime state badly, so drift, secrets, and stateful data need explicit design. The same model works for workflows: flows as YAML in Git, pull requests as change control, a sync task or CI job to apply, and git revert as rollback.

GitOps defined: Git holds the desired state, agents enforce it

GitOps treats a Git repository as the authoritative description of what should be running, and treats every difference between that description and reality as something to fix automatically. The term was coined by Weaveworks in 2017 to describe how they ran Kubernetes clusters: no one ran kubectl apply against production; an in-cluster agent applied whatever was on the main branch.

Three properties separate GitOps from “we keep our YAML in Git”:

  • The repository is the only write path. Engineers change production by opening a pull request, never by editing the live system.
  • An agent, not a person or a CI job, applies the change, and it does so from inside or next to the target environment.
  • The agent keeps comparing declared and live state after the deployment, so manual changes are reverted or flagged instead of silently persisting.

The result is an audit trail you get for free (every change is a commit with an author, a reviewer, and a reason), a rollback path that is a git revert, and a recovery story where rebuilding an environment means pointing a fresh agent at the repository. GitOps builds on infrastructure and pipelines defined as code, but adds the reconciliation loop that keeps the code and the running system from diverging.

The four OpenGitOps principles

The vendor-neutral definition comes from OpenGitOps, a CNCF project run by the GitOps Working Group. Its four principles are short enough to quote and strict enough to disqualify a lot of setups that call themselves GitOps.

  1. Declarative. The desired state of the system is expressed declaratively: what should exist, not the commands to get there. Kubernetes manifests, Helm values, Terraform configurations, and YAML workflow definitions all qualify. A shell script that provisions a server does not.
  2. Versioned and immutable. Desired state is stored in a way that enforces immutability and versioning and keeps a complete history. Git is the usual store, but the principle names the property, not the tool. An OCI registry holding versioned artifacts also satisfies it.
  3. Pulled automatically. Software agents pull the desired state from the source. Nothing outside the target pushes changes in.
  4. Continuously reconciled. Agents keep observing the actual state and applying the desired state, so drift gets corrected without a new deployment.

Two consequences matter in practice. First, Git is conventional, not mandatory: the principles are about immutable, versioned desired state. Second, principles 3 and 4 are what most “GitOps” pipelines skip. A CI job that runs on merge and applies manifests is declarative and versioned, but it pushes, and it never looks at the system again until the next merge.

Pull vs push: the deployment model is what makes it GitOps

The pull model is the defining architectural choice in GitOps: the agent that applies changes lives with the target and reaches out to the repository, instead of an external pipeline reaching into the target.

DimensionPush-based deployment (classic CI/CD)Pull-based deployment (GitOps)
Who applies the changeCI runner after mergeAgent running in or next to the target
CredentialsCI holds write credentials to productionAgent holds read access to Git; CI needs no production access
TriggerPipeline event (merge, tag)Polling interval or a Git webhook that prompts a pull
Drift after deployUndetected until the next runDetected and corrected on every reconcile cycle
RollbackRe-run an old pipeline or a rollback jobRevert the commit; the agent converges
Failure modePartial apply if the pipeline dies mid-runAgent retries until state converges or alerts
Network directionInbound into the environmentOutbound from the environment to Git
Typical toolsGitHub Actions, GitLab CI, JenkinsArgo CD, Flux

Push is not wrong. It is simpler to start with, works for targets that cannot run an agent, and is how most teams deploy to SaaS APIs. But it gives up the two properties that make GitOps resilient: no standing production credentials outside the environment, and continuous correction of drift. Argo CD, for example, polls its repositories every three minutes by default and can be told to refresh immediately through a Git webhook, which keeps the pull model while removing most of the latency.

GitOps vs CI/CD vs DevOps vs infrastructure as code

These four terms overlap, and search results blur them. They sit at different levels: DevOps is a culture, IaC and CI/CD are practices, and GitOps is an operating model that combines them under a specific deployment rule.

DimensionDevOpsInfrastructure as code (IaC)CI/CDGitOps
What it isCulture and organizational modelPractice of defining infrastructure in filesPractice of automating build, test, and releaseOperating model for deployment and drift control
ScopeWhole delivery lifecycleProvisioning and configurationCode to artifact to environmentDesired state to running system, continuously
Primary artifactShared ownership, feedback loopsTerraform, OpenTofu, Ansible, CloudFormation filesPipeline definitionsDeclarative state in a versioned repository
Deployment directionNot prescribedUsually pushed (terraform apply)Usually pushedPulled by an agent
Drift handlingNot prescribedDetected on the next planNot addressedContinuous reconciliation
RollbackNot prescribedRe-apply previous configRe-run previous releaseRevert the commit
Typical toolsAnyTerraform, OpenTofu, Pulumi, AnsibleGitHub Actions, GitLab CI, JenkinsArgo CD, Flux, Git-synced orchestrators
RelationshipUmbrellaGitOps applies it with a reconcilerGitOps replaces its deploy stageUses IaC and CI/CD, adds pull and reconcile

The short version: IaC tells you how to describe infrastructure, CI/CD tells you how to ship changes, and GitOps tells you who is allowed to apply them and how the system stays honest afterwards. A team can do IaC and CI/CD without GitOps; it cannot do GitOps without declarative definitions. The same idea extends to delivery definitions themselves, covered in pipeline as code.

GitOps tools: Argo CD and Flux for clusters, other tools for everything else

Argo CD and Flux are the two GitOps operators most teams mean when they say “GitOps tool”. Both are CNCF graduated projects, both reconcile Kubernetes resources from Git, and the choice between them is mostly about operating style.

  • Argo CD ships a web UI, an Application custom resource, sync waves and hooks, and the app-of-apps pattern for managing many applications from one parent. It suits teams that want a visual view of sync status. The Argo CD guide covers its architecture in depth.
  • Flux is a set of composable controllers (source, kustomize, helm, notification) with no built-in UI and native SOPS decryption. It suits teams that want everything expressed as Kubernetes resources.

Both stop at the cluster boundary. Infrastructure outside Kubernetes and the operational workflows around it need other tools:

Kind of stateCommon GitOps-style toolingHow rollback works
Kubernetes workloads and configArgo CD, FluxRevert the commit; the operator converges
Cloud resources (networks, databases, IAM)Terraform or OpenTofu driven from pull requests (for example with Atlantis), or Crossplane controllersRevert and re-apply; Crossplane reconciles continuously, Terraform on the next apply
Configuration of existing serversAnsible from a Git-triggered pipelineRe-run the previous playbook version
Pipelines, automations, runbooksAn orchestrator that syncs its definitions from GitRevert the commit; the sync restores the previous definition

Terraform is the important nuance. It is declarative and versioned, but it does not reconcile on its own: drift stays invisible until someone runs a plan. Teams close that gap with a scheduled plan that alerts on changes, a pattern detailed in Terraform drift detection.

Ready to orchestrate your infrastructure?

Where GitOps breaks: drift, secrets, and stateful configuration

GitOps reconciles declared configuration. Every hard GitOps problem comes from something that is not declared configuration: runtime state, secret values, fields other controllers own, and differences between environments.

Drift from controllers, not just humans

The textbook drift scenario is an engineer running kubectl edit in production. The more common one is a controller doing its job. A HorizontalPodAutoscaler changes replicas, a mutating webhook injects a sidecar, a cloud provider adds default tags. If Git also declares those fields, the GitOps operator and the controller fight forever. The fix is to declare ownership explicitly: remove the field from the manifest, or tell the operator to ignore it (Argo CD’s ignoreDifferences exists for this). Self-heal should be on for fields Git owns and off for fields it does not.

Secrets do not belong in the repository

A GitOps repository is read by every agent, every CI job, and often every engineer, so plaintext secrets in it are a breach waiting for a clone. The three standard patterns are encrypted secrets committed to Git (Sealed Secrets, SOPS), references resolved at runtime from a secrets manager (External Secrets Operator, Vault), or platform-native secret stores that the declared config only names. In every case, Git holds the reference and the access policy, never the value.

Stateful applications: configuration yes, data no

GitOps handles the configuration of stateful systems well: database version, replica count, storage class, backup schedule. It cannot handle the data. A database’s rows, a queue’s backlog, and a volume’s contents are not desired state, and reverting a commit does not un-run a migration. Two rules keep stateful systems sane under GitOps. Run schema migrations and data operations as explicit, versioned workflow steps with their own ordering and approvals, not as something the reconciler applies opportunistically. And treat backup and restore as operations a workflow performs, because a git revert restores the definition of a database, not its contents.

Environment sprawl

Every environment needs slightly different values: replica counts, endpoints, feature flags. Branch-per-environment repositories look tidy and decay fast, because promotions become merges between long-lived branches and differences hide in merge history. Directory-per-environment on a single main branch, with overlays (Kustomize) or per-environment values files (Helm), keeps every difference visible in one diff and makes promotion a reviewed file change.

Operational discipline is the real cost

Once Git is the production control plane, it needs production-grade governance: protected branches, required reviews, CODEOWNERS on sensitive paths, signed commits where compliance demands it, and an emergency path that still goes through Git. Teams that adopt the tooling without the discipline end up with a reconciler applying unreviewed commits, which is worse than no GitOps. Encoding the review rules themselves as code is the job of policy as code.

GitOps beyond Kubernetes: workflows and operations belong in Git too

GitOps adoption usually covers application manifests and stops there. Meanwhile, the systems that change production most often run outside that loop: the ETL pipeline edited in a UI on Friday, the certificate-renewal job living in a cron table on one VM, the incident runbook that exists as a wiki page and a folder of scripts. These are operations, they have blast radius, and they are typically the least reviewed changes a platform team makes.

Applying GitOps to them means holding them to the same four principles:

OpenGitOps principleApplied to clustersApplied to workflows and runbooks
DeclarativeManifests describe resourcesWorkflow definitions describe tasks, triggers, retries, and dependencies as YAML
Versioned and immutableManifests committed to GitEvery flow and its scripts committed to Git; pull requests are the only edit path
Pulled automaticallyOperator pulls from the repoThe orchestrator pulls definitions from a branch on a schedule or on a Git webhook
Continuously reconciledOperator corrects live driftScheduled sync overwrites UI edits with the Git version, or a dry run reports the difference

The payoff is concrete. A change to a patching runbook gets a code review and a diff, like a change to a Deployment. A bad edit to a nightly pipeline rolls back with git revert instead of someone reconstructing yesterday’s version from memory. And the question “what exactly was running in production on March 3rd” has an answer in commit history. This requires workflows that are declarative to begin with: a definition the orchestrator can apply as a whole, not an imperative program whose behavior depends on how it was deployed.

How to run workflow orchestration the GitOps way with Kestra

Kestra defines every workflow (a flow) as a YAML file, which makes it a direct fit for GitOps: flows, their scripts, and their triggers are plain files that can live in Git, be reviewed in pull requests, and be applied by an agent. Kestra supports two GitOps-compatible deployment patterns: Kestra pulls from Git with its own sync tasks, or a CI pipeline pushes to Kestra on merge. Both keep Git as the source of truth; only the first follows the pull and reconcile principles end to end.

Lay out the repository by namespace

Namespaces are how Kestra groups flows by team or domain, and they map cleanly onto repository folders. A typical operations repository looks like this:

ops-workflows/
_flows/
certificate-renewal.yaml # deployed to company.ops
patching/
monthly-os-patching.yaml # deployed to company.ops.patching
incident/
disk-cleanup-runbook.yaml # deployed to company.ops.incident
_files/
scripts/
rotate_certs.py # namespace file used by certificate-renewal

Flow definitions go under _flows and supporting scripts, SQL, and config under _files, which Kestra stores as namespace files that flows can reference. Subfolders under _flows map to child namespaces when you enable child-namespace sync.

Pattern 1: Kestra pulls from Git and reconciles

The git plugin provides the sync tasks. io.kestra.plugin.git.SyncFlows imports flows from a branch into a target namespace and rewrites each flow’s namespace to that target, which is what lets the same repository feed different instances. io.kestra.plugin.git.SyncNamespaceFiles does the same for scripts and files. With delete: true, both remove anything absent from Git, which makes the sync authoritative. Per the Git version control docs, a sync applies the Git version over any conflicting edit made in the UI.

The flow below is the GitOps agent for the company.ops namespace. It runs on every push to main through a GitHub webhook and every 15 minutes as a reconcile loop, so a change made by hand in the UI survives at most one cycle.

id: gitops-sync-company-ops
namespace: company.platform
description: |
GitOps agent for company.ops. Applies the main branch of ops-workflows
on every push, and re-applies it every 15 minutes to undo drift.
# Webhook and schedule can fire close together; run one sync at a time.
concurrency:
limit: 1
tasks:
# Sync scripts first so flows never reference a file that is not there yet.
- id: sync-namespace-files
type: io.kestra.plugin.git.SyncNamespaceFiles
url: https://github.com/acme/ops-workflows
branch: main
username: "{{ secret('GIT_USERNAME') }}"
password: "{{ secret('GIT_TOKEN') }}"
gitDirectory: _files
namespace: company.ops
delete: true
- id: sync-flows
type: io.kestra.plugin.git.SyncFlows
url: https://github.com/acme/ops-workflows
branch: main
username: "{{ secret('GIT_USERNAME') }}"
password: "{{ secret('GIT_TOKEN') }}"
gitDirectory: _flows
targetNamespace: company.ops
# _flows/patching -> company.ops.patching, _flows/incident -> company.ops.incident
includeChildNamespaces: true
# Git is authoritative: flows deleted from the repo are deleted from Kestra.
delete: true
errors:
- id: alert-platform-team
type: io.kestra.plugin.slack.notifications.SlackIncomingWebhook
url: "{{ secret('SLACK_WEBHOOK') }}"
messageText: "GitOps sync of company.ops from main failed (execution {{ execution.id }}). Check the sync diff before re-running."
triggers:
# GitHub push webhook (content type application/json); only pushes to main start a sync.
- id: on-push-to-main
type: io.kestra.plugin.core.trigger.Webhook
key: "{{ secret('GITOPS_WEBHOOK_KEY') }}"
when: "{{ trigger.body.ref == 'refs/heads/main' }}"
# Reconcile loop: re-apply Git even when nothing was pushed.
- id: reconcile-every-15-min
type: io.kestra.plugin.core.trigger.Schedule
cron: "*/15 * * * *"

A few choices in this flow are deliberate. The sync flow lives in company.platform, outside the namespace it manages, so delete: true can never remove the agent itself. Both triggers feed the same flow, and the flow-level concurrency limit queues the second run instead of letting two syncs race. The sync is not retried on failure: a sync with deletes enabled should be inspected, not replayed blindly. In Kestra 2.0, Git sync tasks call the Kestra API, so they need SDK credentials: derived automatically from basic auth on open source, set as namespace or tenant defaults in the Enterprise Edition, or passed inline with the task’s auth property.

For larger setups, io.kestra.plugin.git.NamespaceSync and io.kestra.plugin.git.TenantSync sync a whole namespace or tenant with an opinionated folder layout. They take an explicit sourceOfTruth (GIT or KESTRA), a whenMissingInSource policy (DELETE, KEEP, or FAIL), and protectedNamespaces that are never deleted; the namespace sync blueprint shows a 15-minute reconcile loop. In the Enterprise Edition they also sync apps, unit tests, dashboards, and custom blueprints.

Pattern 2: CI pushes to Kestra on merge

If your organization already standardizes on CI-driven deploys, Kestra fits that model too. The pipeline validates flows on every pull request and deploys them on merge, using the official GitHub Actions (kestra-io/validate-flows-action-v2 and kestra-io/deploy-flows-action-v2), the Kestra CLI (kestra flow validate, then kestra flow namespace update), or the kestra_flow resource of the Kestra Terraform provider. The CI/CD docs cover each option. With the CLI, omitting --no-delete makes the directory authoritative, so flows missing from Git are removed.

This is push-based, so it gives up continuous reconciliation. Compensate with the system.readOnly: true label on production flows, which disables the UI editor; in the Enterprise Edition, server-side updates to read-only flows are also restricted to service accounts and API keys. Teams that already orchestrate their delivery stages around Kestra often combine the two, as described in CI/CD orchestration.

Pull (sync tasks)Push (CI on merge)
Who appliesA Kestra flow on a schedule or webhookGitHub Actions, GitLab CI, CLI, or Terraform
CredentialsKestra holds a Git read tokenCI holds Kestra API credentials
Drift correctionEvery reconcile cycleNone; prevent it with system.readOnly
Best forTeams that want strict GitOps semanticsTeams with established CI deploy pipelines

Pull requests are the change-control gate

Once flows only reach production through Git, the pull request becomes the approval workflow for operations. Branch protection requires a review before a runbook change merges, CODEOWNERS routes changes under _flows/incident/ to the on-call team, and CI runs flow validation so a malformed YAML never reaches main. The review diff is readable because flows are declarative: a reviewer sees “retry raised from 3 to 5 attempts” or “new task calls the patching API”, not a change to an opaque deploy script. In the Enterprise Edition, Policies add a second check inside Kestra itself, enforcing rules on flows at save time and execution time per namespace.

Rollback is git revert

To roll back a bad change, revert the commit and merge the revert. The webhook fires, the sync applies the previous definition, and Kestra records the change as a new revision in the flow’s revision history, so the timeline shows both the bad change and its rollback. Two caveats apply. A revert restores the workflow definition, not the effects of executions that already ran: if the bad version deleted files or changed infrastructure, recovery is its own workflow. And drafts saved in the editor are neither executed nor pushed to Git, so experimentation in the UI does not leak into production.

Handling drift deliberately

Drift in an orchestrator means a flow edited in the UI, a script uploaded by hand, or a flow created outside Git. The scheduled sync above corrects it automatically. If you prefer to see drift before overwriting it, run a second flow with dryRun: true on the sync tasks: it computes the diff between Git and Kestra without applying anything, which turns the reconcile loop into a drift report you can route to a channel or an approval step. Combined with system.readOnly on production flows, this leaves Git as the only path that changes production.

Environments: separate instances, one repository

Kestra’s own guidance is to run at least two instances, development and production, rather than separating environments by tenant or namespace, because tenants share the same underlying resources (see managing environments). The GitOps mapping follows: one repository, a sync flow per instance, each pointed at its branch or directory, with targetNamespace rewriting namespaces where they differ. Environment-specific values stay out of flow definitions: they come from secrets (secret()), namespace variables, or the KV store configured per instance. Namespaces as lightweight environments are fine for getting started on open source, but not for critical operations. For teams that author in the UI and do not want a Git pipeline, Promote (Enterprise Edition) moves flows between instances with a diff review and a confirmation gate; it solves the same problem without GitOps semantics.

GitOps best practices that hold for clusters and workflows alike

The practices that make GitOps work do not depend on whether the desired state is a Deployment or a runbook.

  • Make Git the only write path. Out-of-band changes, even during incidents, go through a pull request afterwards or get reverted by the next reconcile.
  • Keep application source and deployment configuration in separate repositories or at least separate paths, so a code change and a rollout are distinct, reviewable events.
  • Use directories, not long-lived branches, for environments, and promote by changing files in a reviewed pull request.
  • Validate before merge: schema checks, flow validation, policy checks, and a dry-run diff in CI catch errors while they are still cheap.
  • Turn on deletion only after a dry run. Authoritative syncs (delete: true, whenMissingInSource: DELETE, prune) are what make Git the truth, and also what make a misconfigured path dangerous.
  • Store references to secrets, never secret values, and give the agent read-only Git credentials.
  • Decide which fields Git owns. Ignore the ones controllers manage, and let reconciliation enforce the rest.
  • Monitor the agent. A sync that fails silently turns a GitOps setup back into a manual one without anyone noticing.

As GitOps matures inside a platform team, the boundary of “what lives in Git” keeps moving outward: first manifests, then infrastructure, then the workflows that provision, patch, and recover that infrastructure. Treating those workflows as declarative, reviewed, reconciled code is what closes the loop. To try it on your own repository, start from the Git sync blueprint with dryRun: true, read the diff, then turn on deletes.

Terraform, Ansible and Kubernetes, orchestrated from one place.

Frequently asked questions

Find answers to your questions right here, and don't hesitate to Contact Us if you couldn't find what you're looking for.