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.

forest gump en train de courir

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.

bob l’éponge avec une liasse de billets

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.

la documentation ? voyons voir ce qu’on peut trouver

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.

émission TV, c’est du propre

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.

émission YouTube, balek

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

The Beatles, chanson help

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

Suzanne, chanson la flemme

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 :

gitlab runner enregistré automatiquement dans 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 :

pub Chocapic ,mascotte vol sur un tonneau de chocolat au-dessus d’un champ de blé

 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 ? inacceptable meme

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.

émission de cuisine, bon appétit bien sûr

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.

bugs bunny, no

Solution deprecated, rage against the docker-machine

Sentez-vous ce bon fumet délicat ? Oui c’est bien l’odeur de la solution moisie.

Emma Stone, pouce en l’air sarcasme

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/

parc and rec, sérieusement ? meme

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.

the real MVP meme

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.

try hard, or die trying

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 :

go big or go home, chien avec un os plus gros que lui

 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.

meme du poisson méfiant avec la pieuvre camouflée en ambuscade

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.

tirelire cochon radin

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.

David Goodenough, meme joueur du grenier

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. :)

Manual scaling de runner avec Terraform (la solution de gros bourrin)

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.

Obélix met une baffe à un romain

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"}

Homer Simpson, oh yeah whoo hoo

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

film calmos, scène de repas franchouillard

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 ?

grand mère ajuste ces lunettes, à ouai c’est vrai que c’est moche

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

party hard meme

  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

mais, non je vais pas crier … COMBIEN

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.

et bien maintenant, il faut arrêter de penser, tout cours

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 ?

 robot code sur le clavier

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.

oss 117, c’est un bordel