Automatically Setup Your Computer with Ansible Playbook

Setting up a computer can be a major annoyance. Being ‘the tech guy’, I still have vivid nightmares of spending every other weekend reinstalling friends’ and family members’ Windows boxes that have come to a crawl. Now, most of us know enough to backup the important data and that makes the process easier. (If you don’t have backups, go setup Nextcloud, Syncthing, or some other automatic system. Seriously, it’s gonna come back to bite you sooner or later.) But there is still the matter of installing and configuring all your software. Some people have tackled that by writing install scripts. However, there is a better way - Ansible playbook. Let me introduce Ansible. Ansible is a software for computer provisioning. In computing, provisioning means automated installation and configuration of software and services. Provisioning tools, such as Ansible, Chef and Puppet, help you cut down the setup time from hours to mere minutes. Their main advantage over custom scripts is that they are idempotent. That means they only makes the necessary changes. So, if a task fails (for example because ou forgot to connect your Ethernet cable, oops), you can just run it again without any worries. The reason I chose to use Ansible, is that the tasks are easily configurable using yaml files. It is also written in Python, which makes it easy to write your own module for it, in case of need. So what is Ansible playbook? It is the collection of yaml files, describing the tasks to be executed. You can find an introduction to its syntax in Ansible documentation. I will demonstrate how you can setup your computers using an Ansible playbook, using mine as an example. You can find it at my Gitlab page.

Structure

This is the layout of the playbook:

roles/
vars/
run.sh
setup.yaml

Roles, as the names suggests, contains roles. They are collection of tasks, files and variables needed to setup a particular service. Vars contains device-specific Ansible configuration. Setup.yaml is the playbook entry point and run.sh is the script that starts it all. Let’s go through them one by one.

The playbook entry point

Here is a snippet from the setup.yaml file:

---
- hosts: localhost
  connection: local

  vars_prompt:
    - name: config_file
      prompt: "What computer is this"
      private: no

    - name: gitlab_password
      prompt: "Enter your Gitlab password"
      private: yes

  vars_files:
    - vars/{{ config_file }}.yml

  roles:
    - alzadude.firefox-addon
    - common
    - desktop
    - dotfiles
    - {role: laptop, when: laptop} ...

The first two lines are self explanatory. They specify, that the tasks should be run on the same computer the playbook was executed on. Following is the vars_prompt, which prompts user for input and stores it in a variable. I use it to determine which device-specific configuration should be loaded. And also to enter the gitlab password - I don’t want to store it in the playbook. The largest chunk is taken by the roles. As you can see, they can be called conditionally. I have a laptop variable stored in my vars, to determine whether I should install laptop- mode-tools.

Device-specific configuration

Now, let’s look at the configuration files. These are the contents of vars/hp-dm1.yaml, the configuration file for my laptop:

---
user: dak
model: dm1
laptop: true
ssd: true

As you can see, not much going on here. Just my username for the particular machine and some variables to determine, whether to run certain roles.

Writing your roles

As I mentioned earlier, roles are a collection of tasks, files, variables etc. needed to installl and configure a service. They are the heart of a playbook. The role has a specific directory structure, that I describe below.

example_role/
  defaults/
    main.yml
  files/
    ...
  meta/
    main.yml
  tasks/
    main.yml
  templates/
    ...
  vars/
    main.yml

template are files such as systemd timers or configurations, that are necessary for the role. They include variables, which will be filled in, when the template is handled. For things that asre invariable (such as plugins), use the files directory. defaults include the default configuration, for other variables vars is used. meta contains dependencies. Lastly, tasks contain a list of tasks to run. Of course, you might not need all of these for your role. Indeed, the only mandatory section are the tasks. So let’s take a look how the task file might look:

---
- name: install terminal applications
  become: yes
  package:
    name:
      - fish
      - tmux
      - guake
    state: present

- name: make fish default shell
  become: yes
  user:
    name: "{{ user }}"
    shell: /usr/bin/fish

- name: copy guake config
  copy:
    src: guake/
    dest: ~{{ user }}/.gconf/apps/guake/
    mode: 755

The first task installs some software using apt. As you can see, it is possible to use substitution, instead of writing a new task for every piece of software. The state=present means that will check it the item is already on the system, and if not, install it with apt. The next one runs modifies a user on the system. In this case, to make fish its default shell. You could also just run chsh via the command module. But running external commands is not idempotent. So you should use the functionality provided by Ansible modules whenever possible. The last two task copies some configuration files. It automatically looks for them in the role’s files directory.

The script

The last piece of the puzzle is the shell script to run the playbook:

#! /usr/bin/env bash
# Stop unpredictible behavior
set -o errexit # Exit on most errors
set -o nounset # Disallow expansion of unset variables
set -o pipefail # Use last non-zero exit code in a pipeline

# Define constants
readonly PROJECTS_DIR=projects
readonly PLAYBOOK_REPO="https://gitlab.com/radek-sprta/ansible-personal.git"
readonly PLAYBOOK=setup.yml

function main() {
  # Install Git and Ansible
  sudo apt install git ansible
  # Clone the Ansible playbook and run it cd "${HOME}"
  mkdir -p "${PROJECTS_DIR}"
  ansible-pull -U "${PLAYBOOK_REPO}" -i localhost, "${PLAYBOOK}" --ask-become-pass
  # Install Steam
  sudo apt install steam
}

# Run the script
main "$@"

Of course, you could skip writing a script altogether. You could just install Ansible, clone the git repository and run the playbook directly. But that would beat the purpose of automating the setup. Moreover, you need to install Steam. Because of its EULA, Steam unfortunately cannot be installed by Ansible.

Conclusion

I wrote this blog post to showcase how Ansible can be useful even for home use. If you are thinking about setting up your own computer with Ansible now, check my playbook or head over to Ansible documentation