GitOps Infrastructure with Flux!
How to use GitOps for kubernetes using Flux!
GitOps brings DevOps best practices, most notably version control, to your kubernetes infrastructure. The basic idea is that you define the desired state of your infrastructure in a git repository, and a controller reconciles your cluster with that repository accordingly. After wiping and recreating my cluster infrastructure a few times, I knew I wanted to learn this technology.
There are a number of GitOps tools available. During my research I looked at Flux, Fleet by Rancher, and ArgoCD. I’m sure any of these would work great for my simple use case. I decided to use Flux just because it seemed to have a “no-nonsense” CLI approach to things. In retrospect I might have been better off with ArgoCD, which seems like a somewhat more mature user-friendly project. Regardless, Flux is super cool and works great! It took a lot of trial and error, but my flux managed infrastructure is up and running smoothly now. There’s a lot to it, and I’ve tried to touch on some of primary pain points below.
Flux
To get started, Install the Flux CLI.
# yolo pipe
curl -s https://fluxcd.io/install.sh | sudo bash
Bootstrap flux managed cluster.
flux bootstrap github \
--components-extra=image-reflector-controller,image-automation-controller \
--owner=dvignoles \
--repository=flux \
--branch=main \
--path=cluster/base \
--personal \
--token-auth
The above command requires a github token to manage repositories. My flux repository is now ready to go! A single flux deployment/repo can manage multiple clusters, but we will be managing just one cluster rooted in cluster/base
. In a professional setting you might set up a folder structure for differentiating between staging and production.
Do a kubectl -n flux-system get pods
and you should see the various flux controllers running. To engage with our cluster via flux, simply push manifests to your flux repo! Flux reconciles the updated source with the current state of the cluster. When you push a change you can monitor this reconciliation.
flux get kustomizations --watch
Secrets
Even in a private git repository, you probably shouldn’t store any secret passwords or API keys in plain text. You can of course just manage secrets outside of GitOps manually. If you do want to manage secrets declaratively, flux provides integrations for storing encrypted secrets in your repository. I tried out both Bitnami Sealed Secrets and Mozilla SOPS. Both workflows work fine, but I found SOPS a bit more intuitive so that’s what I’m going with. You’ll need to install SOPS and whatever encryption backend you are using. I’m using age, but use whatever suits you. Get both the age
and sops
binaries installed by following along their readmes.
First, generate your age key pair. Remember to also backup your private key somewhere safe!
age-keygen -o age.agekey
# add private key to kubernetes for decryption.
cat age.agekey |
kubectl create secret generic sops-age \
--namespace=flux-system \
--from-file=age.agekey=/dev/stdin
Create a .sops.yaml file at the root of your repository. Paste your public key in. sops
will look for this file when doing encryption. It’s safe to push your public key to your repository.
creation_rules:
- path_regex: .*.yaml
encrypted_regex: ^(data|stringData)$
age: age1etfpwhwrvmd7xqh5a8yn6xx8l0q8e0rh2ca0aaf70yeeawlll9qseh2ljw
Let’s encrypt this demo secret.
# basic-auth.yaml
apiVersion: v1
data:
# htpasswd -nb <user> <pass> | openssl base64
users: YWRtaW46JGFwcjEkMlBIZTlFazEkVGtqeVZoNlBhdjlpeGxWSUtDRTFXMAoK
kind: Secret
metadata:
name: basic-auth
namespace: default
sops --encrypt basic-auth.yaml > basic-auth.sops.yaml
# basic-auth.sops.yaml
apiVersion: v1
data:
#ENC[AES256_GCM,data:ENqPfz2bMSxcPp+QT/Q/TWXHTVLp4t4s2gDbvVxwJg+gVvoNUZdkbmg=,iv:8YTW1+Ip0fG+M/apRQlxQFGHT9NzefmQcaZZHMyp5xE=,tag:iAD/SJbEhSsVpaabihouvg==,type:comment]
users: ENC[AES256_GCM,data:Fzg9VpCkTV0pFX838FGjtUU84z8umJNcmpH0XDA6+pJgAyxz3GYcfhRcKwnHFiksUtUJyiw3JtVizIuH,iv:kUQx43neztD72UdlP4Lh9j00aiGF1s4MUwwOFTogj48=,tag:iVDMf6uLwe5CFzYTn7nejg==,type:str]
kind: Secret
metadata:
name: basic-auth
namespace: default
sops:
kms: []
gcp_kms: []
azure_kv: []
hc_vault: []
age:
- recipient: age1etfpwhwrvmd7xqh5a8yn6xx8l0q8e0rh2ca0aaf70yeeawlll9qseh2ljw
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB5SndBdS9HQ1E5QklrSTlF
dUVqV0xLMmJNT25FbURkZVhTcUtlejNSeVdNCk9kWDVtSGlteU9jTjV0Q0hwSzNU
bnFRWGk0SUxYQkYybkZ5YnVnNjZoSlUKLS0tIGZRMWlHV1ZiMlpkdGtsVkZRNkZG
Y1pOWDZwM3RNWk1mNnhtVVA5cDhuKzQKOaJI2gabvxW8JfulOnQQ3RUHBfhAzG1R
AlQwS9jWmJ0T2PsoVl4y9PgQbYCDj4GTAsp6sMdkzFvgQq4NNmweww==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2022-09-22T00:47:09Z"
mac: ENC[AES256_GCM,data:otL1WG+Heet3pD2wcfhcRJiZA9lfjQa+GELqfobSBtZa+ezjzmijk8WXQAMtE2KTeMujpbsuEQn83A5Zqz/9sd5VT/vK1i71/Ge2SNgBX/nCBw3YM8HvqE9XCWscGo85fg8BKCCYpbGPD0jzvo7xyUKzZojKyxwQDcQKtIKZfQ4=,iv:LWLFYYSb+znveWUtODoIWzXkztsp4/fqjzY8vgghhSU=,tag:V5pIFLFhLVX1oXNkMagD3w==,type:str]
pgp: []
encrypted_regex: ^(data|stringData)$
version: 3.7.3
Push your encrypted secret to flux, and you should see the secret available in your cluster! Getting this workflow set up before anything else will save you some trouble when you need it.
Installing a Helm Release
Flux also supports helm releases. This is particularly useful to me, because I find it can be easy to lose track of the original install commands and values.yml
files that I used when maintaining a helm release. By declaring the helmrelease in gitops, there’s never any confusion. Flux also will also conveniently keep your release up to date if you don’t pin the version.
The HelmRepository
kind keeps the helm repo updated (on a 1hr interval in this case.)
apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: HelmRepository
metadata:
name: longhorn
namespace: flux-system
spec:
interval: 60m
url: https://charts.longhorn.io
The HelmRelease
references the above HelmRepository
. You can also declare any helm values under spec.values
. If i were to set the version as >=1.3.0
, the release would be updated as new versions become available. In this case however, I’d rather pin the version and manually update the release.
apiVersion: helm.toolkit.fluxcd.io/v2beta1
kind: HelmRelease
metadata:
name: longhorn
namespace: longhorn-system
spec:
interval: 60m
chart:
spec:
chart: longhorn
version: '1.3.0'
sourceRef:
kind: HelmRepository
name: longhorn
namespace: flux-system
interval: 30m
That’s the most basic case, but you can also use sources of kind GitRepository
as the source of a helm chart. The the flux docs for more details.
Defining Dependencies
Getting my flux repository set up was smooth and exciting, until I hit this major road bump. Flux doesn’t know which of your kubernetes objects depend on one another by default. For instance, I want my load balancer, metallb, to be up and running before setting up my ingress controller traefik. If flux tries to reconcile traefik before metallb, that’s a problem. I found myself pretty frustrated by these kinds of issues, and didn’t find the documentation particularly helpful as a novice flux user.
I ended up snooping through other flux repositories on the awesome-home-kubernetes repository list for inspiration. These repositories are extremely helpful when looking for practical implementation “advice”. Several of the flux examples I found use a folder structure something like this:
base/
- apps.yaml
- core.yaml
- crds.yaml
apps/
core/
crds/
You can then create kustomizations
, defining the dependencies between these directories for flux.
---
# crds.yaml
apiVersion: kustomize.toolkit.fluxcd.io/v1beta2
kind: Kustomization
metadata:
name: crds
namespace: flux-system
spec:
interval: 10m0s
path: ./cluster/crds
prune: true
sourceRef:
kind: GitRepository
name: flux-system
---
# core.yaml
apiVersion: kustomize.toolkit.fluxcd.io/v1beta2
kind: Kustomization
metadata:
name: core
namespace: flux-system
spec:
interval: 5m0s
dependsOn:
- name: crds
path: ./cluster/core
prune: false
sourceRef:
kind: GitRepository
name: flux-system
decryption:
provider: sops
secretRef:
name: sops-age
---
# apps.yaml
---
apiVersion: kustomize.toolkit.fluxcd.io/v1beta2
kind: Kustomization
metadata:
name: apps
namespace: flux-system
spec:
interval: 5m0s
dependsOn:
- name: core
path: ./cluster/apps
prune: true
sourceRef:
kind: GitRepository
name: flux-system
decryption:
provider: sops
secretRef:
name: sops-age
specs.dependsOn
lets flux know the correct order for reconciliation. You can customize this basic idea for you needs, or jack the structure from my repository if you like!
Custom Resource Definitions
Many Helm Releases have the option to install custom resource types into your cluster bundled in with the application. I found out the hard way that is best practice not to do this when using GitOps. You can run into situations where the Flux reconciler tries to create a custom resource before the helm release is installed. That of course leads to a very frustrating error. It is worth the trouble to separately install those custom resource definitions, and make sure that happens before any resources which depend on them. Using the dependency structure from above, you can reconcile all of your CRDs before anything else. The manifests for CRDs can usually be found in the git repository of whatever project you are installing.
For example, here is a manifest which handles installing the traefik CRDs from their git repository.
apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: GitRepository
metadata:
name: traefik-crd-source
namespace: flux-system
spec:
interval: 30m
url: https://github.com/traefik/traefik-helm-chart.git
ref:
tag: v10.24.0
ignore: |
# exclude all but the CDRs
/*
# path to crds
!/traefik/crds/
---
apiVersion: kustomize.toolkit.fluxcd.io/v1beta2
kind: Kustomization
metadata:
name: traefik-crds
namespace: flux-system
spec:
interval: 15m
prune: false
sourceRef:
kind: GitRepository
name: traefik-crd-source
Conclusion
All referenced *.yaml
files are available in my My flux repository.
Thank you to all the unnamed awesome-home-kubernetes contributors whom I have shamelessly copied from, I could not have gotten anything working without you!