TheCB4

Using Gitlab, Terraform, and Ansible to spin up a Gitlab Runner hosted on Digital Ocean

As part of my One App Three Ways: Weather work I had to set up a Gitlab Runner that would that would support running the Android Emulator. Specifically, I needed a virtual machine that would support KVM and QEMU. I didn't want to do this set up by hand, so I leveraged Gitlab's CI capability along with Terraform and Ansible to be able automatically spin up and tear-down a runner. I've added my referral link at the bottom of the page. This will get you $100 credit. That's equal to two months of CI hosting.

The work is broken up into three primary pieces The Gitlab CI file that uses a Terraform template to run the deployment and keep track of state The Terraform script that provisions a virtual machine with Digital Ocean * The Ansible script that configures the virtual machine with Docker and the Gitlab Runner, and dependencies for the Android Emulator






The VM size to support running the Android Emulator is one with 8GB of RAM available. These VMs start at about $50 per month. The actual terraform folder structure is the main tf file with a digital ocean module that does the work. I also heavily leverage the CI varables capability in Gitlab to hide secrets.



include:
  - template: Terraform.latest.gitlab-ci.yml


stages:
  - validate
  - test
  - build
  - deploy
  - configure
  - cleanup


variables:
  TF_IN_AUTOMATION: "true"
  TF_STATE_NAME: development
  TF_CACHE_KEY: development
  TF_ROOT: sources/terraform
  TF_VAR_droplet_name:  "gitlab-runner"
  TF_VAR_droplet_image: "ubuntu-22-04-x64"
  TF_VAR_droplet_size:  "s-4vcpu-8gb"
  TF_VAR_droplet_region: "nyc1"
  TF_VAR_ssh_fingerprint: "${FINGERPRINT}"
  TF_VAR_do_token: "${DO_TOKEN}"
  TF_VAR_pub_key: "${PUBLIC_KEY}"
  TF_VAR_pvt_key: "${PRIVATE_KEY}"
  TF_LOG: "DEBUG"
  SOME_VAR: 0


fmt:
  allow_failure: false


validate:
  extends: [.terraform:validate]
  before_script:
    - bin/add-dependencies.sh
    - bin/tell-secrets.sh


build:
  extends: [.terraform:build]
  before_script:
    - bin/add-dependencies.sh
    - bin/tell-secrets.sh


deploy:
  extends: [.terraform:deploy]
  before_script:
    - bin/add-dependencies.sh
    - bin/tell-secrets.sh
  script:
    - !reference [.terraform:deploy, script]
    - gitlab-terraform output -json > output.json
  environment:
    name: $TF_STATE_NAME
  artifacts:
    paths:
      - ${CI_PROJECT_DIR}/${TF_ROOT}/output.json


configure:
  stage: configure
  needs: [deploy]
  before_script:
    - bin/add-dependencies.sh
    - bin/tell-secrets.sh
  script:
    - export DEPLOYMENT_ADDRESS=$(jq -r .infra_ip_address.value ${CI_PROJECT_DIR}/${TF_ROOT}/output.json)
    - echo $DEPLOYMENT_ADDRESS >> inventory
    - ansible-playbook -u runner -i inventory --private-key "${PRIVATE_KEY}" -e "pub_key=${PUBLIC_KEY}" -e "runner_token=${RUNNER_TOKEN}" sources/ansible/runner-playbook.yml


destroy:
  extends: [.terraform:destroy]
  needs: []
  before_script:
    - bin/add-dependencies.sh
    - bin/tell-secrets.sh
  environment:
    name: $TF_STATE_NAME
    action: stop






The Digital Ocean Terraform Module

The module uses cloud-config to create a sudoing non-root user for the VM. Once the VM is provisioned, the IP address is stored as an artifact in the Gitlab pipeline to be used by Ansible in configuring the VM.




terraform {
  required_providers {
    digitalocean = {
      source  = "digitalocean/digitalocean"
      version = ">= 2.21.0"
    }
  }
}

provider "digitalocean" {
  # Provider is configured using environment variables:
  # DIGITALOCEAN_TOKEN, DIGITALOCEAN_ACCESS_TOKEN
  token = var.do_token
}

data "digitalocean_ssh_key" "terraform" {
  name = "gitlab-runner"
}

# Create a new Web Droplet in the nyc2 region
resource "digitalocean_droplet" "runner" {
  image      = var.droplet_image
  name       = var.droplet_name
  region     = var.droplet_region
  size       = var.droplet_size
  monitoring = true
  ssh_keys = [
    data.digitalocean_ssh_key.terraform.id
  ]
  user_data = file("./runner/runner.yml")

}



You should be able to see the runner in the Digital Ocean Dashboard

[Gitlab Runner Page]






The Ansible playbook



I create different runners. THe first runner runs the plain docker image; this one is used to create the Android Emulator image. The second runner runs the android emulator image. The last runner is a shell runner, for more general purpose CI if needed. Note that both the plain docker and the android images are running in priviledged mode. This is to provide docker access to KVM on the host.

---
- hosts: all
  remote_user: gitlab-runner
  become: true

  tasks:
  
    - name: Gather all facts of cloud init
      community.general.cloud_init_data_facts:
      register: result

    - ansible.builtin.debug:
        var: result

    - name: Wait for cloud init to finish
      community.general.cloud_init_data_facts:
        filter: status
      register: res
      until: "res.cloud_init_data_facts.status.v1.stage is defined and not res.cloud_init_data_facts.status.v1.stage"
      retries: 50
      delay: 5

    - name: Update APT Cache
      apt:
        update_cache: yes
        force_apt_get: yes

    - name: Upgrade all packages to the latest version
      apt:
        name: "*"
        state: latest
        force_apt_get: yes

    - name: install dependencies dependencies
      apt:
        name: "{{item}}"
        state: present
        # update_cache: yes
      loop:
        - apt-transport-https
        - ca-certificates
        - curl
        - gnupg-agent
        - software-properties-common
        - cpu-checker
        - qemu-system-x86
        - libvirt-daemon-system
        - libvirt-clients 
        - bridge-utils

    - name: add docker GPG key
      apt_key:
        url: https://download.docker.com/linux/ubuntu/gpg
        state: present

    - name: add Docker repository to apt
      apt_repository:
        repo: deb https://download.docker.com/linux/ubuntu bionic stable
        state: present

    - name: install docker
      apt:
        name: "{{item}}"
        state: latest
        # update_cache: yes
      loop:
        - docker-ce
        - docker-ce-cli
        - containerd.io

    - name: check docker is active
      service:
        name: docker
        state: started
        enabled: yes

    - name: Ensure group "docker" exists
      ansible.builtin.group:
        name: docker
        state: present

    - name: adding runner to docker group
      user:
        name: gitlab-runner
        groups: docker
        append: yes

    - name: Install docker-compose
      get_url:
        url: https://github.com/docker/compose/releases/download/1.29.2/docker-compose-Linux-x86_64
        dest: /usr/local/bin/docker-compose
        mode: 'u+x,g+x'

    - name: Change file ownership, group and permissions
      ansible.builtin.file:
        path: /usr/local/bin/docker-compose
        owner: gitlab-runner
        group: docker

    - name: install gitlab-runner dependencies
      apt:
        name: "{{item}}"
        state: present
        # update_cache: yes
      loop:
        - apt-transport-https

    - name: Install gitlab runner
      shell: |
        curl -LJO https://gitlab-runner-downloads.s3.amazonaws.com/latest/deb/gitlab-runner_amd64.deb
        dpkg -i gitlab-runner_amd64.deb
      args:
        creates: /usr/bin/gitlab-runner

    - name: Docker Prune
      ansible.builtin.cron:
        name: Docker Prune
        minute: "15"
        job: /usr/bin/docker system prune -f
        state: present
        user: gitlab-runner

    - name: Register gitlab docker runner
      command: |
        gitlab-runner register \
        --non-interactive \
        --url "https://gitlab.com/" \
        --registration-token "{{ runner_token }}" \
        --executor "docker" \
        --docker-image docker:latest \
        --description "gitlab-runner-docker" \
        --tag-list "thecb4-universe, docker" \
        --run-untagged="false" \
        --locked="true" \
        --docker-privileged

    - name: Register gitlab android image runner
      command: |
        gitlab-runner register \
        --non-interactive \
        --url "https://gitlab.com/" \
        --registration-token "{{ runner_token }}" \
        --executor "docker" \
        --docker-image registry.gitlab.com/thecb4-universe/private/docker/android-x86_64:latest \
        --description "gitlab-runner-android" \
        --tag-list "thecb4-universe, android" \
        --run-untagged="false" \
        --locked="true" \
        --docker-privileged

    - name: Register gitlab shell runner
      command: |
        gitlab-runner register \
        --non-interactive \
        --url "https://gitlab.com/" \
        --registration-token "{{ runner_token }}" \
        --executor "shell" \
        --shell "bash"
        --description "gitlab-runner-android" \
        --tag-list "thecb4-universe, shell" \
        --run-untagged="false" \
        --locked="true" \

    - name: adding runner to libvirt group
      user:
        name: gitlab-runner
        groups: libvirt
        append: yes

    - name: adding runner to kvm group
      user:
        name: gitlab-runner
        groups: kvm
        append: yes



You should be able to see the runners with all greens in your group

[Gitlab Runner Page]






Find me on Twitter and tell me all about it! Digital Ocean referal is here

Tagged with: