diff --git a/.gitignore b/.gitignore
index 5420a22ac2fa9c37a0512079ab4b1f8dd1b32c9e..631f4917f6ca78f78decdc4ef71f82694c1eb6c7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
 *.idea/
+group_vars/ci-runners.yml
diff --git a/README.md b/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..3dc023ef7a9ed4569e282fddc65426878ee0e9bc
--- /dev/null
+++ b/README.md
@@ -0,0 +1,13 @@
+# Ansible playbook for deploying a GAP GitLab CI runner
+
+This playbook is used to install the gitlab-runner package on a VM.
+
+To run this playbook:
+
+ 1. Provision a 'nat_ci' VM in Puppet
+ 2. Get the ip address of the new VM, and configure your ssh environment
+ 3. Create & activate a python virtual environment and install ansible
+ 4. Update inventory.yml so that your VM is defined in the "gitlab-runner" group
+ 5. Update group_vars/ci-runners.yml with your gitlab.geant.net username and personal access token
+ 6. Install the `community.general` collection from Ansible galaxy with the following command: `ansible-galaxy collection install community.general`
+ 7. Run the following command to execute the playbook: `ansible-playbook -i inventory.yml playbook.yml`
diff --git a/group_vars/ci-runners.yml.example b/group_vars/ci-runners.yml.example
new file mode 100644
index 0000000000000000000000000000000000000000..438e6ea8dd2b9a026399da53ed3a38ed8ebbf024
--- /dev/null
+++ b/group_vars/ci-runners.yml.example
@@ -0,0 +1,7 @@
+runner:
+  gitlab_url: 'https://gitlab.geant.org/'
+  access_token: xxx
+  registration_token: xxx
+  runner_tags:
+    - gap
+    - nat
diff --git a/inventory.yml b/inventory.yml
new file mode 100644
index 0000000000000000000000000000000000000000..164ce38b3197e628451a17668d0276a28294258a
--- /dev/null
+++ b/inventory.yml
@@ -0,0 +1,5 @@
+ci-runners:
+  hosts:
+    test-nat-ci01:
+      ansible_host:
+        test-nat-ci01
diff --git a/playbook.yml b/playbook.yml
new file mode 100644
index 0000000000000000000000000000000000000000..ffe8d2e3b3f34efcc5d5230cddd72dd06c66e49a
--- /dev/null
+++ b/playbook.yml
@@ -0,0 +1,5 @@
+- name: Install and set up a GitLab CI runner
+  hosts: ci-runners
+  become: true
+  roles:
+    - gitlab-runner
diff --git a/roles/gitlab-runner/files/pin-gitlab-runner.pref b/roles/gitlab-runner/files/pin-gitlab-runner.pref
new file mode 100644
index 0000000000000000000000000000000000000000..9998a89877e11fdc6c32ce2811c99ae67d7f4959
--- /dev/null
+++ b/roles/gitlab-runner/files/pin-gitlab-runner.pref
@@ -0,0 +1,4 @@
+Explanation: Prefer GitLab provided packages over the Debian native ones
+Package: gitlab-runner
+Pin: origin packages.gitlab.com
+Pin-Priority: 1001
diff --git a/roles/gitlab-runner/tasks/main.yml b/roles/gitlab-runner/tasks/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..097aa00f7718b7e26836d489d0d87e3a663436f5
--- /dev/null
+++ b/roles/gitlab-runner/tasks/main.yml
@@ -0,0 +1,34 @@
+- name: Add GitLab runner APT repository
+  ansible.builtin.shell:
+    cmd: curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh | bash
+  become: true
+
+- name: Pin GitLab runner package to correct repository
+  ansible.builtin.copy:
+    src: pin-gitlab-runner.pref
+    dest: /etc/apt/preferences.d/pin-gitlab-runner.pref
+
+- name: Install GitLab runner package
+  ansible.builtin.apt:
+    update_cache: true
+    pkg:
+      - gitlab-runner
+
+- name: Install gitlab Python package
+  ansible.builtin.pip:
+    name: python-gitlab
+
+- name: Register runner
+  no_log: true
+  community.general.gitlab_runner:
+    api_url: '{{ runner.gitlab_url }}'
+    api_token: '{{ runner.access_token }}'
+    registration_token: '{{ runner.registration_token }}'
+    description: '{{ inventory_hostname_short }}'
+    project: 'nat/gap'
+    state: present
+    tag_list: '{{ runner.runner_tags + [inventory_hostname_short] }}'
+    run_untagged: false
+    locked: true
+    access_level_on_creation: true
+    access_level: 'not_protected'