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.

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 Packagespackage: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.

Flask

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

fromflaskimportFlaskfromredisimportRedisapp=Flask(__name__)redis=Redis(host='redis',port=6379)@app.route('/')defhello():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:

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:

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:

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 projectconductor:# Base image for the conductor containerbase:"ubuntu:xenial"# Title of the Projectproject_name:"ansible-container-demo"services:# DB Backendredis:from:"redis"ports:-"6379"# Applicationflask:from:"ubuntu:xenial"roles:# Applying a role to a container-role:flaskports:-"5000"command:["gunicorn","--bind","0.0.0.0:5000","app:app","--chdir","/app"]# Proxy Frontendnginx:from:"ubuntu:xenial"roles:# Syntax for expressing extra role variables-role:"nginx"# These varibles are passed to the nginx config templatenginx_proxy_host:"flask"nginx_proxy_port:5000nginx_listen_port:8080ports:-"8080:8080"command:["nginx","-c","/etc/nginx/nginx_ansible.conf","-g","daemonoff;"]

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.