Skip to main content

Command Palette

Search for a command to run...

Streamlining Kubernetes Deployments with GitLab CI/CD and Helm Charts

Updated
7 min read
Streamlining Kubernetes Deployments with GitLab CI/CD and Helm Charts
A

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 templates directory

  • Chart.yaml file

  • values.yaml file

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.

  1. The helm package command 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>.tgz

  2. The artifacts keyword tells GitLab to upload the generated artifacts from the configured helm directory so that subsequent stages can use the artifacts.

  3. 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.

  4. The helm release stage needs the helm package stage in order to start, and it will push the generated archive from the package stage to a remote repository using the helm push command

  5. Note that these stages will only run when there are any changes in the helm directory deployments/helm

  6. Another thing is the versioning, which for simplicity purposes I have kept to be parsed from the Chart.yaml file’s version parameter, 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

  1. The common helm-deploy stage initializes a helm repository where the charts can be downloaded from.

  2. The HELM_RELEASE_NAME is a variable that comes from the stages that extend the helm-deploy stage, as we need dynamic release names depending on the environment we are doing the deployment on.

  3. As our packaging and deployment pipeline is same, we can use the same HELM_CHART_NAME to install the chart we just pushed in the previous step.

  4. The HELM_OPTS is 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 chart

  5. Notice 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.

  6. The HELM_DEV_VER and HELM_SIT_VER are 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 !

Connect with me on Linkedin or X