Right, so you’ve got Flux deployed and your manifests are happily syncing from Git. Fantastic. But let’s be honest: you’re not really doing GitOps until you’ve automated the most common, tedious, and error-prone task of them all—updating a flipping image tag.

You know the drill. A new version of your app (v1.2.3) gets built, pushed to your registry, and now you, the brilliant human, have to:

  1. git clone the repo.
  2. Manually find and edit deployment.yaml (or worse, a Kustomize patch).
  3. git commit -m "bump image to v1.2.3 because I am a glorified search-and-replace tool".
  4. git push.
  5. Wait for Flux to sync the change.
  6. Hope you didn’t typo v.1.2.3 and break everything.

This is absurd. We have robots for this. Flux’s Image Automation controllers are those robots. Their job is to watch your container registry, notice new tags, and—this is the key part—write a new commit back to your Git repository with the updated tag. The automation loop is closed. You get a pull request or a direct commit (your choice, you maniac) and Flux applies the change itself. It’s beautiful.

How the Magic Trick Works: Observers and Reflectors

The system has two main parts, and you need both. Don’t skip one and then email me asking why it’s broken. I will know.

First, you need an ImageRepository (the observer). This tells Flux what to watch. You point it at a container registry and a repository name. It will sit there, politely polling the registry (you can configure the interval, because you’re polite too), and compiling a list of all available tags.

# This goes in your cluster, not your Git repo.
apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImageRepository
metadata:
  name: my-app-repo
  namespace: flux-system
spec:
  image: ghcr.io/your-org/your-awesome-app
  interval: 5m # Check every 5 minutes. Don't set this to 5s, you'll annoy the registry.

Second, you need an ImageUpdateAutomation (the reflector). This is the part that actually does the scary thing: writing to Git. It’s constantly watching all the ImageRepository resources. When it sees a new tag that matches a filter you’ve defined, it springs into action. It will:

  1. Clone your Git repo.
  2. Run a sed-like find-and-replace across the specified paths.
  3. Commit the change back to the branch.
  4. Push it.

You must tell it how to commit and where to push. This is where you configure your Git credentials (usually with a deploy key).

# Also applied to the cluster.
apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImageUpdateAutomation
metadata:
  name: update-app-images
  namespace: flux-system
spec:
  interval: 1m # How often to check for new images and run the update
  sourceRef:
    kind: GitRepository
    name: flux-system # The name of your main GitRepository object
  git:
    commit:
      messageTemplate: |-
        chore: automated image update
        {{ range .Updated.Images }}
        - {{ .Name }} to {{ .NewTag }}
        {{ end }}
    # This is crucial for authentication
    push:
      branch: main
  update:
    strategy: Setters # The other option is 'Letters', ignore it for now.
  # This is the money part: where to look and what to change.
  patchStrategicMerge:
    files:
      - ./apps/my-app/overlays/production/deployment.yaml
    target:
      kind: Deployment
      name: my-app-deployment
      container: my-app-container # The specific container in the pod spec

The “What Tag Should I Even Use?” Problem

You’re not just interested in any new tag. If your team pushes main-123abc on every commit, you probably don’t want to automatically deploy every single one to production. The ImageRepository resource uses a tag policy to filter this.

The most useful one is SemVer. It does exactly what you think: finds the highest version that matches a pattern.

# Inside your ImageRepository spec
spec:
  image: ghcr.io/your-org/your-awesome-app
  interval: 5m
  # Tag policy section
  tagPolicy:
    semver:
      range: 1.0.x # Will pick the latest v1.0.x patch version. Stable.
      # range: '>=1.0.0 <2.0.0' # More complex example for all non-breaking changes.

For those of you living on the edge with latest or commit SHAs, there’s a Regex policy. Use this power wisely.

tagPolicy:
  regex: '^main-[a-f0-9]+-(?P<ts>[0-9]+)' # Extract a timestamp from the tag

The Crucial Git Security Dance

This is the part where everyone gets stuck. The ImageUpdateAutomation needs write access to your Git repo. No write access, no automated commits. The best practice is to use a deploy key.

  1. Generate an SSH key pair: ssh-keygen -t ed25519 -C "flux-image-automation" -f flux-image-automation.

  2. Add the public key (flux-image-automation.pub) as a Deploy Key in your GitHub/GitLab repo. You must grant it write access. The checkbox is right there. Don’t forget to check it.

  3. Create a Kubernetes Secret with the private key.

    kubectl create secret generic flux-image-automation-ssh \
      --namespace=flux-system \
      --from-file=identity=./flux-image-automation
    
  4. Tell your ImageUpdateAutomation to use it:

    spec:
      git:
        checkout:
          ref:
            branch: main
        commit:
          # ... message template ...
        push:
          branch: main
        # This references the secret you just created
        secretRef:
          name: flux-image-automation-ssh
    

Best Practices and “Oh, That’s Why”

  • Use Pull Requests, You Maniac: Direct commits to main are fine for a demo. For anything real, configure your ImageUpdateAutomation to push to a branch (automated/image-updates) and open a Pull Request. This gives you a chance to run CI checks before the change is merged and synced. This is configured in the git.push.branch field.
  • Be Specific with Your Container Names: In your patchStrategicMerge section, always specify the container name. If your Pod has multiple containers, and you don’t specify, Flux will just update the first one, which is rarely what you want.
  • The Mighty Message Template: That messageTemplate isn’t just for show. It’s your audit log. The {{ .Updated.Images }} block will list every image that was updated in that commit, which is invaluable when debugging.
  • It’s Just YAML: Remember, Flux isn’t parsing your YAML with black magic. It’s essentially doing a structured find-and-replace. If your deployment manifest is built in a weird, non-standard way (e.g., the tag is in a ConfigMap that gets referenced), this strategy won’t work. Keep it simple.

Get this right, and you’ll never manually bump a tag again. You’ll just sit back and watch the robots argue with each other in Git commits. It’s the future.