GitOps with Flux!
In this post, we will explore GitOps and how it can be implemented with Flux on a Kubernetes cluster.
What is Flux?
Flux is a popular GitOps tool used to manage and automate deployments on Kubernetes clusters. It uses a Git repository as the source of truth for your infrastructure and application configurations.
Creating a Kubernetes Cluster
For this demo, we’ll be using KinD (Kubernetes IN Docker), which allows you to create a local Kubernetes cluster for development and testing purposes.
First, create a cluster using KinD:
kind create cluster --name demo
Output:
Creating cluster "demo" ...
✓ Ensuring node image (kindest/node:v1.25.3) 🖼
✓ Preparing nodes 📦
✓ Writing configuration 📜
✓ Starting control-plane 🕹️
✓ Installing CNI 🔌
✓ Installing StorageClass 💾
Set kubectl context to "kind-demo"
You can now use your cluster with:
kubectl cluster-info --context kind-demo
Once the cluster is created, you can check its info:
kubectl cluster-info --context kind-demo
Output:
Kubernetes control plane is running at https://127.0.0.1:65535
CoreDNS is running at https://127.0.0.1:65535/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.
The cluster is ready!
Installing Flux CLI
To install the Flux CLI, run the following command:
brew install fluxcd/tap/flux
Bootstrap Flux
Before we can bootstrap Flux, we need to create a GitHub personal access token. You can create a token with either the admin:org
or repo
permission, depending on whether you need Flux to create a repository or you already have an existing one.
Export the token as an environment variable:
export GITHUB_TOKEN=<your-token>
Run the bootstrap command to create a repository on your personal GitHub account and configure Flux to sync with it:
flux bootstrap github \
--owner=<your-github-username> \
--repository=<your-github-repository> \
--path=clusters/demo \
--personal
Output:
► connecting to github.com
✔ repository "https://github.com/<your-github-username>/<your-github-repository>" created
► cloning branch "main" from Git repository "https://github.com/<your-github-username>/<your-github-repository>.git"
✔ cloned repository
► generating component manifests
✔ generated component manifests
✔ committed sync manifests to "main" ("0f908b5013bb6b21c0f6a1b37980a8d85a1dfad2")
► pushing component manifests to "https://github.com/<your-github-username>/<your-github-repository>.git"
► installing components in "flux-system" namespace
✔ installed components
✔ reconciled components
► determining if source secret "flux-system/flux-system" exists
► generating source secret
✔ public key: ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBBOH8h11Rm92kX9XASEtIsuMDu3AqMTIUZV/6DV0N+w6Kb0tgvE3FY0DXqCkbhpSbiqSK5ojMVrpR3WNjqGGE4Bp+gzEthJ/+L3ocajYbIJRP45pTXnVFWT98fvuz0A9WA==
✔ configured deploy key "flux-system-main-flux-system-./clusters/demo" for "https://github.com/<your-github-username>/<your-github-repository>"
► applying source secret "flux-system/flux-system"
✔ reconciled source secret
► generating sync manifests
✔ generated sync manifests
✔ committed sync manifests to "main" ("c09327b2fcd9db4ac1d5cbe907425a44658631af")
► pushing sync manifests to "https://github.com/<your-github-username>/<your-github-repository>.git"
► applying sync manifests
✔ reconciled sync configuration
◎ waiting for Kustomization "flux-system/flux-system" to be reconciled
✔ Kustomization reconciled successfully
► confirming components are healthy
✔ helm-controller: deployment ready
✔ kustomize-controller: deployment ready
✔ notification-controller: deployment ready
✔ source-controller: deployment ready
✔ all components are healthy
This command will create a private git repository and generate Flux manifests that are committed to the main branch. It will also install the Flux components in the flux-system namespace. Once the repository has been created and configured, you can clone it locally using the following command:
git clone git@github.com:<your-github-username>/<your-github-repository>.git
Directory Structure
The directory structure of the repository after Flux is bootstrapped is as follows:
<your-github-repository>
└── clusters
└── demo
└── flux-system
├── gotk-components.yaml
├── gotk-sync.yaml
└── kustomization.yaml
This directory contains the Flux manifests that were generated during the bootstrap process.
That’s it! You have now bootstrapped Flux and have a Kubernetes cluster ready for GitOps.
Every file located in the directory clusters/demo
are deployed to your kubernetes cluster because of manifest gotk-sync.yaml
:
# This manifest was generated by flux. DO NOT EDIT.
---
apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: GitRepository
metadata:
name: flux-system
namespace: flux-system
spec:
interval: 1m0s
ref:
branch: main
secretRef:
name: flux-system
url: ssh://git@github.com/<your-github-username>/<your-github-repository>
---
apiVersion: kustomize.toolkit.fluxcd.io/v1beta2
kind: Kustomization
metadata:
name: flux-system
namespace: flux-system
spec:
interval: 10m0s
path: ./clusters/demo
prune: true
sourceRef:
kind: GitRepository
name: flux-system
This file contains two manifests, GitRepository
and Kustomization
. The Kustomization
manifest references the GitRepository
on the path ./clusters/demo
.
This Kustomization
manifest has an interval of 10 minutes which Flux named Reconcile.
Now let’s give this directory a good structure.
How to do it
Create the following three manifests in the path of your cluster:
clusters/demo/repositories.yaml
---
apiVersion: kustomize.toolkit.fluxcd.io/v1beta2
kind: Kustomization
metadata:
name: repositories
namespace: flux-system
spec:
interval: 3m
path: ./repositories
prune: true
sourceRef:
kind: GitRepository
name: flux-system
clusters/demo/infrastructure.yaml
---
apiVersion: kustomize.toolkit.fluxcd.io/v1beta2
kind: Kustomization
metadata:
name: infrastructure
namespace: flux-system
spec:
interval: 3m
dependsOn:
- name: repositories
path: ./deploy/infrastructure/overlays/demo
prune: true
sourceRef:
kind: GitRepository
name: flux-system
clusters/demo/applications.yaml
---
apiVersion: kustomize.toolkit.fluxcd.io/v1beta2
kind: Kustomization
metadata:
name: applications
namespace: flux-system
spec:
interval: 3m
dependsOn:
- name: infrastructure
path: ./deploy/applications/overlays/demo
prune: true
sourceRef:
kind: GitRepository
name: flux-system
As you can see, those three Kustomization
have paths and dependencies on each other. Let’s create directories:
<your-github-repository>
├── clusters
│ └── demo
│ ├── applications.yaml
│ ├── flux-system
│ │ ├── gotk-components.yaml
│ │ ├── gotk-sync.yaml
│ │ └── kustomization.yaml
│ ├── infrastructure.yaml
│ └── repositories.yaml
├── deploy
│ ├── applications
│ │ └── overlays
│ │ └── demo
│ └── infrastructure
│ └── overlays
│ └── demo
└── repositories
Create all the necessary repositories in the repositories
directory. For example:
repositories/bitnami.yaml
---
apiVersion: source.toolkit.fluxcd.io/v1beta1
kind: HelmRepository
metadata:
name: bitnami
namespace: flux-system
spec:
interval: 720h
timeout: 5m
url: https://charts.bitnami.com/bitnami
repositories/banzaicloud.yaml
---
apiVersion: source.toolkit.fluxcd.io/v1beta1
kind: HelmRepository
metadata:
name: banzaicloud
namespace: flux-system
spec:
interval: 720h
timeout: 5m
url: https://kubernetes-charts.banzaicloud.com
We can use these repositories to deploy infrastructure or applications. Let’s create our infrastructure stack for two environments, production and staging.
Since we do not want to repeat the code for every cluster and environment, we can use the base
directory in the infrastructure and application directories.
Create the following files to deploy MySQL:
deploy/infrastructure/base/mysql/helm-release.yaml
---
apiVersion: helm.toolkit.fluxcd.io/v2beta1
kind: HelmRelease
metadata:
name: mysql
spec:
interval: 720h
chart:
spec:
chart: mysql
version: '9.4.5'
sourceRef:
kind: HelmRepository
name: bitnami
namespace: flux-system
interval: 720h
upgrade:
remediation:
remediateLastFailure: true
values:
valuesFrom:
- kind: ConfigMap
name: mysql-values
- kind: ConfigMap
name: mysql-values-overrides
deploy/infrastructure/base/mysql/kustomization.yaml
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- helm-release.yaml
configMapGenerator:
- name: mysql-values
files:
- values.yaml=values.yaml
configurations:
- kustomizeconfig.yaml
deploy/infrastructure/base/mysql/kustomizeconfig.yaml
---
nameReference:
- kind: ConfigMap
version: v1
fieldSpecs:
- path: spec/valuesFrom/name
kind: HelmRelease
deploy/infrastructure/base/mysql/values.yaml
---
fullnameOverride: mysql
architecture: standalone
auth:
rootPassword: "demo"
createDatabase: true
database: "demo"
Now we are ready to use this base in the overlay directory by addressing this directory and overriding the Helm value for every deployment. For this to be done, we need to create namespaces for production and staging. Create the following files in the infrastructure overlays directory:
deploy/infrastructure/overlays/demo/production/kustomization.yaml
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: production
resources:
- namespace.yaml
- mysql
deploy/infrastructure/overlays/demo/production/namespace.yaml
---
apiVersion: v1
kind: Namespace
metadata:
name: production
deploy/infrastructure/overlays/demo/production/mysql/kustomization.yaml
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../../../base/mysql
configMapGenerator:
- name: mysql-values-overrides
files:
- values-overrides.yaml=values.yaml
deploy/infrastructure/overlays/demo/production/mysql/values-overrides.yaml
---
auth:
rootPassword: "demo-prod"
createDatabase: true
database: "demo-prod"
deploy/infrastructure/overlays/demo/staging/kustomization.yaml
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: staging
resources:
- namespace.yaml
- mysql
deploy/infrastructure/overlays/demo/staging/namespace.yaml
---
apiVersion: v1
kind: Namespace
metadata:
name: staging
deploy/infrastructure/overlays/demo/staging/mysql/kustomization.yaml
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../../../base/mysql
configMapGenerator:
- name: mysql-values-overrides
files:
- values-overrides.yaml=values.yaml
deploy/infrastructure/overlays/demo/staging/mysql/values-overrides.yaml
---
auth:
rootPassword: "demo-stag"
createDatabase: true
database: "demo-stag"
Now that the infrastructure part is complete, we can deploy Drupal to the applications
directory, same as infrastructure
, with the help of base. Create the
following files:
deploy/applications/base/drupal/helm-release.yaml
---
apiVersion: helm.toolkit.fluxcd.io/v2beta1
kind: HelmRelease
metadata:
name: drupal
spec:
interval: 720h
chart:
spec:
chart: drupal
version: '13.0.9'
sourceRef:
kind: HelmRepository
name: bitnami
namespace: flux-system
interval: 720h
upgrade:
remediation:
remediateLastFailure: true
values:
valuesFrom:
- kind: ConfigMap
name: drupal-values
- kind: ConfigMap
name: drupal-values-overrides
deploy/applications/base/drupal/kustomization.yaml
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- helm-release.yaml
configMapGenerator:
- name: drupal-values
files:
- values.yaml=values.yaml
configurations:
- kustomizeconfig.yaml
deploy/applications/base/drupal/kustomizeconfig.yaml
---
nameReference:
- kind: ConfigMap
version: v1
fieldSpecs:
- path: spec/valuesFrom/name
kind: HelmRelease
deploy/applications/base/drupal/values.yaml
---
fullnameOverride: drupal
persistence:
enabled: false
mariadb:
enabled: false
externalDatabase:
host: ""
user: bn_drupal
password: ""
database: bitnami_drupal
drupalPassword: demo
deploy/applications/overlays/demo/production/kustomization.yaml
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: production
resources:
- drupal
deploy/applications/overlays/demo/production/drupal/kustomization.yaml
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../../../base/drupal
configMapGenerator:
- name: drupal-values-overrides
files:
- values.yaml=values-overrides.yaml
deploy/applications/overlays/demo/production/drupal/values-overrides.yaml
---
fullnameOverride: drupal-prod
externalDatabase:
host: mysql
user: root
password: demo-prod
database: demo-prod
deploy/applications/overlays/demo/staging/kustomization.yaml
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: staging
resources:
- drupal
deploy/applications/overlays/demo/staging/drupal/kustomization.yaml
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../../../base/drupal
configMapGenerator:
- name: drupal-values-overrides
files:
- values.yaml=values-overrides.yaml
deploy/applications/overlays/demo/staging/drupal/values-overrides.yaml
---
fullnameOverride: drupal-stag
externalDatabase:
host: mysql
user: root
password: demo-stag
database: demo-stag
The final look of the directory should be like this:
gitops
├── clusters
│ └── demo
│ ├── applications.yaml
│ ├── flux-system
│ │ ├── gotk-components.yaml
│ │ ├── gotk-sync.yaml
│ │ └── kustomization.yaml
│ ├── infrastructure.yaml
│ └── repositories.yaml
├── deploy
│ ├── applications
│ │ ├── base
│ │ │ └── drupal
│ │ │ ├── helm-release.yaml
│ │ │ ├── kustomization.yaml
│ │ │ ├── kustomizeconfig.yaml
│ │ │ └── values.yaml
│ │ └── overlays
│ │ └── demo
│ │ ├── production
│ │ │ ├── drupal
│ │ │ │ ├── kustomization.yaml
│ │ │ │ └── values-overrides.yaml
│ │ │ └── kustomization.yaml
│ │ └── staging
│ │ ├── drupal
│ │ │ ├── kustomization.yaml
│ │ │ └── values-overrides.yaml
│ │ └── kustomization.yaml
│ └── infrastructure
│ ├── base
│ │ └── mysql
│ │ ├── helm-release.yaml
│ │ ├── kustomization.yaml
│ │ ├── kustomizeconfig.yaml
│ │ └── values.yaml
│ └── overlays
│ └── demo
│ ├── production
│ │ ├── kustomization.yaml
│ │ ├── mysql
│ │ │ ├── kustomization.yaml
│ │ │ └── values-overrides.yaml
│ │ └── namespace.yaml
│ └── staging
│ ├── kustomization.yaml
│ ├── mysql
│ │ ├── kustomization.yaml
│ │ └── values-overrides.yaml
│ └── namespace.yaml
└── repositories
├── banzaicloud.yaml
└── bitnami.yaml
We need to push our code and wait a couple of minutes for Flux to reconcile the repository and deploy everything to the Kubernetes cluster. We can also force reconcile with the following command:
flux reconcile kustomization repositories --with-source
You can access the full code repository here: https://github.com/pooryasheikh/gitops
To-do
- Configure image auto-update
- Configure Sops
- Add HashiCorp Vault to the cluster and read secrets with an init container
- Create a common Helm chart and use it as a HelmGitRepository