Every team we meet that runs Kubernetes has the same two artifacts: a cluster that mostly works, and a deployment process nobody fully trusts. Someone has kubectl access from their laptop. A pipeline applies manifests, but only for two of the five namespaces. Staging drifted from production three months ago and nobody noticed until an incident made it obvious.

GitOps is the pattern that fixes this — and on AKS, with the GitHub estate most of our clients already have, it’s less new tooling than it is a decision to stop deploying any other way. This article is the workflow we run in engagements, as six concrete steps: what to do, what to capture as evidence, and what “done” looks like at each stage.

The loop you’re building

Before the steps, hold this picture in your head. The whole workflow exists to make it true:

The GitOps loop on AKS: a developer commit runs app-repo CI that pushes an image to ACR; image automation opens a promotion pull request on the platform-config repo; once reviewed and merged, Flux on AKS pulls the desired state and reconciles the cluster, reverting drift.
Figure 1 — The GitOps loop on AKS. CI ends at the registry. From there, change flows through a pull request on the platform-config repo, and Flux pulls it into the cluster. The deployment gate is code review, not a pipeline button.

“If your deployment process can’t answer who changed this, when, and who approved it from a single git log, you don’t have a deployment process. You have a habit.”

— Deop platform engineering field notes

Step 1 · Decide pull over push — and write down why

Half a day · output: a one-page decision record

Most teams arrive with a push model: CI builds an image, then a pipeline stage runs kubectl apply or helm upgrade against the cluster. It works — and it carries three costs that compound as you grow:

Our controller default on AKS is Flux, because Microsoft ships it as a managed cluster extension (microsoft.flux) — Azure handles the controller’s lifecycle and you can wire GitOps fleet-wide through Azure Policy. We reach for Argo CD when developers will live in the deployment UI daily. The pattern matters more than the tool; don’t let this choice stall you a quarter.

Step 2 · Stand up the platform-config repo

1–2 days · output: one repo that is the source of truth for every cluster

The mistake we see most is manifests scattered across every application repo. It works at three services and collapses at thirty. Our default is one platform config repo, separate from application code:

# platform-config/ — desired state for the whole estate
clusters/
  prod-canadacentral/      # Flux Kustomizations per cluster
  staging-canadacentral/
infrastructure/
  ingress-nginx/           # shared platform components
  cert-manager/
  monitoring/
apps/
  base/orders-api/         # one Kustomize base per app
  overlays/
    staging/orders-api/    # env-specific patches only
    prod/orders-api/

Application repos build and test code, then a pull request against this repo changes an image tag. Branch protection, CODEOWNERS, and required reviews now govern production exactly the way they already govern code.

A GitHub branch protection settings screen for the platform-config repo main branch: require a pull request before merging with one approval, require review from Code Owners for the production overlays path, require status checks validate-kustomize, policy-tests/conftest and diff-preview, and do not allow bypassing the rules even for admins or the automation bot.
Figure 2 — Branch protection as the production deployment gate. The single frame that proves deployments are governed like code.

Step 3 · Bootstrap Flux the AKS-native way

Half a day per cluster, minutes once Policy is in place · output: clusters that reconcile from Git

Generic GitOps tutorials stop at installing the controller. On Azure, four integrations do the heavy lifting — wire them from day one:

# Enable the managed Flux extension and point the cluster at the repo
az k8s-configuration flux create \
  --resource-group rg-platform --cluster-name aks-prod-cc \
  --cluster-type managedClusters --name platform \
  --url https://github.com/deop/platform-config \
  --branch main --kustomization name=cluster path=./clusters/prod-canadacentral
AKS-native GitOps architecture: Flux controllers running as the microsoft.flux extension inside the AKS cluster pull from the GitHub platform-config repo, Azure Container Registry and Azure Key Vault using a federated workload identity, then apply workloads to reconciled namespaces while External Secrets projects secrets at runtime and Defender reports admission and drift findings.
Figure 3 — AKS-native GitOps architecture. Everything external is reached by pull with a federated managed identity. No PATs, no registry pull secrets, no secret values in Git.
The Azure portal GitOps blade for cluster aks-prod-cc showing one Flux configuration named platform, cluster scope, state Compliant, source github.com/deop/platform-config on branch main, updated 4 minutes ago, with three healthy Kustomizations (cluster, infrastructure, apps) at revision main@sha1:8f3c21a.
Figure 4 — The managed Flux configuration reporting Compliant in the AKS GitOps blade. Pairs with the CLI output below as proof the loop is live.
$ flux get kustomizations
NAME            REVISION            SUSPENDED   READY   MESSAGE
cluster         main@sha1:8f3c21a   False       True    Applied revision: main@sha1:8f3c21a
infrastructure  main@sha1:8f3c21a   False       True    Applied revision: main@sha1:8f3c21a
apps            main@sha1:8f3c21a   False       True    Applied revision: main@sha1:8f3c21a

Step 4 · Automate the promotion PR

1 day · output: humans approve deployments, robots write them

The image-tag PR itself should be automated. Flux’s image automation controllers watch ACR and open the PR for you — the humans only approve:

apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImageUpdateAutomation
metadata:
  name: orders-api-staging
  namespace: flux-system
spec:
  sourceRef: { kind: GitRepository, name: platform-config }
  git:
    commit:
      messageTemplate: "chore: promote {{range .Updated.Images}}{{println .}}{{end}}"
    push: { branch: staging-promotions }
  update:
    path: ./apps/overlays/staging
    strategy: Setters
The promotion pipeline as pull requests: a new image tag in ACR opens a staging PR that auto-merges on green checks and policy tests, then a soak period watches alerts and SLOs, then a production PR requires human CODEOWNERS approval. The same mechanism runs in both environments; only the branch protection differs.
Figure 5 — Promotion is a pull request, not a pipeline button. Staging merges itself on green checks; production waits for a named human.
A GitHub production promotion pull request #482, “chore: promote orders-api:v1.4.2”, opened by flux-bot: changes approved by a Code Owner with write access, all three checks (validate-kustomize, policy-tests/conftest, diff-preview) passed, ready to squash and merge.
Figure 6 — A production promotion PR: written by automation, approved by a person. The bot authors the change; a CODEOWNER carries the accountability.

Step 5 · Cut over in parallel — then revoke push credentials

2–3 sprints per team · output: GitOps as the only deployment path

You don’t flip thirty services in a weekend; the new path has to earn trust. Our sequence:

The honest timeline: two to three sprints from pilot to credential revocation, per team. Faster usually means the revocation step got skipped — and GitOps you can bypass is GitOps nobody trusts.

“You don’t flip a switch. You run both systems in parallel until the new one has earned the cutover.”

— from our Azure DevOps → GitHub Enterprise migration playbook, and it’s just as true here

Step 6 · Operate: make drift a finding, not a mystery

Ongoing · output: boring deployments

Six months in, the healthy version of this looks boring — which is the point:

Kubernetes doesn’t get easier by adding tools. It gets easier when there’s exactly one way changes happen — and that way is one your whole organization already knows how to review.

References