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:
xcode
brew
-
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)