Yesterday I published: Bash + GNU Stow: take a walk while your new macbook is being set up. I was already taking a look at ansible so I decided to finish my playbooks to match exactly what my dotfiles script does.
So today we are going to learn how to automate our dev environment with ansible.
Bootstrapping
Similar to my dotfiles script, we need to install necessary dependencies before running the playbooks. For that we need these dependencies:
xcodebrew-
git,python
Putting into code:
bootstrap() {
info "Bootstraping..."
info "Installing xcode"
install_xcode
info "Installing HomeBrew"
install_homebrew
info "Installing python3"
install_brew_formula "python3"
info "Installing git"
install_brew_formula "git"
PATH="/usr/local/bin:$(/opt/homebrew/bin/python3 -m site --user-base)/bin:$PATH"
export PATH
info "Installing pip"
curl https://bootstrap.pypa.io/get-pip.py | python3
}
Installing ansible
I'm using virtualenv to fetch all ansible dependencies, without installing them globally.
python3 -m pip install --user virtualenv
virtualenv venv
. venv/bin/activate
info "Installing Ansible"
pip install ansible
info "Setting up Ansible"
ansible-galaxy collection install -r setup/requirements.yml
Requirements are:
- name: community.crypto
- name: community.general
The collection community.crypto will be used to generate SSH keys.
At this point we have all our requirements. Let's write the playbook.
autoenv playbook
To replicate my .dotfiles commands, I have set up the following tasks:
[+] Installing all the apps, brew packages and casks
For this we can use the community.general. We can define some list variables with all the apps/packages/apps we use. Then we execute as follows:
- name: Install Homebrew formulas
community.general.homebrew:
name: "{{ homebrew['formulas'] }}"
tags: [packages, homebrew_formulas]
Same for taps and casks.
For App store apps we use mas and ansible.builtin.command to run a shell command in loop:
- name: Install app store apps
ansible.builtin.command: "mas install {{ item }}"
loop: "{{ homebrew['mas'] }}"
tags: [packages, mac_app_store]
[+] Writing all my macOS settings
Here we will need community.general.osx_defaults. We need to define a list with settings, having domain, key, value and type for each setting. E.g.
- domain: com.apple.TimeMachine
key: DoNotOfferNewDisksForBackup
name: Disable prompting to use new exteral drives as Time Machine volume
type: bool
value: 'true'
After defining all our settings, this task is defined as follows:
- name: Set macOS default settings
community.general.osx_defaults:
domain: "{{ item['domain'] }}"
key: "{{ item['key'] }}"
type: "{{ item['type'] | default(omit) }}"
value: "{{ item['value'] }}"
loop: "{{ defaults }}"
tags: system_settings
[+] Stowing dotfiles
I've seen many setups with ansible using the file copy functionality to manag dotfiles, but this is not suitable for me. I prefer using stow so any change on the symlinks can be pushed to the git repo.
I opted for using the same bash script from my dotfiles repo and call the script as a command:
- name: Install Dotfiles
ansible.builtin.command: sh ~/.autoenv/dotfiles/install.sh
tags: dotfiles
[+] Setting vs code as default for all the source code extensions
For this I just needed to set a list variable with all the extension and then loop into a shell command:
- name: Set VSCode as default editor
ansible.builtin.shell: |
local exts=("{{ fileExtensions | join(' ') }}")
for ext in $exts; do
duti -s com.microsoft.VSCode $ext all
done
exit 0
tags: settings
[+] Installing vim-plug
Same as before, another shell command will do the trick.
But here I also wanted to check if vim-plug was already installed. And for that ansible provides a file stat api and conditional tasks using the when keyword.
- name: Check vim-plugged file
stat:
path: ~/.local/share/nvim/site/autoload/plug.vim
register: vim_plug
- name: Install neovim plugin manager
ansible.builtin.shell: sh -c 'curl -fLo $HOME/.local/share/nvim/site/autoload/plug.vim --create-dirs https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim'
when: not vim_plug.stat.exists
[+] Generating SSH keys
As mentioned before, here we make use of the community.crypto collection to generate a ssh key.
- name: Create SSH keys
community.crypto.openssh_keypair:
path: "{{ ansible_user_dir }}/.ssh/id_{{ item }}"
passphrase: "{{ ssh_passphrase }}"
type: "{{ item }}"
size: 4096
comment: "{{ ansible_user_id }}@{{ ansible_hostname }} {{ ansible_date_time['date'] }}"
loop: "{{ ssh_key_types_to_generate | split(',') }}"
loop_control:
label: "{{ item }}"
Note: ssh_passphrase and ssh_key_types_to_generate are variables.
[+] Uploading keys to github
In this task I want to upload my ssh key to github. For that I need a github token and use the community.general collection:
- name: Register SSH key with Github
vars:
github_keys:
- "{{ ansible_user_dir }}/.ssh/id_ed25519.pub"
pubkey: "{{ lookup('first_found', github_keys, errors='ignore') }}"
community.general.github_key:
name: "{{ ansible_user_id }}@{{ ansible_hostname }}"
pubkey: "{{ lookup('file', pubkey) }}"
state: present
token: "{{ github['personal_token'] }}"
tags: github
[+] Installing pip packages
To install our pip packages we can use the ansible.buitin.pip command and declare a list variable with the packages to install:
- name: Install Python packages
ansible.builtin.pip:
executable: "{{ pip }}"
name: "{{ pypi_packages }}"
extra_args: --user
tags: [core, python]
That's it.
Post install
So I had a few things left off that weren't suitable to run with ansible since they need some interaction: running rustup-init, installing nvim plugins and restarting the system. Basically:
nvim +PlugInstall +qall
if ! hash rustc &>/dev/null; then
info "Triggering Rust-up"
rustup-init
fi
info "Done"
info "System must restart. Restart?"
select yn in "y" "n"; do
case $yn in
y ) sudo shutdown -r now; break;;
n ) exit;;
esac
done
We are completely done 🤖
Conclusions
I decided to try ansible out of curiosity to manage my dev environment. It is a very robust and versatile solution but it feels like an overkill for this task. I definitely like to set configuration files in yaml but my configurations/variables inside bash files are not too bloated to justify the switch (though I wrote the whole thing for ansible already). For now I'd prefer to continue using my bash+stow solution.
What do you think? Would you rather use ansible?
autoenv
My ansible playbooks to setup macOS laptos with a development environment.
Running
export GITHUB_TOKEN="<- token ->"
curl -sO https://raw.githubusercontent.com/protiumx/autoenv/main/autoenv
The script will install the initial requirements:
- xCode
- Hombrew
- Git
- Python3 and PIP
- Virtual env
- Ansible
From there, ansible takes over with the autoenv playbook.
When ansible is done, the post-install script run commands that are not suitable for ansible.
Github Token
ansible will upload the ssh key to github, for that you need to export a GITHUB_TOKEN before running the scripts.
Customization
Most of the customizable configs reside on the grou-vars definitions.
You can check all the system settings, brew packages/casks and app store apps that will be installed.
👽


Top comments (0)