Skip to main content

I use Ansible to configure macOS!

·8 mins
automation macos ansible
Romain Boulanger
Author
Romain Boulanger
Cloud Architect with DevSecOps mindset
Table of Contents

Post updates:

November 26, 2024: Configuration adjustment for macOS Sequoia and creation of the English version of the article.

March 21, 2024: Changes to the playbook and associated roles for macOS Sonoma. New sections added to expand content and explain certain concepts in more detail.

Introduction
#

The end of the year is approaching fast! It is a perfect time to clean out your machines and reinstall your operating system.

In this post, I’m going to show you how I install macOS, and in particular the latest version: Sequoia.

On a day-to-day basis, I try to automate as many things as possible, and this is also the case for my laptop, which is configured with the most convenient tool for the job: Ansible.

In fact, Ansible lets you define a set of tasks to be carried out to install, update or configure various tools and software present within an operating system.

Naturally, I’m going to take you through all the roles I’ve designed to facilitate macOS configuration.

Before diving in, let’s explore the steps to customise this operating system.

Configuring macOS
#

Installing packages
#

When talking about installation on macOS, it’s hard not to mention Homebrew, offering a package and dependency manager that fills an important gap on this operating system.

This makes it possible to install simple binaries like ansible (named formulae) or applications with a graphical interface like firefox (named casks).

The brew command line eliminates the need for manual installations and provides the ability to update your entire system in a single operation.

As far as the Mac App Store is concerned, the mas command lets you install the applications you want using an identifier consisting of a series of numbers.

Configuration files
#

As you may be know, macOS is a proprietary operating system, which can occasionally complicate the automation of certain settings or features.

Nevertheless, it is possible to use command-line tools to make changes. These tools provide a means of updating .plist files, containing an application’s configuration.

This file format is not unique to macOS, but is also found in iOS, iPadOS and all Apple systems.

In terms of structure, it is an XML made up of a multitude of key/value pairs, bearing in mind that the value is necessarily typed.

For example, to modify parameters within the Finder, you need to go through the com.apple.finder.plist file.

These .plist files are binaries that you can read with the plutil utility like this: plutil -p file.plist.

In addition, and as mentioned above, macOS provides two command-line tools for manipulating them:

  • The first command, defaults, is fairly comprehensive and allows you to add, modify and delete files;
  • The second command, /usr/libexec/PlistBuddy, provides similar functionality to the first but uses a more complex syntax. However, it offers greater flexibility for managing multiple values within the files.

Now, how does all this work on the Ansible side?

Ansible and macOS
#

Without going over Ansible again, this tool offers a fairly wide range of modules for the various tasks defined.

The first step is to install formulae and casks. Ansible has modules such as community.general.homebrew and community.general.homebrew_cask that are sufficiently configurable to suit my needs.

It is even possible to reference external repositories (named taps) in the community.general.homebrew_tap module, giving you a wide choice of possibilities!

For system configuration, it is recommended using the community.general.osx_defaults module to carry out the defaults command tasks. For ListBuddy, on the other hand, there is currently no module, so I used good traditional ansible.builtin.shell with a few checks upstream to ensure that the result was resilient.

The playbook
#

Before starting
#

You can find my code repository at this URL in GitHub :

axinorm/macbook-setup

Shell
11
2

I suggest you clone it and adjust the parameters in the mymac.yml file to suit your needs.

You should also test this playbook on a virtual machine before running it on your own system to avoid any problems.

Roles
#

Within the playbook, several roles have been defined to separate the configuration and make it scalable:

  • preflight: Preliminary stage that checks that the system settings application (System Settings) is closed. Failure to do so could result in conflicts when modifying certain parameters;

  • hostname: Changing the name of the machine and its network name;

  • homebrew: installing taps, formulae and casks using modules associated with Homebrew;

  • appstore: Installing software from the App Store. For this role to work properly, you need to be logged in with your Apple ID;

  • rosetta: Activating Rosetta 2, the macOS utility for emulating applications running on Intel architecture rather than Apple Silicon (arm64);

  • git : Configuring Git for the current user;

  • zsh: Setting up zsh and plugins. This role adds Oh My Zsh for more customisation and the powerlevel10k theme;

  • tmux: Configuring tmux and installing plugins, not forgetting all the associated customisation scripts;

  • ssh: Adding directories essential for the ssh command to work properly, with the possibility of adding custom files such as private or public keys. It is recommended to encrypt them beforehand with the ansible-vault command;

  • vim: Basic configuration of Vim;

  • iterm2: installing and configuring iTerm2: changing some settings and adding a theme;

  • vscodium: installing VSCodium and its extensions;

  • macos_settings: Configuring the system and most of the applications installed with macOS. This role is made up of sub-categories:

    • configure_settings.yml: Changing the default values and removing the sound when macOS is launched ;
    • configure_systemuiserver.yml: Modifying the default values for the graphical interface;
    • configure_clock.yml: Setting the clock at the top right of the screen;
    • configure_dock.yml: Changing the default values and configuration of applications in the Dock;
    • configure_finder.yml: Changing default values, updating folder visibility and deleting tags;
    • configure_safari.yml and configure_textedit.yml: Changing default values for Safari and TextEdit;
    • configure_network.yml: Configuring DNS for defined interfaces;
    • configure_siri.yml: Setting up Siri;
    • configure_others.yml: Modifying default values for parameters not defined above and configuring Night Shift with the [nightlight] tool (https://github.com/smudge/nightlight).
If you’d like to learn more about tmux, I wrote an article explaining the tool’s features and configuration. You can find it here.

Bear in mind that these roles have been tested on macOS Sequoia and may not be suitable for other versions. Especially as this macOS has modified several internal structures in the .plist files of various services.

Inventory and variables
#

To make life easier, an inventory has already been defined in inventory/hosts, pointing to the localhost being the target macOS to be configured.

By default a mymac group is set up, both in the inventory and in the inventory/group_vars folder under the name mymac.yml with a few values by way of example.

Preferences and styles vary from person to person, so I suggest thoroughly reviewing and customizing the settings to suit your needs.

Here are a few examples of how to define formulae and casks within Homebrew :

homebrew:
  taps: []

  formulae:
    - ansible
    - ansible-lint

[...]

  casks:
    - firefox
    - iina
    - vscodium

Similarly, to configure the Finder, you can modify the finder.defaults variable to configure the .plist file:

finder:
  defaults:
    - { name: Allow quitting via ⌘ + Q; doing so will also hide desktop icons, key: QuitMenuItem, type: bool, value: true }
    - { name: Show hidden files, key: AppleShowAllFiles, type: bool, value: true }
    - { name: Show status bar, key: ShowStatusBar, type: bool, value: true }
    - { name: Show path bar, key: ShowPathbar, type: bool, value: true }
    - { name: Use list view in all Finder windows by default, key: FXPreferredViewStyle, type: string, value: Nlsv }
    - { name: Keep folders on top when sorting by name, key: _FXSortFoldersFirst, type: bool, value: true }
    - { name: When performing a search, search the current folder by default, key: FXDefaultSearchScope, type: string, value: SCcf }
[...]

Installation
#

Before you start running the playbook setup.yml, make sure you have followed the section above to configure your inventory.

In addition, on a clean system, certain tools are essential for the playbook to work properly. You can find them in the setup.sh script.

The chicken and egg story, again and again!

Here’s how the script works:

  • Installing Command Line Tools for macOS:
xcode-select --install
  • Setting up the Homebrew package manager:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
  • Installing and configuring Ansible:
brew install ansible
  • Creating a folder for the Git code and cloning the code repository. This part can be adapted to suit your needs:
mkdir -p ~/dev
git clone https://github.com/axinorm/macbook-setup ~/dev/macbook-setup

Everything looks set to go ahead with the roll-out of the playbook!

Running the playbook
#

Before running the playbook, there’s one final configuration step: ensuring the application running the playbook has full disk access to access certain properties.

To do this, you can add the Terminal application to Privacy & Security > Full Disk Access in the system settings.

Running the playbook is straightforward:

ansible-playbook setup.yml --inventory inventory/hosts --ask-become-pass

The --ask-become-pass parameter is essential because some of the tasks performed by Ansible require elevated privileges.

Once the playbook is complete, I recommend that you restart your Mac to finish the installation!

Final word
#

As you can see, creating an installation playbook is relatively easy. The biggest task is to delve into the settings and the .plist files in order to find the right value for you. Not to mention running a series of tests to see if the result matches your expectations.

Personally, with each major macOS release, I use this playbook to perform a clean installation, ensuring it stays updated with every new version.

As mentioned earlier, feel free to use this code snippet as a starting point for installing your operating system.

In any case, I hope you find these few lines of code useful and time-saving!