- Published on
GitLab Runner with Docker for Windows Executor
- Authors
- Name
- Boris Khasanov
Recenltly I worked on a task to provision a new GitLab Runner with Docker for Windows Executor. The Windows VM which hosted the runner needed to be configured with Terraform and PowerShell.
While working on the task, I discovered that GitLab documentation was a bit inaccurate, outdated, or not detailed enough.
I also did not find any guides how to provision a Runner in unattended mode.
So, I decided to publish this article to make other people's work easer if they need to work on the similar task.
We will start from the ground up without any infrastructure and finish with a working Runner building a sample project with Docker for Windows.
Table of Contents
Note on Security
The guide is focussed on automation rather than security, so trade off were made to keep it short. Terraform state and access keys are stored locally, service account permissions are too loose etc.
Let me know in the comments if you'd like to see the part two of the guide where we harden security.
Before you begin
Clone the GitHub repository and cd to the working folder terraform
.
git clone https://github.com/kpoxo6op/gitlab-runner-docker-windows.git
cd gitlab-runner-docker-windows/terraform
Install Terraform.
Google Cloud
Some manual steps are required due to sensitivity of the billing details we need to provide.
Create a new project with a unique name. I'll be using runner-demo-xxxx
in this article. GCP project names are globally unique, so your project name will be different from mine.
Enable Billing for your project.
Note: Google provides a credit for all new accounts, you won't be charged if you complete this guide. Just don't forget to clean up resources when you are done.
Grant Permissions
Install Google Cloud SDK.
Enable Compute API so get permission for creating a Windows machine. This machine will host our GitLab Runner and Docker for Windows.
gcloud services enable compute.googleapis.com
Create Service account Terraform Admin
.
PROJECT_ID=$(gcloud config get-value project)
gcloud iam service-accounts create terraform-admin \
--description="Service account for Terraform operations" \
--display-name="Terraform Admin"
gcloud projects add-iam-policy-binding ${PROJECT_ID} \
--member=serviceAccount:terraform-admin@${PROJECT_ID}.iam.gserviceaccount.com \
--role=roles/compute.admin \
--role=roles/iam.serviceAccountUser
Create the key for Terraform Admin
, save it to disk.
gcloud iam service-accounts keys create ~/terraform-admin-key.json \
--iam-account terraform-admin@${PROJECT_ID}.iam.gserviceaccount.com
# verify the key
cat ~/terraform-admin-key.json
Terraform will connect to Google Cloud as Terraform Admin
with the key in our home directory.
Gitlab
Sign up for a GitLab account.
Create any Group and Project with any name during the sign up process (GitLab won't let you skip it). We will create another group with terraform later.
Create a Personal Access Token with api
scope. We will use it the next step.
Working folder
Let's edit/review files in terraform
folder.
.env
file
Edit Create the .env
file from the sample provided.
cp .env.sample .env
cat .env
export GITLAB_TOKEN="glpat-xxxxxxxxxxxxxxxxxxxxxxxxxxxx"
export TF_VAR_my_ip_address="0.0.0.0/32"
export GOOGLE_APPLICATION_CREDENTIALS="/home/${USER}/terraform-admin-key.json"
Set environment variable values:
GITLAB_TOKEN
: GitLab token from the previous step.
TF_VAR_my_ip_address
: Your IP address in case you want to access the Windows VM we are about to create.
GOOGLE_APPLICATION_CREDENTIALS
: Service Account keys from Google Cloud step.
Review provider.tf
Both Google and GitLab providers use credentials defined in .env
file.
terraform {
required_providers {
gitlab = {
source = "gitlabhq/gitlab"
version = "16.11.0"
}
google = {
source = "hashicorp/google"
version = "5.26.0"
}
}
}
provider "google" {
project = var.project_name
region = var.gcp_region
}
provider "gitlab" {
base_url = "https://gitlab.com/api/v4/"
}
Edit variables.tf
Set project_name
to your unique Google project name. Optionally, set GCP zone and region too.
variable "my_ip_address" {
description = "The IP address allowed for RDP access"
type = string
}
variable "project_name" {
description = "Google Cloud project name"
type = string
# your project name
default = "runner-demo-xxxx"
}
variable "gitlab_token" {
description = "gitlab personal token"
type = string
}
variable "gcp_zone" {
description = "Google Cloud Zone"
type = string
default = "australia-southeast1-a"
}
variable "gcp_region" {
description = "Google Cloud Region"
type = string
default = "australia-southeast1"
}
Review gitlab.tf
In this file we:
- create GitLab project
- add the pipeline file to the project
- create the Runner on Gitlab side of things
We set long build_timeout
for the project builds because we don't want our build to fail while the Windows VM is being provisioned.
gitlab_user_runner
will produce the authentication token for the new runner. We we will use it in the next file.
cat gitlab.tf
resource "gitlab_project" "project" {
name = "GitLab-${var.project_name}"
description = "Build an app inside Windows Container"
visibility_level = "public"
build_timeout = "36000"
}
resource "gitlab_repository_file" "pipeline" {
project = gitlab_project.project.id
file_path = ".gitlab-ci.yml"
branch = "main"
content = base64encode(file("${path.module}/.gitlab-ci.yml"))
commit_message = "Init pipeline"
author_name = "Terraform"
}
resource "gitlab_user_runner" "runner" {
runner_type = "project_type"
project_id = gitlab_project.project.id
description = "Runner with Docker for Windows executor"
tag_list = ["windows", "docker"]
}
Review gcp_windows_vm.tf
Note: this is not a full file, only the interesting parts.
We template the authentication token gitlab_user_runner.runner.token
into the Windows startup script startup.ps1
.
We allow RDP and SSH access from the IP defined in the .env
file.
data "template_file" "startup_script" {
template = file("${path.module}/startup.ps1")
vars = {
runner_token = gitlab_user_runner.runner.token
}
}
resource "google_compute_instance" "windows_vm" {
metadata = {
windows-startup-script-ps1 = data.template_file.startup_script.rendered
}
}
resource "google_compute_firewall" "allow_rdp" {
source_ranges = [var.my_ip_address]
}
resource "google_compute_firewall" "allow_ssh" {
source_ranges = [var.my_ip_address]
}
startup.ps1
Note: this is not a full file, only the interesting parts.
This is the most important and confusing part of the project to get right.
First, we install docker for Windows with install-docker-ce.ps1 provided by Microsoft.
The script will do some prep work, reboot the VM and start executing itself again when the VM starts.
Second, we wait until the second reboot when Docker service starts up.
Third, we register the runner with Docker for Windows executor. Let's look at the parameters:
--token
: Terraform from gcp_windows_vm.tf will substitute${runner_roken}
with the token created by gitlab.tf--docker-image
,--docker-helper-image
are set to nanoserver version because we don't want to download over 10GB of data with the default images.--docker-user
is set to Administrator to work around 'Access Denied' issue.
Finally, we install, start and verify the runner service.
.\install-docker-ce.ps1 -Force -DockerVersion '26.1.1'
if (Get-Service *docker* -ea SilentlyContinue) {
Invoke-WebRequest -Uri $gitlabRunnerUrl -OutFile $runnerExe
Write-Output "Register runner with token ${runner_token}"
$registerParams = @(
"register",
"--builds-dir", $runnerDir,
"--cache-dir", $runnerDir,
"--config", "$runnerDir\config.toml",
"--description", "Docker for Windows runner",
"--executor", "docker-windows",
"--non-interactive",
"--token", "${runner_token}",
"--url", "https://gitlab.com/",
"--docker-image",
"mcr.microsoft.com/powershell:lts-nanoserver-ltsc2022",
"--docker-helper-image",
"registry.gitlab.com/gitlab-org/gitlab-runner/gitlab-runner-helper:x86_64-bleeding-nanoserver21H2",
"--docker-user", "ContainerAdministrator"
)
& $runnerExe @registerParams
Write-Output "Install runner service"
$commonParams = @{
FilePath = $runnerExe
NoNewWindow = $true
Wait = $true
}
$installArgs = @(
"install",
"--working-directory", $runnerDir,
"--config", "$runnerDir\config.toml"
)
Start-Process @commonParams -ArgumentList $installArgs
Write-Output "Start runner service"
Start-Process @commonParams -ArgumentList "start"
Write-Output "Verify runners"
& $runnerExe "verify"
Get-WinEvent -ProviderName gitlab-runner | Format-Table -wrap -auto
} else {
Write-Output "Waiting for Docker before registering runner"
}
It may be possible to execute Docker and Runner stages in separate Google startup scripts to make the code simpler. I could not make it working, either Docker or Runner parts would break and and annoy me a lot.
Run the Demo
Run terraform.
source .env
terraform init
terraform plan
terraform apply
Go to GitLab projects, navigate to the pipeline. The pipeline should be in pending
state while the runner VM is being configured.
Go to CI/CD settings -> Runners -> click Expand
. Your should see "Runner has never contacted this instance" message.
I was too slow taking the perfect screenshot, and my runner came online already.
Check what VM is doing. Note: Connection errors may occur because the VM will restart.
Inspect the runner config. Check the running services, check the runner logs. You may see a new lines being written to a log file.
gcloud compute ssh gitlab-runner-windows
pwsh
Get-Content C:\Logs\startup.txt -Wait
#Ctrl+C to exit
Get-Content C:\GitLab-Runner\config.toml
Get-Service docker, gitlab-runner
Get-Eventlog Application -Source gitlab-runner -Newest 20 |
Format-Table -Wrap -Auto
Go back to Gitlab. The runner should be Online, the job should be completed.
Clean Up
Run terraform destroy to tear down the Google Cloud VM and Gitlab Project.
terraform destroy --auto-approve
Links to Other Guides
I will be adding links to other related guides here. Feel free to suggest a link in the comments.
- Thomas'es guide at codingwiththomas.com. Thomas creates his own Dockerfile for Windows container (
--docker-image
parameter for runner executable). Watch out for--tag-list
parameter as it no longer works. Runner tags need to be set at the GitLab server now (gitlab.tf).