Kubernetes
14 min readMay 5, 2026

Flux CD: GitOps for Kubernetes with Source Controller and Kustomize

Flux (CNCF Graduated) implements GitOps for Kubernetes: a set of controllers that continuously reconcile the cluster state against declarations in a Git repository. Unlike Argo CD's UI-first approach, Flux is entirely Kubernetes-native — every configuration is a CRD, deployable via the same GitOps workflow it manages.

AJ
Ajeet Yadav
Platform & Cloud Engineer
Flux CD: GitOps for Kubernetes with Source Controller and Kustomize

GitOps inverts the deployment model: instead of a CI pipeline pushing changes to the cluster, the cluster continuously pulls from Git and reconciles itself. Flux implements this with a set of controllers that watch Git repositories, Helm repositories, and OCI registries — and apply changes as soon as they're committed, without any external push mechanism.

The practical difference: there are no kubectl apply commands in your CI pipeline. The pipeline builds an image, pushes it to a registry, and commits a version bump to Git. Flux does the rest.


Flux Architecture

Flux is composed of five controllers deployed as a system:

ControllerResponsibility
source-controllerFetches and caches artifacts from Git, Helm repos, OCI registries, S3
kustomize-controllerApplies Kubernetes manifests (Kustomize or plain YAML) from source artifacts
helm-controllerManages Helm releases from HelmRelease objects
notification-controllerRoutes events from controllers to Slack, GitHub, PagerDuty, webhooks
image-automation-controllerScans image registries and commits version bumps to Git

Each controller is independently reconciling. GitRepository just fetches — it doesn't apply. Kustomization references a GitRepository and applies. This separation means you can have multiple Kustomization objects pointing at the same GitRepository for different paths or namespaces.


Bootstrap

Flux bootstrap installs the controllers and sets up the GitOps loop in a single command:

bash
flux bootstrap github \
  --owner=my-org \
  --repository=fleet-infra \
  --branch=main \
  --path=clusters/production \
  --personal   # --personal = owner is a user account (not an org); add --token-auth to authenticate via HTTPS PAT instead of SSH key

Bootstrap:

  1. Creates the flux-system namespace and installs the controllers via manifests
  2. Creates a GitRepository object pointing at the repo
  3. Creates a Kustomization object pointing at clusters/production/ in that repo
  4. Commits the generated manifests to the repo (so the cluster reconciles itself to match)

For EKS with AWS CodeCommit or when you don't want GitHub-specific tooling:

bash
flux bootstrap git \
  --url=https://git-codecommit.us-east-1.amazonaws.com/v1/repos/fleet-infra \
  --branch=main \
  --path=clusters/production

GitRepository

GitRepository defines a source — Flux polls it and caches the fetched artifact:

yaml
1apiVersion: source.toolkit.fluxcd.io/v1
2kind: GitRepository
3metadata:
4  name: fleet-infra
5  namespace: flux-system
6spec:
7  interval: 1m        # Poll every minute
8  url: https://github.com/my-org/fleet-infra
9  ref:
10    branch: main
11  secretRef:
12    name: flux-system   # SSH key or HTTPS credentials

For a monorepo with multiple teams, a single GitRepository can be shared by multiple Kustomization objects — each watching a different subdirectory.


Kustomization

Kustomization in Flux (not the same as kustomize.config.k8s.io/v1beta1) is a reconciliation unit — it watches a source and applies a path from it:

yaml
1apiVersion: kustomize.toolkit.fluxcd.io/v1
2kind: Kustomization
3metadata:
4  name: payments-api
5  namespace: flux-system
6spec:
7  interval: 5m       # Reconcile every 5 minutes (drift correction)
8  path: "./apps/payments-api"
9  sourceRef:
10    kind: GitRepository
11    name: fleet-infra
12  targetNamespace: production
13  prune: true        # Delete resources removed from Git
14  healthChecks:
15    - apiVersion: apps/v1
16      kind: Deployment
17      name: payments-api
18      namespace: production
19  timeout: 2m
20  retryInterval: 30s
21  # Force reconciliation even if no Git change (useful for config drift correction)
22  force: false
23  # Post-build variable substitution (replaces ${VAR} in manifests)
24  postBuild:
25    substituteFrom:
26      - kind: ConfigMap
27        name: cluster-config
28      - kind: Secret
29        name: cluster-secrets
30        optional: true

prune: true is GitOps correctness — if you delete a YAML file from the repo, Flux deletes the corresponding resource from the cluster. Without it, deleted manifests leave orphan resources.

healthChecks make Flux wait for the reconciled resources to become healthy before marking the Kustomization as Ready. This enables dependency ordering between Kustomizations.

Dependency Ordering

yaml
1# cert-manager must be healthy before apps that depend on Certificate resources
2apiVersion: kustomize.toolkit.fluxcd.io/v1
3kind: Kustomization
4metadata:
5  name: apps
6  namespace: flux-system
7spec:
8  dependsOn:
9    - name: cert-manager    # Wait for cert-manager Kustomization to be Ready
10    - name: external-dns
11  path: "./apps"
12  sourceRef:
13    kind: GitRepository
14    name: fleet-infra

HelmRelease

HelmRelease manages a Helm chart deployment — equivalent to helm install / helm upgrade in a GitOps loop:

yaml
1apiVersion: source.toolkit.fluxcd.io/v1
2kind: HelmRepository
3metadata:
4  name: prometheus-community
5  namespace: flux-system
6spec:
7  interval: 1h
8  url: https://prometheus-community.github.io/helm-charts
9---
10apiVersion: helm.toolkit.fluxcd.io/v2
11kind: HelmRelease
12metadata:
13  name: kube-prometheus-stack
14  namespace: monitoring
15spec:
16  interval: 1h
17  chart:
18    spec:
19      chart: kube-prometheus-stack
20      version: ">=65.0.0 <66.0.0"    # SemVer range — Flux picks latest matching
21      sourceRef:
22        kind: HelmRepository
23        name: prometheus-community
24        namespace: flux-system
25  values:
26    prometheus:
27      prometheusSpec:
28        retention: 30d
29        storageSpec:
30          volumeClaimTemplate:
31            spec:
32              storageClassName: gp3
33              resources:
34                requests:
35                  storage: 50Gi
36  # Override values from a ConfigMap or Secret (for cluster-specific config)
37  valuesFrom:
38    - kind: ConfigMap
39      name: prometheus-values-override
40      valuesKey: values.yaml
41    - kind: Secret
42      name: alertmanager-slack-webhook
43      valuesKey: slack_webhook_url
44      targetPath: alertmanager.config.global.slack_api_url
45  install:
46    crds: CreateReplace   # Manage CRDs during install
47  upgrade:
48    crds: CreateReplace
49    remediation:
50      retries: 3          # Retry failed upgrades
51  rollback:
52    timeout: 5m
53    cleanupOnFail: true

OCI Helm Repository

For Helm charts stored in OCI registries (common for private charts or AWS ECR):

yaml
1apiVersion: source.toolkit.fluxcd.io/v1
2kind: HelmRepository
3metadata:
4  name: karpenter
5  namespace: flux-system
6spec:
7  type: oci
8  url: oci://public.ecr.aws/karpenter
9  interval: 1h

Multi-Tenancy with Flux

The recommended Flux multi-tenancy model gives each team their own namespace with scoped permissions:

yaml
1# Platform team manages the tenancy itself
2# apps/tenants/payments-team/kustomization.yaml
3apiVersion: kustomize.toolkit.fluxcd.io/v1
4kind: Kustomization
5metadata:
6  name: payments-team
7  namespace: flux-system
8spec:
9  interval: 5m
10  path: "./tenants/payments"
11  sourceRef:
12    kind: GitRepository
13    name: fleet-infra
14  serviceAccountName: payments-reconciler   # Run reconciliation as a restricted SA
15  targetNamespace: payments-production
16  prune: true
yaml
1# ServiceAccount with limited RBAC — team can only deploy in their namespace
2apiVersion: v1
3kind: ServiceAccount
4metadata:
5  name: payments-reconciler
6  namespace: flux-system
7---
8apiVersion: rbac.authorization.k8s.io/v1
9kind: RoleBinding
10metadata:
11  name: payments-reconciler
12  namespace: payments-production
13subjects:
14  - kind: ServiceAccount
15    name: payments-reconciler
16    namespace: flux-system
17roleRef:
18  kind: ClusterRole
19  name: edit   # Use 'edit' not 'cluster-admin' — edit grants full namespace access without exposing all Secrets cluster-wide
20  apiGroup: rbac.authorization.k8s.io

The serviceAccountName field in Kustomization means the controller impersonates that SA during reconciliation — the team's Flux instance can only create/update resources in their namespace.


Image Automation

Flux can scan an image registry and automatically commit version bumps to Git. For the CI pipeline that produces the images Flux watches, see GitHub Actions CI/CD for Kubernetes.

yaml
1apiVersion: image.toolkit.fluxcd.io/v1beta2
2kind: ImageRepository
3metadata:
4  name: payments-api
5  namespace: flux-system
6spec:
7  image: 123456789.dkr.ecr.us-east-1.amazonaws.com/payments-api
8  interval: 5m
9  secretRef:
10    name: ecr-credentials
11---
12apiVersion: image.toolkit.fluxcd.io/v1beta2
13kind: ImagePolicy
14metadata:
15  name: payments-api
16  namespace: flux-system
17spec:
18  imageRepositoryRef:
19    name: payments-api
20  policy:
21    semver:
22      range: ">=1.0.0"    # Only stable versions (no pre-release tags)

In the deployment manifest, mark the image tag for automation:

yaml
# apps/payments-api/deployment.yaml
containers:
  - name: api
    image: 123456789.dkr.ecr.us-east-1.amazonaws.com/payments-api:1.4.2  # {"$imagepolicy": "flux-system:payments-api"}
yaml
1apiVersion: image.toolkit.fluxcd.io/v1beta2   # v1beta2 — v1beta1 is removed in Flux 2.3+
2kind: ImageUpdateAutomation
3metadata:
4  name: flux-system
5  namespace: flux-system
6spec:
7  interval: 5m
8  sourceRef:
9    kind: GitRepository
10    name: fleet-infra
11  git:
12    commit:
13      author:
14        name: Flux
15        email: flux@codingprotocols.com
16      messageTemplate: "chore: update {{range .Updated.Images}}{{.}}{{end}}"
17    push:
18      branch: main
19  update:
20    path: "./apps"
21    strategy: Setters    # Updates files marked with {"$imagepolicy": ...} comments

Flux vs Argo CD

AspectFluxArgo CD
ArchitectureControllers as CRDs, no UIControllers + UI/API server
ConfigurationKubernetes resources (CRDs)mix of CRDs and UI/CLI
Multi-tenancyNative (serviceAccountName per Kustomization)AppProject-based
Image automationNative (image-automation-controller)External (Argo CD Image Updater, separate tool)
Multi-sourceMultiple sources via spec.sourceRefs (v2.3+)Multi-source Applications (v2.6+)
CNCF statusGraduatedGraduated
UIOptional (Weave GitOps)Built-in

Both are mature, production-proven tools. Flux's lack of a built-in UI is a feature for teams that want fully declarative configuration — there's no UI state to diverge from Git. Argo CD's UI is valuable for teams that need visual diff and manual sync approval workflows.


Frequently Asked Questions

How do I force an immediate reconciliation?

bash
1# Reconcile a specific Kustomization immediately
2flux reconcile kustomization payments-api
3
4# Reconcile including pulling latest from Git first
5flux reconcile kustomization payments-api --with-source
6
7# Suspend and resume reconciliation (for maintenance)
8flux suspend kustomization payments-api
9flux resume kustomization payments-api

How do I debug a stuck HelmRelease?

bash
1# Check HelmRelease status
2flux get helmrelease kube-prometheus-stack -n monitoring
3
4# Get full event history
5kubectl describe helmrelease kube-prometheus-stack -n monitoring
6
7# Get Helm history (what Helm knows about this release)
8helm history kube-prometheus-stack -n monitoring

How do I manage Flux's own configuration in GitOps?

Flux bootstraps itself into the clusters/<name>/flux-system/ directory in the repo. Any changes to Flux's own configuration (updating controller versions, adding new source controllers) are done by modifying files in that directory and pushing to Git. Flux reconciles its own controllers via the bootstrap Kustomization.


Variable Substitution

Flux can substitute variables in manifests using postBuild.substituteFrom, letting you keep environment-specific values in a ConfigMap or Secret rather than duplicating them across manifests:

yaml
1apiVersion: kustomize.toolkit.fluxcd.io/v1
2kind: Kustomization
3metadata:
4  name: apps
5spec:
6  postBuild:
7    substituteFrom:
8      - kind: ConfigMap
9        name: cluster-vars    # Non-sensitive cluster-level variables
10      - kind: Secret
11        name: cluster-secrets # Sensitive values (stored encrypted or via ESO)
yaml
1# cluster-vars ConfigMap
2apiVersion: v1
3kind: ConfigMap
4metadata:
5  name: cluster-vars
6  namespace: flux-system
7data:
8  CLUSTER_NAME: production
9  AWS_REGION: us-east-1
10  DOMAIN: myapp.example.com

In manifests, use ${VARIABLE_NAME}:

yaml
1apiVersion: networking.k8s.io/v1
2kind: Ingress
3metadata:
4  name: api
5spec:
6  rules:
7    - host: api.${DOMAIN}

Secrets: SOPS + Age Encryption

Flux integrates with SOPS for encrypted secrets in Git — the age private key lives only in the cluster, so Git/GitHub never sees plaintext values:

bash
1# Generate age key pair
2age-keygen -o age.agekey
3
4# Create Kubernetes secret from the private key
5kubectl create secret generic sops-age-key \
6  --from-file=age.agekey \
7  -n flux-system
8
9# Encrypt a secret file
10sops --age=$(cat age.agekey | grep "public key" | awk '{print $4}') \
11  --encrypt \
12  --encrypted-regex '^(data|stringData)$' \
13  secret.yaml > secret.sops.yaml

Reference the decryption provider in the Kustomization:

yaml
spec:
  decryption:
    provider: sops
    secretRef:
      name: sops-age-key

Encrypted secrets committed to Git are decrypted by the Flux decryption provider during reconciliation. For production, consider using External Secrets Operator alongside Flux rather than SOPS — ESO syncs from AWS SSM/Secrets Manager/Vault, reducing the need to commit any secret material to Git.


Monitoring Flux

bash
1# Check all Flux resources
2flux get all -A
3
4# Watch for reconciliation errors
5flux logs --all-namespaces --level=error --follow
6
7# Reconcile immediately (don't wait for interval)
8flux reconcile source git fleet-infra
9flux reconcile kustomization apps

Prometheus metrics from Flux controllers:

yaml
# Key metrics to alert on:
# gotk_reconcile_error_total — non-zero means reconciliation is failing
# gotk_reconcile_duration_seconds — reconciliation latency
# gotk_suspend_status — 1 means a Flux resource is suspended (may be intentional or forgotten)
yaml
1# Prometheus alert rule
2- alert: FluxReconciliationFailed
3  expr: gotk_reconcile_error_total > 0
4  for: 5m
5  labels:
6    severity: warning
7  annotations:
8    summary: "Flux reconciliation failing for {{ $labels.name }}"

For Argo CD-based GitOps with multi-cluster ApplicationSets, see Argo CD ApplicationSet: Multi-Cluster Deployments. For External Secrets Operator managed via Flux for GitOps secret workflows, see External Secrets Operator for Kubernetes. For Argo CD as the alternative GitOps controller (UI-first, ApplicationSet, image updater), see Argo CD: GitOps Continuous Delivery for Kubernetes.

Setting up a Flux-based GitOps pipeline for a Kubernetes platform? Talk to us at Coding Protocols — we help platform teams design GitOps workflows that enforce cluster state consistency and enable self-service deployments for development teams.

Related Topics

Flux
GitOps
Kubernetes
Kustomize
Helm
Platform Engineering
CNCF
CI/CD

Read Next