Streamlining Kubernetes Deployments with GitLab CI/CD and Helm Charts

Software engineer, passionate about learning and exploring distributed systems, tinkering around with Frontend, learning on the go
Using helm and gitlab CI CD for deployments
Helm is a package manager for managing Kubernetes manifests, and it allows for easy management of deploying them on a kubernetes namespace, by giving you great options such as version management, patching, modification and rollbacks with the help of a few commands 🔥.
It is the go to package manager for Kubernetes, as it provides an easy way to also manage multiple environment specific configuration values, and also providing out of the box go-templating which can be helpful in resolving commonly used values with the help of template aliases.
Helm introduces a concept of “Helm Charts” where a chart basically means that your kubernetes manifests (Like Deployment, Config-Maps, Services & Pod autoscalers) will be contained in an organized archive, along with a default values file, which is ready to be installed with a single command on a kubernetes namespace.
👉Let’s take a look at some of the most common use-cases for using helm and how it can make deployments easy when used with GitLab CI-CD
Basic structure for creating Helm charts
🎯It is recommended to have a simple directory structure like below that helps Helm to parse the required objects for creating a chart

The above structure defines the 3 important objects needed for creating a helm chart:
The
templatesdirectoryChart.yamlfilevalues.yamlfile
Templates
The templates directory houses any of your valid kubernetes manifest that will be part of your helm chart.
Below is a simple deployment.yaml file
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}-{{ .Chart.Name }}
labels:
app: {{ .Chart.Name }}
environment: {{ .Values.environment }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app: {{ .Chart.Name }}
template:
metadata:
labels:
app: {{ .Chart.Name }}
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
ports:
- containerPort: {{ .Values.service.port }}
env:
- name: ENVIRONMENT
value: "{{ .Values.environment }}"
resources:
limits:
memory: "{{ .Values.resources.limits.memory }}"
cpu: "{{ .Values.resources.limits.cpu }}"
requests:
memory: "{{ .Values.resources.requests.memory }}"
cpu: "{{ .Values.resources.requests.cpu }}"
Values YAML file
This file is responsible to provide default values in case any environment specific values.yaml is unable to provide the value for the field. It’s a good practice to give default values to prevent any issues during creation of the chart.
# Replica configuration
replicaCount: 1
# Image configuration
image:
repository: my-app-container
tag: latest
# Service configuration
service:
port: 80
# Environment
environment: dev
# Resource limits and requests
resources:
limits:
memory: "512Mi"
cpu: "500m"
requests:
memory: "256Mi"
cpu: "250m"
As you can see, the default values file will provide the default values for e.g. {{ .Values.replicaCount }} gets resolved by this file as 1 if none of the specific values file (present in values directory) are able to resolve this place holder. This is an example of how go-templating works in case of Helm.
Chart YAML file
This file will have the basic metadata for creation of a chart. A sample file consists of the below fields (for a look on all the fields this file supports, check out this link)
apiVersion: v1
description: A helm chart
name: my-helm-chart
version: 1.0.0
In the deployment file we see something like {{ .Chart.Name }} which will get resolved by the name key present in this file. Helm has built-in objects, one of which is this Chart yaml file and it supports other resolvers like {{ .Chart.Version }} too.
Note that following the version field with semantic versioning is a good way to version your helm charts.
Using GitLab Jobs to Build, Package and Install the chart
When using GitLab CI/CD, we can simply create common jobs to build the helm chart, package it and push it to a helm repository, and use environment-specific stages to install the same helm chart we originally pushed by applying specific configuration on the charts.
Here’s how it looks:
Our basic CI/CD pipeline definition
stage:
- helm_package
- helm_release
- deploy_dev
- deploy_sit
variables:
HELM_REPO: "https://example-helm-repo.com/charts"
HELM_REPO_USERNAME: $HELM_REPO_USERNAME
HELM_REPO_PASSWORD: $HELM_REPO_PASSWORD
HELM_DIR: deployments/helm
.helm_build:
image: alpine/helm:3.8.0
script:
- helm package $HELM_DIR --destination $HELM_DIR --version $HELM_CHART_VER $HELM_OPTIONS
artifacts:
- $HELM_DIR/*.tgz
helm-package:
stage: helm_package
extends: .helm_build
before_script:
# Extract the 'version' value from Chart.yaml
- export HELM_CHART_VER=$(grep '^version:' $HELM_DIR/Chart.yaml | awk '{print $2}')
rules:
- if: '$CI_COMMIT_BRANCH == "master"'
changes:
- deployments/helm/**/*
helm-release:
image: alpine/helm:3.8.0
stage: helm_release
needs: [helm-package]
script:
# Get the name of the helm archive from the artifacts path of helm-package job
- CHART_FILE=$(ls $HELM_DIR/*.tgz | head -n 1)
- echo "Found chart: $CHART_FILE"
- helm repo add my-repo $HELM_REPO
# Push the helm archive to the remote repo
- helm push $CHART_FILE my-repo
rules:
- if: '$CI_COMMIT_BRANCH == "master"'
changes:
- deployments/helm/**/*
The above pipeline defines the key jobs for packaging our chart and releasing them to a public or internal helm repo, depending on your requirements.
The
helm packagecommand packages all the contents of our helm directory into a helm chart in the form of a zip archive, with the name typically being set as<chart-name>-<version>.tgzThe
artifactskeyword tells GitLab to upload the generated artifacts from the configured helm directory so that subsequent stages can use the artifacts.The variables starting with
$are encrypted variables that can be in the GitLab Environment variables to ensure that sensitive data like repository username and password can be masked and encrypted in the jobs.The
helm releasestage needs thehelm packagestage in order to start, and it will push the generated archive from the package stage to a remote repository using thehelm pushcommandNote that these stages will only run when there are any changes in the helm directory
deployments/helmAnother thing is the versioning, which for simplicity purposes I have kept to be parsed from the
Chart.yamlfile’sversionparameter, but for practical purposes, it will be helpful to have a release stage that takes care of semantic versioning and generates a version based off the previous commits on the master branch, and then uses that version to version the helm chart.
Defining the environment specific jobs to take advantange of the helm charts
Now that we have our helm charts packaged and published to our remote repository, we can now leverage the helm commands to seamlessly deploy and undeploy our helm charts depending on the environment.
.helm_deploy:
image: alpine/helm:3.8.0
script:
- export HELM_CHART_NAME=$(grep '^name:' $HELM_DIR/Chart.yaml | awk '{print $2}')
- helm repo add my-repo $HELM_REPO
- helm repo update
- helm upgrade --install $HELM_RELEASE_NAME my-repo/$HELM_CHART_NAME $HELM_OPTS --namespace $NAMESPACE
deploy-dev:
stage: deploy_dev
extends: .helm_deploy
before_script:
- export HELM_OPTS="-f "deployments/helm/values.yaml" -f "deployments/helm/$ENVIRONMENT/values.yaml" --version $HELM_DEV_VER --set environment=$ENVIRONMENT"
variables:
- ENVIRONMENT: dev
- HELM_RELEASE_NAME: dev-release
- NAMESPACE: my-kube-namespace
rules:
- if: '$CI_COMMIT_BRANCH == "master"'
when: manual
deploy-sit:
stage: deploy_sit
extends: .helm_deploy
needs:
- deploy-dev
before_script:
- export HELM_OPTS="-f "deployments/helm/values.yaml" -f "deployments/helm/$ENVIRONMENT/values.yaml" --version $HELM_SIT_VER --set environment=$ENVIRONMENT"
variables:
- ENVIRONMENT: sit
- HELM_RELEASE_NAME: sit-release
- NAMESPACE: my-kube-namespace
rules:
- if: '$CI_COMMIT_BRANCH == "master"'
when: manual
Let’s understand the above stage definitions for dev and sit environments
The common
helm-deploystage initializes a helm repository where the charts can be downloaded from.The
HELM_RELEASE_NAMEis a variable that comes from the stages that extend thehelm-deploystage, as we need dynamic release names depending on the environment we are doing the deployment on.As our packaging and deployment pipeline is same, we can use the same
HELM_CHART_NAMEto install the chart we just pushed in the previous step.The
HELM_OPTSis a variable that will also come from env-specific stages that we can pass to manually override any variables in the helm chart or pass any env specific values file, like for e.g.-f “deployments/helm/$ENVIRONMENT/values.yaml”which passes the particular environment values file to the chartNotice that the ordering of
-f “deployments/helm/values.yaml” -f “deployments/helm/$ENVIRONMENT/values.yaml”is important, as this tells helm to first apply the default values file to the chart, and then apply the env specific values file, so that we always have env specific values for the fields and only if we do not define any value in the env specific file, we use its default value from the default values file.The
HELM_DEV_VERandHELM_SIT_VERare two variables that can be passed during the runtime of the job, as it is possible to use two separate versions of the same chart on different environments for practical purposes.
Creating deployments like this is just one of the ways to use helm and GitLab together for doing automated deployments.
Hopefully this article helps you understand the basics of doing kubernetes deployments using Helm and GitLab.
Thanks for reading it !


