Playing with Ansible... on Docker

(TL;DR: The project presented here, with all the necessary files and setup steps,
can be found on this repository)

So, this week I decide to study Ansible and, after reading and watching some tutorials about
the technology, I decided to get my hands dirty.

So, my first think was:

OK, I need some machines to provision, so quite probably I’ll need to
create some EC2 instances to play with Ansible…

But then I realised:

Wait a second… I’ve Docker… Hum…

What’s Ansible?

Ansible is a set of tools that automates software provisioning, configuration of services and application deployment. More information can be found on the official website.

The project

So, before starting, we need to define the architecture and the roles of each component.

On this project, we’ll have four Docker containers running:

One container with Ansible installed. This will be our “master” machine, responsible to provision
the other ones.

Three clean containers, with only sshd (ssh deamon) installed and configured. These containers will play
the role of servers, it means, the machines that will actually be prepared to run our service.

Also, we need some service to test that our servers are well configured and ready to use. So, we’re going to use this project, which is a simple “hello world” written in NodeJS that I found on Github.

Architecture and actors defined, let’s move to the setup.

The server “machine”

So, first, we need a clean Docker image, having only the ssh daemon installed. This clean image will have none of our dependencies installed, since the objective here is to see Ansible taking care of that.

This image creates a Docker container with SSH access enabled, necessary in order to Ansible run the remote commands. The SSH access is set using root and ansible as user and password, respectively.

SSH config

To avoid strict host key checking when connection to the servers the first time, we need to disable this check in the ssh config file:

Host *
StrictHostKeyChecking no

Ansible hosts

Ansible relies on /etc/ansible/hosts files to define which machines are going to be provisioned. Ansible supports groups of machines, so you can refer to a group in the playbook instead of each machine individually. Since we’ve defined an architecture with 3 server machines, our hosts file will be the following:

These hostnames were defined based on the containers names given by docker compose when scaling the number of servers. Don’t worry: it will become clearer when we run the entire docker environment.

The server_hosts:vars section is used to define some extra parameters for Ansible when provisioning machines from the server_hosts group. In our case, we’re defining the Python interpreter and the SSH authentication credentials to connect to our servers.

We could use some ssh public key here to avoid hardcoding the auth credentials, but for the sake of simplicity, we’re using the user:pass approach.

Ansible playbook

The most important file of the whole project: the Ansible playbook.

Here’s the file content with comments, explaining the purpose of each task:

---# Provision all hosts-hosts:allremote_user:roottasks:# Install Git binary.# Git is necessary to download our NodeJS project from Github.-name:Install Gitinclude_role:name:ANXS.git# Downloads the Node project from Github and save# on `hello-world/` directory-name:Download projectgit:repo:'https://github.com/azat-co/nodejs-hello-world'dest:'hello-world'# Install NodeJS library-name:Install NodeJSinclude_role:name:geerlingguy.nodejs# Install all the necessary Node modules for the project,# using `npm`.-name:Install project dependenciescommand:npm installargs:chdir:hello-world/# Install Forever tool.# This tool is used to run the Node server in background# and keep tracking of the running process-name:Install Forevernpm:name=forever global=yes state=present# This is an auxiliary task, used to identify if our Node service# is already running (avoids restarting the service on each playbook# execution)-name:Get Forever's list of running processescommand:forever listregister:forever_listchanged_when:false# Start the node server using "Foverer"# The `when` clause identifies if the server is already# running. If so, this task is skipped-name:Start servicecommand:forever start web.jswhen:"forever_list.stdout.find('hello-world/web.js')==-1"args:chdir:hello-world/

ansible: this is our main container, with Ansible installed. This is the container that will dispatch the commands for the other containers. Some configuration files for ssh and Ansible are mapped as volumes on this container, since they’re going to be used to provision the servers.

server: this is our “server” container. It’s created based on the clean Docker image we’ve defined in Dockerfile file, with only sshd installed.

Getting hands dirty

Architecture explained, it’s time to see the things running.

So, first, let’s start the base environment:

$ docker-compose run ansible bash

This will start the ansible container and one server container, opening a console with the former one:

Unfortunately, docker-compose run doesn’t support the scale flag, so we need to scale the servers containers manually. In a separated terminal, run:

$ docker-compose scale server=3

This will scale our servers to three containers, with the hostnames ansibledocker_server_1, ansibledocker_server_2 and ansibledocker_server_3. Not by coincidence, these are exactly the same hosts defined on the Ansible’s host file, as explained before.

Back to the Ansible console, we can now test that our 3 servers are accessible. Therefore, we can use the Ansible’s ping role:

$ ansible all -m ping

Some readers are getting the following error when running the ping:

Failed to connect to the host via ssh: Bad owner or permissions on /root/.ssh/config

Now that all servers are accessible, we can prepare to run the playbook. First, we need to download some additional Ansible roles. The best place to find reusable Ansible content is Ansible Galaxy. It behaves like a central repository for Ansible, containing roles for basically everything you can imagine. Worth to give a check :)

For each one of the requests above, you should get a Hello World as response:

Success!

Hooray!

You’ve just provisioned 3 servers with NodeJS using Ansible :)

Conclusion

This project uses Docker for convenience. Therefore, we don’t have to bother about starting Amazon servers only for testing purposes. However, since the containers used here behave exactly like a plain machine with sshd installed, the very same setup should work on any cloud or bare-metal architecture.

The objective of this project isn’t to teach how to provision Docker containers using Ansible. As stated by Michael DeHaan, creator of Ansible, Docker containers typically have a single responsibility and, thus, much less configuration. So, the overhead of having a complete Ansible configuration to provision them are, most of the time, unnecessary.

Finally, the entire project, with the configuration files, Ansible playbook and everything else presented here can be found on this repository.

Hope you enjoyed this post and happy coding! :)

Troubleshooting

1. Failed to connect to the host via ssh: Bad owner or permissions on /root/.ssh/config

This happens because Docker compose is mapping the ssh config file with the wrong permissions.

Checking ssh man documentation, we can read this:

Because of the potential for abuse, this file must have strict permissions:read/write for the user, and not writable by others.It may be group-writable provided that the group in question contains only the user.