autoenv: relax, ansible will setup your dev environment for you
Tags: ansible, productivity, automation, macos
Published: 2021-12-30 16:54:59

Yesterday I published: [Bash + GNU Stow: take a walk while your new macbook is being set up](https://dev.to/protium/bash-gnu-stow-take-a-walk-while-your-new-macbook-is-being-set-up-p1o). I was already taking a look at [ansible](https://www.ansible.com/) so I decided to finish my playbooks to match exactly what my [dotfiles](https://github.com/protiumx/.dotfiles) script does.

So today we are going to learn how to automate our dev environment with `ansible`.

![intro](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/orjvlcjog3awm18ggt8q.png)

## 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:
```sh
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](https://virtualenv.pypa.io/en/latest/) to fetch all `ansible` dependencies, without installing them globally.

```sh
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:
```yml
- 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:

```yml
- 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](https://github.com/mas-cli/mas) and `ansible.builtin.command` to run a shell command in **loop:**

```yml
- 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.
```yml
- 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:
```yml
- 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](https://www.gnu.org/software/stow/manual/stow.html) 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:
```yml
- 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:
```yml
- 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.
```yml
- 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**.
```yml
- 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:
```yml
- 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:
```yml
- 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:
```sh
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 :robot:

## 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](https://github.com/protiumx/.dotfiles) solution.

What do you think? Would you rather use `ansible`?

{% github protiumx/autoenv %}

![ansi](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gbqeni2klo77w1cerfe2.png)
 

:alien: