I was tired of manually running kubectl apply and having my Kubernetes cluster state drift from what was defined in my Git repository. I wanted a true GitOps workflow, where Git is the single source of truth and every change is automatically and declaratively deployed. I found my solution with Flux2. It has completely transformed how I manage Kubernetes deployments. Here’s my guide to how I set it up with GitLab.
Why I Chose Flux2
I had used Flux v1 before, but I found its monolithic design and inefficient image scanning to be a bottleneck. Flux2 is a huge improvement. It has a modular, controller-based architecture that’s much more powerful. The Source Controller watches my Git repo, the Kustomize and Helm controllers apply my manifests, and the Image Automation controllers handle automatic updates. This separation of concerns is what makes Flux2 so robust.
My Setup Process
Step 1: Bootstrap Flux2
The first thing I do is install the flux CLI and set up a GitLab personal access token with api and write_repository scopes.
Then, the magic happens with the flux bootstrap gitlab command. This single command installs all the Flux components into my cluster, creates a deploy key in my GitLab repo, and commits the initial Flux configuration. I make sure to include the image automation components, as they are key to my automated image update workflow.
export GITLAB_TOKEN=<your-gitlab-personal-access-token>
flux bootstrap gitlab \
--owner=my-group/my-project \
--repository=cluster-config \
--branch=main \
--path=./flux-system \
--components-extra=image-reflector-controller,image-automation-controller \
--interval=30s
After this runs, I have a flux-system directory in my cluster-config repository, and the Flux controllers are running in my cluster.
Step 2: Structure the Git Repository
I organize my repository with a clear structure:
.
├── charts/ # My application Helm charts
├── flux-system/ # Flux's own configuration
└── releases/ # My application release definitions
Step 3: Configure Flux to Manage Releases
I tell Flux to manage all the application releases defined in my releases directory. I do this by creating a flux-system/releases.yaml file. This file defines two key things:
- A
Kustomizationthat tells Flux to watch the./releasespath in my Git repo. - An
ImageUpdateAutomationthat tells Flux how to commit changes back to the repo when it finds a new container image.
# flux-system/releases.yaml
apiVersion: kustomize.toolkit.fluxcd.io/v1beta1
kind: Kustomization
metadata:
name: app-releases
namespace: flux-system
spec:
interval: 30s
path: ./releases # Watch this directory
prune: true
sourceRef:
kind: GitRepository
name: flux-system
---
apiVersion: image.toolkit.fluxcd.io/v1beta1
kind: ImageUpdateAutomation
metadata:
name: app-image-update
namespace: flux-system
spec:
interval: 30s
sourceRef:
kind: GitRepository
name: flux-system
git:
commit:
author:
email: flux-bot@example.com
name: flux-bot
messageTemplate: "Automated image update for {{ .AutomationObject }}"
push:
branch: main
update:
path: ./releases
strategy: Setters # Use the $imagepolicy comments
Step 4: Deploying an Application
To add a new application, I’ve created a template. This template defines three resources:
ImageRepository: Tells Flux where to find my container image.ImagePolicy: Defines my versioning strategy. I use Semantic Versioning (semver) for production.HelmRelease: Defines the application deployment itself.
Here’s the crucial part of my HelmRelease template, where I use a special comment to tell Flux how to update the image tag:
# templates/release-template-semver.yaml
apiVersion: helm.toolkit.fluxcd.io/v2beta1
kind: HelmRelease
metadata:
name: REPO_NAME
namespace: production-PROJECT_NAME
spec:
chart:
spec:
chart: ./charts/PROJECT_NAME/REPO_NAME
sourceRef:
kind: GitRepository
name: flux-system
interval: 30s
values:
image:
repository: "gcr.io/my-project/PROJECT_NAME/REPO_NAME" # {"$imagepolicy": "flux-system:PROJECT_NAME-REPO_NAME:name"}
tag: "1.0.0" # {"$imagepolicy": "flux-system:PROJECT_NAME-REPO_NAME:tag"}
When I want to deploy a new app, I just fill out this template, commit it to the releases/production directory, and push. Flux takes care of the rest.
My Workflow in Action
Once this is all set up, my workflow is a thing of beauty. It looks like this:
sequenceDiagram
participant Dev as Developer
participant Git as GitLab
participant Flux as Flux Controllers
participant K8s as Kubernetes
participant Registry as Container Registry
Dev->>Git: 1. Push code changes
Note over Git: CI/CD builds image
Git->>Registry: 2. Push image myapp:1.2.1
Registry-->>Flux: 3. Image Reflector scans for new tags
Flux->>Git: 4. Image Automation commits manifest update (tag: 1.2.1)
Git-->>Flux: 5. Source Controller detects new commit
Flux->>K8s: 6. Helm Controller applies updated release
K8s->>K8s: 7. Rolling update to v1.2.1
A developer pushes code, our CI pipeline builds and pushes a new container image with a SemVer tag (e.g., 1.2.1). The Flux Image Reflector sees the new tag. The Image Automation controller then automatically commits a change to my HelmRelease manifest in GitLab, updating the image tag to 1.2.1. The Flux Source Controller sees this new commit, and the Helm Controller applies the upgrade to my cluster. It’s a fully automated, hands-off continuous deployment.
My Final Thoughts
My key takeaway from implementing Flux2 is that it truly delivers on the promise of GitOps. By making Git the source of truth, every change is auditable and reversible through a simple git revert. My advice is to use separate branches or repos for different environments, use SemVer for production image policies, and set up notifications so you know when a sync fails. This declarative, automated approach has made my deployments faster, safer, and completely transparent.
Related Articles
- Canary Deployment with Flagger - Automated progressive delivery
- Blue-Green Deployment with Traefik - Zero-downtime releases
- Kafka on Kubernetes - Deploy with Helm and Flux