Hello World! with Ansible Container

Lately I have been looking in to Ansible Container as a way to keep all of my infrastructure declaration consistent. Using Ansible, I am able to control my entire VM lifecycle from start to finish, configure applications and services, run tests and even update my blog. What I could not do however, was build and provision container images.

Ansible Container looked fantastic, but every time I attempted to get started I was let down by the lack of resources available. Therefore, this guide will walk through creating an Ansible Container project from start to finish. We will be creating a simple Python Flask application, utilising a nginx proxy for the front-end and a redis database for the back-end.

But first, lets discuss why a tool like this is required in the first place.

Why use Ansible Container?

Over the past couple of years there has been an explosion of interest in Container based Microservices and Automated orchestration tools such as Ansible. However, getting these systems to work together has always been counterintuitive. In order to build container images, you would often start by creating messy Dockerfiles, stringing services together with docker-compose, then finally trying to orchestrate deployment via a platform such as Kubernetes (perhaps using Ansible to automate some of the process).

Ansible Container takes the pain out of the equation, and allows you to directly build Docker images using the existing Ansible concepts you know and love.

This means, rather than having a Dockerfile that looks like this (from the official docs):

RUN apt-get update && apt-get install -y \
    aufs-tools \
    automake \
    build-essential \
    curl \
    dpkg-sig \
    libcap-dev \
    libsqlite3-dev \
    mercurial \
    reprepro \
    ruby1.9.1 \
    ruby1.9.1-dev \
    s3cmd=1.1.* \
 && rm -rf /var/lib/apt/lists/*

You can use Ansible roles, and therefore completely utilise the benefits of variables, templating and modules to result in something with intuitive syntax, like this:

- name: Install Packages
  package:
      name: "{{ packages }}"
      state: present

The reason Dockerfiles are so difficult to understand stems from the way they utilise layering. Essentially, every new line in a Dockerfile creates a new “layer”, which adds to the filesize. As container images are designed to be shared, it is often advantageous to ensure image sizes are kept as small as possible, which results in creative ways to string commands together and condense as much as possible in to each line.

Although the layering strategy within Ansible Container is not clear at this point in time, it is evident that the use of Ansible tasks and roles is a great improvement over a Dockerfile based workflow.

Getting Started

This guide requires the following packages to be installed:

  • docker
  • python
  • python-pip (pip)

Creating a virtual environment

I personally like using Pipenv for creating python virtual environments. To install Pipenv, run:

pip install pipenv

We can then create our project directory and initiate the virtual environment:

mkdir ansible-container-demo
cd ansible-container-demo
pipenv install

Install Ansible Container

Ansible Container is distributed as a base package with the ability to install support for specific container engines depending on your environment.

At the time of writing, there is support for three engines:

  • k8s Kubernetes
  • docker Docker engine
  • openshift RedHat Openshift

As we are using Docker, we will install the package with docker support in our virtual environment:

pipenv install ansible-container[docker]

Set up a new project

With everything we need installed, we can now use Ansible Container to generate a skeleton project.

First, enter the Virtual Environment by running:

pipenv shell

Then initialise the Ansible Container skeleton project:

ansible-container init

This should create the following files within the project directory:

ansible.cfg
ansible-requirements.txt
container.yml
meta.yml
requirements.yml

Make sure to look at the Ansible Container getting started guide for an in-depth explanation of each of their functions.

Note

If you would like to skip the walkthrough (and tedious copy-pasting), you can download the project from Ansible Galaxy:

ansible-container init bendews.container-flask-demo

And navigate to Running the project, otherwise continue to the next step.

Ansible Roles

One of the largest benefits of using Ansible-Container over Dockerfiles and Docker Compose is the ability to easily build containers using the familiar Ansible syntax.

We will be creating two roles:

  • flask
  • nginx

From the project directory, run the following command to create the associated subdirectories:

mkdir -p roles/nginx/tasks roles/nginx/templates roles/flask/tasks roles/flask/files

Your project structure should now resemble the following:

├── ansible.cfg
├── container.yml
├── ansible-requirements.txt
├── meta.yml
├── requirements.yml
└── roles
    ├── flask
    │   ├── files
    │   └── tasks
    └── nginx
        ├── tasks
        └── templates

Flask

To create our Flask application, create a new file app.py in the roles/flask/files directory with the following content:

from flask import Flask
from redis import Redis

app = Flask(__name__)
redis = Redis(host='redis', port=6379)

@app.route('/')
def hello():
    count = redis.incr('hits')
    return 'Hello World! I have been seen {} times.\n'.format(count)

if __name__ == "__main__":
    app.run(host="0.0.0.0", debug=True)

Our Ansible playbook will take this application and copy it to the docker image when it is being built, as well as ensuring all the required dependencies are installed and functioning.

To create the Ansible playbook, create a new file main.yml in the roles/flask/tasks directory with the following content:

---
- name: Update APT Repositories
  apt:
    update_cache: yes
    cache_valid_time: 600

- name: Install APT Requirements
  apt:
    name: "{{ item }}"
    state: present
  with_items:
    - "python-pip"

- name: Install Pip Requirements
  pip:
    name: "{{ item }}"
    state: present
  with_items:
    - "flask"
    - "gunicorn"
    - "redis"

- name: Create App Folder
  file: 
   path: "/app"
   state: directory

- name: Copy Flask App
  copy:
    src: app.py
    dest: /app/app.py

Nginx

When configuring our Nginx proxy instance, we will be dynamically creating a configuration file with our desired parameters. To create the Nginx template, create a new file virtualhost.j2 in the roles/nginx/templates directory with the following content:

# THIS FILE IS MANAGED BY ANSIBLE
worker_processes 1;

events {
  worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    sendfile        on;
    keepalive_timeout  65;

    proxy_buffer_size   128k;
    proxy_buffers   4 256k;
    proxy_busy_buffers_size   256k;

    server {
        listen {{ nginx_listen_port }};

        location / {
            proxy_pass http://{{ nginx_proxy_host }}:{{ nginx_proxy_port }}/;
            proxy_hide_header X-Frame-Options;
            proxy_set_header X-Forwarded-Host $host:$server_port;
            proxy_set_header X-Forwarded-Server $host;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }
    }
}

We will then implement the process for installing and configuring the templated configuration file within our Ansible playbook. To create the Ansible playbook, create a new file main.yml in the roles/nginx/tasks directory with the following content:

---
- name: Update APT
  apt:
    update_cache: yes
    cache_valid_time: 600

- name: Install APT Requirements
  apt:
    name: "{{ item }}"
    state: present
  with_items:
    - "nginx"

- name: Copy config template
  template:
    src: virtualhost.j2
    dest: /etc/nginx/nginx_ansible.conf

Describing the project

Now that all the files are in place, we can start to bring the project together.

To orchestrate multi-container projects, Ansible Container uses a YAML formatted file, container.yml. This file describes what images to use, what containers to run, container attributes and what to do with the created images. This is very similar to a Docker Compose file.

Study the container.yml file below, then use it to replace the contents of the container.yml file in the project root.

# container.yml Syntax Version (don't change this)
version: "2"
settings:
  # Settings for the "conductor" container.
  # The Conductor container is a container used to
  # orchestrate the build of other containers in the project
  conductor:
    # Base image for the conductor container
    base: "ubuntu:xenial"
  # Title of the Project
  project_name: "ansible-container-demo"

services:
  
  # DB Backend
  redis:
    from: "redis"
    ports:
      - "6379"

  # Application
  flask:
    from: "ubuntu:xenial"
    roles:
      # Applying a role to a container
      - role: flask
    ports:
      - "5000"
    command: ["gunicorn", "--bind", "0.0.0.0:5000", "app:app", "--chdir", "/app"]
  
  # Proxy Frontend
  nginx:
    from: "ubuntu:xenial"
    roles:
      # Syntax for expressing extra role variables
      - role: "nginx"
        # These varibles are passed to the nginx config template
        nginx_proxy_host: "flask"
        nginx_proxy_port: 5000
        nginx_listen_port: 8080
    ports:
      - "8080:8080"
    command: ["nginx", "-c", "/etc/nginx/nginx_ansible.conf", "-g", "daemon off;"]

Running the project

Finally, we can now build and run our Docker containers. Ensure you are in the project root directory, and run the following command:

ansible-container build

This will now process the playbooks and build the Docker images. Once that has completed successfully, you can then run the project with:

ansible-container run

Wrapping up

Congratulations! You have successfully used Ansible to create a multi-container application in Docker. The application is split in to three tiers: Presentation (nginx), Logic (flask) and Storage (redis), and is great to use as a base to kickstart your own projects.

Please see this project on Github or Ansible Galaxy for more information.