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 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.
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-democd ansible-container-demopipenv 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
Kubernetesdocker
Docker engineopenshift
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.cfgansible-requirements.txtcontainer.ymlmeta.ymlrequirements.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 Flaskfrom redis import Redisapp = 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 Repositoriesapt:update_cache: yescache_valid_time: 600- name: Install APT Requirementsapt:name: "{{ item }}"state: presentwith_items:- "python-pip"- name: Install Pip Requirementspip:name: "{{ item }}"state: presentwith_items:- "flask"- "gunicorn"- "redis"- name: Create App Folderfile:path: "/app"state: directory- name: Copy Flask Appcopy:src: app.pydest: /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 ANSIBLEworker_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 APTapt:update_cache: yescache_valid_time: 600- name: Install APT Requirementsapt:name: "{{ item }}"state: presentwith_items:- "nginx"- name: Copy config templatetemplate:src: virtualhost.j2dest: /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 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", "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.