GitOps on AKS with the Managed Flux Extension: Setting Up the Foundation

GitOps on AKS with the Managed Flux Extension: Setting Up the Foundation

Use Terraform to create an AKS cluster, install the managed Flux extension, point it at a Git repository, and watch Git changes and drift correction happen without running kubectl apply.

I am learning Flux at the moment. So far I have only used the open source version, installed with flux bootstrap, mostly to understand how the pieces fit together. I have not run it in production.

AKS also offers Flux as a managed cluster extension, and I wanted to know how that compares. Is it the same Flux with a different installer, or does it change how you work with it? The best way to find out is to build it, so I am turning that into a series. Over the next posts I will go through Flux concepts one at a time on an AKS cluster that uses the extension: Helm releases, dependencies between Kustomizations, multiple environments, and so on, noting where it differs from what I saw with OSS Flux.

This post is the foundation: a cluster with the extension installed, connected to a Git repository, and a first look at what “Flux is reconciling” means in practice. Everything is in my Code-Snippets repository. I explain the design and the important commands here, and the repo has the exact files.

Coming from OSS Flux? I wrote about the self-managed route in Bootstrapping Flux on AKS: Connect a Git Repository with GitRepository and Kustomization CRDs, which uses flux bootstrap. This series uses the Azure-managed extension instead, and the next section explains the difference.


Two Ways to Run Flux on AKS

Self-managed Flux. You install the Flux CLI, run flux bootstrap, and Flux commits its own manifests into your Git repository. You own the upgrades, you choose the version, and you have full access to everything Flux can do.

The AKS Flux extension (microsoft.flux). Azure installs the Flux controllers as a cluster extension and keeps them updated on a release train (Stable, in my setup). You describe what to sync as an Azure resource, a “Flux configuration”, and Azure’s agents create the Flux objects in the cluster for you.

I do not think either one wins in every case. These are the things that stood out to me about the extension, and I will keep testing them as the series goes on:

  • Installing and removing Flux becomes part of your infrastructure code instead of a separate step on somebody’s laptop.
  • The extension is managed and upgraded for you, so there is less Flux version housekeeping.
  • The Azure portal shows the compliance state of every configuration, which is nice when other people need to see what is going on without learning kubectl.

The trade-off is that you describe your sources through Azure resources (or the az k8s-configuration flux CLI) rather than hand-writing the Flux YAML. One consequence is worth knowing up front:

Do not run flux bootstrap on a cluster that already has the extension. Both would try to manage the Flux controllers and CRDs in flux-system, so pick one approach per cluster. I have not tested what happens if you mix them, and I do not plan to.


What We Are Building

The demo creates:

  • A small AKS cluster (two nodes, Azure CNI Overlay with Cilium, system-assigned managed identity, Free control-plane tier).
  • The managed Flux extension, installed into the flux-system namespace.
  • A Flux configuration called cluster-config that points at a public Git repository and path.
  • An optional sample app (Podinfo) so there is something to look at.

Terraform owns the Azure resources. Flux owns everything that lives inside the Git path. The two do not overlap, and that separation is the whole point:

You ── commit ──> Git repository
                      ▲
                      │ outbound HTTPS pull (every 60s)
                      │
        ┌─────────────┴──────────────── AKS ────────────────┐
        │  flux-system:    source-controller                 │
        │                  kustomize-controller              │
        │                  helm-controller                   │
        │                  notification-controller           │
        │                  Azure Flux configuration agents   │
        │                                                    │
        │  cluster-config: GitRepository                     │
        │                  Flux Kustomization                │
        │                  ("cluster-config-apps")           │
        │                                                    │
        │  your app namespaces: whatever the Git path holds  │
        └────────────────────────────────────────────────────┘

Notice the arrow direction. Nothing pushes into the cluster. Flux reaches out to Git over outbound HTTPS, so you do not need to open an inbound path from GitHub to your cluster or hand your pipeline cluster credentials.


Prerequisites

  • Azure CLI, Terraform 1.6 or later, kubectl, jq, and Git.
  • An Azure subscription where you can create AKS and Microsoft.KubernetesConfiguration resources. Resource provider registration needs subscription-level permissions, so ask an admin if you cannot do it yourself.
  • Quota for two Standard_D2s_v6 nodes in your region. Change the VM size or region in the variables if that does not work for you.
  • A public Git repository with a published branch. Flux cannot see uncommitted files or a branch that only exists on your laptop.

The Terraform, Piece by Piece

I will skip the resource group and the cluster itself, since they are standard. If you want the full file, it is in the repo. The interesting part is the two resources that make Flux happen.

1. Install the extension

resource "azurerm_kubernetes_cluster_extension" "flux" {
  name              = "flux"
  cluster_id        = azurerm_kubernetes_cluster.this.id
  extension_type    = "microsoft.flux"
  release_train     = "Stable"
  release_namespace = "flux-system"
}

That is it. No Flux CLI, no bootstrap. I did not pin a version, so Azure’s auto-upgrade behavior applies. For a demo that is fine, and for production you may want more control.

2. Tell Flux what to sync

The extension on its own does not know about any repository. It just runs the controllers. You also need a Flux configuration:

resource "azurerm_kubernetes_flux_configuration" "this" {
  name                              = "cluster-config"
  cluster_id                        = azurerm_kubernetes_cluster.this.id
  namespace                         = "cluster-config"
  scope                             = "cluster"
  continuous_reconciliation_enabled = true

  git_repository {
    url                      = var.git_repository_url
    reference_type           = "branch"
    reference_value          = var.git_branch
    sync_interval_in_seconds = 60
    timeout_in_seconds       = 120
  }

  kustomizations {
    name                       = "apps"
    path                       = var.gitops_path
    garbage_collection_enabled = true
    sync_interval_in_seconds   = 60
    retry_interval_in_seconds  = 30
    timeout_in_seconds         = 300
  }

  depends_on = [azurerm_kubernetes_cluster_extension.flux]
}

A few things I want to point out, because they are the settings that matter day to day:

  • sync_interval_in_seconds = 60 on the repository is how often Flux checks Git for new commits.
  • path is relative to the repository root, not your local working directory. Getting this wrong is an easy way to end up with a “path not found” error.
  • garbage_collection_enabled = true turns on pruning. If you delete a manifest from Git, Flux deletes the object from the cluster. That is exactly what you want from GitOps, and also exactly why you should not experiment with deleting a Namespace in a cluster you care about.
  • scope = "cluster" lets the configuration manage resources in any namespace. That is convenient, but it is a powerful permission, so only trusted people should be able to merge to the tracked branch. In a later post I want to look at namespace-scoped configurations for multi-team clusters.
  • depends_on makes sure the extension exists before the configuration is created, and Terraform also destroys them in the reverse order.

A confusing detail: two things called “Kustomization”

When you start, it is easy to mix these up:

WhatWhere it livesPurpose
kustomization.yamlIn your Git repositoryA Kustomize build file listing resources and overlays.
Flux KustomizationA Kubernetes object in the clusterTells Flux which path to build and apply, and how often.

Same word, completely different things. In this setup the Flux Kustomization is generated for you from the kustomizations block above. You never write that YAML by hand. The kustomization.yaml files in the repo are plain Kustomize and just describe what to build.


The Sample App in Git

For the demo I use Podinfo, a tiny Go app made for this kind of thing. The gitops folder looks like this:

gitops/
  kustomization.yaml     # includes infra and apps
  infra/
    kustomization.yaml
    namespace.yaml       # the flux-demo namespace
  apps/
    kustomization.yaml   # sets the namespace and a replica override
    deployment.yaml
    service.yaml

The top-level kustomization.yaml includes both infra and apps, so the single cluster-config-apps Flux Kustomization applies the namespace and the workload together. That keeps this first post simple. Splitting infrastructure from applications, with dependencies between the two, is a good topic for the next post.

Two small details in the Deployment are worth a mention. First, I run it hardened (non-root, read-only root filesystem, dropped capabilities, resource limits, probes), and the namespace enforces the restricted Pod Security level. Second, the Deployment says replicas: 2, but apps/kustomization.yaml overrides it to 3:

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: flux-demo
resources:
  - deployment.yaml
  - service.yaml
replicas:
  - name: podinfo
    count: 3

I left that in deliberately. When you test the Git-change flow later, remember that the number to edit is in the Kustomize file, not in deployment.yaml.


Deploy It

Because Flux can only pull from a published branch, the first decision is where your manifests live. Two options:

  1. Use my repository directly. It is public, so you can point at it and just watch Flux do its thing.
  2. Fork it (or copy the gitops folder into your own repo). You need this if you want to push changes, which is the interesting part, so I recommend it.

Clone the repo, sign in, and copy the example variables file:

cd AKS-Flux-Extension
az login
az account set --subscription "<your-subscription-name>"
cp terraform/terraform.tfvars.example terraform/terraform.tfvars

Edit terraform/terraform.tfvars. For a fork of Code-Snippets it looks like this:

git_repository_url = "https://github.com/YOUR-ACCOUNT/Code-Snippets.git"
git_branch         = "main"
gitops_path        = "./AKS-Flux-Extension/gitops"

If you copy the gitops folder into your own repo instead, use gitops_path = "./gitops". Point at the gitops root rather than just gitops/apps, otherwise Flux will not create the namespace.

Then run the script:

bash deploy.sh
export KUBECONFIG="$PWD/kubeconfig"

The script registers the resource providers, formats and validates the Terraform, applies it, downloads a dedicated kubeconfig into the folder (gitignored), and prints the Flux objects.

Heads up: deploy.sh runs terraform apply -auto-approve. It will not show you the plan first. Read your variables before you run it, and if you prefer to see the plan, run the Terraform commands yourself.

The apply itself takes several minutes. Also, a successful terraform apply only proves Azure created the resources. It does not prove Flux managed to pull your repository and apply the manifests. That is a separate question, and we will check it next.


Check That Flux Is Actually Working

Install the Azure CLI extensions for inspecting extensions and configurations:

az extension add --name k8s-extension --upgrade
az extension add --name k8s-configuration --upgrade

RESOURCE_GROUP="$(terraform -chdir=terraform output -raw resource_group_name)"
CLUSTER_NAME="$(terraform -chdir=terraform output -raw cluster_name)"

az k8s-extension show --resource-group "$RESOURCE_GROUP" \
  --cluster-name "$CLUSTER_NAME" --cluster-type managedClusters --name flux

az k8s-configuration flux show --resource-group "$RESOURCE_GROUP" \
  --cluster-name "$CLUSTER_NAME" --cluster-type managedClusters --name cluster-config

You want the extension in a Succeeded provisioning state and the configuration to become Compliant. It may show non-compliant for a short time at first. Give it a minute or two before you worry.

Then look from inside the cluster:

kubectl get pods -n flux-system
kubectl get gitrepositories.source.toolkit.fluxcd.io -n cluster-config
kubectl get kustomizations.kustomize.toolkit.fluxcd.io -n cluster-config

Both the GitRepository and the Flux Kustomization should report READY=True. Notice the name of the Kustomization: I declared it as apps in Terraform, but the object in the cluster is called cluster-config-apps. Azure builds the name from the configuration name plus the Kustomization name.

If you prefer clicking, open the AKS resource in the Azure portal, go to GitOps, and select cluster-config. You will see the same compliance information there. That is a nice extra compared with OSS Flux, where you would need kubectl or the Flux CLI for the same view.

What Azure generated for you

I said earlier that you never write the Flux YAML yourself in this setup. Here is what Azure actually created from the Terraform. Use kubectl describe for a readable summary with events, or -o yaml for the full object:

kubectl describe gitrepositories.source.toolkit.fluxcd.io -n cluster-config
kubectl describe kustomizations.kustomize.toolkit.fluxcd.io -n cluster-config

kubectl get gitrepositories.source.toolkit.fluxcd.io cluster-config -n cluster-config -o yaml
kubectl get kustomizations.kustomize.toolkit.fluxcd.io cluster-config-apps -n cluster-config -o yaml

This is the GitRepository from my demo cluster, which tracks the main branch of my Code-Snippets repository. I removed a few noisy fields (UIDs, resource versions, and timestamps), and you will see the values from your own terraform.tfvars:

apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
  name: cluster-config
  namespace: cluster-config
  labels:
    clusterconfig.azure.com/is-managed: "true"
    clusterconfig.azure.com/name: cluster-config
    clusterconfig.azure.com/namespace: cluster-config
  ownerReferences:
  - apiVersion: clusterconfig.azure.com/v1alpha1
    blockOwnerDeletion: true
    controller: true
    kind: FluxConfig
    name: cluster-config
spec:
  interval: 1m0s
  ref:
    branch: main
  timeout: 2m0s
  url: https://github.com/kasunsjc/Code-Snippets.git
status:
  artifact:
    revision: main@sha1:c2eb9b3652060eee334b67b2f72356e1ba5868b7
    url: http://source-controller.flux-system.svc.cluster.local./gitrepository/cluster-config/cluster-config/c2eb9b3652060eee334b67b2f72356e1ba5868b7.tar.gz
  conditions:
  - message: stored artifact for revision 'main@sha1:c2eb9b3652060eee334b67b2f72356e1ba5868b7'
    reason: Succeeded
    status: "True"
    type: Ready
  - reason: Succeeded
    status: "True"
    type: ArtifactInStorage

Mapping it back to Terraform is easy: interval is sync_interval_in_seconds = 60, timeout is timeout_in_seconds = 120, and ref.branch and url come from git_repository. The status.artifact.revision is the branch plus the exact commit SHA that Flux fetched, which is how you confirm the cluster has the commit you pushed.

The two things you will not find in plain OSS Flux are the clusterconfig.azure.com labels and the ownerReferences. The owner is a FluxConfig object, which is Azure’s own custom resource. That tells you who is in charge: these objects are generated from the Azure configuration, so make changes through Terraform or the Azure CLI rather than editing them with kubectl.

And this is the Flux Kustomization:

apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: cluster-config-apps
  namespace: cluster-config
  labels:
    clusterconfig.azure.com/is-managed: "true"
    clusterconfig.azure.com/name: cluster-config
    clusterconfig.azure.com/namespace: cluster-config
  ownerReferences:
  - apiVersion: clusterconfig.azure.com/v1alpha1
    blockOwnerDeletion: true
    controller: true
    kind: FluxConfig
    name: cluster-config
spec:
  force: false
  interval: 1m0s
  patches:
  - patch: |2

      - op:   add
        path: /spec/chart/spec/sourceRef
        value: {"name": "cluster-config", "namespace": "cluster-config", "kind": "GitRepository"}
    target:
      annotationSelector: clusterconfig.azure.com/use-managed-source=true
      group: helm.toolkit.fluxcd.io
      kind: HelmRelease
  - patch: |2

      - op:   add
        path: /spec/sourceRef
        value: {"name": "cluster-config", "namespace": "cluster-config", "kind": "GitRepository"}
    target:
      annotationSelector: clusterconfig.azure.com/use-managed-source=true
      group: kustomize.toolkit.fluxcd.io
      kind: Kustomization
  path: ./AKS-Flux-Extension/gitops
  postBuild: {}
  prune: true
  retryInterval: 30s
  serviceAccountName: flux-applier
  sourceRef:
    kind: GitRepository
    name: cluster-config
    namespace: cluster-config
  timeout: 5m0s
  wait: true
status:
  conditions:
  - message: 'Applied revision: main@sha1:c2eb9b3652060eee334b67b2f72356e1ba5868b7'
    reason: ReconciliationSucceeded
    status: "True"
    type: Ready
  - message: Health check passed in 71.838908ms
    reason: Succeeded
    status: "True"
    type: Healthy
  inventory:
    entries:
    - id: _flux-demo__Namespace
      v: v1
    - id: flux-demo_podinfo__Service
      v: v1
    - id: flux-demo_podinfo_apps_Deployment
      v: v1
  lastAppliedRevision: main@sha1:c2eb9b3652060eee334b67b2f72356e1ba5868b7

A few things stand out here:

  • path, interval, retryInterval, timeout and prune map straight to the kustomizations block in Terraform (prune: true is garbage_collection_enabled = true).
  • wait: true means Flux waits for the applied resources to become healthy, which is where the Healthy condition comes from. A Ready=True Kustomization therefore tells you more than “the YAML was applied”.
  • serviceAccountName: flux-applier means Flux applies the manifests as that service account rather than as itself. With scope = "cluster", I checked and this service account is bound to the cluster-admin ClusterRole (flux-applier-binding). That is what makes cluster scope powerful, and it is why I wrote earlier that only trusted people should be able to merge to the tracked branch.
  • status.inventory is the list of objects Flux owns and will prune if they disappear from Git. For the sample app that is the namespace, the Service, and the Deployment.
  • The two patches are added by Azure, not by me. From reading them, they redirect a HelmRelease or a nested Flux Kustomization that carries the clusterconfig.azure.com/use-managed-source=true annotation to this same managed GitRepository. I expect that to matter when we get to Helm, so I will come back to it.

Check the app

Now check the workload itself:

kubectl wait --for=create deployment/podinfo -n flux-demo --timeout=600s
kubectl rollout status deployment/podinfo -n flux-demo --timeout=300s
kubectl port-forward -n flux-demo service/podinfo 9898:9898

Open http://localhost:9898 while the port-forward is running:

Podinfo UI showing the message Hello from AKS Flux GitOps, served by a Podinfo pod, version 6.7.1

You can also use curl http://localhost:9898/ | jq from a second terminal and you will get JSON with the same message.

Blank page in Brave? The Podinfo UI loads Vue and Vuetify from a CDN, and Brave Shields can block those scripts. That is a browser issue and not a Flux or Kubernetes problem. Allow the scripts for localhost, or try another browser.

At this point we did something worth stopping to appreciate: we never ran kubectl apply for the app. The namespace, Deployment, and Service all came from Git.


Try It: A Change Through Git

Time to see GitOps work. In your fork, change the message or the replica count, commit, and push to the same branch Terraform points at.

For example, in gitops/apps/kustomization.yaml, change count: 3 to count: 4, then push:

git add gitops/apps/kustomization.yaml
git commit -m "Scale podinfo to 4 replicas"
git push

Then watch the cluster:

kubectl get deployment podinfo -n flux-demo --watch

Within a few reconciliation intervals the Deployment should move to 4 replicas. There is no second Terraform run, no pipeline, and no kubectl. If you track main and normally work through pull requests, remember that Flux only sees the change once it is merged.

If you want to be sure that the cluster has the commit you expect, look at the revision reported on the Flux Kustomization:

kubectl describe kustomizations.kustomize.toolkit.fluxcd.io -n cluster-config

Try It: Drift Correction

This is the part that changes how you think about the cluster. Make a manual change that disagrees with Git:

kubectl scale deployment/podinfo -n flux-demo --replicas=1
kubectl get deployment podinfo -n flux-demo --watch

Flux notices that the live state no longer matches Git and puts it back, usually within a few intervals. The cluster is not the source of truth, Git is. If you want a permanent change, commit it.

Here is my terminal during the test. The Deployment drops to 1 replica right after the kubectl scale, and then goes back to 3 on its own (the red box). I did not touch anything between those two states:

Terminal output of kubectl get deployment podinfo --watch. After scaling to 1 replica, the READY column changes from 1/1 to 1/3, UP-TO-DATE jumps to 3, and the Deployment reaches 3/3 again without any manual action

What the logs show

The watch tells you that Flux fixed it. The controller logs tell you what Flux did and when. Drift correction is done by the kustomize-controller, so look there. Each reconciliation logs the result of a server-side apply for every object, and I filter on configured because anything Flux actually had to change shows up with that word. Objects that already match Git are reported as unchanged:

kubectl logs -n flux-system deployment/kustomize-controller --since=30m | grep '"configured"'

This is the full sequence from my cluster, with the repeated controller fields removed to make it readable. The scale command ran at 12:05:54 UTC (from the Kubernetes event timestamps), between two normal reconciliations:

{"ts":"2026-10-31T12:05:19.714Z","msg":"server-side apply completed","Kustomization":{"name":"cluster-config-apps","namespace":"cluster-config"},"output":{"Deployment/flux-demo/podinfo":"unchanged","Namespace/flux-demo":"unchanged","Service/flux-demo/podinfo":"unchanged"},"revision":"main@sha1:c2eb9b3652060eee334b67b2f72356e1ba5868b7"}
{"ts":"2026-10-31T12:05:19.846Z","msg":"Reconciliation finished in 335.475433ms, next run in 1m0s"}

{"ts":"2026-10-31T12:06:18.239Z","msg":"server-side apply completed","Kustomization":{"name":"cluster-config-apps","namespace":"cluster-config"},"output":{"Deployment/flux-demo/podinfo":"configured","Namespace/flux-demo":"unchanged","Service/flux-demo/podinfo":"unchanged"},"revision":"main@sha1:c2eb9b3652060eee334b67b2f72356e1ba5868b7"}
{"ts":"2026-10-31T12:06:23.420Z","msg":"Reconciliation finished in 5.418177543s, next run in 1m0s"}

{"ts":"2026-10-31T12:07:25.699Z","msg":"server-side apply completed","Kustomization":{"name":"cluster-config-apps","namespace":"cluster-config"},"output":{"Deployment/flux-demo/podinfo":"unchanged","Namespace/flux-demo":"unchanged","Service/flux-demo/podinfo":"unchanged"},"revision":"main@sha1:c2eb9b3652060eee334b67b2f72356e1ba5868b7"}
{"ts":"2026-10-31T12:07:25.823Z","msg":"Reconciliation finished in 414.151768ms, next run in 1m0s"}

Read it as three steps:

  1. 12:05:19: everything is unchanged. This is the normal heartbeat. Flux applies the whole path every minute, even when nothing is wrong.
  2. 12:06:18: the Deployment is configured. Flux compared the live object with Git, found that replicas was 1 instead of 3, and applied Git’s version. The Namespace and Service were not touched. Note that the revision is the same commit as before. Nothing changed in Git, so this is drift correction and not a new deployment.
  3. 12:07:25: back to unchanged, because the cluster matches Git again.

Two details I noticed:

  • The reconciliation took 5.4 seconds instead of the usual 300 to 400 milliseconds. My guess is that this is wait: true on the Kustomization (see the YAML above): Flux waits for the Deployment to become healthy again before it reports success.
  • It was not instant. The scale happened at 12:05:54 and the fix landed at 12:06:18, about 24 seconds later. With a 60-second interval, the worst case is a bit over a minute.

Flux also records this as Kubernetes events, which is handy if you do not have access to the controller logs:

kubectl get events -n cluster-config --sort-by=.lastTimestamp
kubectl get events -n flux-demo --sort-by=.lastTimestamp
# namespace: cluster-config
Normal  Progressing   kustomization/cluster-config-apps   Deployment/flux-demo/podinfo configured
# namespace: flux-demo
Normal  ScalingReplicaSet  deployment/podinfo   Scaled up replica set podinfo-6d884f55cc from 1 to 3
Normal  SuccessfulCreate   replicaset/podinfo-6d884f55cc   Created pod: podinfo-6d884f55cc-hwjnr
Normal  SuccessfulCreate   replicaset/podinfo-6d884f55cc   Created pod: podinfo-6d884f55cc-rs856
Warning Unhealthy          pod/podinfo-6d884f55cc-hwjnr    Readiness probe failed: ... connect: connection refused

The first event is Flux (Progressing ... configured) and the rest are Kubernetes reacting to it. The Unhealthy warnings are the readiness probe firing while the two new pods start, and they stop once the pods are ready. They are expected here, and not a sign that the correction failed.

Rolling back works the same way. Revert the commit, push, and Flux reconciles to the reverted state. Your rollback has the same audit trail as any other change.

A word of caution: only try this on a Deployment that is not controlled by an HPA or KEDA, or the two controllers will argue with each other. We will get to that interaction in a later post.

And since pruning is on, remember that deleting something from Git deletes it from the cluster. That is great for tidy-ups and dangerous for namespaces, because removing a Namespace removes everything in it. Use disposable resources when you experiment.


When It Does Not Work

Terraform being green and the app being missing is the most common situation, so here is a checklist to start from:

SymptomWhat to check
Source is not readyThe repo URL, that the branch is actually published, and outbound connectivity to Git.
Path not foundgitops_path is relative to the repository root.
Namespace or prerequisite missingInclude it in the Git path, or create it before reconciliation.
Pods are PendingNode capacity, VM quota, and scheduling events.
ImagePullBackOffRegistry connectivity, the image tag, and pull credentials.
Non-compliant in Azure at firstWait a couple of minutes, then check the source and Kustomization errors.

These commands usually tell you the story:

kubectl describe gitrepositories.source.toolkit.fluxcd.io -n cluster-config
kubectl describe kustomizations.kustomize.toolkit.fluxcd.io -n cluster-config
kubectl logs -n flux-system deployment/source-controller --tail=100
kubectl logs -n flux-system deployment/kustomize-controller --tail=100
kubectl get events -n flux-demo --sort-by=.lastTimestamp

If your network is locked down, remember that the Azure agents need outbound HTTPS to management.azure.com, <region>.dp.kubernetesconfiguration.azure.com, login.microsoftonline.com, and mcr.microsoft.com, in addition to the normal AKS egress requirements.


Production Notes

This is a learning environment, not a landing zone. Before you copy the pattern, consider at least these differences:

  • A private or restricted API server and Entra ID with Azure RBAC. The demo uses a public API and local kubeconfig to keep things simple.
  • Private Git with proper authentication. Public repos need no credentials, which is why the demo works at all. Private repos need extra configuration. Never put tokens in URLs, manifests, or committed tfvars.
  • Protected branches and reviews. With cluster scope, whoever can merge to the tracked branch can change anything in the cluster.
  • Pinned image digests. Image automation is not enabled here, so version changes must be committed to Git. That is a sensible default.
  • Namespace-scoped configurations for teams that share a cluster.
  • A remote Terraform backend. The local state file is sensitive. It is gitignored, but it is still sensitive.

Clean Up

The control plane is on the Free tier, but the worker VMs and disks still cost money. When you are done:

bash cleanup.sh
unset KUBECONFIG

You will be asked to type destroy. Terraform removes the Flux configuration first, then the extension, then the cluster. If your Git path creates anything outside the demo resource groups, like public IPs or retained disks, check for leftovers afterwards, because Terraform does not know about those.


What Is Next

We now have a repeatable foundation: a cluster, managed Flux, a Git source, and proof that changes and drift both go through Git. In the next posts I will build on this cluster and cover:

  • Deploying applications with Helm using HelmRepository and HelmRelease.
  • Splitting infrastructure and apps and ordering them with dependencies.
  • Handling multiple environments from one repository.
  • Other Flux concepts that come up once you run this for real.

I will update this list as the series takes shape. If there is a Flux topic on AKS you would like me to cover, let me know.


References

Found this helpful?
Back to all posts