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.

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:
| Controller | Responsibility |
|---|---|
| source-controller | Fetches and caches artifacts from Git, Helm repos, OCI registries, S3 |
| kustomize-controller | Applies Kubernetes manifests (Kustomize or plain YAML) from source artifacts |
| helm-controller | Manages Helm releases from HelmRelease objects |
| notification-controller | Routes events from controllers to Slack, GitHub, PagerDuty, webhooks |
| image-automation-controller | Scans 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:
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 keyBootstrap:
- Creates the
flux-systemnamespace and installs the controllers via manifests - Creates a
GitRepositoryobject pointing at the repo - Creates a
Kustomizationobject pointing atclusters/production/in that repo - 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:
flux bootstrap git \
--url=https://git-codecommit.us-east-1.amazonaws.com/v1/repos/fleet-infra \
--branch=main \
--path=clusters/productionGitRepository
GitRepository defines a source — Flux polls it and caches the fetched artifact:
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 credentialsFor 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:
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: trueprune: 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
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-infraHelmRelease
HelmRelease manages a Helm chart deployment — equivalent to helm install / helm upgrade in a GitOps loop:
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: trueOCI Helm Repository
For Helm charts stored in OCI registries (common for private charts or AWS ECR):
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: 1hMulti-Tenancy with Flux
The recommended Flux multi-tenancy model gives each team their own namespace with scoped permissions:
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: true1# 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.ioThe 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.
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:
# 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"}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": ...} commentsFlux vs Argo CD
| Aspect | Flux | Argo CD |
|---|---|---|
| Architecture | Controllers as CRDs, no UI | Controllers + UI/API server |
| Configuration | Kubernetes resources (CRDs) | mix of CRDs and UI/CLI |
| Multi-tenancy | Native (serviceAccountName per Kustomization) | AppProject-based |
| Image automation | Native (image-automation-controller) | External (Argo CD Image Updater, separate tool) |
| Multi-source | Multiple sources via spec.sourceRefs (v2.3+) | Multi-source Applications (v2.6+) |
| CNCF status | Graduated | Graduated |
| UI | Optional (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?
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-apiHow do I debug a stuck HelmRelease?
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 monitoringHow 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:
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)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.comIn manifests, use ${VARIABLE_NAME}:
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:
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.yamlReference the decryption provider in the Kustomization:
spec:
decryption:
provider: sops
secretRef:
name: sops-age-keyEncrypted 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
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 appsPrometheus metrics from Flux controllers:
# 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)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.


