Calm Production Deployments With Gitops Workflows

gitops

Calm Production Deployments With Gitops Workflows

Make releases predictable without turning every change into a ceremony.

Start With A Clear Operating Model

Gitops is often described as “using Git for deployments,” which is true in the same way that a map is “paper for travelling.” It’s technically correct, but it leaves out the useful parts. The practical idea is simple: Git becomes the declared source of truth for the desired state of our environments, and a controller continuously works to make reality match that declaration.

That changes how we think about releases. Instead of an engineer running a deployment command from a laptop, a pull request changes a version, configuration value, or manifest. After review and merge, the Gitops controller sees the new commit and applies it to the correct cluster. The deployment is no longer dependent on someone remembering a command, finding the right credentials, or having a reliable home Wi-Fi connection. We’ve all met that deployment process. It has character, but not the good kind.

The OpenGitOps principles provide a useful baseline: declarative configuration, versioned and immutable desired state, automated pull, and continuous reconciliation. Those principles are more valuable than any particular tool. We can use Flux, Argo CD, or a well-managed in-house controller, but the operating model needs to stay consistent.

Before selecting tooling, we should agree on a few questions. Which repository owns application code? Which repository owns environment configuration? Who can approve production changes? What happens when a manual cluster edit conflicts with Git? How quickly should a bad release be rolled back?

Gitops works best when those answers are boring and written down. If our release process depends on tribal knowledge, heroic debugging, or a private spreadsheet called final-final-v7.xlsx, Gitops gives us a chance to retire some unnecessary drama.

Give Repositories Clear, Narrow Responsibilities

A Gitops repository should make it easy to answer one question: “What should this environment look like right now?” We don’t need a giant repository that contains every script, every chart, every application, and a fossil record of experiments from 2019. We need a layout that lets people find ownership and intent quickly.

A common pattern separates application source from deployment configuration. The application repository contains code, tests, Dockerfile, and build metadata. The environment repository contains Kubernetes manifests, Helm values, Kustomize overlays, policy configuration, and references to approved image versions. This separation gives us an important control point: building an image does not automatically mean deploying it to production.

For example, a modest Kustomize structure might look like this:

platform-config/
├── apps/
│   └── payments/
│       ├── base/
│       │   ├── deployment.yaml
│       │   ├── service.yaml
│       │   └── kustomization.yaml
│       └── overlays/
│           ├── staging/
│           └── production/
└── clusters/
    ├── staging/
    │   └── kustomization.yaml
    └── production/
        └── kustomization.yaml

The production overlay should contain only what differs from the base: replicas, ingress hostnames, resource limits, image tags, or a tightly justified feature flag. If every environment has a fully copied manifest set, drift arrives quickly and then settles in like an unwanted houseguest.

We should also decide whether teams manage their own paths or submit changes through a platform team. There’s no universal answer. Small organisations may centralise review initially; larger ones often delegate by namespace, service, or cluster. Either way, repository permissions must match operational responsibility. A team that owns a production service should be able to propose its deployment change, while production approval remains appropriately protected.

Finally, keep generated files out where possible. Human-readable configuration makes code review meaningful. If a reviewer cannot tell what a pull request will do, our process is merely Git-shaped.

Let Reconciliation Do The Routine Work

Continuous reconciliation is the part of Gitops that turns stored configuration into an operating practice. A controller regularly checks the repository, compares the declared state with the cluster, and applies changes when they differ. This means a deployment can happen after a merge without granting broad cluster credentials to every developer or CI job.

With Flux, for example, we can define a source repository and tell the controller which path to reconcile:

apiVersion: fluxcd.io/v1
kind: Kustomization
metadata:
  name: payments-production
  namespace: flux-system
spec:
  interval: 5m
  path: ./clusters/production
  prune: true
  sourceRef:
    kind: GitRepository
    name: platform-config
  healthChecks:
    - apiVersion: apps/v1
      kind: Deployment
      name: payments
      namespace: payments
  timeout: 3m

The details vary by tool, but the important settings deserve care. A short interval provides faster updates, but it does not replace alerting or good deployment checks. Pruning removes resources that no longer exist in Git, which is useful but should be introduced carefully. We should never discover its consequences by deleting a directory on a Friday afternoon and hoping for the best.

Reconciliation also addresses manual drift. If someone changes a deployment directly with kubectl, the controller may restore the Git-defined state. That can feel strict at first, especially to engineers accustomed to making live fixes. But it creates a healthy distinction: emergency actions may be necessary, yet the durable fix must be recorded in Git immediately afterward.

We should define an emergency path explicitly. It may allow break-glass access, but it should log the action, alert the responsible team, and require follow-up reconciliation. Production is not a museum exhibit; people will need to intervene sometimes. Gitops simply makes sure that intervention does not become invisible infrastructure folklore.

Promote Immutable Versions, Not Fresh Builds

A safe Gitops release process promotes the same tested artifact through environments. We build an image once, identify it immutably, deploy it to development or staging, validate it, and then update production to reference that exact artifact. We should avoid rebuilding the same commit separately for each environment because separate builds can produce separate results.

Image tags such as latest, stable, or release-candidate are convenient until they are not. They move over time, which makes incident investigation unnecessarily difficult. A commit SHA, semantic version tied to an immutable registry entry, or image digest gives us a fixed reference. When someone asks what was running at 14:03 during an outage, we should be able to answer without consulting a crystal ball.

A promotion pull request can be deliberately small:

images:
  - name: registry.example.com/payments
    newName: registry.example.com/payments
    newTag: "1.18.4"

That tiny change has several advantages. Reviewers see precisely what is moving. Rollback is a normal Git revert or a version update. Audit history is built into the repository. And the CI pipeline can verify that the referenced image exists, was scanned, has passed tests, and was previously deployed to staging.

This does not mean every deployment requires a meeting. Routine changes should flow quickly after appropriate automated checks and ownership review. The level of review should reflect risk: a text-only configuration change is not the same as a database migration, a network policy rewrite, or a service handling payments.

We should also treat migrations as first-class release concerns. If application version A cannot safely coexist with schema version B, a simple “change image tag” workflow is insufficient. Use backward-compatible migrations where possible, break changes into stages, and make rollback expectations explicit. Gitops improves consistency, but it cannot repeal database physics. We’ve checked.

Keep Secrets Out Of Plain Git History

Gitops does not mean placing production credentials in a public-looking YAML file and trusting repository permissions to save us. Configuration belongs in Git; secret material needs an extra layer of protection. Even private repositories are copied, cloned, backed up, and occasionally granted access more broadly than intended.

A common approach encrypts secret values before they enter the repository. Tools such as SOPS can encrypt selected values using cloud KMS keys, PGP keys, or age keys. The Gitops controller then decrypts them only within the approved reconciliation path. Here is the shape of an encrypted Kubernetes secret:

apiVersion: v1
kind: Secret
metadata:
  name: payments-db
  namespace: payments
type: Opaque
stringData:
  username: ENC[AES256_GCM,data:...]
  password: ENC[AES256_GCM,data:...]
sops:
  kms: []
  age:
    - recipient: age1examplekey
      enc: |
        -----BEGIN AGE ENCRYPTED FILE-----
        ...

Encryption is only one part of the design. We should separate permissions for editing encrypted files from permissions for decrypting them. Not every engineer who can propose a production manifest change needs the ability to read database credentials. Likewise, the controller should have access only to the secrets required for its scope, not a master key to every environment.

External secret managers are another valid option. In that model, Git stores a reference to a secret rather than the value itself, and a controller retrieves it from a managed vault. This can simplify rotation and centralise auditing, though it adds another dependency to our deployment path.

Whichever model we choose, we need regular rotation, access reviews, and incident procedures. If a secret lands in plain Git, removing the current file is not enough: Git history, clones, build logs, and backups may still contain it. Rotate first, clean history where appropriate, and assume the old value is compromised. It’s less glamorous than a launch announcement, but it keeps the launch from becoming an incident report.

Build Guardrails Into Pull Requests And Clusters

Gitops makes pull requests the front door for change, so we should make that door sturdy without making it painfully slow. The goal is not to force every engineer through a maze of approvals. The goal is to catch predictable mistakes before they reach a cluster.

At minimum, configuration pull requests should render manifests and validate their syntax. For Kubernetes, that may include schema validation, Kustomize or Helm rendering, policy checks, image reference verification, and a comparison against the current environment. A reviewer should be able to see whether a pull request changes one image tag or accidentally removes an ingress, a service account, and half the namespace.

Policy checks help enforce standards that humans are likely to miss during a busy review. We can require resource requests and limits, disallow privileged containers, restrict host networking, prevent unapproved registries, and ensure labels support ownership and cost allocation. These checks should return useful messages. “Denied by policy 47” teaches nobody much, apart from how to become annoyed.

Cluster-side guardrails matter too. Git review cannot protect us from every bad configuration, especially if a controller is misconfigured or an approved change has unexpected effects. Admission controls, namespace boundaries, workload identity, and least-privilege service accounts provide a second line of defence. We should also ensure the Gitops controller cannot modify more clusters or namespaces than it needs.

Argo CD and Flux both support patterns for scoped reconciliation, health checks, and status reporting. Tooling is helpful, but ownership is the real guardrail. Every deployed service needs a named team, escalation route, and documented expectation for who reviews changes. A repository full of technically valid YAML without accountable owners is just a better-organised pile of risk.

Measure Whether Releases Are Actually Improving

We should not declare Gitops successful because a dashboard looks impressive or because we replaced deployment scripts with YAML. The real question is whether releases have become safer, clearer, and easier to operate.

Start with a few measures that connect directly to delivery work. How long does it take for an approved production change to reach the cluster? How often do deployments fail or require rollback? How long does recovery take after a bad release? How many configuration changes are made directly in production rather than through Git? We do not need a wall of charts. We need enough evidence to spot friction and regressions.

Controller health deserves attention as well. Monitor reconciliation failures, source-fetch errors, drift alerts, degraded workloads, and how long applications remain out of sync. A Gitops controller silently failing to pull a repository can create a confusing situation: the pull request is merged, everyone believes the release is complete, and production remains unchanged. That is a particularly awkward kind of success.

We should review incidents through the Gitops lens. Did Git history show the intended change? Did the correct environment receive it? Were health checks meaningful? Was rollback straightforward? Did an emergency fix get captured in Git afterward? These questions improve the system without turning every retrospective into a tool trial.

Adoption should be gradual. Begin with one service that has a cooperative owner, clear deployment needs, and manageable risk. Document the path, fix the rough edges, and then extend the model. A rushed migration of every production workload usually creates a large, expensive learning exercise.

When Gitops is working well, deployment becomes less mysterious. Changes are visible, approvals are traceable, recovery is ordinary, and fewer people need to remember special commands. That’s not magic. It’s just good operational housekeeping, which is wonderfully underrated.

Share