Using Ansible to Manage Personal Computers

Ansible is a tool for automating system management. It is maintained by RedHat, and serves the same role as other tools like SaltStack, chef, puppet, and cfengine. A few years ago I started looking for a tool to manage my personal machines so that setting up a new computer did not take three days. I decided to give Ansible a try because it is simple, agentless (it doesn't require a server process), and is written in Python. So far, I have been pretty happy with it, and setting up a new computer now takes a few hours.

I started using Ansible when I did a fresh Ubuntu install on my desktop computer. I decided that I was not going to do any manual configuration, and was going to write an Ansible playbook to do it instead. So, when I remembered a tool to install, rather using apt install, I added it to my Ansible playbook and reran Ansible. If I needed to edit an /ect configuration file, I wrote a task to do it in the playbook. Of course, it took a little longer to get setup in the beginning, but now doing a reinstall or setting up a new computer is quick and simple.

In this post I will document my setup in case I need to recreate it from scratch at some time in the future. Perhaps some others will find it useful as well. One warning, Ansible seems to be under active development, and I think there have been new features added since I started using it. My playbook may not be using current best practices.

Bootstrapping

I have a directory named ansible/ in my home directory that contains all of the configuration files and is under version control. So when I setup a new machine, the first thing to do is copy this folder to the new machine. The next thing to do, is get Ansible installed. Ansible is a written in python and available in PyPi, but there are a few packages it relies on that I install with apt, so I created a short script named bootstrap.sh in the top level directory. It looks like this:

#! /bin/bash

# install a minimal set of tools needed to run the ansible playbooks

set -e

if which apt > /dev/null 2>&1
then
  sudo apt update
  sudo apt install python3 python3-pip python3-apt
fi
pip3 install ansible

This assumes we are on a Debian based system with the apt package manager, which is the case for all of my machines.

A basic playbook

I have a playbook named setup-system.yml that I use to configure my entire system. When I make a configuration change to the system, I edit this playbook and rerun Ansible. That way, I will have the configuration change saved for any new machines, and I can also reconfigure my other machines (laptops, work computers, etc) easily.

An Ansible playbook contains a list of task that will be executed in sequential order. If a task will need root privileges to run, you can specify which user Ansible should "become" when running a task. To start out, I used Ansible to install all of my software from the Ubuntu repositories. It looked something like this

# setup-system.yml
---
- hosts: localhost
  connection: local
  become: yes
  become_user: root
  tasks:
  - name: Install Packages
    block:
    - name: Install System Packages
      become_user: root
      apt:
        update_cache: yes
        autoremove: no
        pkg:
         - vim-gtk
         - zathura
         - feh
         - scrot
         - pandoc
         - ranger
         - zsh
         - taskwarrior
         - texlive-latex-recommended
         - texlive-latex-extra
         - texlive-science
         - texlive-extra-utils
         - gnuplot
         - rlwrap
         - imagemagick
         - stow
         - i3
         - i3blocks
         - rofi
         - git
         - tig
         - cmake
         - g++-10
         - gcc-10
         - clang
         - clang-tools
         - clang-format
         - clang-tidy
         - clangd
         - gdb
         - ninja-build
         - htop
         - firefox
         - chromium-browser
         - pass
         - jabref
         - entr
         - beets
         - id3v2
         - mplayer
         - ffmpeg
         - dos2unix
         - librecad
         - rename
         - xjobs
         - valgrind

My actual list is quite a bit longer. Again, any time I find a package I need to install, I add it to this list and rerun Ansible.

Installing packages is just one part of setting up a system. There are also configuration files to edit, files to organize, and third-party software to install. Ansible can do this too. For example, I use zsh instead of bash, and I need to edit /etc/passwd to make this the default shell for my user. Ansible has module (a pre-defined script that can be used to perform a task) for this (there are a lot of these). To change the default shell for my user, I added this task

  - name: Configure cclark account
    user:
      name: cclark
      create_home: yes
      shell: /bin/zsh

Note that this is an item in the list under tasks in the above YAML file.

I also use a common set of directories on all my machines, which I then keep synronized with unison. Unison is the best file syncronization tool I have found, but it is written in OCaml and you hae to make sure the same version of OCaml is installed on each machine, or you can get some strange errors. The best thing I have found is to install opam and then use that to install a specific version of OCaml and Unison, which is tedious to do manually, but simple with Ansible. Here is the task that creates the directories.

  - name: Create special directories for cclark
    file:
      path: "{{item}}"
      owner: cclark
      state: directory
    loop:
      - /home/cclark/Software
      - /home/cclark/Code
      - /home/cclark/bin
      - /home/cclark/Downloads
      - /home/cclark/.ssh

And here are the task that install Unison:

    - name: Install opam
      become_user: cclark
      block:
        - name : "Download opam installer"
          get_url:
            url: "https://raw.githubusercontent.com/ocaml/opam/master/shell/install.sh"
            dest: /home/cclark/Software/opam-installer.sh
        - name : "Create bin dir for opam binary"
          file:
            path: /home/cclark/bin
            state: directory
        - name : "Run opam installer"
          expect:
            command: sh opam-installer.sh --no-backup
            chdir: /home/cclark/Software
            creates: /home/cclark/bin/opam
            responses:
              ".*Where should it be installed ?.*": /home/cclark/bin/
              ".* does not exist. Create ?.*": y
        - name : "Initialize opam"
          expect:
            command: /home/cclark/bin/opam init --disable-sandboxing
            creates: /home/cclark/.opam/config
            timeout: 1000
            responses:
              ".*Do you want opam to modify.*": n
              ".*A hook can be added.*": n
    - name: Install eigen-compitible unison
      become_user: cclark
      block:
        - name: Switch opam compiler to a version we know works
          expect:
            command: /home/cclark/bin/opam switch create 4.09.1
            creates: /home/cclark/.opam/4.09.1/bin/ocaml
            timeout: 1000
            responses:
              ".*.*clean up?": y
        - name: Install unison 2.51.2
          command:
            cmd: /home/cclark/bin/opam install -y unison.2.51.2
            creates: /home/cclark/.opam/4.09.1/bin/unison

The first task downloads the opam installer and runs it using the expect since it is an interactive installer. The second builds unison with a version of the OCaml compiler that I found I needed to use to sync files with a Gentoo machine.

So, as you can see, Ansible let's you automate quite a few things related to setting up a new computer, and since I started using it, I have not had a problem with different machine configurations drifting apart. I just edit the playbook and run Ansible on all of my computers.

Running

Once you ahve Ansible installed and a playbook ready, you need to run it. That is what the ansible-playbook command is for. To run playbook-setup.yml, open a terminal and run

$ ansible-playbook -i localhost -K playbooks/setup-system.yml 

The -i option here specifies the "inventory" to run the playbook on. Ansible is actually designed to configure remote machines, so you can maintain a list of hostnames that you want to manage. This is called an inventory. Here, I am just running it on the local machine. The -K option is required to run tasks that need root permission. It will ask you for the sudo password and use it for task that have become_user: root set.

Wrapping up

One of the reasons I ended up choosing ansible for my machine configuration is its simplicity. As you can see, all you need to do is install a few dependencies with apt, then install Ansible with pip3, write a plain text file and you are ready to go. There are many more features than I showed here, or even use in my playbook. It's possible to only run tasks if certain things are ture (for example, if the OS is Ubuntu, or if the hostname is MyCoolHost). But you can learn about these features as needed. You don't have to spend a bunch of time learning how everythign works before you can get a simple, useful, playbook working.

links

social