Bootstrapping Flux on AKS: Connect a Git Repository with GitRepository and Kustomization CRDs

Bootstrapping Flux on AKS: Connect a Git Repository with GitRepository and Kustomization CRDs

A practical walkthrough to bootstrap Flux on an AKS cluster, connect it to an external Git repository (GitHub, Azure DevOps, GitLab, and more), and declare reconciliation with GitRepository and Kustomization CRDs.

Flux is one of the most popular GitOps tools for Kubernetes. Instead of running kubectl apply from a pipeline, you declare the desired state in Git and Flux continuously reconciles the cluster to match — no push required.

This post covers bootstrapping Flux on AKS from scratch, then connecting it to an external Git repository (GitHub, Azure DevOps, GitLab, or any Git server), and walking through the two core CRDs you will use every day: GitRepository and Kustomization.

📖 Official docs used in this post:


How Flux works

Flux runs a set of controllers inside your cluster. Each controller watches a specific set of CRDs and acts on them:

Git / OCI / Helm / Bucket source

     │  (poll every N seconds)

┌──────────────────────────────────────────────────────────────────────┐
│  source-controller                                                   │
│  CRDs: GitRepository · OCIRepository · HelmRepository               │
│        HelmChart · Bucket · ExternalArtifact                        │
│  Fetches artifacts and makes them available cluster-internally       │
└───────────────────────────┬──────────────────────────────────────────┘
                            │  artifact ref
          ┌─────────────────┴─────────────────┐
          ▼                                   ▼
┌─────────────────────────┐       ┌───────────────────────────────┐
│  kustomize-controller   │       │  helm-controller              │
│  CRD: Kustomization     │       │  CRD: HelmRelease             │
│  Renders & applies      │       │  Installs / upgrades charts   │
│  Kustomize overlays     │       │  from HelmRepository source   │
└─────────────────────────┘       └───────────────────────────────┘

The source-controller supports six source CRDs, giving you flexibility in where manifests and charts live:

CRDWhat it watchesDocs
GitRepositoryAny Git repository (SSH or HTTPS)Ref
OCIRepositoryOCI-compatible container registriesRef
HelmRepositoryHelm chart repositories (HTTP/S or OCI)Ref
HelmChartA specific chart from a HelmRepositoryRef
BucketS3-compatible object storage bucketsRef
ExternalArtifactArbitrary HTTP(S) artifactsRef

This post focuses on the two resources you wire together for plain-YAML GitOps:

  • GitRepository — tells the source-controller where to pull from, which branch/tag/commit, and how to authenticate.
  • Kustomization — tells the kustomize-controller which path in that artifact to apply, in which namespace, and how often.

Prerequisites

Before you start you need:

  • An AKS cluster running Kubernetes 1.28 or later.
  • kubectl configured with cluster access.
  • Azure CLIInstall guide
  • Flux CLIInstall guide
  • A GitHub (or any Git) repository you control.

Install the Flux CLI

# macOS
brew install fluxcd/tap/flux

# Linux (curl installer)
curl -fsSL https://fluxcd.io/install.sh -o /tmp/install-flux.sh
sudo bash /tmp/install-flux.sh
rm /tmp/install-flux.sh

Verify it is installed:

flux --version

Connect to your AKS cluster

az aks get-credentials \
  --resource-group <resource-group> \
  --name <aks-cluster-name> \
  --overwrite-existing

kubectl get nodes

Pre-flight check

Before installing Flux, confirm your cluster meets the minimum requirements:

flux check --pre

All checks must pass before proceeding.


Bootstrap Flux with GitHub

flux bootstrap is the recommended first-time install. It installs Flux controllers into your cluster and commits the Flux manifests into your Git repository so the cluster manages its own Flux installation going forward.

Required PAT permissions

Flux supports both fine-grained and classic PATs. Fine-grained tokens are recommended because they can be scoped to a single repository.

Fine-grained PAT (recommended) — go to GitHub → Settings → Developer settings → Personal access tokens → Fine-grained tokens:

PermissionAccess levelNotes
AdministrationRead-onlyRequired to read repository settings. Set to Read and write only if using --token-auth=false (SSH deploy key mode) so Flux can register the key.
ContentsRead and writeRequired to read and push the Flux manifests to the repo.
MetadataRead-onlyAutomatically granted; needed to resolve repo metadata.

Classic PAT — go to GitHub → Settings → Developer settings → Personal access tokens → Tokens (classic):

  • Check all permissions under the repo scope.

🔑 Keep the token expiry short and rotate it before it expires — the Flux controllers store it as a cluster Secret and will stop syncing if the token expires. See PAT secret in the Flux docs.

Export credentials

export GITHUB_USER=<your-github-username>
export GITHUB_TOKEN=<your-github-token>
export GITHUB_REPO=aks-gitops

Run bootstrap

flux bootstrap github \
  --token-auth \
  --owner=$GITHUB_USER \
  --repository=$GITHUB_REPO \
  --branch=main \
  --path=clusters/prod \
  --personal \
  --private

What happens:

  1. Flux controllers are installed in the flux-system namespace.
  2. The PAT is stored in the cluster as a Kubernetes Secret named flux-system in the flux-system namespace. The controllers use this PAT to access Git over HTTPS — no deploy key is created.
  3. The Flux component manifests and a GitRepository + Kustomization pointing at clusters/prod are pushed to the repository.
  4. The cluster immediately begins reconciling from that path.

⚠️ SSH vs HTTPS: --token-auth (used above) is the HTTPS path — your PAT is stored as a cluster Secret. If you want SSH deploy keys instead, drop --token-auth and add --token-auth=false. Flux will then generate an SSH key pair, store the private key in the cluster Secret, and register the public key as a GitHub deploy key via the API. See GitHub Deploy Keys in the docs.

After bootstrap your repository will contain:

clusters/
└── prod/
    └── flux-system/
        ├── gotk-components.yaml    # Flux CRDs and controllers
        ├── gotk-sync.yaml          # GitRepository + Kustomization for flux-system
        └── kustomization.yaml      # Kustomize entry point

Flux has first-class bootstrap support for many Git providers beyond GitHub:

ProviderBootstrap guide
GitLabdocs
Bitbucketdocs
Azure DevOpsdocs
AWS CodeCommitdocs
Google Cloud Sourcedocs
Giteadocs
Oracle VBSdocs
Any Git serverdocs

Verify the bootstrap

# Check Flux controller pods
kubectl get pods -n flux-system

# Check all GitRepository sources
flux get sources git -A

# Check all Kustomizations
flux get kustomizations -A

# Full health check
flux check

All resources should show Ready=True. If any are not, jump to the troubleshooting section.


Connecting an external Git repository with SSH

The bootstrap command sets up the flux-system repository, but in practice you often want to point a separate application repository at the same cluster. The clean way to do this is with an SSH deploy key and a Secret — no personal tokens, no passwords, nothing that ties to a person’s account.

Step 1 — Generate an SSH key pair

ssh-keygen -t ed25519 -C "flux@aks-prod" -f ./flux-deploy-key -N ""

This creates two files:

  • flux-deploy-key — private key (stays in the cluster as a Kubernetes Secret)
  • flux-deploy-key.pub — public key (added to the Git repository as a deploy key)

Step 2 — Add the public key to your repository

GitHub:

  1. Open the repository → SettingsDeploy keysAdd deploy key.
  2. Paste the contents of flux-deploy-key.pub.
  3. Keep Allow write access unchecked — Flux only needs read access for application repos.

GitLab: Repository → Settings → Repository → Deploy keys.

Azure DevOps: Project Settings → Repositories → select repo → Security → SSH keys.

Step 3 — Create the Kubernetes Secret

Store the private key as a Secret in the namespace where you will create the GitRepository:

kubectl create secret generic flux-app-repo-auth \
  --namespace=flux-system \
  --from-file=identity=./flux-deploy-key \
  --from-file=identity.pub=./flux-deploy-key.pub \
  --from-literal=known_hosts="$(ssh-keyscan -t rsa,ecdsa,ed25519 github.com 2>/dev/null)"

The known_hosts entry prevents Flux from refusing the connection on a host-key mismatch. Verify the host key fingerprint out-of-band before you trust it, and adjust the hostname if you are using GitLab, Azure DevOps, or a self-hosted server.

Immediately remove the local key files:

rm flux-deploy-key flux-deploy-key.pub

The GitRepository CRD

GitRepository is a Source API resource. It tells the source-controller to clone a specific repository at a specific interval and make the result available as an artifact for downstream controllers.

Full annotated example

apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
  name: aks-apps
  namespace: flux-system
spec:
  # How often Flux polls for new commits.
  # Supported suffixes: s, m, h. Minimum: 1m in production.
  interval: 1m

  # SSH URL of the repository. Use SSH (git@) for deploy-key auth.
  # HTTPS URLs are supported too — replace secretRef with a token secret.
  url: ssh://git@github.com/<org>/<repo>.git

  # Which ref to track. You can use branch, tag, semver, or commit.
  ref:
    branch: main
    # tag: v1.2.3
    # semver: ">=1.0.0 <2.0.0"
    # commit: abc1234

  # Reference to the Secret created in the previous step.
  secretRef:
    name: flux-app-repo-auth

  # Optional: only reconcile when files under these paths change.
  # Useful for monorepos — avoids reconciling all kustomizations
  # when an unrelated part of the repo changes.
  ignore: |
    # ignore everything except the k8s directory
    /*
    !/k8s/

Apply it:

kubectl apply -f gitrepository.yaml

Check the status:

flux get source git aks-apps -n flux-system

A healthy output looks like:

NAME        REVISION        SUSPENDED  READY   MESSAGE
aks-apps    main@sha1:...   False      True    stored artifact for revision 'main@sha1:...'

If the READY column shows False, describe the resource for the exact error:

kubectl describe gitrepository aks-apps -n flux-system

The Kustomization CRD

Kustomization is a Kustomize API resource. It tells the kustomize-controller which path in a source artifact to render and apply, and in which target namespace.

⚠️ Do not confuse this with the native kustomize.config.k8s.io/v1beta1/Kustomization — that is the plain Kustomize file. The Flux CRD is kustomize.toolkit.fluxcd.io/v1/Kustomization and is a different resource entirely.

Full annotated example

apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: aks-apps
  namespace: flux-system
spec:
  # How often the controller reconciles even if nothing in Git changed.
  # Detects drift (someone ran kubectl edit, for example).
  interval: 5m

  # Re-apply immediately if a previous apply failed.
  retryInterval: 1m

  # Path inside the GitRepository artifact to apply.
  # Must contain a kustomization.yaml or plain manifests.
  path: "./k8s/overlays/prod"

  # Delete resources from the cluster when they are removed from Git.
  prune: true

  # Namespace to use when the manifests do not specify one.
  targetNamespace: demo

  # Kubernetes RBAC: which ServiceAccount the controller impersonates
  # when applying. Scopes blast radius to least privilege.
  serviceAccountName: flux-reconciler

  # Reference to the GitRepository (or other source) to pull from.
  sourceRef:
    kind: GitRepository
    name: aks-apps

  # Wait for all applied resources to become ready before marking
  # this Kustomization as Ready.
  wait: true
  timeout: 2m

  # Optional: pass values into the kustomization from the cluster
  # (e.g. inject the cluster name into a ConfigMap).
  postBuild:
    substitute:
      CLUSTER_NAME: "aks-prod"
    substituteFrom:
      - kind: ConfigMap
        name: cluster-vars

Apply it:

kubectl apply -f kustomization.yaml

Watch reconciliation:

flux get kustomizations aks-apps -n flux-system --watch

Force an immediate reconcile without waiting for the interval:

flux reconcile kustomization aks-apps --with-source

A clean layout separates the cluster bootstrap config (managed by Flux’s own flux-system kustomization) from your application config:

my-gitops-repo/
├── clusters/
│   └── prod/
│       └── flux-system/          # managed by flux bootstrap
│           ├── gotk-components.yaml
│           ├── gotk-sync.yaml
│           └── kustomization.yaml
├── infrastructure/               # platform-level config
│   ├── namespaces/
│   ├── rbac/
│   └── cert-manager/
└── apps/
    ├── base/                     # shared base manifests
    │   └── nginx/
    │       ├── deployment.yaml
    │       ├── service.yaml
    │       └── kustomization.yaml
    └── overlays/
        ├── dev/                  # dev-specific patches
        └── prod/                 # prod-specific patches

Each environment overlay has its own Kustomization CR pointing at the right path and source. This means promoting from dev to prod is a Git commit, not a manual kubectl apply.


Add your first workload

This section puts both CRDs together in a single end-to-end example. A GitRepository tells Flux where your app repo lives; a Kustomization tells it which path to apply. Both are required — the Kustomization cannot reconcile without a ready source to pull from.

Step 1 — Create the app repo Secret (SSH auth)

If your app repository is separate from the bootstrap repo, create the SSH auth Secret first (see the Connecting an external Git repository with SSH section for how to generate the key and add the public key to the repo):

kubectl create secret generic flux-app-repo-auth \
  --namespace=flux-system \
  --from-file=identity=./flux-deploy-key \
  --from-file=identity.pub=./flux-deploy-key.pub \
  --from-literal=known_hosts="$(ssh-keyscan -t rsa,ecdsa,ed25519 github.com 2>/dev/null)"

Step 2 — Create the GitRepository CR

Create flux-system/gitrepository-aks-apps.yaml and commit it to the bootstrap repository under clusters/prod/, or apply it directly:

apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
  name: aks-apps
  namespace: flux-system
spec:
  interval: 1m
  url: ssh://git@github.com/<org>/<app-repo>.git
  ref:
    branch: main
  secretRef:
    name: flux-app-repo-auth

Apply and confirm the source is ready before proceeding:

kubectl apply -f gitrepository-aks-apps.yaml

flux get source git aks-apps -n flux-system
# NAME      REVISION          SUSPENDED  READY  MESSAGE
# aks-apps  main@sha1:abc123  False      True   stored artifact ...

The READY=True line means the source-controller has successfully cloned the repo and stored the artifact. Only then will a Kustomization that references it be able to reconcile.

Step 3 — Add the app manifests to the repo

Create apps/base/demo/deployment.yaml in your app repository:

apiVersion: v1
kind: Namespace
metadata:
  name: demo
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
  namespace: demo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx:1.27
          ports:
            - containerPort: 80

Step 4 — Add the plain Kustomize file

Add apps/base/demo/kustomization.yaml (the native Kustomize file — not a Flux CRD):

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - deployment.yaml

Step 5 — Create the Flux Kustomization CR

This CR references GitRepository/aks-apps created in Step 2. Without that source being Ready, this Kustomization will stay in a Not Ready state — always create the GitRepository first.

apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: demo-app
  namespace: flux-system
spec:
  interval: 5m
  path: "./apps/base/demo"
  prune: true
  sourceRef:
    kind: GitRepository
    name: aks-apps

Commit and push both the GitRepository and Kustomization manifests. Flux will detect new commits within the interval window and apply them. Verify the full chain:

# Source is fetching
flux get source git aks-apps -n flux-system

# Kustomization is reconciling
flux get kustomization demo-app -n flux-system

# Workload is running
kubectl get deployment nginx -n demo

Troubleshooting

Authentication failure to the Git repository

kubectl describe gitrepository aks-apps -n flux-system | grep -A10 "Status:"

Common causes:

  • Public key was not added to the repository’s deploy keys.
  • known_hosts in the Secret is missing or incorrect — re-run ssh-keyscan <hostname> and recreate the Secret.
  • The Secret name in secretRef does not match what was created.

Flux controllers not becoming ready after bootstrap

kubectl get pods -n flux-system
kubectl get events -n flux-system --sort-by=.lastTimestamp

If a pod is CrashLoopBackOff, check its logs:

kubectl logs -n flux-system deploy/source-controller
kubectl logs -n flux-system deploy/kustomize-controller

Kustomization stuck in Not Ready

flux get kustomization aks-apps -n flux-system
kubectl describe kustomization aks-apps -n flux-system

Check the Message field — it usually contains the exact error (missing CRD, RBAC denial, invalid YAML). Fix the manifests in Git, commit, and force a reconcile:

flux reconcile kustomization aks-apps --with-source

Reconciliation not picking up new commits

  • Confirm you pushed to the same branch configured in spec.ref.branch.
  • Verify spec.path in the Kustomization matches where your manifests live.
  • Check the source is fetching:
flux get source git aks-apps -n flux-system

Once Flux is running you can progressively harden and extend it:

StepDescriptionDocs
Secrets managementEncrypt secrets in Git with SOPS + Azure Key VaultFlux SOPS guide
Image automationLet Flux update image tags from Azure Container RegistryImage automation
NotificationsAlert Slack or Teams on reconciliation failuresNotification controller
Multi-tenancyScope Kustomizations to least-privilege ServiceAccountsMulti-tenancy lockdown
MonitoringScrape Flux metrics with Azure Managed PrometheusFlux monitoring

GitOps with Flux and AKS means every cluster change is a Git commit — auditable, reviewable, and reversible. Once you understand how GitRepository and Kustomization fit together, you have the building blocks for everything else.

Found this helpful?
Back to all posts