Skip to content

Development Environment Setup with Chezmoi

Published: at 09:23 PM

Development Environment Setup with Chezmoi

Keeping my personal and professional Macbook development environment in sync is a challenge. Daniel Banck recomended chezmoi to me so I gave it a try.

If you need to keep multiple machines in sync, you should check it out. I’ll focus on some interesting capabilities / patterns I saw that I found worth sharing.

My dream for automating my development setup has always been a 1-Click installation. I want to be able to run a script and have my development environment set up the way I like it. I want to be able to run the same script on my personal and professional Macbook and have different tools and environment variables set up for each machine.

Where am I coming from?

I have been using a combination of Homebrew / Homebrew Bundle together with an Ansible playbook to manage my development environment. This setup has been working well for me, but I am always looking for ways to improve my workflow. I symlinked most of my dotfiles, so I can update them where they are used and do occasional commits on the reposiory where they are stored. I had a bunch of bash scripts to automate the setup of my development environment, but I was looking for a more elegant solution. Especially having a different set of tools and environment variables for my personal and professional Macbook was a pain. I had one script that updated my brewfile automaticaly, which sounds great in theory, but in practice I had a nearly unmaintainable brewfile. brew bundle dump adds all formulas installed with brew, including the ones that come as dependencies. This made cleaning up formulas a pain, there was no deliberate decision to include or exclude things from the setup anymore.

You can find my old setup under DanielMSchmidt/personal-provisioning if you are interested.

What is Chezmoi?

Chezmoi is a tool that helps you manage your dotfiles across multiple machines. It is written in Go and uses a simple configuration file to manage your dotfiles. It has a templating engine built in, so you can use the same configuration file for multiple machines and have different values for different machines. That’s a huge win for me. It also integrates tightly with 1Password, so you can store your secrets in 1Password and use them in your dotfiles. I am relying heavily on 1Password, so this is a big plus for me. It can also run scripts only once or each time a file changes, which makes running the automation a lot faster.

Keeping it simple

In my last automation I had a portion that installed Homebrew and ran xcode install in the same script I used for every run. I rely on these tools in the rest of my workflows, but essentially they are an extra step to be run on a fresh machine. This time I have a very simple startup script that can be run through curl that kicks the entire process of:

# Source: https://github.com/DanielMSchmidt/dotfiles/blob/74d5cf6d4e74e2aab652c29523bbf5fed54ab979/.startup.sh#L7-L24
# Install XCode Command Line Tools if necessary
xcode-select --install || echo "XCode already installed"

# Install Homebrew if necessary
if which -s brew; then
    echo 'Homebrew is already installed'
else
    /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
    (
        echo
        echo 'eval "$(/opt/homebrew/bin/brew shellenv)"'
    ) >>$HOME/.zprofile
    eval "$(/opt/homebrew/bin/brew shellenv)"
fi

brew install chezmoi
chezmoi init danielmschmidt
chezmoi apply

In the day to day work, I will only need to run chezmoi apply to apply the changes to my dotfiles. Adding new files under version control is as simple as running chezmoi add <file>. To template a file I just need to add a .tmpl to the file and use the templating engine in the file.

The rest of my one-time setup (setting system prefereances / default editor / shell) is done in a one-time run script. All I had to do to make it run only once per machine was naming it run_once_after_setup-<name>.sh. Very simple.

Handling differences between machines

This was my biggest pain point in my old setup and it’s one of the strenghts of chezmoi. This is all I need to do to have different values for different machines:

// Source: https://github.com/DanielMSchmidt/dotfiles/blob/d36f2533e3b7a6a915692ee482cb461df9c94352/.chezmoi.toml.tmpl#L1-L4
{{- $isWorkComputer := promptBoolOnce . "isWorkComputer" "Is this your work computer" -}}

[data]
    isWorkComputer = {{ $isWorkComputer }}

If I want to have for example different environment variables containing secrets in my shell, I just need to add the .tmpl extension to the shell config and add some templating to the file:

// Source: https://github.com/DanielMSchmidt/dotfiles/blob/d36f2533e3b7a6a915692ee482cb461df9c94352/dot_config/fish/conf.d/secrets.fish.tmpl#L1-L7
set -x GITHUB_TOKEN {{ onepasswordRead "op://gv2pj3dcyg2wxx3ahtvgdamp2e/dv6xj4ndkqmenm63d3f45dts3e/token" "EI6GPO6VNJAVLGDDID3B75JI6E" }}

{{ if .isWorkComputer -}}
set -x HOMEBREW_GITHUB_API_TOKEN {{ onepasswordRead "op://gv2pj3dcyg2wxx3ahtvgdamp2e/dv6xj4ndkqmenm63d3f45dts3e/token" "EI6GPO6VNJAVLGDDID3B75JI6E" }}
set -x BOB_GITHUB_TOKEN {{ onepasswordRead "op://gv2pj3dcyg2wxx3ahtvgdamp2e/dv6xj4ndkqmenm63d3f45dts3e/token" "EI6GPO6VNJAVLGDDID3B75JI6E" }}
set -x NGROK_API_KEY {{ onepasswordRead "op://dm6p7vq2kzadhezt5qhuvhavxe/tflocal-cloud Ngrok API key/password" "2HZZS3CSKVA7REGL25XWFDGOPE" }}
{{ end -}}

{{ if .isWorkComputer -}} is all we need, making it very simple to adjust the configuration for different machines.

Using 1Password

Another problem I had was handling secrets. I like to keep them accessible in my shell, but I don’t want to store them in plain text in my dotfiles. Chezmoi has a tight integration with 1Password, so I can store my secrets in 1Password and use them in my dotfiles.

I can even do this across different vaults and different accounts. This is a huge win for me, as I am using 1Password for all my secrets. Let me show you how this works on the example of adding a GPG key for signing commits. In my .gitconfig (or dot_gitconfig.tmpl as chezmoi tracks it) I have these lines:

// Source: https://github.com/DanielMSchmidt/dotfiles/blob/d36f2533e3b7a6a915692ee482cb461df9c94352/dot_gitconfig.tmpl#L22-L25
[user]
	name = Daniel Schmidt
	email = [email protected]
	signingkey = {{ onepasswordRead "op://gv2pj3dcyg2wxx3ahtvgdamp2e/okuhh55hdfabbnwvgzomsywrxe/id" "EI6GPO6VNJAVLGDDID3B75JI6E" }}

I am accessing the secret through onepasswordRead "op://<vault_id>/<secret_id>/<key>" "<account_id>". The easiest way to get these values was to use the 1Password CLI and run op item get "name_of_your_secret" --share-link. This returns a link that contains the vault_id, secret_id and account_id: https://start.1password.com/open/i?a=<account_id>&h=<account_url>&i=<secret_id>&v=<vault_id>. It took me a minute to find this way, but it works like a charm.

The key mentioned above is the field name, to double check you can run op item get "name_of_your_secret" --format json and take a look at fields.label.

Most of my secrets were accessed this way, but in case of the GPG key I had used the document upload feature of 1Password. Downloading it required me to use the op read --out-file command manually in a script that runs after the chezmoi apply command. We will talk about this script in the next section.

Running scripts on updates

To run a script it changes you need to name it run_onchange-<name>.sh. If you make it a template by adding an extra .tmpl you can make the script respond to changing secrets / inputs). If you want to run it if a different file changed you can use a hash of the file in a comment in the script:

// Source: https://github.com/DanielMSchmidt/dotfiles/blob/d36f2533e3b7a6a915692ee482cb461df9c94352/run_onchange_mise-install.sh.tmpl#L3-L8
set -ex

# hash of the mise config file, used to determine if we need to rerun this script
# mise/config.toml hash: {{ include "dot_config/mise/config.toml" | sha256sum }}

mise install -y

If you need to react to a changing secret that you access through the 1Password CLI you can use the onepassword template command to inline a JSON value of the secret into the script. In this case I decided for the version since it changes when the secret changes:

// Source: https://github.com/DanielMSchmidt/dotfiles/blob/d36f2533e3b7a6a915692ee482cb461df9c94352/run_onchange_import-gpg-key.sh.tmpl#L1-L23
#!/bin/bash

set -e

# We use the version of the secret to determine if we need to update the secret
# This needs to be kept in sync with the .gitconfig file
# Secret version: {{ (onepassword "okuhh55hdfabbnwvgzomsywrxe" "gv2pj3dcyg2wxx3ahtvgdamp2e" "EI6GPO6VNJAVLGDDID3B75JI6E").version }}

# Early abort if the key is already added
if gpg --list-secret-keys | grep -q {{ onepasswordRead "op://gv2pj3dcyg2wxx3ahtvgdamp2e/okuhh55hdfabbnwvgzomsywrxe/id" "EI6GPO6VNJAVLGDDID3B75JI6E" }}; then
    exit 0
fi

# EOF the key into a temporary file

tmpDir=$(mktemp -d)
tmpFile="${tmpDir}/private.key"

# Manually use the onepassword command to get the file
op read --out-file $tmpFile op://gv2pj3dcyg2wxx3ahtvgdamp2e/okuhh55hdfabbnwvgzomsywrxe/gmail.key --account "EI6GPO6VNJAVLGDDID3B75JI6E"

# Import the key
gpg --import $tmpFile

Conclusion

I am so far very happy with the setup, it seems way simpler than any other automation I tried. Both my Macbooks run M-series processors and OSX so I did not have to work around any differences between the machines, but the chezmoi docs gave me confidence that the tool could handle this if I needed it.

I also have to say it was very quick to setup, especially since I had dotfiles to look at for inspiration. I hope this blog post helps you to get started with chezmoi and that you will enjoy it as much as I do. If you have any questions or feedback, feel free to reach out to me on Twitter / X.




Previous Post
Inline GitHub Code Snippets through Remark
Next Post
Discriminated Union Pattern in Go