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.