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:
- Configuring the firewall
- Installing the GitLab Runner
- Registering the GitLab Runner
- Configuring the CI of the repository
- Executing a basic test to confirm it works
- 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 every 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 losing 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, that’s 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.
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.
Installing the GitLab Runner
Due to the way how MacOS distinguishes between SSH and GUI sessions, the GitLab Runner 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:
- Log in with the user the GitLab Runner should be executed with. In our case “hetzner”.
- Open up a terminal.
- Execute the command
brew install gitlab-runner
to install the GitLab runner via Homebrew. - 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.
- Open a terminal, e.g. over SSH, and execute the command
gitlab-runner register
. - The required URL and token can be found in the CI/CD settings.
- Description of the runner is
macos runner
. This can be changed later on. - The tags section determines by which tag the runner can be addressed in the
.gitlab-ci.yml
inside your repository. In this case justmacos
will suffice. This can be changed later on. - In the CI/CD settings under the
Runners
section the runner should be now visible with a green and active state. - By clicking on the runner, the settings can be checked, especially the
Last Contact
field should displayjust 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. All we now need to do now in order 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.
-
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”.
-
Install the GitLab Runner for the user via
brew install gitlab-runner
. -
Log into your server, e.g. via SSH, as root.
-
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>
-
Reboot your server.
-
You can check that the GitLab Runner service is now running via
ps aux | grep -i gitlab
. -
You can look at the logs of the GitLab Runner service via
tail -f /Users/hetzner/logs/gitlab-runner.log
.