diff --git a/.gitignore b/.gitignore index 9fadef5a83ce29b8f9130f13dc1948f052878305..8997722737be790c0a5835861112f0fe024420d7 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ /terraform.tfstate /terraform.tfstate.backup +/id_rsa +/id_rsa.pub diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 514d96f1a657030958ae23b630cf578d0efb481b..85ce6e3426aee3d6c8c8ac3c4ae7726096c5d71e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -22,6 +22,8 @@ validate: - linux - small extends: .terraform:validate + before_script: + - cp $SSH_PRIVATE_KEY id_rsa build: tags: @@ -30,6 +32,8 @@ build: extends: .terraform:build environment: name: $TF_STATE_NAME + before_script: + - cp $SSH_PRIVATE_KEY id_rsa deploy: tags: @@ -45,5 +49,7 @@ execute: tags: - terraform - docker + rules: + - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH script: - echo Greetings from runner! diff --git a/CHANGES.md b/CHANGES.md new file mode 100644 index 0000000000000000000000000000000000000000..bbb7eb032b0a36b9863bca4e6c2f1a29fe7f8fbc --- /dev/null +++ b/CHANGES.md @@ -0,0 +1,3 @@ +# 2023-02-13 + +- !1 Unregister gitlab runner on VM destruction diff --git a/README.md b/README.md index 4c42cd06d27bd95f75d2550292e6c640409aef89..aa3d0325d1c677b89f366a1dda5335f439f956fe 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,22 @@ project), projects using Terraform need: a variable `REGISTRATION_TOKEN` that points to another project dedicated for the build itself.) +- This project needs a pair of passphrase-less SSH private/public keys + for the GitLab shared runner to be able to connect to the deployed + runners to unregister them from GitLab before deletion. + You can use the following command to create a pair of SSH private/public keys + without passphrase in the current directory (files `id_rsa` and `id_rsa.pub`): + `ssh-keygen -b 4096 -f id_rsa -N ""`. + - The contents of the private key file `id_rsa` should be added as a variable + `SSH_PRIVATE_KEY` of type **File** in CI/CD settings. + See the + [intro](https://gitlab.inria.fr/gitlabci_gallery/intro#cicd-variables) + project for details on how to set a CI/CD variable. + - The public key file `id_rsa.pub` should be registered on ci.inria.fr portal + to allow the dedicated user to connect to the hosted virtual machines + ([portal documentation](https://ci.inria.fr/doc/page/slaves_access_tutorial/#register-your-ssh-public-key)). + for details on how to register a public key on the portal. + - The repository contains a `backend.tf` file for connecting Terraform with GitLab. This may be convenient to delete it locally for running `terraform` @@ -140,6 +156,17 @@ variable "REGISTRATION_TOKEN" { The variable is marked as sensitive to prevent Terraform from showing its value in output ([Terraform documentation](https://developer.hashicorp.com/terraform/language/values/variables#suppressing-values-in-cli-output)). +The SSH public key is passed through the variable `SSH_PUBLIC_KEY`. +```terraform +variable "SSH_PUBLIC_KEY" { + type = string +} +``` +The value of the `SSH_PUBLIC_KEY` variable will be stored in the file +`~/.ssh/authorized_keys` in virtual machines, so that Terraform can +connect to the virtual machines with the private key to unregister the +runners before destroying the machines. + In this example, we set up only one resource: ```terraform resource "cloudstack_instance" "custom_instance" { @@ -147,7 +174,7 @@ resource "cloudstack_instance" "custom_instance" { ## in VM names. name = "gitlabcigallery-terraform-custom-instance" service_offering = "Custom" - template = "ubuntu-20.04-cloudinit" + template = "ubuntu-20.04-lts" zone = "zone-ci" details = { cpuNumber = 4 @@ -156,7 +183,21 @@ resource "cloudstack_instance" "custom_instance" { expunge = true user_data = templatefile("cloud-init.sh.tftpl", { REGISTRATION_TOKEN = var.REGISTRATION_TOKEN + SSH_PUBLIC_KEY = var.SSH_PUBLIC_KEY }) + connection { + type = "ssh" + host = self.name + user = "ci" + private_key = file("id_rsa") + bastion_host = "ci-ssh.inria.fr" + bastion_user = "gter001" + bastion_private_key = file("id_rsa") + } + provisioner "remote-exec" { + when = destroy + inline = ["sudo gitlab-runner unregister --all-runners || true"] + } } ``` @@ -189,9 +230,24 @@ resource "cloudstack_instance" "custom_instance" { [`cloud-init.sh.tftpl`](cloud-init.sh.tftpl) by substituting `${REGISTRATION_TOKEN}` with the value of the variable passed to Terraform. - -The script [`cloud-init.sh.tftpl`](cloud-init.sh.tftpl) installs -`gitlab-runner` and `docker.io` on the virtual machine and registers the runner. +- We pass also the `SSH_PUBLIC_KEY` to the template file to have its + value written in the `~/.ssh/authorized_keys` file. + We configure the connection via ssh to the runner: `gter001` is the + login of the dedicated user on ci.inria.fr, and we will make sure + in the next section that the private key is written in the file + `id_rsa`. + We cannot use a variable for passing the path to this file, + since the connection is used by a destroy provisioner, that + cannot refer to variables. + This destroy provisioner executes `gitlab-runner unregister` + before the destruction of the virtual machine; failures are ignored + in case of the `gitlab-runner` command was not yet installed + when destroying occurs. + +The script [`cloud-init.sh.tftpl`](cloud-init.sh.tftpl) registers +the SSH public key in `~ci/.ssh/authorized_keys`, +installs `gitlab-runner` and `docker.io` on the virtual machine, +and registers the runner. ## The pipeline specification file [`.gitlab-ci.yml`](.gitlab-ci.yml) @@ -239,6 +295,21 @@ stages: the same project, but we could have chosen to register the runner to another project by adjusting the `REGISTRATION_TOKEN` variable.) +Every job that will use the Terraform configuration file needs to copy +the file referred by `SSH_PRIVATE_KEY` into the file `id_rsa`. +To copy the file without overriding all the script, +we use the `before_script` key. For instance, in the job `validate`: + +```yaml +validate: + tags: + - linux + - small + extends: .terraform:validate + before_script: + - cp $SSH_PRIVATE_KEY id_rsa +``` + ## Ignored files in [`.gitignore`](.gitignore) [`.gitignore`](.gitignore) instructs git to ignore the local files diff --git a/cloud-init.sh.tftpl b/cloud-init.sh.tftpl index daab7bbf577b9be9fe158bc5ce9880a24baa4048..23949122194d8246d3f541f2043c0ed176fede8c 100644 --- a/cloud-init.sh.tftpl +++ b/cloud-init.sh.tftpl @@ -1,15 +1,20 @@ -#!/bin/sh +#!/bin/bash # Standard output and errors are redirected to /root/log.txt to ease # debugging. ->>/root/log.txt 2>&1 ( +( + # To be able to run `sudo gitlab-runner unregister --all-runners` on + # VM destruction. + echo 'ci ALL=(ALL) NOPASSWD:ALL' >/etc/sudoers.d/90-ci + mkdir -p -m 700 ~ci/.ssh + echo ${SSH_PUBLIC_KEY} >>~ci/.ssh/authorized_keys # GitLab needs a recent version of `gitlab-runner` to be compatible with # the instance running on gitlab.inria.fr. The version packaged by default # on Ubuntu is regularly out of date. curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | sudo bash - apt-get update + # apt-get update performed by the script above # We install docker.io to be able to register a docker executor apt-get install --yes gitlab-runner docker.io gitlab-runner register --non-interactive --tag-list terraform,docker \ --executor docker --docker-image alpine --url https://gitlab.inria.fr \ --registration-token ${REGISTRATION_TOKEN} -) +) >>/root/log.txt 2>&1 diff --git a/main.tf b/main.tf index 950a1b04accd0a5a4868b634ac782ca1ed3ec389..dca84281a5193fdad2c9f5115c55a13d2a1d7da1 100644 --- a/main.tf +++ b/main.tf @@ -20,12 +20,16 @@ variable "REGISTRATION_TOKEN" { sensitive = true } +variable "SSH_PUBLIC_KEY" { + type = string +} + resource "cloudstack_instance" "custom_instance" { ## It is a good practice to have the "{project name}-" prefix ## in VM names. name = "gitlabcigallery-terraform-custom-instance" service_offering = "Custom" - template = "ubuntu-20.04-cloudinit" + template = "ubuntu-20.04-lts" zone = "zone-ci" details = { cpuNumber = 4 @@ -34,5 +38,19 @@ resource "cloudstack_instance" "custom_instance" { expunge = true user_data = templatefile("cloud-init.sh.tftpl", { REGISTRATION_TOKEN = var.REGISTRATION_TOKEN + SSH_PUBLIC_KEY = var.SSH_PUBLIC_KEY }) + connection { + type = "ssh" + host = self.name + user = "ci" + private_key = file("id_rsa") + bastion_host = "ci-ssh.inria.fr" + bastion_user = "gter001" + bastion_private_key = file("id_rsa") + } + provisioner "remote-exec" { + when = destroy + inline = ["sudo gitlab-runner unregister --all-runners || true"] + } }