Skip to main content

RKways tech blog

Extending GitLab Runner Helm chart with Qbec

GitLab Runner is perfectly capable of spawning multiple different runners from a single instance. It just need to register itself into GitLab few times. Unfortunately the GitLab Runner Helm chart supports only single registration call. On the other hand - registration scripts are executed from ConfigMap and there is even a pre-entrypoint-script hook ready. In the end it’s a matter of a few changes in ConfigMap to make GitLab Runner register per-architecture runners with different tags.

Enter the Qbec

The qbec project page describes it as a tool to configure and create Kubernetes objects on multiple environments. Sure, but to me its real strength comes from the fact that it’s based on jsonnet. Let’s see how it will help with the necessary changes to the GitLab Runner Helm chart.

Rendering the Helm chart

There are two ways now to render Helm chart in qbec:

This article and the underlying project still uses the older expandHelmTemplate function.

Working with the chart objects

Rendering Helm chart with expandHelmTemplate function returns an array of Kubernetes objects. It’s not a particularly handy format if you want to apply some changes to them. Two helper functions from k8slab-kube-libsonnet project address this problem:

The intermediate structure can be easily navigated by the getByPath function.

local k8slab = import 'k8slab-kube-libsonnet/k8slab.libsonnet';
local expandHelmTemplate = std.native('expandHelmTemplate');

local chartPath = ...;
local chartValues = ...;
local chartOpts = {
  nameTemplate: ...,
  namespace: ...,
  thisFile: std.thisFile,
  verbose: true,
};

local renderedChart = expandHelmTemplate(chartPath, chartValues, chartOpts);
local renderedChartTree = k8slab.arrayByKindAndName(renderedChart);

local configMapName = 'xxx';

local changes = {
  ConfigMap+: {
    [configMapName]+: {
      data+: {
        'some-added-data-field': k8slab.getByPath(renderedChartTree, 'ConfigMap.xxx.data.yyy', ''),
      }
    }
  }
};

local output = k8slab.arrayFromKindAndName(renderedChartTree + changes);
output

You can see the described functions in action:

Customizations

The Helm chart values parameter holds a dummy runners.config entry. Real entries are generated from the template string and parameters. This template is a multi-line string with %(name)s placeholders as you can see. There are two sets of parameters in the arm64 and the amd64 fields. They are made of some common parameters possibly overwritten by per-environment values and the final local customizations.

Rendered templates are put into the GitLab Runner ConfigMap object.

With the multiple runners.config entries prepared now it’s time for the registration hook.

The original code is replaced with exit 0. Slightly patched version is kept under different name in the ConfigMap - it’s modified to use /configmaps/${CONFIG_TEMPLATE_K8SLAB} configuration file instead of hard coded /configmaps/config.template.toml path. This changed registration script is called multiple times with different CONFIG_TEMPLATE_K8SLAB and RUNNER_TAG_LIST variables in the pre-entrypoint-script.

That’s it - now a single manager Pod registers two GitLab runners.

Final notes

Later k8slab-infra-gitlab-runners versions are slightly more complicated as the code now is better suited for more than two registrations and have some security improvements. The links in this article points to a simpler version which should be easier to understand if you don’t have much experience with jsonnet, especially with features like object and array comprehension.