Napsty logo

Cloud Antics


A technical blog guiding cloud solutions


Using ansible to build and orchestrate clean docker images

Building containers and composing them together can often be a messy process, especially when using Ansible. You need a Dockerfile with basic bash’y commands, an ansible role to deploy them, and some sort of tool like Kubernetes or docker-compose that don’t all exactly communicate very well.

Enter ansible-container: a new tool by the awesome folks at ansible to more tightly integrate your roles with docker to create flattened images, and cleanly test and deploy your entire stack easily.

NOTE: this project is relatively immature (ok REALLY immature), but given it’s potential to simplify lives and code; it deserves a good amount of attention in its beta form.

Goal

In this article, we’re going to demonstrate how to set up a quick apache / mysql site in static, single layer containers. Drupal makes this nice and easy for demonstration purposes, so we’ll spin up a working example.

In the process we attempt to explain the workings of the ansible-container tool and how it will make your ansible ecosystem more integrated.

How ansible-container works

Ansible container works by spinning up an ‘orchestration’ container that acts as a ansible-playbook runner connecting to other containers as if they were named inventory groups.

Conceptually, it looks something like this:

Ansible Container Diagram

What makes ansible-container nice is that you can use all your existing ansible roles to provision your instances. All that needs to be done is set up ansible-container config to know how to apply them.

Getting started

First you’ll need some tools to get things running properly; I recommend running this on a ubuntu xenial VM.

If you want an actual working example, use the repo here.

Ensure you use the most recent version of docker

Xenial:

sudo apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys 58118E89F3A912897C070ADBF76221572C52609D

echo deb https://apt.dockerproject.org/repo ubuntu-xenial main | sudo tee /etc/apt/sources.list.d/docker.list

sudo apt-get update

Follow instructions here for systems other than Xenial.

Install system pre-req’s

sudo apt-get install docker-engine python python-pip libffi-dev libssl-dev python-dev git -y

Setup virtualenv (optional)

sudo pip install virtualenv
virtualenv venv
source venv/bin/activate

Install ansible-container

git clone https://github.com/ansible/ansible-container.git ansible-container
cd ansible-container

# build and install the package on your local machine
python setup.py install

Setup docker to allow access via port rather than socket

Ansible-container works by having an orchestration guest container that talks to the host docker engine. As a result, you need to modify the init script for docker-engine to bind to a port accessible to your containers. Edit /etc/systemd/system/multi-user.target.wants/docker.service file:

cat - <<EOF | sudo tee /etc/systemd/system/docker.service
[Service]
ExecStart=/usr/bin/docker daemon -H 0.0.0.0:2375
EOF

Then restart the engine:

sudo systemctl daemon-reload && sudo service docker restart

You’ll also want to set an environment variable so your client docker calls know how to talk to the docker engine:

export DOCKER_HOST="tcp://<host IP address>:2375"

NOTE: This leaves your docker instance open to anyone on your network. There are two ways to lock this down: secure your docker engine via the steps here, or isolate it to a specific local ip address that only your container and host have access to.

Init new ansible-container project

This can be run in an existing project, but to keep things simple, lets start with the basic setup.

Start by running the init command in a new directory to get a skeleton layout:

mkdir my-ac-project && cd my-ac-project
ansible-container init

This creates a directory structure that looks like so:

my-ac-project
|- ansible
   |- container.yml       # docker-compose compatible definition of containers
   |- main.yml            # ansible-playbook that gets run in orchestration container to provision containers
   |- requirements.txt    # any additional python dependencies needed in orchestration container

Modify your docker-compose / ansible-container file

Start with the template files given to you by the ansible-container init command, you should modify the container.yml and main.yml files to fit your needs.

To accomplish the goals in this article, we want to set up our files to look something like this:

# in ansible/container.yml

version: "1"                       # docker-compose file compatibility version
services:
  apache:                          # how our apache container should be configured
    image: php:5.6-apache          # staring image you want to apply ansible commands to
    command: apache2-foreground    # container commands are NOT run on build, and
                                   # if we want them run on run time, they need to be here.

    links:                         # make sure we connect to the mysql container
      - mysql
    ports:
      - "80:80"
    environment:
      HTTPD_PREFIX: /usr/local/apache2
      PATH: ${PATH}:/usr/local/apache2/bin

  mysql:                           # make sure we create a mysql container too
    image: mysql:5.6
    command: docker-entrypoint.sh mysqld
    ports:
      - "3306"
    environment:                   # some startup variables on run time
      MYSQL_ROOT_PASSWORD: root_pass
      MYSQL_DATABASE: drupal
      MYSQL_USER: drupal
      MYSQL_PASSWORD: drupal_password
# in ansible/main.yml


# Install some basic tools on all of our containers so they'll work as
# ansible targets
- hosts: all
  gather_facts: false
  tasks:
    - raw: which python || apt-get update
    - raw: which python || apt-get install -y python python-apt


# These are the tasks that get run on our apache hosts at build time
# To simplify this for your setup, you can use all the techniques you use
# already, such as including roles, other playbooks, etc.
- hosts: apache
  vars:
    web_root: /var/www/html
    drupal_path: "{{ web_root }}/drupal-8.1.3"

  tasks:
    - name: install needed packages for drupal
      apt:
        name: "{{ item }}"
        state: latest
      with_items:
        - php5
        - git
        - libapache2-mod-php5
        - php5-mysql
        - php5-gd

    - name: create directory for webroot
      file:
        state: directory
        dest: "{{ web_root }}"

    - name: get drupal package
      unarchive:
        src: https://ftp.drupal.org/files/projects/drupal-8.1.3.tar.gz
        copy: no
        dest: "{{ web_root }}"
        owner: www-data
        group: www-data

    - name: copy in default settings file
      shell: "cp {{ drupal_path }}/sites/default/default.settings.php {{ drupal_path }}/sites/default/settings.php"

    - name: set up settings file with dynamic docker host arg
      lineinfile:
        state: present
        dest: "{{ drupal_path }}/sites/default/settings.php"
        owner: www-data
        insertafter: "^ \\$databases = array();"
        line: >
          $databases['default']['default'] = array (
          'database' => 'drupal',
          'username' => 'drupal',
          'password' => 'drupal_password',
          'host' => getenv('MYSQL_PORT_3306_TCP_ADDR'),
          'port' => '3306',
          'driver' => 'mysql',
          'prefix' => '',
          'collation' => 'utf8mb4_general_ci',
          );


## You'll still need to run the install.php script in drupal to set up the database,
## but that can be easily automated away too

Build images using build subcommand

Now that your docker composition is set up the way you want, you can start by building the flattened images that will consist of your service:

ansible-container [--debug] build

NOTE: During the build process, your containers don’t run with the command in your container.yml file, but rather a special command meant to keep the instance running while ansible does it’s work. This means that if theres anything that you need to be running in order to set up the containers, start it in your ansible playbook.

That “build” command is something like:

command: sh -c "while true; do sleep 1; done"

Run the newly built images, and test

Bring up the new containers and make sure everything works as intended.

ansible-container run

If everything works as intended, you can ship to whatever repository you want using the ‘shipit’ command:

ansible-container shipit

Troubleshooting

Can’t find build container

If during the build process, you encounter an error like this:

NameError: No image with the name ansible-container-builder

E: GPG error: http://httpredir.debian.org stable InRelease: Clearsigned file isn't valid, got 'NODATA' (does the network require authentication?)

The trick here is to ensure that your networking is working properly: not able to reading the clearsign file indicates something is breaking the ability to pull down apt packages.

Timeout with long running ansible commands

Certain long running processes in ansible will cause your build command to timeout, which can be resolved by setting this environment variable:

export COMPOSE_HTTP_TIMEOUT=50000

Timeout on “flattening image”

This occurs because your system doesn’t have enough allocatable memory based on the size of your images. Try allocating more memory to the system or shrinking the size of your finalized images.

References