Chef on macOS

Share

At PSPDFKit we're currently using 20 Mac minis to run our continuous integration. Manually setting up these machines is a very time consuming and error prone process. Keeping all these machines in sync by hand is almost impossible. That's why we're using Chef to describe our infrastructure in code.

Over the years we grew from using 2 Mac minis to around 20, hosted in various data centers around the world. These 2 Macs were running several virtual machines, so we could run more jobs in parallel, but we ran into various issues with our Jenkins connection and the iOS Simulator. Running one job on one machine at a time without virtualization has proven the most reliable.

The reason we have so many machines is the sheer number of different Jenkins jobs we need to run on them. We have Jenkins jobs for iOS tests (with and without ASAN and TSAN enabled), tvOS tests, watchOS tests, macOS tests, Android tests, C++ tests, tests targeting different web browsers, Elixir tests, end-to-end tests for our sync platform and several jobs building releases of all our different products.

We needed a reliable and reproducible way to set up these machines. We started writing our macOS setup with Ansible, because it seemed like a simpler solution at the time. But we soon realized that writing Ruby code in Chef recipes is way more powerful than the YAML syntax in Ansible playbooks. Chef's Supermarket is also a big advantage. Using recipes from cookbooks like homebrew and build-essential is a huge timesaver.

This post is meant to help you get started with Chef on macOS, not to be a Chef tutorial. If you've never used Chef please take a look at their documentation first. We assume that you have installed the Chef DK and already created a cookbook for you to work in.

Add a Test Kitchen

A Test Kitchen allows you to test your cookbook in a temporary environment that resembles production.
Think of it as a virtual machine in which you confirm that things are working before you deploy your code to a production environment.
The workflow is as follows:

Installing Xcode

A common task on macOS is to install Xcode, which is a fairly complicated procedure, but all the heavy lifting in our xcode.rb recipe is handled by the xcode-install gem.
It downloads and unpacks Xcode, accepts the license, installs command line tools and even simulators.

In attributes/default.rb we define what Xcode and simulator versions we want to install:

The xcode.rb recipe then installs our specified Xcode version. xcode-install needs credentials to access the Apple Developer Center. We save those credentials as data bag items and then set them as environment variables:

Chef Supermarket Cookbooks

The Chef Supermarket contains a few cookbooks that are especially interesting on macOS:

build-essential

The build-essential cookbook installs packages required for compiling C software from source. In the case of macOS it installs the Xcode command line tools. This cookbook is important if you want to install Xcode with the xcode-install gem, because xcode-install has a dependency on a gem with native extensions, which means you need the Xcode command line tools to build it. So you need to run the build-essential::default recipe before installing the xcode-install gem.

homebrew

The homebrew cookbook installs Homebrew and under Chef 11 the Homebrew package provider is set as the default package provider. Installing the Android SDK for example is as easy as package android-sdk.