La vie c’est comme une boîte de chocolat, si tu n’es pas assez rapide, il reste toujours les chocolats à la liqueur dégueu.
Alors comme on ne veut pas s’enfiler une boite complète de mon chéri, on va sortir les doigts de l’attribut fessier et on va bricoler du sale.

Bonne année à tous, j’espère que vous n’avez pas commencé l’année la tête dans la cuvette 🤪
J’adore vraiment Gitlab-ci, ça permet de faire de la CI/CD vraiment flexible et ça s’adapte à tout, à condition de savoir tordre un peu l’outil.
Mais par contre la gestion des runners j’ai toujours trouvé ça chiant.
Ça tombe bien aujourd’hui j’ai un peu de temps pour regarder les différentes options possibles pour la gestion et la configuration des runners.
gitlab runner en dur
Comme dans la vraie vie, les projets sont toujours un peu déviants du standard, ça finit toujours pas bricoler des trucs chelous dans les runners, je préfère avoir des runners en dur en mode compute.
Mais par contre je n’utilise jamais les runners gitlab en mode SSH, c’est bien mieux d’utiliser les runners avec docker pour exécuter la charge de travaille dans des containers.
Ça évite d’avoir à installer les dépendances dans le runners docker en dur et maintenir une liste infâme de dépendances et une brique qu’on a toute la peine du monde à dupliquer et maintenir.
Parceque’à un moment ou un autre, ça ne rentre pas dans Kaniko, un build un peu déviant qui dure plus d’une heure, qui balance une quantité de logs qui dépasse la limite, une configuration particulière qui empêche l’utilisation de Kaniko pour builder des images docker, alors faut binder un socket docker dans la conf du runner pour faire du docker Dind.
Kaniko pour ceux qui ne connaissent pas, c’est une astuce de Google pour builder des images docker dans un container docker sans mode privilégié et sans socket binding. C’est très sympa quand on build des trucs simples, mais dès qu’on fait un truc un peu tordu, les limites sont très vite franchies.
Seulement voilà, c’est usant de trainer des computes runners avec presque rien dedans et ça coûte des sous inutilement 90% du temps.

Un truc qui m’énerve grave sur Gitlab, c’est le token de runner qu’on doit récupérer en mode clic clic dans l’interface.
Celui qui permet l’enregistrement manuel du runner qui aura un nouveau token attribué ensuite qui identifie ce runner unique.
C’est bon quoi ! on est en 2022, on sait lancer des VMs au km à coup de terraform Packer. 🤷
Pourquoi on ne pourrait pas enregistrer 200 runners via API,récupérer les tokens et les injecter dans la conf des runners au moment du provisioning des machines. 🤔
Tu es qui Gitlab pour m’imposer de cliquer dans ton UI et me traîner des enregistrements manuels ? Je vous ai déjà dit que je déteste Gitlab ? Non ?
Et bien, j’aime bien Gitlab, mais des fois ils font chier, voilà c’est dit. 🤷
Vous allez me dire gnagnagna tu n’as pas lu la doc, y’a des trucs qui existent pour faire des runners autoscaler.
On va déjà commercer par là pour une fois, je fais le tour des solutions qui existent et après on va voir si je change d’idée.

Gitlab runner Kubernetes
Pourquoi pas ne pas essayer les gitlab runners dans kubernetes ?
Pour éviter de traîner des nœuds kube constamment allumés, on va tenter de rentrer un GKE autopilot.
Pour les gens qui ne sont pas trop familiers avec GKE autopilot, c’est un Kubernetes managé qui s’occupe lui-même de provisionner les nœuds compute en fonction des ressources demandées (spoiler, ça n’existe que sur GCP pour le moment).
Ça permet de ne plus s’occuper du tout des nœuds et de ne payer que pour les ressources consommées.
L’idée c’est d’éviter de consommer des nœuds Kube allumés et à taille fixe tout le temps, ce qui est encore plus con que traîner des workers gitlab en dur puisqu’on paye autant, mais avec la complexité de Kubernetes en plus.
On dit que l’autopilot va poper des ressources à la demande et consommer moins qu’un Kube avec des nœuds à taille fixe.
On traîne en plus la complexité de Kube, mais on va dire que pour des besoins simples, si c’est fiable ça peut faire le café.
Petite précision qui a son importance, vous devez avoir un Gitlab contactable via internet (j’ai fait un compte gitlab.com)
puisque dans mon cas mon cluster Kube sera sur Google et que j’ai la flemme de faire une installation via réseau privé.
Ça me permet aussi de faire du sale sans trop nettoyer ensuite le Gitlab hihi.

On met l’avion en mode autopilot
Donc pavé de bonnes intentions, mais muni de ma meilleure flemme, j’attrape un terraform GKE autopilot et on regarde le bouzin.
https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/container_cluster
Boom option enable_autopilot, easy dans le maki. 🤷
1
2
3
4
5
6
7
8
9
10
11
|
resource "google_service_account" "default" {
account_id = "service-account-id"
display_name = "Service Account"
}
resource "google_container_cluster" "primary" {
project = "tonprojetici"
name = "my-gke-cluster"
location = "europe-west1"
enable_autopilot = true
}
|
Bon évidement, il faut activer l’API Kubernetes Engine dans GCP et s’il vous faut un code de prod un peu plus sérieux il faudra plutôt faire un cluster privé et une conf comme il faut.
Mais là pour le coup je m’en fous.

Bonne petite digression, mais le provider Terraform Google actuel est cassé pour mon besoin là tout de suite, rah la la, mais c’est relou.
1
|
│ Error: googleapi: Error 400: Max pods constraint on node pools for Autopilot clusters should be 32., badRequest
|
Donc au passage, si vous avez besoin pour une raison ou pour une autre de bloquer la version du provider que vous utilisez il faut faire comme ça :
1
2
3
4
5
6
7
8
|
terraform {
required_providers {
google = {
version = "<=4.3.0"
source = "hashicorp/google"
}
}
}
|
Donc j’ai mon cluster rapidos crados, ce n’était pas trop compliqué, maintenant on passe à la conf reloux pâte à choux.
Helm, I need somebody Helm

https://docs.gitlab.com/runner/install/kubernetes.html
Déjà blablabla Helm chart, mouais, on pensera ce qu’on voudra, mais Helm, c’est quand même de la grosse merde.
On cache la complexité d’exécution, on ne comprend pas vraiment ce qui run, et quand c’est cassé c’est un vrai bordel.
Mais bon y’en à qui aime, no hard feelings, c’est pas vous, c’est moi. 🤪
On déroule le merdier :
1
|
helm repo add gitlab https://charts.gitlab.io
|
le fichier de conf à fournir est là, ce n’est pas super clair dans la doc :
https://gitlab.com/gitlab-org/charts/gitlab-runner/blob/main/values.yaml
Je n’ai pas envie de lire, donc je fais le strict minimum soyons clair. 🤭🤭

Je renseigne :
1
2
|
gitlabUrl:
runnerRegistrationToken:
|
Et je fais un apply du Helm, modulo quelques bricoles :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
» kubectl create ns gitlab
namespace/gitlab created
» kubectl get ns
NAME STATUS AGE
default Active 49m
gitlab Active 2s
kube-node-lease Active 49m
kube-public Active 49m
kube-system Active 49m
» helm install --namespace gitlab gitlab-runner -f values.yaml gitlab/gitlab-runner
W0126 15:43:34.964296 26695 warnings.go:70]
Autopilot set default resource requests for Deployment gitlab/gitlab-runner-gitlab-runner,
as resource requests were not specified. See http://g.co/gke/autopilot-defaults.
NAME: gitlab-runner
LAST DEPLOYED: Wed Jan 26 15:43:32 2022
NAMESPACE: gitlab
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
Your GitLab Runner should now be registered against the GitLab instance reachable at: "https://gitlab.com/"
Runner namespace "gitlab" was found in runners.config template.
» kubectl get pods -n gitlab
NAME READY STATUS RESTARTS AGE
gitlab-runner-gitlab-runner-698445f965-rmx9p 1/1 Running 0 113s
|
On a un pods qui part, et c’est sympa parce que avec autopilot je ne paye que le coût du Kube master et ce pods qui est là tranquille et qui fait sa vie.
Maintenant on va casser un peu plus le matos et voir comment ça réagit, déjà on va balancer un build Gitlab bidon pour voir si le service est rendu.
J’ai mon runner qui est bien enregistré dans mon projet Gitlab :

1
2
3
4
5
6
|
image: busybox:latest
build1:
stage: build
script:
- echo "mange tes morts !"
|
On commit le truc, premier build sur un champ de blé, et paf Chocapic :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
Running with gitlab-runner 14.7.0 (98daeee0)
on gitlab-runner-gitlab-runner-698445f965-rmx9p zCm4LzV1
Resolving secrets
00:00
Preparing the "kubernetes" executor
00:00
Using Kubernetes namespace: gitlab
Using Kubernetes executor with image busybox:latest ...
Using attach strategy to execute scripts...
Preparing environment
00:00
ERROR: Job failed (system failure): prepare environment: setting up credentials: secrets is forbidden: User "system:serviceaccount:gitlab:default"
cannot create resource "secrets" in API group "" in the namespace "gitlab".
Check https://docs.gitlab.com/runner/shells/index.html#shell-profile-loading for more information
|
Les 3 lettres de la loose CKC.
Un souci d’RBAC (Role-based access control, le système de permission de Kubernetes), ce n’est probablement rien, mais comme je n’ai pas lu la doc, je suis le lien du message d’erreur 🤷 :
https://docs.gitlab.com/runner/shells/index.html#shell-profile-loading
Merci, mais cette doc est complètement inutile. 🤪
Par contre c’est très con parce que j’ai trouvé l’info en fouillant dans la doc :
1
2
3
4
5
6
7
|
Enabling RBAC support
If your cluster has RBAC enabled, you can choose to either have the chart create its own service account or provide one on your own.
To have the chart create the service account for you, set rbac.create to true:
rbac:
create: true
|
Pourquoi ne pas activer cette option par défaut ?
Si je me traîne un Helm chart c’est parce que je n’ai pas envie de comprendre comment ça marche non ?

Là tu commences à me souler hein, j’aurai eu le temps de faire deux trois runners manuellement, mais pour la beauté du geste on continue. 🤪
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
Running with gitlab-runner 14.7.0 (98daeee0)
on gitlab-runner-gitlab-runner-84b6bb67f7-px479 8UnbYJVb
Resolving secrets
00:00
Preparing the "kubernetes" executor
00:00
Using Kubernetes namespace: gitlab
Using Kubernetes executor with image busybox:latest ...
Using attach strategy to execute scripts...
Preparing environment
Waiting for pod gitlab/runner-8unbyjvb-project-33157472-concurrent-0lx6bg to be running, status is Pending
Unschedulable: "0/2 nodes are available: 2 Insufficient cpu, 2 Insufficient memory."
Waiting for pod gitlab/runner-8unbyjvb-project-33157472-concurrent-0lx6bg to be running, status is Pending
Unschedulable: "0/2 nodes are available: 2 Insufficient cpu, 2 Insufficient memory."
Waiting for pod gitlab/runner-8unbyjvb-project-33157472-concurrent-0lx6bg to be running, status is Pending
Unschedulable: "0/3 nodes are available: 1 node(s) had taint {node.kubernetes.io/not-ready: }, that the pod didn't tolerate, 2 Insufficient cpu, 2 Insufficient memory."
[plein de fois les mêmes lignes]
Waiting for pod gitlab/runner-8unbyjvb-project-33157472-concurrent-0lx6bg to be running, status is Pending
Unschedulable: "0/3 nodes are available: 1 node(s) had taint {node.kubernetes.io/not-ready: }, that the pod didn't tolerate, 2 Insufficient cpu, 2 Insufficient memory."
ContainersNotReady: "containers with unready status: [build helper]"
ContainersNotReady: "containers with unready status: [build helper]"
Waiting for pod gitlab/runner-8unbyjvb-project-33157472-concurrent-0lx6bg to be running, status is Pending
ContainersNotInitialized: "containers with incomplete status: [init-permissions]"
ContainersNotReady: "containers with unready status: [build helper]"
ContainersNotReady: "containers with unready status: [build helper]"
Waiting for pod gitlab/runner-8unbyjvb-project-33157472-concurrent-0lx6bg to be running, status is Pending
ContainersNotReady: "containers with unready status: [build helper]"
ContainersNotReady: "containers with unready status: [build helper]"
[plein de fois les mêmes lignes]
Running on runner-8unbyjvb-project-33157472-concurrent-0lx6bg via gitlab-runner-gitlab-runner-84b6bb67f7-px479...
Getting source from Git repository
Fetching changes with git depth set to 20...
Initialized empty Git repository in /builds/cyberpunkachien/kubernetes-runner/.git/
Created fresh repository.
Checking out 5f930909 as main...
Skipping Git submodules setup
Executing "step_script" stage of the job script
00:00
$ echo "mange tes morts !"
mange tes morts !
Cleaning up project directory and file based variables
00:01
Job succeeded
|
Résultat:
- 2 minutes 35 secondes pour afficher mange tes morts avec un echo
- 122 lignes de logs pour une commande echo
Hum, non, j’en ai marre, j’arrête là !
On peut expliquer la lenteur d’exécution, part le fait que autopilot doit provisionner des nœuds et les rendre disponibles pour poper des pods.
Par contre je peux aller prendre une pelle, déterrer, cuisiner et manger mes morts pour utiliser docker in docker et faire des conf un peu plus compliquées, rapidement et de façon fiable.

Pour l’histoire, je me suis retrouvé avec une boucle d’enregistrement de runner en cassant ma conf, obligé de faire une loop Python avec le Gitlab python SDK pour enlever les runners qui s’enregistraient à la chaine.
Donc pour cette configuration précise c’est non.
Merci, mais non merci.

Solution deprecated, rage against the docker-machine
Sentez-vous ce bon fumet délicat ? Oui c’est bien l’odeur de la solution moisie.

Il existe une solution pour faire des runners Gitlab autoscalable sur AWS (officiellement déprécier) et beta/gama/alpha roméo non officiel déprécier sur GCP.
Souvenez-vous, on ne fait pas du AWS ici, c’est beaucoup trop mainstream et chiant, on est des gens cool on fait du google.
Une fois de plus on va le payer en rhésus B et en fluide lacrymal très probablement. 😏
Donc déjà un grand merci à docker pour avoir complètement supprimé la doc de docker-machine pour la remplacer pour une page de dépréciation complètement inutile :
https://docs.docker.com/machine/

Tu veux enterrer ton produit, c’est ton droit, mais moi j’ai grandi avec Lara Croft, donc on va tuer des loups et piller des tombes.
Et un vrai merci à Gitlab d’avoir forké l’outil, ça permet de continuer à bricoler en attendant une solution de remplacement un peu plus supportée.

Donc on creuse un peu dans les tombeaux et on trouve des trucs un peu comme ça :
https://gitlab.com/gitlab-org/gitlab-runner/-/blob/667d783d7e208e7b9d9509f7c37237c292d30c68/docs/configuration/runner_autoscale_gcp/index.md
https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/2418/diffs#17f08eb60e1988a9ba39a76d9b1f4d0ddf20e4af
Donc là déjà on part sur l’option de bricoler une machine avec un runner qui pourrait provisionner ensuite d’autres runners.
Ça veut dire une machine modeste allumée tout le temps, ça devient valable à partir du moment où on provisionne au moins 2 machines de puissance moyenne ou balèze ponctuellement.
Et on utilise des machines preemptible pour réduire encore plus les coûts (les machines preemptible sont des machines qu’on peut utiliser max 24h et supprimables par Google sans préavis en échange d’un discount entre 60 et 90 %).
Donc ça veut déjà dire sur le papier qu’on accepte:
- d’avoir un temps de build augmenté pour avoir au moins 1 machine qui pope pour lancer le premier build
- la complexité de traîner un runner docker machine
- des builds interrompus de manière impromptue si la machine preemptible est coupée au moment du build
- une solution deprecated avec la doc officielle disparue et qui tiens au bon vouloir d’un fork de Gitlab
Et bien vous savez quoi ? Je tente quand même.

Parce qu’aujourd’hui, j’ai décidé d’être une multinationale et de produire 100 build à la seconde en moyenne. 🤭🤭
Il faut savoir se projeter dans la vie et voir grand. 😏
Un nouveau niveau de débile
Étape 1, faire marcher docker machine.
Rien que faire poper une machine avec docker-machine, c’est un niveau d’embêtement que je n’ai pas envie d’avoir.
Créer à la main une entrée firewall docker-machine port 22 ouvert (l’outil n’est pas capable de le créer lui-même).
Google ouvre le port 22 par défaut mais docker machine veut son ouverture de firewall personnel, avec un nom fixé dans le code de l’outil.
Un nom de machine déjà utilisé provoque des conflits de ressources.
Il conserve un état local et on peut le dégager comme ça :
1
2
3
4
5
6
7
8
9
10
11
12
|
gitlab-runner@gitlab-runner:/home/debian$ docker-machine ls
NAME ACTIVE DRIVER STATE URL SWARM DOCKER ERRORS
docker-machine-test-vm - google Error Unknown google: could not find default credentials. See https://developers.google.com/accounts/docs/application-default-credentials for more information.
docker-machine-test-vm4 - google Error Unknown google: could not find default credentials. See https://developers.google.com/accounts/docs/application-default-credentials for more information.
vm01 - google Error Unknown google: could not find default credentials. See https://developers.google.com/accounts/docs/application-default-credentials for more information.
vm02 - google Error Unknown google: could not find default credentials. See https://developers.google.com/accounts/docs/application-default-credentials for more information.
gitlab-runner@gitlab-runner:/home/debian$ GOOGLE_APPLICATION_CREDENTIALS=/home/gitlab-runner/key.json docker-machine rm vm01
About to remove vm01
WARNING: This action will delete both local reference and remote instance.
Are you sure? (y/n): y
|
Allez, on a réussi à faire poper une machine, on persévère dans la connerie :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
GOOGLE_APPLICATION_CREDENTIALS=/home/gitlab-runner/key.json docker-machine create --driver google
--google-project onionland
--google-disk-size 25
--google-machine-image https://www.googleapis.com/compute/v1/projects/ubuntu-os-cloud/global/images/family/ubuntu-1804-lts
--google-machine-type n1-standard-1
--google-network network
--google-subnetwork subnetwork
--google-preemptible=1
--google-scopes https://www.googleapis.com/auth/logging.write,https://www.googleapis.com/auth/monitoring
--google-username ubuntu
--google-zone europe-west3-b
--google-network "default"
--google-subnetwork "default"
docker-machine-test-vm5
Running pre-create checks...
(docker-machine-test-vm5) Check that the project exists
(docker-machine-test-vm5) Check if the instance already exists
Creating machine...
(docker-machine-test-vm5) Generating SSH Key
(docker-machine-test-vm5) Creating host...
(docker-machine-test-vm5) Opening firewall ports
(docker-machine-test-vm5) Creating instance
(docker-machine-test-vm5) Waiting for Instance
(docker-machine-test-vm5) Uploading SSH Key
Waiting for machine to be running, this may take a few minutes...
Detecting operating system of created instance...
Waiting for SSH to be available...
Detecting the provisioner...
Provisioning with ubuntu(systemd)...
Installing Docker...
Copying certs to the local machine directory...
Copying certs to the remote machine...
Setting Docker configuration on the remote daemon...
Checking connection to Docker...
Docker is up and running!
To see how to connect your Docker Client to the Docker Engine running on this virtual machine, run: docker-machine env docker-machine-test-vm5
|
Je vous passe les erreurs nulles du genre je connais pas le network,
j’ai gardé l’image Ubuntu un peu nulle par défaut, on verra ensuite si ont fait un truc plus malin plus tard.
À première vu ça semble OK, il va falloir adapter ça à la conf de mon runner.

C’est affreusement lent, mais bon on s’y attend un peu hein. :)
Petite info qui a son importance, j’ai laissé volontairement une adresse publique parce que je fais mon radin et mon runner docker-machine est sur ma machine locale, pour réduire encore un peu plus les coûts comme je suis sur une souscription perso.
Boom un morceau de doc glané :
https://github.com/Nordstrom/docker-machine/blob/master/docs/drivers/gce.md
Et là paf, rattrapée par la radinerie :
https://stackoverflow.com/questions/47227042/docker-machine-do-not-work-with-google-cloud-service-account
Du coup ça ne peut pas marcher en mode on premise, génial.

On reprend les mêmes et on recommence hey, on est plus à ça près.
On était en mode bricole crados, on va scripter un peu plus le bouzin t’en qu’à refaire.
On va commencer par faire une image packer du runner pour pouvoir reconstruire le runner docker-machine en cas de soucis.
L’idée c’est de pouvoir reconstruire la machine et d’injecter la conf au moment du provisionning avec Terraform.
le script injecté dans Packer :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
#!/bin/bash
set -xe
export DEBIAN_FRONTEND=noninteractive
sudo apt -y update && sudo apt -y upgrade
sudo apt install -y curl git
curl -O "https://gitlab-docker-machine-downloads.s3.amazonaws.com/v0.16.2-gitlab.11/docker-machine-Linux-x86_64"
sudo cp docker-machine-Linux-x86_64 /usr/local/bin/docker-machine
sudo chmod +x /usr/local/bin/docker-machine
sudo apt-get update -y
sudo apt-get install \
ca-certificates \
curl \
gnupg \
lsb-release
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh || true
#sudo usermod -aG docker gitlab-runner
sudo curl -LO "https://gitlab-runner-downloads.s3.amazonaws.com/latest/deb/gitlab-runner_amd64.deb"
sudo chmod +x gitlab-runner_amd64.deb
sudo dpkg -i gitlab-runner_amd64.deb
|
Le fichier de configuration packer :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
{
"builders": [
{
"type": "googlecompute",
"project_id": "onionland",
"source_image_family": "debian-11",
"ssh_username": "packer",
"zone": "europe-west1-b",
"image_name": "docker-machine",
"disk_size": "150"
}
],
"provisioners": [
{
"type": "shell",
"script": "cloud-init.sh"
}
]
}
|
on obtient une image docker-machine qu’on injecte ensuite dans Terraform :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
resource "google_service_account" "default" {
project = "onionland"
account_id = "sa-docker-machine"
display_name = "sa docker machine"
}
resource "google_project_iam_member" "compute_admin" {
project = "onionland"
role = "roles/compute.admin"
member = "serviceAccount:${google_service_account.default.email}"
}
resource "google_project_iam_member" "service-account-user" {
project = "onionland"
role = "roles/iam.serviceAccountUser"
member = "serviceAccount:${google_service_account.default.email}"
}
resource "google_compute_instance" "docker-machine" {
project = "onionland"
name = "gitlab-docker-machine-runner"
machine_type = "e2-micro"
zone = "europe-west1-b"
boot_disk {
initialize_params {
image = "docker-machine"
size = "200"
type = "pd-balanced"
}
}
network_interface {
network = "default"
access_config {
// Ephemeral public IP
}
}
metadata_startup_script = "${file("cloud-init.sh")}"
|
Rien de compliqué, il faut juste donner les bonnes permissions au compute.
Le cloud-init suivant contient la conf que j’ai mise en place pour mon runner (token en dur obtenu de mon précédent runner).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
#!/bin/bash
set -xe
cat << EOF > /etc/gitlab-runner/config.toml
concurrent = 1
check_interval = 0
[session_server]
session_timeout = 1800
[[runners]]
name = "docker-machine runner"
url = "https://gitlab.com/"
token = "yDcbss8nzQejeQ_MFfmf"
executor = "docker+machine"
[runners.custom_build_dir]
[runners.cache]
[runners.cache.s3]
[runners.cache.gcs]
[runners.cache.azure]
[runners.docker]
tls_verify = false
image = "debian"
disable_entrypoint_overwrite = false
oom_kill_disable = false
disable_cache = false
privileged = true
volumes = ["/var/run/docker.sock:/var/run/docker.sock", "/cache"]
shm_size = 0
[runners.machine]
IdleCount = 0
IdleScaleFactor = 0.0
IdleCountMin = 0
IdleTime = 10
MaxBuilds = 100
MachineDriver = "google"
MachineName = "auto-scale-%s"
MachineOptions = ["google-project=onionland",
"google-disk-size=25",
"google-machine-image=https://www.googleapis.com/compute/v1/projects/ubuntu-os-cloud/global/images/family/ubuntu-1804-lts",
"google-machine-type=n1-standard-1",
"google-network=network",
"google-subnetwork=subnetwork",
"google-preemptible=1",
"google-scopes=https://www.googleapis.com/auth/logging.write,https://www.googleapis.com/auth/monitoring",
"google-username=ubuntu",
"google-zone=europe-west3-b",
"google-network=default",
"google-subnetwork=default"]
EOF
|
Résultat d’exécution :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
Running with gitlab-runner 14.7.0 (98daeee0)
on docker-machine runner yDcbss8n
Resolving secrets
00:00
Preparing the "docker+machine" executor
Using Docker executor with image busybox:latest ...
Pulling docker image busybox:latest ...
Using docker image sha256:beae173ccac6ad749f76713cf4440fe3d21d1043fe616dfbe30775815d1d0f6a for busybox:latest with digest busybox@sha256:5acba83a746c7608ed544dc1533b87c737a0b0fb730301639a0179f9344b1678 ...
Preparing environment
00:01
Running on runner-ydcbss8n-project-33157472-concurrent-0 via runner-ydcbss8n-auto-scale-1643299157-968b4da3...
Getting source from Git repository
00:02
Fetching changes with git depth set to 20...
Initialized empty Git repository in /builds/cyberpunkachien/kubernetes-runner/.git/
Created fresh repository.
Checking out 5f930909 as main...
Skipping Git submodules setup
Executing "step_script" stage of the job script
00:00
Using docker image sha256:beae173ccac6ad749f76713cf4440fe3d21d1043fe616dfbe30775815d1d0f6a for busybox:latest with digest busybox@sha256:5acba83a746c7608ed544dc1533b87c737a0b0fb730301639a0179f9344b1678 ...
$ echo "mange tes morts !"
mange tes morts !
Cleaning up project directory and file based variables
00:01
Job succeeded
|
Durée : 2 minutes 3 secondes
Si ça tourne mal, comme le runner est enregistré manuellement on ne pollue jamais les enregistrements côté Gitlab.
On peut avoir des machines provisionnées en permanence avec le paramètre IdleCountMin.
Dans mon cas je veux que le runner soit provisionné au moment où on l’utilise, donc j’ai mis 0.
IdleTime permet de laisser le runner provisionner pendant un délai après utilisation, je choisis 10 minutes comme je fais des tests.
Prendre garde à bien supprimer toutes les machines qui tournent au cas où on coupe salement la machine docker-machine.
Je confirme la suppression de ma machine runner après 10 minutes.
Le résultat est acceptable, dans la mesure où la complexité est réduite par rapport à la configuration du runner Kube.
On a une machine très petite qui doit tout le temps tourner, mais qui peut assez vite nous en poper plusieurs de puissance supérieure.
La configuration du runner est beaucoup plus facile d’accès et ne risque pas de tout péter dans Gitlab on peut ajouter le docker binding sans soucis.

Oh ça va, c’est pas si mal, presque mieux que Kubernetes. 🤷
On s’appuie massivement sur une techno dépréciée, bof documentée, très bricolée.
Le truc peut mourir à tout moment, mais pour le moment c’est une alternative crédible au bricolage tout pourri que je vais faire ensuite. :)
Je me fais la réflexion que finalement à quoi ça sert tout ça ?
Économiser un peu d’argent VS avoir un système plutôt fragile et compliqué.
Je me dis qu’en vrai on pourrait tout aussi bien prè-enregistrer des runners, chopper des tokens et scaler en semi-auto à partir de zéro. 🤔
En mode scaling manuelle du pauvre, on décide d’un nombre max de machines et on pop les runners avec un script au besoin.
Idéalement, on devrait trouver une solution pour enregistrer les runners sans besoin de gitlab register pour juste chopper le token qui nous intéresse et l’injecter plus tard dans notre cloud-init.
Pour le coup on va dire que je suis maxi radin et que je veux un truc de bourrin que je peux casser et reconstruire rapidement.

En vérité, je suis convaincu qu’un runner unique bien configuré et bien dimensionné est suffisant pour la plupart des besoins et tant pis si ça coûte un peu.
Dédicace à mon camarade Manu qui m’a montré le planificateur qui permet d’éteindre et d’allumer des machines pendant des plages horaires programmées.
On lit la doc maintenant
https://docs.gitlab.com/ee/api/runners.html#register-a-new-runner
Ici, y’a une jolie doc avec un call API sympathique,
J’ai dû passer à coté un paquet de fois, mais on peut tout à fait enregistrer des kms de runner via l’API sans aucun souci.
Autant vous dire que je suis refait et qu’on ne va pas hésiter à jouer avec. 🤪
1
2
3
|
curl --request POST "https://gitlab.com/api/v4/runners" -F "token=token_register" -F "description=runner" -F "tag_list=docker" -F "run_untagged=true" -F "is_shared=true"
{"id":13416922,"token":"runner_token"}
|

Trop d’excitation d’un coup, je suis parti sur une solution complètement folle avec l’idée de templater du Terraform avec du Python et de gérer enregistrement et suppression des runners.
On ne va pas faire ça, j’ai essayé, c’est beaucoup trop complexe et fragile.
Du coup tout l’inverse de ce que je voulais faire au départ.
On va repartir sur une idée bien plus simple et rustique comme j’aime bien, avec du blanc et du saucisson. 🤪

Enregistrement de runners au km
L’idée c’est de recycler l’image faite avec packer pour produire des runners avec une boucle Terraform toute bête.
On fournit en variable une liste de token runner et on template le fichiers de conf de Gitlab ci.
De cette façon on a une liste de runner constructible et destructible à volonté et plus jamais on fait un SSH sur les machines pour changer une conf.
Par contre pour avoir une liste de token enregistrée, on va faire un script Python qui nous génère un fichier de variable avec autant de token qu’on veut de runner.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
|
#!/usr/bin/env python3
# coding:utf8
import requests
import argparse
import sys
import os
import logging
import subprocess
import json
logging.basicConfig(level=logging.INFO)
RUNNER_REGISTER_TOKEN = "ici ton token d'enregistrement"
def parse_command_args():
description = """This script register runner(s) to gitlab api, get the runner's token(s)
and apply terraform in order to manually scale gitlab runners.
"""
clean_args = []
parser = argparse.ArgumentParser(description=description)
parser.add_argument(
"--nb_runners",
type=int,
dest="nb_runners",
help="nb runners you want to apply on terraform",
required=True
)
args = parser.parse_args()
return args
def register_gitlab_runner(tags, description, run_untagged="true", is_shared="true"):
runner = {}
url = f"https://gitlab.com/api/v4/runners?token={RUNNER_REGISTER_TOKEN}"
payload = {
"description": description,
"tag_list": tags,
"run_untagged": run_untagged,
"is_shared": is_shared,
}
r = requests.post(url, json=payload)
runner = r.json()
runner["name"] = description
logging.info(f"register: {description}")
return runner
def persist_runners_info(runners_info):
json_persistance = json.dumps(runners_info)
runners_persistance ="""
variable "gitlab_tokens" {
type = list(string)
default = """+ json_persistance + """
}
"""
write_tf_file(runners_persistance)
def write_tf_file(runners_persistance):
with open("variables.tf", "w", encoding="utf-8") as tf_file:
tf_file.write(runners_persistance)
def registrer_runners(nb_runners):
runners=[]
for run_nb in range(1, nb_runners):
runners.append(register_gitlab_runner("docker", f"runner {run_nb}")['token'])
persist_runners_info(runners)
def main():
args = parse_command_args()
registrer_runners(args.nb_runners)
if __name__ == "__main__":
main()
|
Au final que fait ce script pas très beau ?

Il récupère en paramètre le nombre de runner qu’on veut.
Il fait une boucle sur l’API en enregistrant les runners sous cette forme ‘runner {n}’.
Une fois les runners enregistrés, on fait une liste de token et on la forme en fichier de variables Terraform.
1
2
3
4
|
variable "gitlab_tokens" {
type = list(string)
default = ["XfR4ogG93sM5LVCpz45C", "tTRuWBteQWo37tBdfy6y"]
}
|
Et ensuite il suffit d’apply ce fichier Terraform et on a 2 runners configurés et prêt à bosser (ça marche pour 2, ça marche pour 100).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
resource "google_compute_instance" "gitlab-runner" {
project = "onionland"
name = "gitlab-runner-${count.index}"
count = length(var.gitlab_tokens)
machine_type = "e2-micro"
zone = "europe-west1-b"
boot_disk {
initialize_params {
image = "docker-machine"
size = "200"
type = "pd-balanced"
}
}
network_interface {
network = "default"
access_config {
// Ephemeral public IP
}
}
metadata_startup_script = "${data.template_file.init[count.index].rendered}"
}
data "template_file" "init" {
count = length(var.gitlab_tokens)
template = file("cloud-init.tpl")
vars = {
runner_name = "gitlab-runner-${count.index}"
gitlab_token = "${var.gitlab_tokens[count.index]}"
}
}
|
Le Terraform est presque le même que le précédent, mais bien plus léger.
L’astuce (que tout ceux qui font un peu de TF connaissent) c’est le count qui permet de faire une boucle de ressources sur une liste (ici nos token).
Le token et le nom du runner sont templatés dans le fichier cloudinit, lui aussi plus simple.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
#!/bin/bash
set -xe
cat << EOF > /etc/gitlab-runner/config.toml
concurrent = 10
check_interval = 0
[session_server]
session_timeout = 1800
[[runners]]
name = "${runner_name}"
url = "https://gitlab.com/"
token = "${gitlab_token}"
executor = "docker"
[runners.custom_build_dir]
[runners.cache]
[runners.cache.s3]
[runners.cache.gcs]
[runners.cache.azure]
[runners.docker]
tls_verify = false
image = "debian"
privileged = false
disable_entrypoint_overwrite = false
oom_kill_disable = false
disable_cache = false
volumes = ["/var/run/docker.sock:/var/run/docker.sock", "/cache"]
shm_size = 0
EOF
systemctl restart gitlab-runner
|
Et là on balance le bouzin et c’est l’éclate. 🤪

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
|
Running with gitlab-runner 14.7.0 (98daeee0)
on gitlab-runner-0 XfR4ogG9
Resolving secrets
00:00
Preparing the "docker" executor
Using Docker executor with image docker:19.03.12 ...
Pulling docker image docker:19.03.12 ...
Using docker image sha256:81f5749c9058a7284e6acd8e126f2b882765a17b9ead14422b51cde1a110b85c for docker:19.03.12 with digest docker@sha256:d41efe7ad0df5a709cfd4e627c7e45104f39bbc08b1b40d7fb718c562b3ce135 ...
Preparing environment
Running on runner-xfr4ogg9-project-33157472-concurrent-0 via gitlab-runner-0...
Getting source from Git repository
00:02
Fetching changes with git depth set to 20...
Initialized empty Git repository in /builds/cyberpunkachien/kubernetes-runner/.git/
Created fresh repository.
Checking out de52931b as main...
Skipping Git submodules setup
Executing "step_script" stage of the job script
Using docker image sha256:81f5749c9058a7284e6acd8e126f2b882765a17b9ead14422b51cde1a110b85c for docker:19.03.12 with digest docker@sha256:d41efe7ad0df5a709cfd4e627c7e45104f39bbc08b1b40d7fb718c562b3ce135 ...
$ docker info
WARNING: No kernel memory limit support
WARNING: No oom kill disable support
Client:
Debug Mode: false
Server:
Containers: 3
Running: 1
Paused: 0
Stopped: 2
Images: 2
Server Version: 20.10.12
Storage Driver: overlay2
Backing Filesystem: extfs
Supports d_type: true
Native Overlay Diff: true
userxattr: false
Logging Driver: json-file
Cgroup Driver: systemd
Plugins:
Volume: local
Network: bridge host ipvlan macvlan null overlay
Log: awslogs fluentd gcplogs gelf journald json-file local logentries splunk syslog
Swarm: inactive
Runtimes: io.containerd.runc.v2 io.containerd.runtime.v1.linux runc
Default Runtime: runc
Init Binary: docker-init
containerd version: 7b11cfaabd73bb80907dd23182b9347b4245eb5d
runc version: v1.0.2-0-g52b36a2
init version: de40ad0
Security Options:
apparmor
seccomp
Profile: default
cgroupns
Kernel Version: 5.10.0-11-cloud-amd64
Operating System: Debian GNU/Linux 11 (bullseye)
OSType: linux
Architecture: x86_64
CPUs: 2
Total Memory: 975.4MiB
Name: gitlab-runner-0
ID: UX3L:F7MI:QS2N:YXKT:NYFG:XEJO:3HWN:AFRE:LNEC:A3VK:5DIB:2YU5
Docker Root Dir: /var/lib/docker
Debug Mode: false
Registry: https://index.docker.io/v1/
Labels:
Experimental: false
Insecure Registries:
127.0.0.0/8
Live Restore Enabled: false
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
registry.gitlab.com/gitlab-org/gitlab-runner/gitlab-runner-helper x86_64-98daeee0 c89158451cd5 13 seconds ago 66.9MB
docker 19.03.12 81f5749c9058 19 months ago 211MB
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
09af6b1b44bc 81f5749c9058 "docker-entrypoint.s…" 1 second ago Up Less than a second runner-xfr4ogg9-project-33157472-concurrent-0-d15e64998940dc7b-build-2
$ docker build .
Step 1/2 : FROM alpine:latest
latest: Pulling from library/alpine
59bf1c3509f3: Pulling fs layer
59bf1c3509f3: Verifying Checksum
59bf1c3509f3: Download complete
59bf1c3509f3: Pull complete
Digest: sha256:21a3deaa0d32a8057914f36584b5288d2e5ecc984380bc0118285c70fa8c9300
Status: Downloaded newer image for alpine:latest
---> c059bfaa849c
Step 2/2 : RUN apk update
---> Running in 86e85dc81991
fetch https://dl-cdn.alpinelinux.org/alpine/v3.15/main/x86_64/APKINDEX.tar.gz
fetch https://dl-cdn.alpinelinux.org/alpine/v3.15/community/x86_64/APKINDEX.tar.gz
v3.15.0-242-gf2c09d7474 [https://dl-cdn.alpinelinux.org/alpine/v3.15/main]
v3.15.0-239-g755d336b9e [https://dl-cdn.alpinelinux.org/alpine/v3.15/community]
OK: 15848 distinct packages available
Removing intermediate container 86e85dc81991
---> 71be116c5e04
Successfully built 71be116c5e04
Cleaning up project directory and file based variables
00:00
Job succeeded
|
Durée : 22 secondes

C’est pas mal les computes en dur finalement, hein ? 🤪
on l’a échappé belle, j’ai bien cru que ma solution de gros bourrin était moins efficace.
Conclusion
Débranche ton cerveau, encore une fois c’est la solution de bourrin qui marche le mieux.

Kubernetes, ça semble séduisant et cool, mais c’est bien trop fragile et overkill pour un truc aussi idiot.
Les computes c’est ringard j’arrête pas de vous le dire, mais ça reste encore la solution la plus simple et la plus efficace en terme de mise en place et d’exécution.
L’important c’est la capacité de destruction et reconstruction et la vitesse d’exécution de tes pipelines.
Parce que finalement la vraie guerre, ce n’est pas tant de scale à l’infini des runners, mais en combien de temps tu exécutes tes tâches bout à bout et combien de fois ton système tombe en panne.
2 minutes à chaque build juste pour économiser un peu de sous, c’est autant de temps perdu pour tes devs qui attendent la fin du build.
Et imagine la frustration de malade quand tu développes le code des pipelines de builds et que tu push du code comme un singe épileptique ?

Parfois, la sur-optimisation ajoute de la fragilité dans ton système et les solutions les plus modernes et élégantes sur le papier sont une tanné à maintenir.
Faire tourner un compute en permanence pour éviter de faire tourner en permanence d’autres computes c’est quand même super con.
Je rêve d’un jour où Gitlab proposera buildin une solution pour construire des runners à la demande en mode full managé ou alors en charge de travail serverless.
Mais en attendant, une image de machine bien packagée et un peu d’astuce, ça reste pour moi la meilleure des solutions.
