About

This is a simple introduction to GitOps using GitLab, Flux, and Helm charts. The objective of this is to deploy a small microservice application written in Go onto a Kubernetes cluster with minimal manual interactions. There are a few notes to consider; the pipelines, triggers, conditions, etc. are kept in the simplest form to keep things easier to understand and replicate. There are many strategies to test, build, and deploy applications as well as create deployment pipelines. I believe readers can customize, improve, and make it more secure to suit their needs.

I used GitLab Community Edition (v17.0.1) with a local Kubernetes cluster (v1.28.10) and Flux (v2.3.0) deployed on Proxmox VMs. Author’s full repo can be found here.

Disclaimer: The source code used in this is used for learning purposes only. If you are using them for production work, please consider your project infrastructure and security needs.

Repo structure

├── app
│ ├── api
│ │ └── maths
│ ├── client
│ └── internal
├── charts
│ ├── client
│ └── maths-api
└── clusters
└── playground
├── calculator
└── flux-system

 

Above is the structure of the repo. app directory contains the application source code. charts directory contains the helm charts for two services. clusters directory contains Kubernetes and flux resources to deploy the application onto a cluster.

Application

The application is a tiny calculator which takes an expression from the user and returns the result. client service handles the front end and maths-api takes the expression, resolves it and returns an answer. maths-API also has a few unit tests configured. Both services have Docker files to create an Alpine Linux-based docker image.

Client service and the maths-api service both use env variables to get the port number to listen to. The client service also gets the endpoint to the maths-api through an env variable.

The version numbers for both client service and maths-api are stored in the .version file in the application source root directory. Changes to this file will be used to trigger test, build, and deploy pipeline jobs.

Building and releasing the application using GitLab-CI

stages:
- test
- build
- release

include:
- local: '$CI_PROJECT_DIR/app/client/.gitlab-ci.yml'
- local: '$CI_PROJECT_DIR/app/api/maths/.gitlab-ci.yml'
- local: '$CI_PROJECT_DIR/charts/client/.gitlab-ci.yml'
- local: '$CI_PROJECT_DIR/charts/maths-api/.gitlab-ci.yml'

 

Above is the .gitlab-ci.yml file in the root directory of the repo. The four CI configuration files included need to be created for the application and the helm charts. Let’s start with the application building.

CI variables

GL_PAT: Password or access token for the docker registry
IMAGE_REGISTRY: Docker registry host
IMAGE_REGISTRY_USER: Docker registry username


Client service

Complete CI file is here.

Build stage

Go language version 1.22 docker image is used to build the client service. The build stage will compile the source code and retain the executable with the .version file as an artefact. There is a filter setup to trigger the build pipeline. The pipeline only triggers when the .version file is modified and committed.

Note: Here I used the .version file for simplicity. This can be set up using many ways as per the language/framework the application is written in. Using git tags is another alternative. I picked the .version file because this is a mono repo and both services will have separate version numbers.

build-client:
image: golang:1.22.4-alpine3.20
stage: build
artifacts:
paths:
- "$CI_PROJECT_DIR/app/client/client"
- "$CI_PROJECT_DIR/app/client/.version"
script:
- cd "$CI_PROJECT_DIR/app/client/"
- go build -o client
only:
changes:
- app/client/.version


Release stage

The release stage is dependent on the build stage. This stage uses a docker image with docker-in-docker service to build the docker image for the client service using the included docker file.

FROM alpine:3.20
ADD client /client
ADD frontend /frontend
CMD ./client

 

The docker file is a very simple one. It uses Alpine Linux image and copies the executable and the frontend directory, which contains HTML and JavaScript source files.

Just like the build stage, the release stage also triggers when there is a change to the .version file. All CI variables stated above are needed for this stage to log in to the container repo and push the image. .version file is used to get the version number to add it as a tag to the image.

release-client-docker-img:
image: docker:27.1.1
stage: release
dependencies: 
- build-client
services:
- name: docker:27.1.1-dind
alias: docker
variables:
DOCKER_HOST: tcp://docker:2375/
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: ""
script:
- echo $GL_PAT | docker login $IMAGE_REGISTRY -u $IMAGE_REGISTRY_USER --password-stdin
- VERSION=`cat $CI_PROJECT_DIR/app/client/.version`
- cd "$CI_PROJECT_DIR/app/client/"
- docker build -t client:latest -t $IMAGE_REGISTRY_PATH/client_v:$VERSION -t $IMAGE_REGISTRY_PATH/client_v:latest .
- docker image push $IMAGE_REGISTRY_PATH/client_v --all-tags
only:
changes:
- app/client/.version


maths-api service

CI pipelines and the docker file for building and releasing the maths-api service are similar to the client service. The only addition is the test stage, which uses Golang docker image to run the unit tests under the internal maths module. The build stage is dependent on the test stage and the release stage is dependent on the build stage. This means if any unit tests fail, the whole pipeline will stop.

FROM alpine:3.20
ADD maths_api /maths_api
CMD ./maths_api
test-maths-api:
image: golang:1.22.4-alpine3.20
stage: test
script: 
- cd "$CI_PROJECT_DIR/app/internal/maths"
- go test .
only:
changes:
- app/api/maths/.version

build-maths-api:
image: golang:1.22.4-alpine3.20
stage: build
artifacts:
paths:
- "$CI_PROJECT_DIR/app/api/maths/maths_api"
- "$CI_PROJECT_DIR/app/api/maths/.version"
dependencies:
- test-maths-api
script: 
- cd "$CI_PROJECT_DIR/app/api/maths"
- go build -o maths_api
only:
changes:
- app/api/maths/.version

release-maths-api-docker-img:
image: docker:27.1.1
stage: release
dependencies: 
- build-maths-api
services:
- name: docker:27.1.1-dind
alias: docker
variables:
DOCKER_HOST: tcp://docker:2375/
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: ""
script:
- echo $GL_PAT | docker login $IMAGE_REGISTRY -u $IMAGE_REGISTRY_USER --password-stdin
- VERSION=`cat $CI_PROJECT_DIR/app/api/maths/.version`
- cd "$CI_PROJECT_DIR/app/api/maths"
- docker build -t maths_api:latest -t $IMAGE_REGISTRY_PATH/maths_api_v:$VERSION -t $IMAGE_REGISTRY_PATH/maths_api_v:latest .
- docker push $IMAGE_REGISTRY_PATH/maths_api_v --all-tags
only:
changes:
- app/api/maths/.version

 

Once all the pipelines for both services are completed, docker images will be pushed to the container image registry.

Setting up Flux

Configuring Flux is simple enough. I followed the official docs for bootstrapping Flux for GitLab (https://fluxcd.io/flux/installation/bootstrap/gitlab/). As mentioned in the repo documentation, to set up Flux, Kubernetes cluster admin rights and full access to the GitLab project are needed. In the repo, clusters/playground is the path for the flux-system manifests.

Helm charts

Link to Helm chart source is here.

Helm charts for client and maths-api service are also similar. The only difference is the environmental variables specifying the port numbers to listen to, and the maths-api endpoint is specified for the client service. values.yaml file specifies the app name, port, replica count, env vars, and image name with the tag.

Building the Kubernetes manifest file

CI variables

CI_KNOWN_HOSTS: Known host entry for GitLab host taken from ~/.ssh/known_hosts
SSH_PUSH_KEY: Private key for the deploy key RSA key pair

 

There are build and release stages implemented for the helm charts in /charts/<service>/.gitlab-ci.yml files. The build stage creates the Kubernetes manifest file for the service using the helm template command. The command will use the values specified in the values.yaml file in the helm chart and output the filled-up template. The output is then redirected to the file /clusters/playground/calculator/<service>/release.yml. The path for the release.yml file is already created on the repo beforehand to avoid any errors during the build. This manifest file will be used by Flux to ultimately deploy the application to the Kubernetes cluster. For this stage, alpine/helm image is used.

Even though the build stage creates the release.yml manifest to be deployed, it is not yet readable to Flux. The release stage which is dependent on the build stage will take the release.yml file then it will commit it to the repo. For this stage, a GitLab project deploys key with write access is used to gain write access to the repo. alpine/git image is used with the release stage.

Both build and release stages are triggered by any changes to /charts/<service>/ directory. Again since both services are very similar, the build and release stages for both services are also similar. The only difference is the paths.

build-maths-api-helm-chart:
image: 
name: alpine/helm:3.15.1
entrypoint: [""] 
stage: build
artifacts:
paths:
- "$CI_PROJECT_DIR/clusters/playground/calculator/maths-api/release.yml"
script:
- helm template maths-api $CI_PROJECT_DIR/charts/maths-api > $CI_PROJECT_DIR/clusters/playground/calculator/maths-api/release.yml
only:
changes:
- charts/maths-api/**/*

release-maths-api-helm-chart:
image: 
name: alpine/git:v2.45.1
entrypoint: [""]
stage: release
dependencies:
- build-maths-api-helm-chart
before_script:
- mkdir ~/.ssh/
- echo "${CI_KNOWN_HOSTS}" > ~/.ssh/known_hosts
- echo "${SSH_PUSH_KEY}" > ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
- git config user.email "ci@gitlab.local"
- git config user.name "CI"
- git remote remove ssh_origin || true
- git remote add ssh_origin "git@$CI_GIT_HOST:$CI_PROJECT_PATH.git"
script:
- git add $CI_PROJECT_DIR/clusters/playground/calculator/maths-api/release.yml
- git commit -m "Adding maths-api heml template output"
- git push ssh_origin HEAD:$CI_COMMIT_REF_NAME
only:
changes:
- charts/maths-api/**/*

 

Next steps

Since this is done in an existing local environment, there is no IaC to provision any infrastructure. But tools like Terraform or Ansible can be configured in the same repo with similar CI pipelines to automate/autoscale infrastructure provisioning as needed.

Conclusion

Now that all the CI stages are configured, once the source code changes for either service and .version file are updated with the new version, the build stage and docker image release stage will be triggered. During these stages, the unit test will be running, then the source code will be compiled, and finally, the docker image will be built with the version tag and pushed into the container repo.

Then when for example in the Helm chart’s values.yaml file is modified with the new image tag, and another CI pipeline with the build and release stage will be triggered. Which will create a single manifest file for Flux and commit it to the same repo.

With Flux running, it will see the new manifest and it will either install or upgrade the manifest into the Kubernetes cluster.

This is a very simplistic take on GitOps for understanding the process and steps behind it. There are many ways to improve this. For example, you can set this up to be deployed to dev, QA, and prod environments. You can have this configured to be run only when MR is approved for the main/master branch. You can also add or remove extra CI stages as needed, especially for provisioning infrastructure.

GitLab with Flux is a very flexible and straightforward way to get into GitOps.

Author

Dhamith Hewamullage is a Senior DevOps Engineer @ CMS. You can read more of his blog posts at https://blog.dhamith.me
Dhamith’s full code sample on GitOps can be found here

Author : Admin
Published Date September 2, 2024
Share Post