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