DEV Community

protium
protium

Posted on

autoenv: relax, ansible will setup your dev environment for you

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.

intro

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Requirements are:

- name: community.crypto
- name: community.general
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

[+] 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'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

[+] 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
Enter fullscreen mode Exit fullscreen mode

[+] 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
Enter fullscreen mode Exit fullscreen mode

[+] 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
Enter fullscreen mode Exit fullscreen mode

[+] 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 }}"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

[+] 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]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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?

GitHub logo protiumx / autoenv

Setup new dev machines with 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
Enter fullscreen mode Exit fullscreen mode

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.

ansi

👽

Discussion (0)