Configuring a minimal LXD image for use with Ansible

I recently decided to spend some time tinkering with LXD. My organisation already uses Docker containers for certain services in production, but they do not address every use case. I wanted something lightweight that would behave more like a traditional instance for use in testing. Our automation tool of choice is Ansible - while we choose to bake some things directly into AMIs, we take a base Ubuntu image from amazon and then use Ansible to provision changes before creating an AMI from that instance. We could, but do not yet use Packer to streamline this. Priorities...

I don't know all of LXD's potential pitfalls yet, but so far I am fairly impressed. Stéphane Graber's fantastic introduction explains the basic setup and usage of LXD, as well as the history of the project.

My immediate challenge was to configure an image that could be used in a similar way to our Ubuntu EC2 AMIs. It must:

  • be controllable via ansible
  • have a baked-in SSH public key
  • allow an authenticated user to use password-less sudo

Setting these things up was simple. First, launch a container based on the vanilla Ubuntu version of your choice (I opted for 16.04, the default):
$ lxc launch ubuntu:

If I wanted this to truly behave like an EC2 instance, I could simply add an authorized_key to the root user. In this case, I'm the only user who will be using this image for testing, so I simply add myself. In order to do this, use lxd to execute a bash shell:
$ lxc execute $CONTAINER_NAME bash

This should spawn a shell with root privileges. We can use this to lay down some basic configuration that is necessary for Ansible

# create a user and home directory
$ useradd -m -d /home/starseed starseed

# Create the .ssh subdirectory in home folder and chown it to my user
$ mkdir -p /home/starseed/.ssh
$ chown starseed:starseed /home/starseed/.ssh

# Create authorized_keys file, chmod and chown it
$ vi /home/starseed/.ssh/authorized_keys
$ chmod 0400 /home/starseed/.ssh/authorized_keys
$ chown starseed /home/starseed/.ssh/authorized_keys

# We need password-less access after authenticating via SSH keypair
# First, create a group
$ groupadd sysadmin

# Allow members of the sysadmin group password-less access
$ echo '%sysadmin ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/sysadmin

# Add my user to the sysadmin group
$ usermod -aG sysadmin starseed

# Change default shell to bash (or something else if you prefer)
$ chsh -s /bin/bash starseed

# Last, ansible depends on Python2 and it isn't present in the basic Ubuntu 16.04 image. Let's install it
$ sudo apt-get update
$ sudo apt-get install python

Now our container is ready to accept password-less SSH access, allows us to sudo and is ready to be provisioned via Ansible. Let's create an image from the configured container!
lxc publish $CONTAINER_NAME --alias ubuntu-1604-ans-minimal

lxc publish sound public - it isn't. From Canonical:

Importantly, because “–public” was passed to the lxc publish command, anyone who can reach your lxd server or the image server at “remote:” will also be able to use the image. Of course, for private images, don’t use “–public”.

Now we can launch containers from this image. List available images:
$ lxc image list

Launch a new container from your modified image:
lxc launch ubuntu1604-minimal $CONTAINER_NAME

Obviously, call it whatever you like. Check that your new container is running:
$ lxc list

As a test, you can run something like this playbook against it:

---
hosts: all  
gather_facts: true  
sudo: true  
tasks:

- name: Display all available vars
  debug: var=hostvars[inventory_hostname]

Which I run with the following command. Inventory file simply contains the IP address which lxd list provides, I haven't setup any DNS. I'm grepping here just for some basic information on OS type, the full list of available vars is rather large (but useful!):

ansible-playbook display-available-vars.yml -i inventories/lxd | grep -i "ansible_distribution"

Output:

"ansible_distribution": "Ubuntu", 
"ansible_distribution_major_version": "16", 
"ansible_distribution_release": "xenial", 
"ansible_distribution_version": "16.04",

And there you have it!