Gitlab CI/CD and Helm + file

Using CI/CD, Helm and Helmfile to deploy

Gitlab CI/CD and Helm + file

Using CI/CD, Helm and Helmfile to deploy

A short post about how I use GitLab CI/CD to automagically deploy to Kubernetes using Helm and Helmfile.

First things first, this is for a setup that isn't completely CI/CD, in some situation we can't simply continuously update the running app to latest. It would however still be nice to keep it managed and tracked via version control, this is where Helmfiles come in.

Helm(file)?

Helm is a package manager for Kubernetes, Helmfile is for managing your Helm (chart) deployments via version control. Helm creates charts that keep track of how and what to do for a deployment. This is based on images from a Docker registry. These charts can be published in multiple ways, for example, the two biggest can be found here. Helmfile(‘s) lists these charts, from multiple repositories, with additional settings/configuration.

Helmfile

The Helmfile will be in the root of our git repos and will look something like this:

---
environments:
  default:
    values:
      - environment/default/values.yaml
    secrets:
      - environment/default/secrets.yaml
    missingFileHandler: warn
---
repositories:
  - name: stable
    url: https://kubernetes-charts.storage.googleapis.com
  - name: incubator
    url: https://kubernetes-charts-incubator.storage.googleapis.com
  - name: bitnami
    url: https://charts.bitnami.com/bitnami
  - name: myrepo
    url: git+https://{{ env "HELM_REPO_USER" | default "helm" }}:{{ requiredEnv "HELM_REPO_PASS" }}@gitlab.com/patvdleer/helm-repo@/charts

helmDefaults:
  # no verify due to unable to sign https://github.com/helm/helm/issues/2843#issuecomment-449047847
  verify: false
  wait: true
  timeout: 720
  recreatePods: false
  force: true

templates:
  default: &default
    chart: myrepo/{{`{{ .Release.Name }}`}}
    namespace: {{ .Namespace }}
    missingFileHandler: Debug
    wait: true
    values:
      - environment/{{`{{ .Environment.Name }}`}}/{{`{{ .Release.Name }}`}}.yaml
    secrets:
      - environment/{{`{{ .Environment.Name }}`}}/{{`{{ .Release.Name }}`}}-secrets.yaml

releases:
{{ if env "DEPLOY_NGINX" | default false }}
  - name: nginx-ingress
    <<: *default
    chart: stable/nginx-ingress
    version: ^1.26.2
    namespace: ingress-nginx
    set:
      - name: rbac.create
        value: true
      - name: controller.image.pullPolicy
        value: "Always"
      - name: controller.stats.enabled
        value: true
      - name: controller.service.type
        value: "ClusterIP"
      - name: controller.kind
        value: "DaemonSet"
      - name: controller.daemonset.useHostPort
        value: true
{{ end }}
  - name: mysql
    version: 1.3.1
    chart: stable/mysql
    values:
      - MYSQL_ROOT_PASSWORD: super_super_secret!!!
    set:
      - name: readinessProbe.initialDelaySeconds
        value: 120
      - name: readinessProbe.timeoutSeconds
        value: 5
  - name: stable/mongo
    <<: *default
    version: 7.2.9
  - name: redis
    <<: *default
    version: 10.2.1
    set:
      - name: usePassword
        value: true
  - name: fluentd
    <<: *default
    values:
      - environment/fluentd.yaml
    chart: stable/fluentd
    version: 2.1.3

For more examples see https://github.com/roboll/helmfile/tree/master/examples.

Don't forget to set the HELM_REPO_PASS as a environment variables (in GitLab) so Helm(file) can access your private chart repo.

Dockerfile

Why Docker? I needed a bunch of tools that aren't available in other images (yet?) so I built it myself.

I need the following:

  • Go
  • Helm + plugins
    • helm-git (to use a git repo as a chart repo)
    • helm-diff
    • helm-secrets
  • Helmfile
  • kubectl
  • SOPS (depends of helm-secrets)
FROM golang:1.13-buster
ENV DEBIAN_FRONTEND noninteractive

RUN apt-get update && apt-get install -y locales \
    && localedef -i en_US -c -f UTF-8 -A /usr/share/locale/locale.alias en_US.UTF-8
ENV LANG en_US.utf8

# SOPS
ENV SOPS_VERSION 3.5.0
RUN DPKG_ARCH=$(dpkg --print-architecture); \
    wget -q https://github.com/mozilla/sops/releases/download/v${SOPS_VERSION}/sops_${SOPS_VERSION}_amd64.deb; \
    apt install ./sops_${SOPS_VERSION}_amd64.deb; \
    rm sops_${SOPS_VERSION}_amd64.deb

# HELM
ENV HELM_VERSION v3.0.1
RUN wget -q https://get.helm.sh/helm-${HELM_VERSION}-linux-amd64.tar.gz; \
    tar -zxvf helm-${HELM_VERSION}-linux-amd64.tar.gz; \
    rm helm-${HELM_VERSION}-linux-amd64.tar.gz; \
    chmod +x linux-amd64/helm; \
    mv linux-amd64/helm /usr/bin/helm

# HELMFILE
ENV HELMFILE_VERSION v0.94.1
RUN wget -q https://github.com/roboll/helmfile/releases/download/${HELMFILE_VERSION}/helmfile_linux_amd64 -O /usr/bin/helmfile; \
    chmod +x /usr/bin/helmfile

# HELM PLUGINS
RUN helm plugin install https://github.com/aslafy-z/helm-git
RUN helm plugin install https://github.com/databus23/helm-diff
RUN helm plugin install https://github.com/futuresimple/helm-secrets

# kubectl
RUN curl -LO https://storage.googleapis.com/kubernetes-release/release/`curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt`/bin/linux/amd64/kubectl; \
    chmod +x ./kubectl; \
    mv ./kubectl /usr/local/bin/kubectl

Pipeline yaml

First lets build the image we'll use later on to actually deploy, as listed below, there is an only/changes/Dockerfile in here. The reason is simple, the image isn't affected by the content of the repository, if the Helmfile changes it won't affect the image so there is no reason to rebuild it.

image: docker:git

services:
  - docker:dind

variables:
  DOCKER_DRIVER: overlay

stages:
  - build
  - lint
  - deploy
  - cleanup

build-image:
  stage: build
  before_script:
    - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
  script:
    - docker build --pull -t "$CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME" .
    - docker push "$CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME"
    - docker push "$CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:latest"
  only:
    changes:
      - Dockerfile

Lint

The next step is linting, this is the first step where we use the above built image. This works because the image is run with the repository as context, i.e. I can run the script and it will be run against my Helmfile.

Yes this can be done automagically via git hook pre-commit or pre-push but I don't always have everything installed on my local workstation.

lint:
  image: $CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME
  stage: lint
  script:
    - helmfile lint

Deploy && Cleanup

Once I know the helmfile is valid I proceed to the next step and deploying it to Kubernetes. This step is split up into two parts, one for branches other than master and master itself. The difference is the subdomain and the cleanup step.

deploy_review:
  image: $CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME
  stage: deploy
  script:
    - helmfile --namespace $KUBE_NAMESPACE sync
  # when: manual
  variables:
    BASE_DOMAIN: $CI_COMMIT_REF_NAME.0x43.nl
  environment:
    name: review/$CI_COMMIT_REF_NAME
    url: http://$CI_COMMIT_REF_NAME.0x43.nl
    on_stop: stop_review
  only:
    - branches
  except:
    - master

stop_review:
  image: $CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME
  stage: cleanup
  when: manual
  script:
    - echo "Stopping" # script is required
  environment:
    name: review/$CI_COMMIT_REF_NAME
    action: stop

deploy_master:
  stage: deploy
  script:
    - helmfile --namespace $KUBE_NAMESPACE sync
  variables:
    BASE_DOMAIN: 0x43.nl
  environment:
    name: production
    url: http://www.0x43.nl
  when: manual
  only:
    - master

The rest should be pretty straight forward, Kubernetes config and setup can all be managed via GitLab.


See also