The "latest" tag in container registries is often a lie, and Flux’s image update policies are the robust, auditable way to manage truth.
Let’s see this in action. Imagine we have a deployment for our web app in Kubernetes managed by Flux. Our container image is my-registry.com/my-app:v1.2.3. We want to automate updating this deployment when a new image is pushed to my-registry.com/my-app:v1.2.4.
Here’s a simplified kustomization.yaml that Flux might be managing:
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: my-app-deployment
namespace: default
spec:
interval: 10m
path: ./apps/my-app/overlays/production
sourceRef:
kind: GitRepository
name: flux-source
# This is where we'll add our image update policy
# imagePolicies:
# - name: my-app-image
# policy:
# semver:
# range: ">=1.2.3 <2.0.0"
# identifier: "v" # For tags like v1.2.3
# # This tells Flux *which* image to watch
# # and *where* to find its definition in our kustomization path
# # The image name here MUST match the image in the deployment manifest
# # that this Kustomization will apply.
# # The `annotation` is a common way to link them.
# # Flux will add/update this annotation on the deployment object.
# aws: # Or 'gcr', 'oci', 'dockerhub', 'azure', 'github' etc.
# image: my-registry.com/my-app
# policy:
# tags:
# regex: "^v[0-9]+\\.[0-9]+\\.[0-9]+$" # Match semantic versioning like v1.2.3
# # You can also use 'allowTags' or 'ignoreTags' for more specific control.
# # For example, to only allow specific pre-release tags:
# # allowTags: ["^v1.2.3-rc.*$"]
# # Or to ignore specific tags:
# # ignoreTags: ["^v1.2.3-beta.*$"]
When Flux detects a new image my-registry.com/my-app:v1.2.4 that matches our semver.range (>=1.2.3 <2.0.0) and the regex for the tag, it will update the annotation on the Kubernetes Deployment object. If the Deployment manifest in our Git repo uses image: my-registry.com/my-app:v1.2.3 and we have configured the Kustomization to update the apps/my-app/overlays/production/deployment.yaml file, Flux will modify that file in Git (or apply the annotation directly if not using Git commit for updates) to point to the new image tag.
The core problem this solves is the inherent unreliability of the latest tag. It’s mutable, can be overwritten, and provides no history. Image update policies, on the other hand, allow you to define precise rules for promotion based on immutable image tags. This means you can track exactly which version of an image is deployed and have confidence in rollbacks or audits.
The mental model is: Flux watches a container registry for new image tags. It filters these tags based on policies you define (like semantic versioning ranges, specific patterns, or allowed/ignored tags). If a new tag matches, Flux then updates a Kubernetes manifest (typically a Deployment, StatefulSet, or DaemonSet) to use that new image tag. This update can either be a direct API call to Kubernetes or, more commonly, a commit back to your Git repository which Flux then applies.
The Kustomization object in Flux is the central piece. It tells Flux what to apply (via sourceRef and path) and when to apply it (interval). The imagePolicies section within the Kustomization is where you define your automated promotion strategy. Each policy has a name, a policy (e.g., semver, digest), and an aws/gcr/oci/dockerhub/azure/github block specifying the image name and its own tag filtering rules.
Flux supports several policy types:
- SemVer: Uses semantic versioning ranges (e.g.,
^1.2.3,>=1.2.0 <2.0.0). - Digest: Pinning to a specific image digest (
sha256:...) for absolute immutability. - Alphabetical: Simple lexicographical sorting of tags.
Crucially, the aws/gcr etc. block defines how Flux finds the image and what tags it’s interested in. The policy.tags.regex is your primary tool here for filtering. You can use standard Go regular expressions. For example, ^release-.*$ would only consider tags starting with "release-".
When Flux finds a matching image tag, it updates an annotation on the Kubernetes resource. By default, for Deployments, it adds fluxcd.io/build.number: <build-number> and fluxcd.io/tag.<image-name>:<digest-or-tag>. If you’re using GitOps to commit changes, Flux will automatically commit these annotation changes back to your Git repository. This provides a clear, auditable trail of every image promotion.
A common pattern is to use a semver policy with a range like ">=1.0.0 <2.0.0". Then, in the registry-specific block, use a regex that matches your desired tag format, e.g., ^v[0-9]+\\.[0-9]+\\.[0-9]+$. This ensures you only promote stable, versioned releases and avoid development or pre-release tags unless explicitly allowed.
The magic happens when Flux reconciles your Kustomization. It checks the registry against your defined policies. If a new, matching tag is found, Flux applies the update. This update is often an annotation change on the Kubernetes resource. Flux then commits this change back to Git if GitOps is configured for it. The next time Flux applies the Kustomization from Git, Kubernetes will see the updated image tag and roll out the new version.
The most surprising thing is how much control you gain over the "latest" lie. You’re not just promoting any new image; you’re promoting images that adhere to a strict, defined contract. This moves from a "hope it works" to a "guaranteed to meet criteria" promotion process.
Flux will automatically add or update the fluxcd.io/tag.<image-name> annotation on the Deployment.