Setting up your local machine using Ansible
Just few days ago, I accidentally messed up my Ubuntu workstation, it was a silly mistake. It’s lucky that I have a setup script to install everything. But it was messy, hard to maintain, and full of litte tricks to check if file exists or if the file has a particular line of text so that I don’t append the same configuration value twice. Since I’m learning Ansible, I decided to “rewrite” that horrible bash script as an Ansible’s playbook so that the next time when I mess up (and I will), I can have something more reliable to use.
This blog post won’t cover the basic concepts of Ansible (I’m an Ansible beginner myself so can’t help you there :D). Anyway, the main idea is to have a simple playbook that I can run ansible-playbook local.yml
to provision my local machine.
My playbook is pretty simple:
- hosts: localhost
become: true
vars:
tasks:
vars
is for setting up variables that I will re-use in different taskstasks
is… a list of tasks
Since I’m setting up a brand new machine, it usually involves a lot of sudo
magic. So, the hard requirement is that the user running ansible must be able to use sudo
without a password. One way to achieve this is to run sudo visudo
and then add username ALL=(ALL) NOPASSWD: ALL
at the end of the file.
Add new APT repositories⌗
The first thing that I usually do when setting up a new machine is to add some custom repositories. The normal flow for this is to first add the official GPG key and then set up the repository. For example, to add the docker repository
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo apt-key add -
sudo add-apt-repository \
"deb [arch=amd64 trusted=yes] https://download.docker.com/linux/debian \
$(lsb_release -cs) \
stable"
This process can be translated into 2 tasks in an Ansible playbook
tasks:
- name: add docker key
apt_key:
url: "https://download.docker.com/linux/ubuntu/gpg"
state: present
- name: add docker repo
apt_repository:
repo: "deb [arch=amd64 trusted=yes] https://download.docker.com/linux/ubuntu focal stable"
filename: "docker"
state: present
The first task is straightforward, apt_key
module handles apt-key add
command. The second task is a bit more interesting
- Instead of getting the lsb release (via
$(lsb_release -cs)
), I specifically setfocal
(aka Ubuntu 20.04) because I’m using Mint instead of Ubuntu and it has a different code name and the official docker repo unfortunately doesn’t support it. I will need to spend some time thinking about a better and more portable approach. - Instead of relying on Ansible to generate the repo file name (to be stored at
/etc/apt/sources.list.d/
), I find out that having a fixed name is better because otherwise you will end up with 2 different files (at some point) defining the same repo andapt
doesn’t like it.
I need more than 1 repos, technically speaking I can just copy/paste those 2 tasks and replace them with proper content, but fortunately Ansible has support for with_items
, think of it as map
tasks:
- name: add necessary keys
apt_key:
url: "{{ item }}"
state: present
loop:
- https://download.docker.com/linux/ubuntu/gpg
- https://packages.microsoft.com/keys/microsoft.asc
- https://dl.google.com/linux/linux_signing_key.pub
- name: add repositories
apt_repository:
repo: "{{ item.repo }}"
filename: "{{ item.filename }}"
state: present
update_cache: false
with_items:
- { repo: "deb [arch=amd64 trusted=yes] https://download.docker.com/linux/ubuntu focal stable", filename: "docker" }
- { repo: "deb [arch=amd64 trusted=yes] https://packages.microsoft.com/repos/vscode stable main", filename: "vscode" }
- { repo: "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main", filename: "chrome" }
- { repo: "deb [arch=amd64 trusted=yes] http://ppa.launchpad.net/longsleep/golang-backports/ubuntu focal main", filename: "golang" }
- { repo: "deb [arch=amd64 trusted=yes] http://ppa.launchpad.net/bamboo-engine/ibus-bamboo/ubuntu focal main", filename: "ibus_bamboo" }
There is also a simpler loop
, for example, here is a task to install multiple packages
- name: install basic packages
apt:
name: "{{ item }}"
state: present
loop:
- curl
- htop
Config templates⌗
Ansible comes with template module which we can use to set up a file in the host with a template. For example, here I am setting up my gitconfig with a template
- hosts: localhost
become: true
vars:
user: "{{ ansible_facts.env.SUDO_USER }}"
full_name: "Tan Nguyen"
tasks:
- name: set git config
become_user: "{{ user }}"
template:
src: gitconfig
dest: "/home/{{ user }}/.gitconfig"
and the template looks like this
[user]
email = {{ email }}
name = {{ full_name }}
[core]
editor = vim
[pull]
rebase = true
It’s self-explanatory, we define several variables in vars
and the template pick them up automatically. One thing to note here is the use of ansible_facts.env.SUDO_USER
which is one of the special variables, it basically refers to the user who is running ansible.
Deal with secrets⌗
It’s common (at least for me) to store secrets in various config files, for example the token to access my private gitlab repositories. Since I’m keeping everything in git, having secrets in plain text is dangerous and wrong in so many levels.
Fortunately, ansible comes with ansible-vault which can be use to do various kind of encryption so that secrets can be safely stored in a version-control system such as git. For my simple setup, I just need an encrypted string
ansible-vault encrypt_string --ask-vault-pass <secret>
New Vault password:
Confirm New Vault password:
!vault |
$ANSIBLE_VAULT;1.1;AES256
35613063313131336431626636393066386536623462646166646431366130363430616266353734
3031363038373637396630663863653434353937656332310a386664343431656231353430633664
37383939326163333630353933343330336532633337323731386266376537373666313135633336
6132386438373533350a613130306233336134646236393434323137646565336261613864386339
3564
Encryption successful
And to use it, simply replace the actual value with it, for example
- hosts: localhost
become: true
vars:
user: "{{ ansible_facts.env.SUDO_USER }}"
full_name: "Tan Nguyen"
email: !vault |
$ANSIBLE_VAULT;1.1;AES256
34363132333730623833383965373463663734653537346330613435633562653831303563313463
3266373363663730333265626239616463623339373135380a616337396639306432313164636462
66356361393438376361653834323862646139376639333163653961303761643937356336643035
6637356262326238370a383438386463303734343034386134656164356263336333633033383830
33613530623761353037666237356463393231666535313030386463343333366534
gitlab_username: !vault |
$ANSIBLE_VAULT;1.1;AES256
39356137383338393432313634396134366134663531636230363264363233656564626538663430
6538326537323930633135323638333564363539393839630a643433633333343837323438323364
61623232363730373938353532373538623039323539666338613430663432633362363333316236
6562316461333636300a383461623162326631636162386339613434393764313838663065633737
3238
gitlab_token: !vault |
$ANSIBLE_VAULT;1.1;AES256
62353563326233316362643734346163623432346238323665396134326665346263613932643333
3339613035386635613039383064356361643365353530340a636166646461336462333834373630
35623361666437383161306565666162393264313731653434343164313064313739363165363730
6335646335633338350a356232396662613463383331393930356430366236626535323137303535
65356465353965336165323737386331396263616435323461333534613136356238
tasks:
- name: set git config
become_user: "{{ user }}"
template:
src: gitconfig
dest: "/home/{{ user }}/.gitconfig"
and refer to them as you would normally do in the template
[user]
email = {{ email }}
name = {{ full_name }}
[core]
editor = vim
[pull]
rebase = true
[merge]
tool = meld
[url "https://{{ gitlab_username }}:{{ gitlab_token }}@gitlab.com"]
insteadOf = https://gitlab.com
Now whenever you want to run the playbook, you need to have specify --ask-vault-pass
OR follow one of the many setups here. For the sake of simplicity, I just use one password for everything and store it somewhere safe (for example a stickit note in my closet :D)
And that’s it! It’s everything I need for my simple Ansible playbook to set up a fresh Ubuntu-based distro. You can check out the full (real) playbook here