logo
< Back Post-Image

Kubernetes Kustomize with Flux CD

Today, we are going to take about two subjects, the first one is kind of the leitmotiv of this blog: GitOps, the other one is the new here: Kustomize.

We are long time users of Helm. Helm is a templating solution for Kubernetes based on Go template. It’s kind of the de facto standart for Kubernetes application packaging. You can find official Helm Charts as well as community Charts for pretty much every middelwares you can think of.

Some of them are very thorough and they will keep you from reinvente the wheel each time you want to deploy a Nginx server for example. But some time, we have to package your own application and let’s face it, creating and maintaining, efficiently, a Helm package is not that easy. And no official Chart to help you here.

First thing that comes to mind is to not use Helm Chart and stay with the native YAML manifests. You shall have one manifest per environment. The main con is the invevitable code duplication that will occur if you have many environments. Code duplication is time consuming and the source of many human errors.

Templating tools can solve those problems. There are many of them, each one with their own features :

This article is not about benchmarking these solutions, they are actually quite different and in the end will achieve the same purpose. We will focus on Kustomize.

Helm is clearly the most used but, imo, it’s not the simplest to use. When you’re starting with Kubernetes templating tools, Kustomize could be a quick-win, espacially since you don’t have to learn Go Template (used by Helm) or anything else.

Let’s start with Kustomize

Kustomize, used to be a standalone tool and is now a fully integrated into kubectl since Kubernetes v1.14.

Kustomize is Kubernetes native and doesn’t need advanced templating knowledge. Like the command kubectl patch, Kustomize use an equivalent principle to create complexe Kubernetes manifests.

Let’s start with this git repository and a Kubernetes cluster.

Create two namespaces:

kubectl create ns preprod
kubectl create ns prod

The directory structure is as follow:

.
├── base
│   ├── helloworld-de.yaml
│   ├── helloworld-hpa.yaml
│   ├── helloworld-svc.yaml
│   └── kustomization.yaml
├── preprod
│   └── kustomization.yaml
└── prod
    ├── kustomization.yaml
    └── replicas-patch.yaml

Let’s take a look at our base directory. It contains our YAML manifests, they are just Kubernetes manifests, nothing more, a Deployment, a Service and an HorizontalPodAutoscaler.

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: helloworld
  labels:
    app: helloworld
spec:
  revisionHistoryLimit: 2
  selector:
    matchLabels:
      app: helloworld
  template:
    metadata:
      labels:
        app: helloworld
    spec:
      containers:
      - name: helloworld
        image: particule/helloworld
        imagePullPolicy: Always
        ports:
        - name: web
          containerPort: 80
apiVersion: autoscaling/v2beta1
kind: HorizontalPodAutoscaler
metadata:
  name: helloworld
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: helloworld
  minReplicas: 1
  maxReplicas: 2
  metrics:
  - type: Resource
    resource:
      name: cpu
      targetAverageUtilization: 60
---
apiVersion: v1
kind: Service
metadata:
  name: helloworld
spec:
  ports:
  - port: 80
  selector:
    app: helloworld
  type: NodePort

You can notice that our resources don’t specify a namespace. Those “base” manifests will never be deployed as is. They will serve as foundation for futurs deployments, each one of them in their own namespace. Furthermore, our base directory contains a kustomization.yaml file with all our YAML files handled by Kustomize:

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- helloworld-de.yaml
- helloworld-svc.yaml
- helloworld-hpa.yaml

Our next goal is to auto-generate YAML manifests for our two environments, preprod and prod. Let’s begin with preprod.

In our preprod directory, we have only one file kustomization.yaml:

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
bases:
- ../base/
namespace: preprod
namePrefix: preprod-

What does that mean ? First, we are going to load every manifests from base directory. Then we are going to patch the namespace of those manifests, and finally we will prefix our resources name with preprod-.

As said earlier, Kustomize is integrated with kubectl, you don’t need a third-party tool.

$ kubectl kustomize preprod

apiVersion: v1
kind: Service
metadata:
  name: preprod-helloworld
  namespace: preprod
spec:
  ports:
  - port: 80
  selector:
    app: helloworld
  type: NodePort
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: helloworld
  name: preprod-helloworld
  namespace: preprod
spec:
  revisionHistoryLimit: 2
  selector:
    matchLabels:
      app: helloworld
  template:
    metadata:
      labels:
        app: helloworld
    spec:
      containers:
      - image: particule/helloworld
        imagePullPolicy: Always
        name: helloworld
        ports:
        - containerPort: 80
          name: web
---
apiVersion: autoscaling/v2beta1
kind: HorizontalPodAutoscaler
metadata:
  name: preprod-helloworld
  namespace: preprod
spec:
  maxReplicas: 2
  metrics:
  - resource:
      name: cpu
      targetAverageUtilization: 60
    type: Resource
  minReplicas: 1
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: preprod-helloworld

You can easily see the differences between base and preprod.”

We can now do the same with prod with a new directory and a new kustomization.yaml:

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
bases:
- ../base/
namespace: prod
namePrefix: prod-
patchesStrategicMerge:
- replicas-patch.yaml

Same as preprod, we change the namespace and add a prefix to our resources names. But as our production deployment, we are going to patch our HorizontalPodAutoscaler. By default, it has a minimum of 1 replica (you can see this default value in the base directory). In production we want it to be set, a minimum, at 2 and, a maximum, at 4.

replicas-patch.yaml:

---
apiVersion: autoscaling/v2beta1
kind: HorizontalPodAutoscaler
metadata:
  name: helloworld
spec:
  minReplicas: 2
  maxReplicas: 4

It’s just a patch, you don’t need to redefine every spec, just the ones you want to change.

After generation:

apiVersion: v1
kind: Service
metadata:
  name: prod-helloworld
  namespace: prod
spec:
  ports:
  - port: 80
  selector:
    app: helloworld
  type: NodePort
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: helloworld
  name: prod-helloworld
  namespace: prod
spec:
  revisionHistoryLimit: 2
  selector:
    matchLabels:
      app: helloworld
  template:
    metadata:
      labels:
        app: helloworld
    spec:
      containers:
      - image: particule/helloworld
        imagePullPolicy: Always
        name: helloworld
        ports:
        - containerPort: 80
          name: web
---
apiVersion: autoscaling/v2beta1
kind: HorizontalPodAutoscaler
metadata:
  name: prod-helloworld
  namespace: prod
spec:
  maxReplicas: 4
  metrics:
  - resource:
      name: cpu
      targetAverageUtilization: 60
    type: Resource
  minReplicas: 2
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: prod-helloworld

Our maxReplicas and minReplicas values have been correctly updated.

We can now apply our two environments:

kubectl apply -k prod/
service/prod-helloworld created
deployment.apps/prod-helloworld created
horizontalpodautoscaler.autoscaling/prod-helloworld created

kubectl apply -k preprod/
service/preprod-helloworld created
deployment.apps/preprod-helloworld created
horizontalpodautoscaler.autoscaling/preprod-helloworld created

Let’s check it out:

kubectl -n preprod get hpa,deployments,services

NAME                                                     REFERENCE                       TARGETS         MINPODS   MAXPODS   REPLICAS   AGE
horizontalpodautoscaler.autoscaling/preprod-helloworld   Deployment/preprod-helloworld   <unknown>/60%   1         2         1          38m

NAME                                 READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/preprod-helloworld   1/1     1            1           38m

NAME                         TYPE       CLUSTER-IP    EXTERNAL-IP   PORT(S)        AGE
service/preprod-helloworld   NodePort   10.103.2.95   <none>        80:30339/TCP   38m
kubectl -n prod get hpa,deployments,services

NAME                                                  REFERENCE                    TARGETS         MINPODS   MAXPODS   REPLICAS   AGE
horizontalpodautoscaler.autoscaling/prod-helloworld   Deployment/prod-helloworld   <unknown>/60%   2         4         2          36m

NAME                              READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/prod-helloworld   2/2     2            2           36m

NAME                      TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
service/prod-helloworld   NodePort   10.107.91.161   <none>        80:31145/TCP   36m

Kustomize allows you to easily override YAML manifests without any templating knowledge. To go further.

Deploy Kustomize template with Flux CD

Kustomize is very handy to generate YAML manifests, but applying them with kubectl is not something you want to do if you to stick with the GitOps way.

We’ve been talking about Flux CD for some time now, espcially here and there. Today we will see how to make Flux and Kustomize work together.

Theoretically, Flux can work with, almost, every templating tools we talked about. But we are going to, once again, focus on Kustomize. After all this is what this article is about.

Let’s start from our previous works and create a new directory kustomize-flux based on our kustomize directory.

The structure is as follow:

.
├── .flux.yaml
├── base
│   ├── helloworld-de.yaml
│   ├── helloworld-hpa.yaml
│   ├── helloworld-svc.yaml
│   └── kustomization.yaml
├── preprod
│   ├── flux-patch.yaml
│   └── kustomization.yaml
├── prod
│   ├── flux-patch.yaml
│   ├── kustomization.yaml
│   └── replicas-patch.yaml
├── values-flux-preprod.yaml
└── values-flux-prod.yaml

What’s new ?

Flux deployment

First, we have two values Helm files which will help us to deploy two Flux instances into our cluster. Yes we use Helm to deploy Flux.

  • flux-preprod
git:
  pollInterval: 1m
  url: ssh://git@github.com/particuleio/gitops-demo.git
  branch: master
  path: kustomize-flux/preprod
syncGarbageCollection:
  enabled: true
manifestGeneration: true
additionalArgs:
-  --git-sync-tag=flux-sync-prod

This instance handles the preprod directory.

  • flux-prod
git:
  pollInterval: 1m
  url: ssh://git@github.com/particuleio/gitops-demo.git
  branch: master
  path: kustomize-flux/prod
syncGarbageCollection:
  enabled: true
manifestGeneration: true
additionalArgs:
-  --git-sync-tag=flux-sync-prod

This instance handles the prod directory.

We can deploy our two Flux instances with the following commands:

helm upgrade -i flux-prod fluxcd/flux --namespace prod --values values-flux-prod.yaml

helm upgrade -i flux-preprod fluxcd/flux --namespace preprod --values values-flux-preprod.yaml
kubectl -n prod get pods
NAME                                   READY   STATUS    RESTARTS   AGE
flux-prod-588b66bb64-fsw5q             1/1     Running   0          31m
flux-prod-memcached-546c87f4d4-8rwtw   1/1     Running   0          34m
kubectl -n preprod get pods
NAME                                      READY   STATUS    RESTARTS   AGE
flux-preprod-6bdc5dfb6-thqx9              1/1     Running   0          32m
flux-preprod-memcached-59f5454c6f-ldl25   1/1     Running   0          34m

.flux.yaml

This file is important, it will tell Flux how to generate our manifests.

version: 1
patchUpdated:
  generators:
    - command: kubectl kustomize .
  patchFile: flux-patch.yaml

We use the same command we used to manualy generate our manifests.

Furthermore, we tell Flux where to find the Flux specific patches. Those include the annotations Flux would use to automatically deploy a new release.

flux-patch.yaml

We already talked about Flux so you already know that Flux can apply YAML files to your cluster but it can also deploy new Docker images based on rules and filter such as semver.

This feature is handled with annotations on the deployment. Those can be different for each environment, you might want to forbid an automatic deployment in production but allow it in preprod.

For example, the file flux-patch.yaml in production:

  • prod/flux.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
  annotations:
    flux.weave.works/locked: "true"
    flux.weave.works/locked_msg: Lock deployment in production
    flux.weave.works/locked_user: Particule
  name: prod-helloworld
  namespace: prod

But in preprod we will automatically deploy every 1.x.x releases.

  • preprod/flux-patch.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
  annotations:
    flux.weave.works/automated: "true"
    flux.weave.works/tag.helloworld: semver:~1
  name: preprod-helloworld
  namespace: preprod

Once everything is ready in our git repository, we can activate Flux for this git repository and allow Flux to pull/push from/to it. To achieve that, you just need to fetch de public key of each Flux instances and add it on your GitHub account (Settings -> Deploy Keys).

kubectl -n preprod logs flux-pod | head

ts=2020-06-10T14:35:43.197547567Z caller=main.go:493 component=cluster identity.pub="ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDNYjo5gddG6fXJg75L3gf2mXNBV+DKd9LPz9ZqK2phhwD0fI7J2LajxKnTQGtxj72VBqU+lweEP8YV15auswyjraIYLgnLEE5POb6H8Cjz0vfVX61j3fcLnH77n48GQDKWo0rYQ9hxSmSthi/E1FGy41thxOYRm/IIErN8whKC0+YWDeKlwLNZatSSs/3XA4Q3eCpdPWwAot8sEWDOexUeno/GyaDhBiHm7gxjKkMPsnW8lj9ovtCzjt2H+vLV57neIcx4hx/bhWr3z+wVxkbnDv8zIfXaziXfy5Ueuz0e9sQ3pE1lbrTkeumQN0ekHNAdRjpIa89RRok6KTfBFN7w8iXoLvuSR1NZe9/aunZwqG0ZDGXQjmE8/AHy00QhXmDQT+1VJX00uq/0Jx87v6yiHV+I3LyA1Rn946S4qpxsvFAqDVyKrxFy6WwDSDhd4GHAlI/gFE6dPn8FXqQtL9NVWUxTqFs6svHTLNq6orQ92oKELcsTPHvUyvflj+5JW6k= root@flux-preprod-865d6d9666-6w4x7"

Once your keys are added, Flux will start deploying resources with Kustomize.

kubectl -n preprod logs -f flux-pod

ts=2020-06-10T14:11:29.602531682Z caller=sync.go:539 method=Sync cmd=apply args= count=3
ts=2020-06-10T14:11:29.98907821Z caller=sync.go:605 method=Sync cmd="kubectl apply -f -" took=386.489628ms err=null output="service/preprod-helloworld unchanged\ndeployment.apps/preprod-helloworld unchanged\nhorizontalpodautoscaler.autoscaling/preprod-helloworld unchanged"
kubectl -n preprod logs -f flux-pod

ts=2020-06-10T14:13:58.283012811Z caller=sync.go:539 method=Sync cmd=apply args= count=3
ts=2020-06-10T14:13:58.684738069Z caller=sync.go:605 method=Sync cmd="kubectl apply -f -" took=401.666475ms err=null output="service/prod-helloworld unchanged\ndeployment.apps/prod-helloworld unchanged\nhorizontalpodautoscaler.autoscaling/prod-helloworld unchanged"

Our resources are correctly applied into our cluster with Flux.

Auto-deployment test

We are going to push a new image to the Docker Registry, the 1.1, and we will see what Flux are going to do with it.

docker push particule/helloworld:1.1

Flux polls the Docker Registry every 5 minutes. Flux will detect the new image and will update our git repository :

Workflow in action

What’s really happening in the cluster ? Here’s the Flux’s workflow deployment for the initial deployment:

  • Flux generates YAML manifests with Kustomize
  • Flux applies the flux-patch.yaml files
  • Flux applies the manifests into the cluster

For a Docker image update:

  • Flux scans the Docker Registry
  • Flux detects the new image
  • Flux updates the corresponding flux-patch.yaml file and push it to the git repository
  • Flux met à jour sur le dépôt git le fichier flux-patch.yaml correspondant
  • The initial workflow apply

To go further

This article is inspired by the Flux community which provides git repository as examples to easily deploy manifests with Kustomize.

The official documentation for this feature is also available.

Conclusion

We already talked about Flux many times. It’s a simple tool full of features that can acheive great things with a small amout of times.

Flux can centralize your Kubernetes manifests and use a GitOps workflow while keeping flexibility with your templating tools.