Provisioning a GitLab CE + CI runners on OpenStack

OpenStack is a very popular open source cloud computing solution. Many providers are based on it, and you can roll your own in your data center. In this example, we'll use the public OpenStack by OVH, located in Montreal, QC (Canada), but we can use any other OpenStack. There're differences in implementation for every custom deployment, but we'll stick with very stable features.

We'll launch one compute instance running Ubuntu LTS 16.04 for GitLab, with a dedicated block device for Docker, and two other compute instances for GitLab CI runners. Security will allow HTTP for everyone, but SSH only for a known IP from our corporate network. To store our builds or releases, we'll create a container, which is in OpenStack terminology—an object storage. The equivalent with AWS S3 is a bucket.

Getting ready

To step through this recipe, you will need the following:

  • A working Terraform installation.
  • An OpenStack account on any OpenStack provider (public or private). This recipe uses an account on OVH's public OpenStack (https://www.ovh.com/us/).
  • An Internet connection.

How to do it…

We'll create:

  • Three compute instances (virtual machines)
  • One keypair
  • One block storage device
  • One security group
  • One object storage bucket

Configuring the OpenStack provider

Let's start by configuring the OpenStack provider. We need four pieces of information: a username, a password, an OpenStack tenant name, and an OpenStack authentication endpoint URL. To make the code very dynamic, let's create variables for those in variables.tf:

variable "user_name" {
  default     = "changeme"
  description = "OpenStack username"
}

variable "password" {
  default     = "hackme"
  description = "OpenStack password"
}

variable "tenant_name" {
  default     = "123456"
  description = "OpenStack Tenant name"
}

variable "auth_url" {
  default     = "https://openstack.url/v2.0"
  description = "OpenStack Authentication Endpoint"
}

Don't forget to override the default values with your own in the terraform.tfvars file!

user_name   = "***"
tenant_name = "***"
password    = "***"
auth_url    = "https://auth.cloud.ovh.net/v2.0/"

Now we're good to go.

Creating a key pair on OpenStack

To authenticate ourselves on the instances, we need to provide the public part of the key pair to OpenStack. This is done using the openstack_compute_keypair_v2 resource, specifying in which region we want the key, and where the key is. Let's add both variables in variables.tf:

variable "region" {
  default     = "GRA1"
  description = "OpenStack Region"
}

variable "ssh_key_file" {
  default     = "keys/admin_key"
  description = "Default SSH key"
}

Next, override them in the terraform.tfvars file:

region      = "BHS1"

Now we can build our resource in the keys.tf file:

resource "openstack_compute_keypair_v2" "ssh" {
  name       = "Admin SSH Public Key"
  region     = "${var.region}"
  public_key = "${file("${var.ssh_key_file}.pub")}"
}

Creating a security group on OpenStack

We know our requirements are to allow HTTP (TCP/80) from anywhere, but SSH (TCP/22) only from one corporate network. Add it right now in variables.tf so we can use it:

variable "allowed_network" {
  default = "1.2.3.4/32"
  description = "The Whitelisted Corporate Network"
}

Don't forget to override with your own network in terraform.tfvars.

Let's create a first security group allowing HTTP for everyone in our region, using the openstack_compute_secgroup_v2 resource in a security.tf file:

 resource "openstack_compute_secgroup_v2" "http-sg" {
  name        = "http-sg"
  description = "HTTP Security Group"
  region      = "${var.region}"

  rule {
    from_port   = 80
    to_port     = 80
    ip_protocol = "tcp"
    cidr        = "0.0.0.0/0"
  }
}

Following the same pattern, create another security group to allow SSH only from our corporate network:

resource "openstack_compute_secgroup_v2" "base-sg" {
  name        = "base-sg"
  description = "Base Security Group"
  region      = "${var.region}"

  rule {
    from_port   = 22
    to_port     = 22
    ip_protocol = "tcp"
    cidr        = "${var.allowed_network}"
  }
}

Creating block storage volumes on OpenStack

In our requirements, we want a dedicated volume to be available to our GitLab instance, for Docker. We decide this one will be 10 GB in size. This volume will be mounted by the compute instance under a dedicated device (likely /dev/vdb). The whole thing is done using the openstack_blockstorage_volume_v2 resource:

resource "openstack_blockstorage_volume_v2" "docker" {
  region      = "${var.region}"
  name        = "docker-vol"
  description = "Docker volume"
  size        = 10
}

Add a simple output in outputs.tf so we know the volume description, name, and size:

output "Block Storage" {
  value = "${openstack_blockstorage_volume_v2.docker.description}: ${openstack_blockstorage_volume_v2.docker.name}, ${openstack_blockstorage_volume_v2.docker.size}GB"
}

We now have every requirement to launch our compute instances.

Creating compute instances on OpenStack

It's now time to create the instances. We know they have to be Ubuntu 16.04, and we decide on a flavor name: a flavor is the type of the machine. It varies from every other OpenStack installation. In our case, it's named vps-ssd-1. Let's define some defaults in the variables.tf file:

variable "image_name" {
  default     = "CentOS"
  description = "Default OpenStack image to boot"
}

variable "flavor_name" {
  default     = "some_flavor"
  description = "OpenStack instance flavor"
}

Also, override them with good values in terraform.tfvars:

image_name  = "Ubuntu 16.04"
flavor_name = "vps-ssd-1"

To create a compute instance, we use a resource named openstack_compute_instance_v2. This resource takes all the parameters we previously declared (name, image, flavor, SSH key, and security groups). Let's try this in instances.tf:

resource "openstack_compute_instance_v2" "gitlab" {
  name            = "gitlab"
  region          = "${var.region}"
  image_name      = "${var.image_name}"
  flavor_name     = "${var.flavor_name}"
  key_pair        = "${openstack_compute_keypair_v2.ssh.name}"
  security_groups = ["${openstack_compute_secgroup_v2.base-sg.name}", "${openstack_compute_secgroup_v2.http-sg.name}"]
}

To attach the block storage volume we created, we need to add a volume {} block inside the resource:

  volume {
    volume_id = "${openstack_blockstorage_volume_v2.docker.id}"
    device    = "/dev/vdb"
  }

Now, an optional but fun part is that the commands needed to format the volume, mount it at the right place, fully update the system, install Docker, and run the GitLab CE container. This is done using the remote-exec provisioner and requires a SSH username. Let's set it as variables.tf:

variable "ssh_username" {
  default     = "ubuntu"
  description = "SSH username"
}

Now we can just type in all the commands to be executed when the instance is ready:

  provisioner "remote-exec" {
    connection {
      user        = "${var.ssh_username}"
      private_key = "${file("${var.ssh_key_file}")}"
    }

    inline = [
      "sudo mkfs.ext4 /dev/vdb",
      "sudo mkdir /var/lib/docker",
      "sudo su -c "echo '/dev/vdb /var/lib/docker ext4 defaults 0 0' >> /etc/fstab"",
      "sudo mount -a",
      "sudo apt update -y",
      "sudo apt upgrade -y",
      "sudo apt install -y docker.io",
      "sudo systemctl enable docker",
      "sudo systemctl start docker",
      "sudo docker run -d -p 80:80 --name gitlab gitlab/gitlab-ce:latest",
    ]
  }

Add a simple output in the outputs.tf file, so we easily know the GitLab instance public IP:

output "GitLab Instance" {
  value = "gitlab: http://${openstack_compute_instance_v2.gitlab.access_ip_v4}"
}

The runner instances are the same, but a little simpler, as they don't need a local volume. However, we need to set the amount of runners we want in variables.tf:

variable "num_runners" {
  default     = "1"
  description = "Number of GitLab CI runners"
}

Override the value to have more runners in terraform.tfvars:

num_runners = "2"

Now we can create our runner instances using the openstack_compute_instance_v2 resource:

resource "openstack_compute_instance_v2" "runner" {
  count           = "${var.num_runners}"
  name            = "gitlab-runner-${count.index+1}"
  region          = "${var.region}"
  image_name      = "${var.image_name}"
  flavor_name     = "${var.flavor_name}"
  key_pair        = "${openstack_compute_keypair_v2.ssh.name}"
  security_groups = ["${openstack_compute_secgroup_v2.base-sg.name}", "${openstack_compute_secgroup_v2.http-sg.name}"]

  provisioner "remote-exec" {
    connection {
      user        = "${var.ssh_username}"
      private_key = "${file("${var.ssh_key_file}")}"
    }

    inline = [
      "sudo apt update -y",
      "sudo apt upgrade -y",
      "sudo apt install -y docker.io",
      "sudo systemctl enable docker",
      "sudo systemctl start docker",
      "sudo docker run -d --name gitlab-runner -v /var/run/docker.sock:/var/run/docker.sock gitlab/gitlab-runner:latest",
    ]
  }
}

This will launch a GitLab CI runner, so builds can be triggered by GitLab! (there's one last step of configuration, though. It's out of the scope of this book, but we need to register each runner to the main GitLab instance by executing docker exec -it gitlab-runner gitlab-runner register and answering the questions).

Add the following output to outputs.tf so we know all the IP addresses of our runners:

output "GitLab Runner Instances" {
  value = "${join(" ", openstack_compute_instance_v2.runner.*.access_ip_v4)}"
}

Creating an object storage container on OpenStack

This one is very simple: it only requires a name and a region. As it's to store releases, let's call it releases, using the openstack_objectstorage_container_v1 resource, in an objectstorage.tf file:

resource "openstack_objectstorage_container_v1" "releases" {
  region = "${var.region}"
  name   = "releases"
}

Add a simple output in outputs.tf so we remember the Object Storage container name:

output "Object Storage" {
  value = "Container name: ${openstack_objectstorage_container_v1.releases.name}"
}

Applying

In the end, do a terraform apply:

$ terraform apply
[...]

Outputs:

Block Storage = Docker volume: docker-vol, 10GB
GitLab Instance = gitlab: http://158.69.95.202
GitLab Runner Instances = 158.69.95.200 158.69.95.201
Object Storage = Container name: releases

Connect to the GitLab instance and enjoy the runners (after GitLab token registration)!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.141.35.185