Technical | 2021-12-15

How to set up a MacOS CI for Gitlab

Create a shortcut with CDPATH

With Symflower CLI, a major part of our product is required to run as a binary on the local machine. Since we use Linux in the company, the first version of Symflower was first and foremost designed to run on that operating system. However, as we aim to deliver our product to a wide customer base, we have been working towards being able to support other operating systems as well.

MacOS is one of the operating systems we always hoped to target, and in this blog post, we would like to show you how we’ve approached the problem of testing Symflower CLI inside of our GitLab CI.

General setup for a MacOS CI

Apple is very strict when it comes to limiting the installation of MacOS, thus, in order to run our CI jobs on MacOS, we would also need Apple hardware. In general, all of our infrastructure is running on Hetzner, either as dedicated servers, or on their cloud. We were delighted to see that they started to offer Apple M1 servers, which enables us to add them into our existing infrastructure without increasing the complexity of the setup.

Once the server was ordered, we needed to do the following steps, which we’ll take a look at one by one:

  1. Configuring the firewall
  2. Installing the GitLab Runner
  3. Registering the GitLab Runner
  4. Configuring the CI of the repository
  5. Executing a basic test to confirm it works
  6. Bonus: install the GitLab Runner without the GUI

Configuring the firewall

Our server comes with the default installation of MacOS, which is a good start. However, the first step one should always consider is the security of a server that is reachable from the internet. For ever server that is hosted by a cloud provider, two areas concerning the network security should be considered before you do anything else: the firewall of the server itself and the firewall of the provider. Let’s look at what we did to isolate our server without loosing control over it.

MacOS is based on FreeBSD, which comes with a packet filter firewall, in short PF. A neat tutorial about configuring a PF firewall can be found on DigitalOcean.

The PF configuration usually resides in /etc/pf.conf. Since our GitLab runner only needs to access our GitLab instance, we can disallow all incoming connections except for SSH. Which we use to configure the server. The following shows our configuration file.

scrub-anchor "com.apple/*"
nat-anchor "com.apple/*"
rdr-anchor "com.apple/*"
dummynet-anchor "com.apple/*"
anchor "com.apple/*"
load anchor "com.apple" from "/etc/pf.anchors/com.apple"

# Allowd ports for incoming connections.
# TCP port 22 is SSH.
tcp_in_pass = "{ 22 }"

# Block all incoming connections by default. Outgoing is allowed.
block in all

# Allow incoming connections to the defined ports.
pass in proto tcp to any port $tcp_in_pass keep state

You can test your firewall configuration by checking if the configuration can be parsed using pfctl -n -f /etc/pf.conf and then forcing a reload using pfctl -f /etc/pf.conf. You know everything worked out if you can still connect over SSH or by looking at the loaded filter rules using pfctl -s all. In case you want to be extra careful, restart the server and check again.

The next network security of concern is the network of the cloud provider. In our case Hetzner which allows us to either use a vswitch or directly change the firewall to the server. For now, the latter is good enough for us, so we’ll allow everyone to reach the SSH service of the server for now.

The following image shows the configuration we applied for our server. Basically we allow incoming traffic to the SSH port and every established TCP connection to the server. The latter is important, as otherwise no outgoing internet access, e.g. opening a website, would work.

Direct firewall configuration of the Hetzner server.

Installing the GitLab Runner

Due to the way how MacOS distinguishes between SSH and GUI sessions, the GitLab Runner](https://docs.gitlab.com/runner/register/) can officially only be installed using the GUI. (We found a way to install the GitLab Runner with just an SSH connection jump to Bonus: install the GitLab Runner without the GUI or read on for the officially supported installation.) We accomplished the access to the GUI for our Hetzner server by requesting a remote desktop connection. Afterwards, the GitLab Runner can be installed using the following steps:

  1. Log in with the user the GitLab Runner should be executed with. In our case “hetzner”.
  2. Open up a terminal.
  3. Execute the command brew install gitlab-runner to install the GitLab runner via Homebrew.
  4. Execute the command brew services start gitlab-runner to start the service of the GitLab Runner.

You can now check using ps aux | grep -i gitlab if your Gitlab Runner is running as service on your server.

Registering the GitLab Runner

Since our GitLab Runner is now active on our server, we need to register it according to GitLab’s documentation. We used the following steps.

  1. Open a terminal, e.g. over SSH, and execute the command gitlab-runner register.
  2. The required URL and token can be found in the CI/CD settings.
  3. Description of the runner is macos runner. This can be changed later on.
  4. The tags section determines by which tag the runner can be addressed in the .gitlab-ci.yml inside your repository. In this case just macos will suffice. This can be changed later on.
  5. In the CI/CD settings under the Runners section the runner should be now visible with a green and active state.
  6. By clicking on the runner, the settings can be checked, especially the Last Contact field should display just now giving the information that connection is established.

Configuring the CI of the repository

Our Gitlab Runner is now active on the server and has an active connection to our GitLab instance. Next, we need to change the configuration of our repository to actually run a CI job inside the server. The configuration can be usually found in the root directory of your repository with the name .gitlab-ci.yml. A more elaborate explanation on how to configure your repository can be found in the official documentation.

Our first test should be a very simple one to check if the CI is working at all and to not be overshadowed by other problems. Hence, we use the following CI configuration to simple output the operating system version and information of the current user for our .gitlab-ci.yml file.

test-macos:
  tags:
    - macos
  script:
    - system_profiler SPSoftwareDataType

Executing a basic test to confirm it works

In the previous section we configured the GitLab CI configuration file of our repository. Everything we now need to run our first CI job with our MacOS server is to commit the change and push it to the repository.

And voila: the test is running on the server!

With the above steps, we have now covered a simple setup for running the required jobs in our targeted environment. As we are constantly working to port our product to MacOS, this use case of GitLab is extremely important, as with it, we get to ensure that every change leads to a working binary.

Did you enjoy this look at how to integrate a dedicated MacOS machine for your CI jobs? If you want to see more content from Symflower, don’t forget to subscribe to our newsletter, and follow us on Twitter, Facebook, and LinkedIn as well.

See you next time!

Bonus: install the GitLab Runner without the GUI

So you do not want to use the GUI to install and maintain your GitLab Runner of your MacOS server? You have come to the right place. The following steps will install and configure the GitLab Runner as a service of a non-root user that is automatically started on-boot without logging in.

  1. Log into your server, e.g. via SSH, as the non-root user you want to run the GitLab Runner with, i.e. for us this is “hetzner”.

  2. Install the GitLab Runner for the user via brew install gitlab-runner.

  3. Log into your server, e.g. via SSH, as root.

  4. Copy the following file to “/Library/LaunchDaemons/homebrew.mxcl.gitlab-runner.plist” and replace “hetzner” with the user name you want to run the GitLab Runner with.

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
      <key>EnvironmentVariables</key>
      <dict>
        <key>PATH</key>
        <string>/opt/homebrew/bin:/opt/homebrew/sbin:/usr/bin:/bin:/usr/sbin:/sbin</string>
      </dict>
      <key>KeepAlive</key>
      <true/>
      <key>Label</key>
      <string>homebrew.mxcl.gitlab-runner</string>
      <key>LegacyTimers</key>
      <true/>
      <key>ProgramArguments</key>
      <array>
        <string>/opt/homebrew/opt/gitlab-runner/bin/gitlab-runner</string>
        <string>--log-format=json</string>
        <string>--log-level=debug</string>
        <string>run</string>
      </array>
      <key>RunAtLoad</key>
      <true/>
      <key>StandardOutPath</key>
      <string>/Users/hetzner/logs/gitlab-runner.log</string>
      <key>StandardErrorPath</key>
      <string>/Users/hetzner/logs/gitlab-runner.log</string>
      <key>UserName</key>
      <string>hetzner</string>
      <key>WorkingDirectory</key>
      <string>/Users/hetzner</string>
    </dict>
    </plist>
    
  5. Reboot your server.

  6. You can check that the GitLab Runner service is now running via ps aux | grep -i gitlab.

  7. You can look at the logs of the GitLab Runner service via tail -f /Users/hetzner/logs/gitlab-runner.log.