This is the second post in a series. Part one is here.

Now that we have our VM, let’s use Ansible to configure it as our development environment.

Config and Playbooks

Ansible instructions are defined in YAML files known as "playbooks", with global configuration in a file called ansible.cfg by default.

Create a file called ansible.cfg containing:

host_key_checking = False

ssh_args = -o ForwardAgent=yes -o ControlMaster=auto -o ControlPersist=60s

This is the ansible version of the SSH options we added to our makefile

We’re going to put all our ansible code in the ansible-playbooks directory, so create that.

Installing default packages

The first thing we need to do is install some system packages. We’ll define the list of these in a variable, to make it easy to change. Create an ansible-playbooks/vars/ directory, and then a file ansible-playbooks/vars/default.yml containing:

  - git
  - build-essential
  - net-tools
  - curl
  - jq
  - neovim
  - silversearcher-ag
  - tmux
  - postgresql-client

That’s my current list of default packages, but of course you can change that as you like. All of these are installed by running apt-get install [whatever], so let’s set that up now.

Create the file ansible-playbooks/playbook.yml containing:

- hosts: all
  become: true
    - vars/default.yml

    - import_tasks: install-packages.yml

hosts: all tells ansible to run this playbook on every host (we only have one). become: true tells ansible to run as root when executing this playbook

This file will be our main manifest, and we’ll import other manifests as needed. The first one is install-packages.yml so create that as ansible-playbooks/install-packages.yml containing:

- name: Install Prerequisites
    name: aptitude
    update_cache: yes
    state: latest
    force_apt_get: yes

# Install a newer version of git than ubuntu 20.4's 2.25
- name: Add git stable repository from PPA and install its signing key on Ubuntu target
    repo: ppa:git-core/ppa

- name: Update apt
    update_cache: yes

- name: Install required system packages
    name: "{{ sys_packages }}"
    state: latest

This is doing a few things:

  • Install aptitude to make it easier to work with packages
  • Add a source for a later version of git than the Ubuntu 20.04 default
  • Install all the packages defined in our ansible-playbooks/vars/default.yml file

Running the playbook

We can run this playbook like this:

ansible-playbook -u root \
  -i "$(cat .ip)," \
  --private-key ~/.ssh/digitalocean_buildvm \
  -e pub_key=~/.ssh/ \

Here, we’re supplying a few things along with our main playbook filename – the SSH private and public keyfiles, and the IP number of our DigitalOcean droplet (VM), via the .ip file we created from our terraform code.

Save this command as an executable file called


ansible-playbook -u root \
  -i "$(cat .ip)," \
  --private-key ~/.ssh/digitalocean_buildvm \
  -e pub_key=~/.ssh/ \

You can add as many playbooks as you like, and import them into your ansible-playbooks/playbook.yml file using import_tasks. You can see my version here if you want some ideas. I keep the full list in the GitHub repository, but comment out whatever I don’t need for a particular project before invoking the command to build my VM.

Automating ansible

Now that we have our script to apply our playbooks to the VM, let’s run it as part of our build step.

Edit the makefile and change the buildvm target to this:

	. .env; make init; make apply; sleep 180; ./

DigitalOcean runs some setup tasks on the VM at build time, so you need a pause between terraform and ansible to allow those tasks to complete, or you get an error like this:

"E: Could not get lock /var/lib/dpkg/lock-frontend. It is held by process 2308 (apt-get)", "E: Unable to acquire the dpkg frontend lock (/var/lib/dpkg/lock-frontend), is another process using it?"

I’ve found that 3 minutes is the minimum time that works reliably.

Non-root setup

Now we have a one-step process for creating our VM and installing all the software we need. But, so far ansible is doing everything as root, and we’re not going to be logging in and developing software as root.

So, we need a non-root user, and we need to run a lot of setup tasks as that user.

Non-root user

We’ll define the username in our ansible-playbooks/vars/default.yml file, along with the name of the SSH public keyfile we want to use to authenticate SSH sessions when we connect as that user.

Add these lines to your ansible-playbooks/vars/default.yml file:

create_user: david
copy_local_key: "{{ lookup('file', lookup('env','HOME') + '/.ssh/') }}"

> Here I’m using the same SSH public key as the one we use to SSH to the VM as > root, but you can use a different key file if you prefer.

The playbook I use to create this user is here. Create that file as ansible-playbooks/user.yml and import it in your ansible-playbooks/playbook.yml like this:

    - import_tasks: install-packages.yml
    - import_tasks: user.yml   # <--- add this line

This creates a non-root user. To get ansible to run configuration tasks as that user, we need a separate playbook. This is mine.

These lines tell ansible to run as our new user, and not as root:

become: false
remote_user: "{{ create_user }}"

To run our non-root playbook as well as our default playbook, all we need to do is supply it as a parameter in our script like this:


set -euo pipefail

ansible-playbook -u root \
  -i "$(cat .ip)," \
  --private-key ~/.ssh/digitalocean_buildvm \
  -e pub_key=~/.ssh/ \
  ansible-playbooks/playbook.yml ansible-playbooks/non-root.yml

You can see the various playbooks I use for both root and non-root tasks here. I’m not going to go through them individually, but if you want an explanation of how (or why) any of them work, just ask via a comment below.

I hope folks find this useful. I’m enjoying working this way, although I don’t know if I’ll continue with this approach when and if I get a decent home internet service again!

One thought on “Using Ansible to configure a cloud VM

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s