Skip to content

Running Ansible as Docker container

Today I am going to show how to put ansible on docker. You may ask why? Well, many reasons, first of all pure curiosity on how to do it, second,
you may end up in environment where you don’t have ansible installed nor you have a
permissions to install anything, but free to pull docker images, a sort of immutable infrastructure.

Apart from learning how to dockerize some tool, you also will have a chance to play with ansible and ansible-playbook, which is one of the most used devops tools these days.

So after a bit of googling I found how to install ansible, it is couple lines of bash script. With this information in hand, all we
have to do is just put the script into the Dockerfile, so I created a file called Dockerfile.ansible.cnf:

FROM ubuntu

USER root

RUN \
  apt-get update && \
  apt-get install -y software-properties-common && \
  apt-add-repository ppa:ansible/ansible && \
  apt-get update && \
  apt-get install -y --force-yes ansible

RUN mkdir /ansible
WORKDIR /ansible

Let’s create an image and tag it:

docker build -f Dockerfile.ansible.cnf -t myansible .

Time to test it:

docker run --name myansible --rm    myansible  ansible --version
ansible 2.4.0.0
  config file = /etc/ansible/ansible.cfg
  configured module search path = [u'/root/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python2.7/dist-packages/ansible
  executable location = /usr/bin/ansible
  python version = 2.7.12 (default, Nov 19 2016, 06:48:10) [GCC 5.4.0 20160609]

Nice work, let’s continue. Having bare ansible won’t hep much, let’s go ahead and create couple hosts with ssh access, that is what ansible is
using to manage servers. One option would be to create a hosts as a VM’s but hey, we promised to dockerize things today, havent’ we?
So instead of having a proper VM, which obviously will be much heavier and harder to setup, we can simply create a Docker image and run as many
hosts as we want as docker containers. Again, a bit of googling and got it how to setup simple sshd serevr, so lets’ create a file called Dockerfile.sshd.cnf:

FROM ubuntu

RUN apt-get update && apt-get install python openssh-server -y

RUN echo 'root:pass' | chpasswd
RUN sed -i 's/PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config

RUN mkdir /var/run/sshd

EXPOSE 22
EXPOSE 80

CMD ["/usr/sbin/sshd", "-D"]

As you can see we created a sshd server and made sure we will be able to login with root credentials, we also exposed two ports, one of which we will use a bit later.

Now lets built it:

docker build -f Dockerfile.sshd.cnf  -t my-docker-ssh .

Sending build context to Docker daemon  132.1kB
Step 1/8 : FROM ubuntu
 ---> 747cb2d60bbe
Step 2/8 : RUN apt-get update && apt-get install python openssh-server -y
 ---> Using cache
 ---> a859cabd7c1e
Step 3/8 : RUN echo 'root:pass' | chpasswd
 ---> Using cache
 ---> b000b777ffc2
Step 4/8 : RUN sed -i 's/PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config
 ---> Using cache
 ---> 45c09690ddbf
Step 5/8 : RUN mkdir /var/run/sshd
 ---> Using cache
 ---> cd9e16808780
Step 6/8 : EXPOSE 22
 ---> Using cache
 ---> 9de9cb671b7d
Step 7/8 : EXPOSE 80
 ---> Using cache
 ---> 4b1bc4e590f4
Step 8/8 : CMD /usr/sbin/sshd -D
 ---> Using cache
 ---> 5c6eadfee2d1
Successfully built 5c6eadfee2d1
Successfully tagged my-docker-ssh:latest

We now can run and test it:

➜  ansible-docker git:(master) ✗ docker run --rm -P -d  --name my-docker-ssh1  my-docker-ssh
90b50bc73c9aae8fd33c9af94a24182e327a97ac23f4665ced1c103630154552
➜  ansible-docker git:(master) ✗ ssh root@localhost -p $(docker port my-docker-ssh1  |grep 22 | sed 's/.*://g')
The authenticity of host '[localhost]:32800 ([::1]:32800)' can't be established.
ECDSA key fingerprint is SHA256:2hDlLV9AyUHPbMibWkTTJGOe2WIbGhwlVx7ZxGlH1e8.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '[localhost]:32800' (ECDSA) to the list of known hosts.
root@localhost's password:
Welcome to Ubuntu 16.04.3 LTS (GNU/Linux 4.9.41-moby x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.

root@90b50bc73c9a:~#

Yay!

But I need some explanation here for those who got confused by the last command.
Let’s find out about running container:

➜  ansible-docker git:(master) ✗ docker ps
CONTAINER ID        IMAGE               COMMAND               CREATED             STATUS              PORTS                                          NAMES
90b50bc73c9a        my-docker-ssh       "/usr/sbin/sshd -D"   38 seconds ago      Up 37 seconds       0.0.0.0:32800->22/tcp, 0.0.0.0:32799->80/tcp   my-docker-ssh1

Now we need to find out it’s ssh port, as 22 is just internal container port, which is exposed as random port on the host:

➜  ansible-docker git:(master) ✗ docker port my-docker-ssh1
22/tcp -> 0.0.0.0:32800
80/tcp -> 0.0.0.0:32799

And finally just pick up the right port:

➜  ansible-docker git:(master) ✗ docker port my-docker-ssh1  |grep 22 | sed 's/.*://g'
32800

Let’s connect with ansible to it now, but before doing that let’s copy over public key so we don’t need to type password every time, as we won’t run ansible manually most of the times
but it will normally be a part of some automation scripts. So first we need to generate the key:

➜  ansible-docker git:(master) ✗ ssh-keygen -f ansible-key

Generating public/private rsa key pair.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in ansible-key.
Your public key has been saved in ansible-key.pub.
The key fingerprint is:
SHA256:YE8uFsPLbcXj+uQ0jBuZqw7/+klpvXb26LKX3GkI9UQ kayanazimov@kayanazimov.local
The key's randomart image is:
+---[RSA 2048]----+
|                 |
|     .   .    E  |
|      * . +  .   |
|     o X o .. .  |
|      = S .. o   |
|     . o O.   .  |
|    .   X *o + . |
|     o o XooB.+  |
|     .==*.=Boo.  |
+----[SHA256]-----+
➜  ansible-docker git:(master) ✗ ls -l ansible-key*
-rw-------@ 1 kayanazimov  staff  1679 19 Oct 14:25 ansible-key
-rw-r--r--@ 1 kayanazimov  staff   411 19 Oct 14:25 ansible-key.pub

And now copy to the host:

➜  ansible-stuff ssh-copy-id -f  -i ansible-key root@localhost -p $(docker port my-docker-ssh1  |grep 22 | sed 's/.*://g')
/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "ansible-key.pub"
root@localhost's password:

Number of key(s) added:        1

Now try logging into the machine, with:   "ssh -p '32800' 'root@localhost'"
and check to make sure that only the key(s) you wanted were added.

Done, and now test connectivity:

➜  ansible-stuff ssh root@localhost -i ansible-key -p $(docker port my-docker-ssh1  |grep 22 | sed 's/.*://g')
Welcome to Ubuntu 16.04.3 LTS (GNU/Linux 4.9.41-moby x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage
Last login: Thu Oct 19 13:19:22 2017 from 172.17.0.1
root@90b50bc73c9a:~#

No password been asked for, cool. Now time to do same with ansible. In order to connect ansible needs to know the host ip address, so let’s find it:

➜  ansible-docker git:(master) ✗ docker inspect my-docker-ssh1 --format {{.NetworkSettings.Networks.bridge.IPAddress}}
172.17.0.2

What is going on here? So inspect command shows much more, but we only need part related to networking:

➜  ansible-docker git:(master) ✗ docker inspect my-docker-ssh1
[
    {
        "Id": "90b50bc73c9aae8fd33c9af94a24182e327a97ac23f4665ced1c103630154552",
        "Created": "2017-10-19T13:19:10.59777695Z",
        "Path": "/usr/sbin/sshd",
        "Args": [
            "-D"
        ],
        "State": {
            "Status": "running",
            "Running": true,
            "Paused": false,
            "Restarting": false,
            "OOMKilled": false,
            "Dead": false,
            "Pid": 30459,
            "ExitCode": 0,
            "Error": "",
            "StartedAt": "2017-10-19T13:19:11.05034271Z",
            "FinishedAt": "0001-01-01T00:00:00Z"
        },
        "Image": "sha256:5c6eadfee2d133bff802fd256f78658abb5ad57d58475d56f11c9cc3347a2ea1",
        "ResolvConfPath": "/var/lib/docker/containers/90b50bc73c9aae8fd33c9af94a24182e327a97ac23f4665ced1c103630154552/resolv.conf",
        "HostnamePath": "/var/lib/docker/containers/90b50bc73c9aae8fd33c9af94a24182e327a97ac23f4665ced1c103630154552/hostname",
        "HostsPath": "/var/lib/docker/containers/90b50bc73c9aae8fd33c9af94a24182e327a97ac23f4665ced1c103630154552/hosts",
        "LogPath": "/var/lib/docker/containers/90b50bc73c9aae8fd33c9af94a24182e327a97ac23f4665ced1c103630154552/90b50bc73c9aae8fd33c9af94a24182e327a97ac23f4665ced1c103630154552-json.log",
        ...
        .....
        

That is why we are using Go templates (https://docs.docker.com/engine/admin/formatting/) to manipulate the output and the get what we exactly want.
We are ready to run ansible with the host ip address, make sure you use whatever ip address you got from previous command,
but most probably, if you don’t have any other containers running, you will have same result 🙂 That is because docker default bridge network has subnet :172.17.0.0/16″:

    
docker run --name myansible --rm  -v `pwd`/ansible-key:/root/.ssh/id_rsa  myansible  ansible all -m raw -a "ls /" -i '172.17.0.2,'

or you can actually put the ip address retrieval right into the command:

docker run --name myansible --rm  -v `pwd`/ansible-key:/root/.ssh/id_rsa  myansible  ansible \
 all -m raw -a "ls -l /"   -i \
 "$(docker inspect my-docker-ssh1 --format {{.NetworkSettings.Networks.bridge.IPAddress}})",

Last one is definitely better if you going to run it from some script, where you would prefer less lines, ok, no matter if we run the one or other we get some error:

172.17.0.2 | UNREACHABLE! => {
    "changed": false,
    "msg": "Failed to connect to the host via ssh: Host key verification failed.\r\n",
    "unreachable": true
}

Hmmm, wtf! Thanks we have google. After couple seconds we find out this:

So turns out we need to set some ansible config, OK, let’s fix it and rebuilt ansible image, create ansible.cfg:

[defaults]
host_key_checking=False
deprecation_warnings=False

copy the file into place ansible will be searching for:

FROM ubuntu

USER root

RUN \
  apt-get update && \
  apt-get install -y software-properties-common && \
  apt-add-repository ppa:ansible/ansible && \
  apt-get update && \
  apt-get install -y --force-yes ansible

COPY ansible.cfg /etc/ansible/ansible.cfg 

RUN mkdir /ansible
WORKDIR /ansible

and rebuild image:

 ansible-docker git:(master) ✗ docker build --no-cache -f Dockerfile.ansible.cnf -t myansible .

Sending build context to Docker daemon  132.1kB
Step 1/6 : FROM ubuntu
 ---> 747cb2d60bbe
Step 2/6 : USER root
 ---> Running in a264569b9906
 ---> 6aeba0cd3fdc
Removing intermediate container a264569b9906
Step 3/6 : RUN apt-get update &&   apt-get install -y software-properties-common &&   apt-add-repository ppa:ansible/ansible &&   apt-get update &&   apt-get install -y --force-yes ansible
 ---> Running in 36c8adb9e6c2
Get:1 http://archive.ubuntu.com/ubuntu xenial InRelease [247 kB]
Get:2 http://archive.ubuntu.com/ubuntu xenial-updates InRelease [102 kB]
Get:3 http://security.ubuntu.com/ubuntu xenial-security InRelease [102 kB]
...
....
......
Removing intermediate container f2a355ec3d7a
Step 6/6 : WORKDIR /ansible
 ---> 56c70827938d
Removing intermediate container 6b78f1b793be
Successfully built 56c70827938d
Successfully tagged myansible:latest

Why “no-cache”? Because there is no garantie docker will figure out what exactly change, so it can end up with same image!

Lets try again:

docker run --name myansible --rm  -v `pwd`/ansible-key:/root/.ssh/id_rsa  myansible  ansible \
 all -m raw -a "ls -l /"   -i \
 "$(docker inspect my-docker-ssh1 --format {{.NetworkSettings.Networks.bridge.IPAddress}})",


172.17.0.2 | SUCCESS | rc=0 >>
total 64
drwxr-xr-x   2 root root 4096 Oct  6 01:38 bin
drwxr-xr-x   2 root root 4096 Apr 12  2016 boot
drwxr-xr-x   5 root root  340 Oct 19 14:06 dev
drwxr-xr-x   1 root root 4096 Oct 19 14:06 etc
drwxr-xr-x   2 root root 4096 Apr 12  2016 home
drwxr-xr-x   1 root root 4096 Sep 13  2015 lib
drwxr-xr-x   2 root root 4096 Oct  6 01:38 lib64
drwxr-xr-x   2 root root 4096 Oct  6 01:38 media
drwxr-xr-x   2 root root 4096 Oct  6 01:38 mnt
drwxr-xr-x   2 root root 4096 Oct  6 01:38 opt
dr-xr-xr-x 159 root root    0 Oct 19 14:06 proc
drwx------   1 root root 4096 Oct 19 14:07 root
drwxr-xr-x   1 root root 4096 Oct 19 14:09 run
drwxr-xr-x   1 root root 4096 Oct 10 20:59 sbin
drwxr-xr-x   2 root root 4096 Oct  6 01:38 srv
dr-xr-xr-x  13 root root    0 Oct 18 16:06 sys
drwxrwxrwt   1 root root 4096 Oct 18 15:25 tmp
drwxr-xr-x   1 root root 4096 Oct  6 01:38 usr
drwxr-xr-x   1 root root 4096 Oct  6 01:38 var
Warning: Permanently added '172.17.0.2' (ECDSA) to the list of known hosts.
Shared connection to 172.17.0.2 closed.

yay! lets run another host and see the power of ansible, as the whole point of ansible it can apply sane command to multiple hosts. So again run new container, name it
as my-docker-ssh2 and copy the key:

docker run --rm -P -d  --name my-docker-ssh2  my-docker-ssh

b31594e59432aabd05e433422906c4ea84f8a179619496c1b82faebdb8cef689
➜  ansible-docker git:(master) ✗ ssh-copy-id -f  -i ansible-key root@localhost -p $(docker port my-docker-ssh2  |grep 22 | sed 's/.*://g')
/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "ansible-key.pub"
The authenticity of host '[localhost]:32804 ([::1]:32804)' can't be established.
ECDSA key fingerprint is SHA256:2hDlLV9AyUHPbMibWkTTJGOe2WIbGhwlVx7ZxGlH1e8.
Are you sure you want to continue connecting (yes/no)? yes
root@localhost's password:

Number of key(s) added:        1

Now try logging into the machine, with:   "ssh -p '32804' 'root@localhost'"
and check to make sure that only the key(s) you wanted were added.

Once done, we are ready to apply command to both hosts:


docker run --name myansible --rm  -v `pwd`/ansible-key:/root/.ssh/id_rsa  myansible  ansible all -m raw -a "ls -l /" -i '172.17.0.2, 172.17.0.3,'
172.17.0.2 | SUCCESS | rc=0 >>
total 64
drwxr-xr-x   2 root root 4096 Oct  6 01:38 bin
drwxr-xr-x   2 root root 4096 Apr 12  2016 boot
drwxr-xr-x   5 root root  340 Oct 19 14:06 dev
drwxr-xr-x   1 root root 4096 Oct 19 14:06 etc
drwxr-xr-x   2 root root 4096 Apr 12  2016 home
drwxr-xr-x   1 root root 4096 Sep 13  2015 lib
drwxr-xr-x   2 root root 4096 Oct  6 01:38 lib64
drwxr-xr-x   2 root root 4096 Oct  6 01:38 media
drwxr-xr-x   2 root root 4096 Oct  6 01:38 mnt
drwxr-xr-x   2 root root 4096 Oct  6 01:38 opt
dr-xr-xr-x 162 root root    0 Oct 19 14:06 proc
drwx------   1 root root 4096 Oct 19 14:07 root
drwxr-xr-x   1 root root 4096 Oct 19 14:20 run
drwxr-xr-x   1 root root 4096 Oct 10 20:59 sbin
drwxr-xr-x   2 root root 4096 Oct  6 01:38 srv
dr-xr-xr-x  13 root root    0 Oct 18 16:06 sys
drwxrwxrwt   1 root root 4096 Oct 18 15:25 tmp
drwxr-xr-x   1 root root 4096 Oct  6 01:38 usr
drwxr-xr-x   1 root root 4096 Oct  6 01:38 var
Warning: Permanently added '172.17.0.2' (ECDSA) to the list of known hosts.
Shared connection to 172.17.0.2 closed.

172.17.0.3 | SUCCESS | rc=0 >>
total 64
drwxr-xr-x   2 root root 4096 Oct  6 01:38 bin
drwxr-xr-x   2 root root 4096 Apr 12  2016 boot
drwxr-xr-x   5 root root  340 Oct 19 14:17 dev
drwxr-xr-x   1 root root 4096 Oct 19 14:17 etc
drwxr-xr-x   2 root root 4096 Apr 12  2016 home
drwxr-xr-x   1 root root 4096 Sep 13  2015 lib
drwxr-xr-x   2 root root 4096 Oct  6 01:38 lib64
drwxr-xr-x   2 root root 4096 Oct  6 01:38 media
drwxr-xr-x   2 root root 4096 Oct  6 01:38 mnt
drwxr-xr-x   2 root root 4096 Oct  6 01:38 opt
dr-xr-xr-x 161 root root    0 Oct 19 14:17 proc
drwx------   1 root root 4096 Oct 19 14:17 root
drwxr-xr-x   1 root root 4096 Oct 19 14:20 run
drwxr-xr-x   1 root root 4096 Oct 10 20:59 sbin
drwxr-xr-x   2 root root 4096 Oct  6 01:38 srv
dr-xr-xr-x  13 root root    0 Oct 18 16:06 sys
drwxrwxrwt   1 root root 4096 Oct 18 15:25 tmp
drwxr-xr-x   1 root root 4096 Oct  6 01:38 usr
drwxr-xr-x   1 root root 4096 Oct  6 01:38 var
Warning: Permanently added '172.17.0.3' (ECDSA) to the list of known hosts.
Shared connection to 172.17.0.3 closed.

Now let’s explain what are all those arguments we need to pass.

Firts we need to bind-mount the private key ansible-key inside of container, so ansible can ssh to the host:

  -v `pwd`/ansible-key:/root/.ssh/id_rsa 

Then we are running ansible command from container, then specify module with command to run

  ansible all -m raw -a "ls -l /"  
 

And finally specify inventory with list of hosts.:

-i '172.17.0.2, 172.17.0.3,' 

Nice way to check the arguments is to use help command:

 docker run --name myansible --rm    myansible  ansible --help | grep "inventory"
  -i INVENTORY, --inventory=INVENTORY, --inventory-file=INVENTORY
                        specify inventory host path
                        host list. --inventory-file is deprecated
                        

docker run --name myansible --rm    myansible  ansible --help | grep "module"
                        module arguments
  -m MODULE_NAME, --module-name=MODULE_NAME
                        module name to execute (default=command)
  -M MODULE_PATH, --module-path=MODULE_PATH
                        prepend colon-separated path(s) to module library
                        (default=[u'/root/.ansible/plugins/modules',
                        u'/usr/share/ansible/plugins/modules'])
Some modules do not make sense in Ad-Hoc (include, meta, etc)                        

Let’s create an inventory file named “hosts” now and use it to refer to the hosts:

[docker-ssh]
172.17.0.2
172.17.0.3

Lets run it to install nginx on the hosts:


 docker run --name myansible --rm -v `pwd`/ansible-key:/root/.ssh/id_rsa -v `pwd`/hosts:/etc/ansible/hosts  myansible  ansible docker-ssh -m raw -a "apt-get install nginx -y; service nginx restart"
172.17.0.2 | SUCCESS | rc=0 >>
Reading package lists... Done
Building dependency tree
Reading state information... Done
nginx is already the newest version (1.10.3-0ubuntu0.16.04.2).
0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.
 * Restarting nginx nginx
   ...done.
Warning: Permanently added '172.17.0.2' (ECDSA) to the list of known hosts.
Shared connection to 172.17.0.2 closed.

172.17.0.3 | SUCCESS | rc=0 >>
Reading package lists... Done
Building dependency tree
Reading state information... Done
nginx is already the newest version (1.10.3-0ubuntu0.16.04.2).
0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.
 * Restarting nginx nginx
   ...done.
Warning: Permanently added '172.17.0.3' (ECDSA) to the list of known hosts.
Shared connection to 172.17.0.3 closed.

Now let’s check the nginx:


curl localhost:32801 -o /dev/null  ; curl localhost:32803 -o /dev/null
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   612  100   612    0     0   100k      0 --:--:-- --:--:-- --:--:--  119k
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   612  100   612    0     0  90172      0 --:--:-- --:--:-- --:--:--   99k

all works!

This time we didn’t specify inventory as we used default hosts file. Ansible will look here for it: “/etc/ansible/hosts”.

Finally let’s try ansible-playbook. That is what you normally will use, rather than single commands. Let’s create test playbook test-playbook.yml:


---
- hosts: docker-ssh
  remote_user: root
  tasks:
    - name: show dir content
      command: ls -l /

Let’s look at the command:


docker run --name myansible --rm -v `pwd`/ansible-key:/root/.ssh/id_rsa -v `pwd`/hosts:/etc/ansible/hosts  -v `pwd`/test-playbook.yml:/tmp/test-playbook.yml  myansible  ansible-playbook  /tmp/test-playbook.yml


PLAY [docker-ssh] **************************************************************

TASK [Gathering Facts] *********************************************************
ok: [172.17.0.2]
ok: [172.17.0.3]

TASK [show dir content] ********************************************************
changed: [172.17.0.2]
changed: [172.17.0.3]

PLAY RECAP *********************************************************************
172.17.0.2                 : ok=2    changed=1    unreachable=0    failed=0
172.17.0.3                 : ok=2    changed=1    unreachable=0    failed=0

All good, if you wish to see more logs add more ‘v’ arguments to the ansible:


docker run --name myansible --rm -v `pwd`/ansible-key:/root/.ssh/id_rsa -v `pwd`/hosts:/etc/ansible/hosts  -v `pwd`/test-playbook.yml:/tmp/test-playbook.yml  myansible  ansible-playbook  -v /tmp/test-playbook.yml        

docker run --name myansible --rm -v `pwd`/ansible-key:/root/.ssh/id_rsa -v `pwd`/hosts:/etc/ansible/hosts  -v `pwd`/test-playbook.yml:/tmp/test-playbook.yml  myansible  ansible-playbook  -vvv /tmp/test-playbook.yml        

As you see I just keep adding more ‘v’s and it changes the logging level.
Nice workout for our brain muscles! The code available in the repo below, see you later:

git clone https://github.com/kenych/dockerizing-ansible && cd dockerizing-ansible