How-to Guides¶
Scope
Task recipes for Flux 2.9: install the CLI, bootstrap with the CLI or the Flux Operator, upgrade across removed APIs, deliver from Git and OCI, run Helm releases, automate image updates, decrypt SOPS secrets, lock down multi-tenancy, template apps with ResourceSets, handle monorepos, wire webhooks and alerts, and troubleshoot. Commands were checked against the flux2 v2.9.5 CLI source. For API versions and flags see the Reference; for the why, see the Explanation.
Install the Flux CLI¶
# Homebrew (macOS/Linux)
brew install fluxcd/tap/flux
# Install script (macOS/Linux)
curl -s https://fluxcd.io/install.sh | sudo bash
# Container image with kubectl + flux
docker run --rm ghcr.io/fluxcd/flux-cli:v2.9.5 version --client
# Check the cluster meets the prerequisites and whether a newer Flux exists
flux check --pre
Other channels: mise use -g flux2@latest, yay -S flux-bin, nix-env -i fluxcd, choco install flux (Flux installation). The CLI is supported within one minor version of the controllers.
Bootstrap Flux¶
Bootstrap with the Flux CLI (GitHub)¶
flux bootstrap creates or reuses the repository, commits the Flux manifests under --path, installs the controllers, and creates the root flux-system GitRepository and Kustomization.
export GITHUB_TOKEN=<personal-access-token>
# Repository owned by an organization
flux bootstrap github \
--owner=my-org \
--repository=fleet-infra \
--branch=main \
--path=clusters/production
# Repository on a personal account
flux bootstrap github \
--owner=my-user \
--repository=fleet-infra \
--branch=main \
--path=clusters/production \
--personal
# Include image automation and source-watcher (ArtifactGenerator)
flux bootstrap github \
--owner=my-org --repository=fleet-infra --path=clusters/production \
--components-extra=image-reflector-controller,image-automation-controller,source-watcher
flux check
Equivalent subcommands exist for gitlab, gitea, bitbucket-server, and generic git. Since 2.9 you can also bootstrap onto AWS CodeCommit with Workload Identity and sign bootstrap commits with SSH keys.
Bootstrap with the Flux Operator¶
The Flux Operator replaces the bootstrap commit loop with a declarative FluxInstance. Use it when you manage fleets, want automated upgrades, or want the Flux Web UI.
helm install flux-operator oci://ghcr.io/controlplaneio-fluxcd/charts/flux-operator \
--namespace flux-system --create-namespace
# Credentials for a private repository
flux create secret git flux-system \
--url=https://github.com/my-org/fleet-infra.git \
--username=git \
--password=$GITHUB_TOKEN
apiVersion: fluxcd.controlplane.io/v1
kind: FluxInstance
metadata:
name: flux # must be named flux
namespace: flux-system
spec:
distribution:
version: "2.x" # track the latest 2.x release
registry: "ghcr.io/fluxcd"
components:
- source-controller
- kustomize-controller
- helm-controller
- notification-controller
cluster:
type: kubernetes
multitenant: false
networkPolicy: true
sync:
kind: GitRepository
url: "https://github.com/my-org/fleet-infra.git"
ref: "refs/heads/main"
path: "clusters/production"
pullSecret: "flux-system"
kubectl apply -f flux-instance.yaml
kubectl -n flux-system get fluxreport/flux -o yaml # health, versions, sync status
kubectl -n flux-system port-forward svc/flux-operator 9080:9080 # Flux Web UI
Existing bootstrapped clusters can be migrated; follow the operator's migration guide.
Upgrade Flux¶
Migrate removed APIs first
Flux 2.8 removed source/v1beta2, kustomize/v1beta2 and helm/v2beta2; Flux 2.9 removed image/v1beta2 and notification/v1beta2. Objects stored with those versions block the upgrade. Follow Upgrade Procedure for Flux v2.7+.
# 1. Rewrite manifests in Git to the latest API versions
flux migrate -f . --extensions=.yml,.yaml,.tpl
git commit -am "Migrate Flux APIs" && git push
# 2. Migrate objects stored in etcd (needs cluster-admin)
flux migrate
# 3a. Bootstrapped cluster: re-run bootstrap with the new CLI
flux bootstrap github --owner=my-org --repository=fleet-infra --path=clusters/production
# 3b. GitOps-only upgrade: regenerate the components file and let Flux apply it
flux install --export > ./clusters/production/flux-system/gotk-components.yaml
git commit -am "Update to $(flux -v)" && git push
flux reconcile ks flux-system --with-source
# 4. Verify
flux check
With the Flux Operator, upgrade the operator (Helm chart, or a ResourceSet that tracks it) and the FluxInstance picks up the new 2.x release on its own. Review breaking changes before 2.9: the default Helm post-render strategy changed from nohooks to combined, and GCR Receivers now need email and audience in their Secret.
Deliver from Git¶
# Git source (public or with --secret-ref)
flux create source git podinfo \
--url=https://github.com/stefanprodan/podinfo \
--branch=master \
--interval=1m
# Kustomization that applies a path from that source
flux create kustomization podinfo \
--source=GitRepository/podinfo \
--path="./kustomize" \
--prune=true \
--wait=true \
--interval=60m \
--retry-interval=2m \
--health-check-timeout=3m \
--target-namespace=default
# Export instead of applying, to commit the YAML to Git
flux create kustomization podinfo --source=GitRepository/podinfo --path="./kustomize" \
--prune=true --interval=60m --export > podinfo-ks.yaml
Authenticate with a GitHub App¶
flux create secret githubapp ghapp-secret \
--app-id=1 \
--app-installation-owner=my-org \
--app-private-key=~/private-key.pem
Then set spec.provider: github and spec.secretRef.name: ghapp-secret on the GitRepository.
Order Deployments with Dependencies¶
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: apps
namespace: flux-system
spec:
interval: 60m
retryInterval: 2m
timeout: 5m
prune: true
wait: true
sourceRef:
kind: GitRepository
name: flux-system
path: ./apps/production
dependsOn:
- name: infrastructure # wait until infrastructure is Ready
Deliver from OCI Artifacts¶
Push manifests from CI as an OCI artifact, sign it, and pull it in the cluster. No Git credentials are needed in the cluster.
# In CI: push, tag, and sign
flux push artifact oci://ghcr.io/my-org/manifests/app:$(git rev-parse --short HEAD) \
--path="./deploy" \
--source="$(git config --get remote.origin.url)" \
--revision="$(git branch --show-current)@sha1:$(git rev-parse HEAD)"
flux tag artifact oci://ghcr.io/my-org/manifests/app:$(git rev-parse --short HEAD) --tag=latest
cosign sign --yes ghcr.io/my-org/manifests/app@<digest>
apiVersion: source.toolkit.fluxcd.io/v1
kind: OCIRepository
metadata:
name: app
namespace: flux-system
spec:
interval: 5m
url: oci://ghcr.io/my-org/manifests/app
ref:
tag: latest
verify:
provider: cosign
matchOIDCIdentity:
- issuer: "^https://token.actions.githubusercontent.com$"
subject: "^https://github.com/my-org/app.*$"
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: app
namespace: flux-system
spec:
interval: 60m
prune: true
sourceRef:
kind: OCIRepository
name: app
path: ./
# CLI equivalent for a public artifact, pinned by semver
flux create source oci podinfo \
--url=oci://ghcr.io/stefanprodan/manifests/podinfo \
--tag-semver=">=6.0.0" \
--interval=10m
Manage Helm Releases¶
Prefer OCI charts referenced with chartRef. The HelmRelease upgrades automatically when the chart digest changes.
apiVersion: source.toolkit.fluxcd.io/v1
kind: OCIRepository
metadata:
name: podinfo
namespace: apps
spec:
interval: 10m
url: oci://ghcr.io/stefanprodan/charts/podinfo
ref:
semver: ">=6.0.0"
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: podinfo
namespace: apps
spec:
interval: 30m
chartRef:
kind: OCIRepository
name: podinfo
install:
remediation:
retries: 3
upgrade:
remediation:
retries: 3
remediateLastFailure: true
driftDetection:
mode: enabled
ignore:
- paths: ["/spec/replicas"]
target:
kind: Deployment
valuesFrom:
- kind: ConfigMap
name: podinfo-values
values:
replicaCount: 2
Classic HTTP Helm repositories still work with HelmRepository + spec.chart.spec:
flux create source helm podinfo \
--url=https://stefanprodan.github.io/podinfo \
--interval=1h
flux create helmrelease podinfo \
--source=HelmRepository/podinfo \
--chart=podinfo \
--chart-version=">=6.0.0" \
--target-namespace=apps \
--create-target-namespace \
--values=./values.yaml
Helm v4 behaviour (Flux 2.8+)
New releases use server-side apply and kstatus health checks. Existing releases keep client-side apply until you opt in. To keep Helm 3 behaviour everywhere, start helm-controller with --feature-gates=UseHelm3Defaults=true. If a chart's hooks break under the 2.9 default post-render strategy, set spec.postRenderStrategy: nohooks.
flux reconcile hr podinfo -n apps --with-source # fetch chart and upgrade now
flux reconcile hr podinfo -n apps --force # one-off forced upgrade
flux reconcile hr podinfo -n apps --reset # reset exhausted remediation retries
flux debug hr podinfo -n apps --show-values # final merged values
Automate Image Updates¶
Requires the image controllers (--components-extra=image-reflector-controller,image-automation-controller). The APIs are GA (image.toolkit.fluxcd.io/v1) since Flux 2.7; v1beta2 was removed in 2.9.
apiVersion: image.toolkit.fluxcd.io/v1
kind: ImageRepository
metadata:
name: podinfo
namespace: flux-system
spec:
image: ghcr.io/stefanprodan/podinfo
interval: 5m
---
apiVersion: image.toolkit.fluxcd.io/v1
kind: ImagePolicy
metadata:
name: podinfo
namespace: flux-system
spec:
imageRepositoryRef:
name: podinfo
policy:
semver:
range: ">=6.0.0 <7.0.0"
---
apiVersion: image.toolkit.fluxcd.io/v1
kind: ImageUpdateAutomation
metadata:
name: flux-system
namespace: flux-system
spec:
interval: 30m
sourceRef:
kind: GitRepository
name: flux-system
git:
checkout:
ref:
branch: main
commit:
author:
email: fluxcdbot@users.noreply.github.com
name: fluxcdbot
push:
branch: main
update:
path: ./clusters/production
strategy: Setters
Mark the field to update in the Deployment manifest:
flux create image repository podinfo --image=ghcr.io/stefanprodan/podinfo --interval=5m
flux create image policy podinfo --image-ref=podinfo --select-semver=">=6.0.0"
flux get images all -A
flux reconcile image repository podinfo
Decrypt Secrets with SOPS¶
This uses age keys and the per-Kustomization spec.decryption setting (Flux SOPS guide).
# 1. Generate an age key and store the private key in the cluster
age-keygen -o age.agekey
kubectl -n flux-system create secret generic sops-age \
--from-file=age.agekey=age.agekey
# 2. Encrypt only data fields, with the public key
sops --age=<age-public-key> \
--encrypt --encrypted-regex '^(data|stringData)$' \
--in-place clusters/production/secrets/db.yaml
# 3. Tell the Kustomization to decrypt
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: secrets
namespace: flux-system
spec:
interval: 60m
prune: true
sourceRef:
kind: GitRepository
name: flux-system
path: ./clusters/production/secrets
decryption:
provider: sops
secretRef:
name: sops-age
Key management
Anyone who can read sops-age can decrypt every secret in the repository. Restrict it with RBAC, keep a backup of the private key outside the cluster, and prefer cloud KMS with Workload Identity (spec.decryption.serviceAccountName, Flux 2.7+) or OpenBao/Vault Kubernetes auth (Flux 2.9+) so no key material lives in the cluster. Controller-global age decryption also exists (Flux 2.7+) if you want one key for all Kustomizations.
Lock Down Multi-Tenancy¶
Apply these patches in clusters/<name>/flux-system/kustomization.yaml before (or when re-running) bootstrap (Flux multi-tenancy):
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- gotk-components.yaml
- gotk-sync.yaml
patches:
- patch: |
- op: add
path: /spec/template/spec/containers/0/args/-
value: --no-cross-namespace-refs=true
target:
kind: Deployment
name: "(kustomize-controller|helm-controller|notification-controller|image-reflector-controller|image-automation-controller)"
- patch: |
- op: add
path: /spec/template/spec/containers/0/args/-
value: --no-remote-bases=true
target:
kind: Deployment
name: "kustomize-controller"
- patch: |
- op: add
path: /spec/template/spec/containers/0/args/-
value: --default-service-account=default
target:
kind: Deployment
name: "(kustomize-controller|helm-controller)"
- patch: |
- op: add
path: /spec/serviceAccountName
value: kustomize-controller
target:
kind: Kustomization
name: "flux-system"
Onboard a tenant (namespace, ServiceAccount, RoleBinding) and generate its manifests for Git:
flux create tenant team-a \
--with-namespace=team-a \
--cluster-role=admin \
--export > tenants/team-a/rbac.yaml
With the Flux Operator, set spec.cluster.multitenant: true on the FluxInstance instead of patching. A worked example lives in fluxcd/flux2-multi-tenancy.
Template Apps with ResourceSets¶
A ResourceSet (Flux Operator) stamps out one HelmRelease + OCIRepository per input. It fills the role of an Argo CD ApplicationSet.
apiVersion: fluxcd.controlplane.io/v1
kind: ResourceSet
metadata:
name: podinfo
namespace: default
spec:
inputs:
- tenant: "team1"
app:
version: "6.7.x"
replicas: 2
- tenant: "team2"
app:
version: "6.6.x"
replicas: 3
resources:
- apiVersion: source.toolkit.fluxcd.io/v1
kind: OCIRepository
metadata:
name: podinfo-<< inputs.tenant >>
namespace: default
spec:
interval: 10m
url: oci://ghcr.io/stefanprodan/charts/podinfo
ref:
semver: << inputs.app.version | quote >>
- apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: podinfo-<< inputs.tenant >>
namespace: default
spec:
interval: 1h
releaseName: podinfo-<< inputs.tenant >>
chartRef:
kind: OCIRepository
name: podinfo-<< inputs.tenant >>
values:
replicaCount: << inputs.app.replicas | int >>
kubectl apply -f podinfo-resourceset.yaml
kubectl wait resourceset/podinfo --for=condition=ready --timeout=5m
kubectl events --for resourceset/podinfo
For per-pull-request preview environments, pair it with a ResourceSetInputProvider (GitHub PR guide).
Let Other Controllers Own Fields¶
Stop Flux from reverting fields that an HPA or webhook manages (Flux 2.9+):
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: podinfo
namespace: flux-system
spec:
# ...
ignore:
- target:
kind: Deployment
name: podinfo
paths:
- "/spec/replicas"
Always scope rules with target; a rule without one applies to every object in the Kustomization. Escape / inside keys as ~1 (for example /metadata/annotations/external-dns.alpha.kubernetes.io~1hostname).
Handle a Monorepo¶
Pick the lightest fix that works:
-
Ignore paths to shrink the artifact (the clone is still full):
-
Sparse checkout so only the listed directories are fetched into the artifact:
spec.sparseCheckout: [deploy]. - Split sources per team or service: several
GitRepositoryobjects with narrowignorerules. -
Decompose with ArtifactGenerator (source-watcher) so each app gets its own
ExternalArtifactrevision and only changed apps reconcile:apiVersion: source.extensions.fluxcd.io/v1beta1 kind: ArtifactGenerator metadata: name: monorepo-apps namespace: flux-system spec: sources: - alias: monorepo kind: GitRepository name: monorepo pathPattern: "@monorepo/apps/{app}/envs/{env}" # directory discovery, Flux 2.9+ artifacts: - name: "{app}-{env}" copy: - from: "@monorepo/apps/{app}/envs/{env}/**" to: "@artifact/"Consume each artifact with a Kustomization whose
sourceRefiskind: ExternalArtifact, name: <app>-<env>. -
Build OCI artifacts in CI (see Deliver from OCI Artifacts); this is the most scalable pattern for very large repositories.
Set Up Webhooks and Alerts¶
Trigger reconciliation on push instead of waiting for the poll interval:
TOKEN=$(head -c 12 /dev/urandom | shasum | cut -d ' ' -f1)
kubectl -n flux-system create secret generic receiver-token --from-literal=token=$TOKEN
flux create receiver github-receiver \
--type=github \
--event=ping --event=push \
--secret-ref=receiver-token \
--resource=GitRepository/flux-system
flux get receivers # shows the webhook path to configure in GitHub
Expose the webhook-receiver Service (port 9292 on notification-controller) through an Ingress or Gateway. In CI, flux trigger receiver <name> --url=<receiver-url> --token=<token> calls a generic receiver without hand-crafting the request (Flux 2.9+).
Send failures to Slack:
kubectl -n flux-system create secret generic slack-token --from-literal=token=<bot-token>
flux create alert-provider slack --type=slack --channel=gitops-alerts --secret-ref=slack-token
flux create alert on-call --provider-ref=slack --event-severity=error \
--event-source='Kustomization/*' --event-source='HelmRelease/*'
Extend the CLI with Plugins¶
Flux 2.9 added a plugin system (RFC-0013). Plugins install to ~/fluxcd/plugins and run as flux <plugin>.
flux plugin search
flux plugin install schema@0.5.0 # pin a version, or @sha256:<digest> for CI
flux plugin install mirror
flux plugin list
flux plugin update schema
flux plugin uninstall schema
schema validates manifests against Kubernetes, OpenShift, Gateway API and Flux schemas plus CEL rules; mirror copies Helm charts, OCI artifacts and images between registries. ControlPlane publishes an operator plugin for Flux Operator resources.
Troubleshooting¶
# Overall status
flux check
flux get all -A
flux get sources git -A
flux get kustomizations -A --status-selector ready=false
flux get helmreleases -A
# Why did it fail?
flux events --for Kustomization/apps -n flux-system
flux logs --kind=Kustomization --name=apps --since=1h
flux debug ks apps --show-status
flux debug ks apps --show-history
flux tree kustomization flux-system --compact
# Preview and render locally before pushing
flux diff kustomization apps --path ./apps/production
flux build kustomization apps --path ./apps/production --dry-run
# Force action
flux reconcile source git flux-system
flux reconcile kustomization apps --with-source
# Pause and resume
flux suspend kustomization apps
flux resume kustomization apps
# Export for backup or review
flux export source git --all > sources.yaml
flux export kustomization --all > kustomizations.yaml
| Issue | Diagnosis | Fix |
|---|---|---|
| Source not ready | flux get sources git -A, flux events --for GitRepository/<name> |
Check URL, credentials Secret, known_hosts, provider (GitHub App, Workload Identity) |
| Kustomization failed build | flux build kustomization <name> --path ... |
Fix YAML, missing components (spec.ignoreMissingComponents), unresolved ${VAR} (strict substitution is on by default since Flux 2.9) |
| Kustomization stuck on health check | flux debug ks <name> --show-status |
Fix the failing workload; raise spec.timeout; enable CancelHealthCheckOnNewRevision to pick up fixes faster |
| HelmRelease stuck / retries exhausted | flux get hr -A, flux debug hr <name> --show-history |
Fix values or chart version, then flux reconcile hr <name> --reset |
| Helm hooks break after 2.9 upgrade | Events mention post-render | Set spec.postRenderStrategy: nohooks |
| Image not updating | flux get images all -A |
Check ImagePolicy range and registry auth, setter marker namespace:name, push permissions |
| Upgrade fails with unknown API version | CRD validation error on v1beta* |
Run flux migrate and flux migrate -f ., then re-apply |
| Resources flapping between Flux and HPA/webhook | Repeated drift events | Add spec.ignore rules (2.9+) or HelmRelease driftDetection.ignore |