Ansible Control Node – Docker Image

If you’re part of a team that’s working on network automation with tools such as Ansible, managing package versions and dependencies can sometimes be a challenge. For example, if you’ve updated your playbook to take advantage of Ansible 2.5’s network_cli connection type but your coworker still has Ansible 2.4 installed on her/his local workstation, they will not be able to properly run the workflow.

Enter the Control Node.


One potential solution to this problem is to package all of the tools together, and version control them along with your playbooks. Packaging the tools in a portable manner also allows team members running different operating systems such as MacOS, Linux, or Windows, to be able to work with the appropriate tooling.

The above is a perfect use-case for containers, and more specifically the Docker platform. By creating a Docker image containing Ansible and any additional modules or libraries you require, your team can choose to build the image locally from the Dockerfile, or can store the pre-built image in a container registry such as the Azure Container Registry or Amazon’s Elastic Container Registry. Additionally, by packaging up the tools used in your network automation, it becomes easy to integrate into some sort of CI/CD pipeline.

Initial Build

This blog post makes the assumption that Docker is already installed on the host workstation. For more information on installing Docker, reference Docker’s online documentation.

Docker images are created from a Dockerfile, which is a text document containing a list of imperative steps to build the desired output image, such as installing packages, creating files, etc.

Create a new folder to house the project artefacts, and create a blank Dockerfile:

$ mkdir ansible-docker $ touch Dockerfile

Open the Dockerfile in your text-editor of choice.

The first decision to be made is to select the base image used as the seed OS. For this project we’ll use Alpine Linux, a lightweight distribution, in order to maintain an efficient file-size of the image once built; however, other base images such as Ubuntu, Fedora, or CoreOS are potential options. Adding a label to the Dockerfile with the contact information is a common practice.

FROM alpine:3.7 LABEL maintainer="mrtheplague"

The next step is to add a group of dependencies required to install Ansible. Alpine uses the apk package manager, with the syntax apk add. Prior to adding the packages, it’s worthwhile to update the fetch the latest package list by using the apk update. Docker uses the syntax RUN to run commands once the base image has been instantiated, for example RUN apk update.

RUN apk update RUN apk add gcc \ musl-dev \ libffi-dev \ python \ python-dev \ py-pip \ make \ openssl-dev \ py-lxml

Python Packages

Now that we have a base Dockerfile we can instantiate a container. One of the our goals is to version control our packages, so in order to do this we’ll take advantage of Python’s pip package tool and its freeze parameter. To gather the list of Python packages and their appropriate versions, build a base Docker image on which to install Ansible, and then the packages are “frozen” to a text file.

Building the base Docker image is done by using the docker build command, specifying a name for the image using the -t switch, and identifying the location of the Dockerfile. The docker images command displays a list of locally cached images, and can be used to verify that the image has been built.

$ docker build -t mrtheplague/ansible-docker ./ Sending build context to Docker daemon 90.11kB Step 1/9 : FROM alpine:3.7 ---> 3fd9065eaf02 Step 2/9 : LABEL maintainer="mrtheplague" ---> Using cache ---> 5a32760ae2ba <-- Output excluded for brevity --> Successfully built 682b0091b98f Successfully tagged mrtheplague/ansible:latest $ docker images REPOSITORY TAG IMAGE ID CREATED SIZE mrtheplague/ansible-docker latest a1511ef40f0a About a minute ago 286MB alpine 3.7 3fd9065eaf02 5 months ago 4.15MB

From here, instantiate a container and drop into a shell.

$ docker run -i -t mrtheplague/ansible-docker:latest /bin/sh

Once in the shell, proceed to install Ansible using Python pip. pip will determine the necessary Python package dependencies, and will install them along with Ansible.

# pip install ansible Collecting ansible Downloading (10.2MB) 100% |████████████████████████████████| 10.2MB 123kB/s Collecting jinja2 (from ansible) Downloading (126kB) 100% |████████████████████████████████| 133kB 5.3MB/s <-- Output excluded for brevity --> Installing collected packages: MarkupSafe, jinja2, PyYAML, pyasn1, pycparser, cffi, six, bcrypt, idna, asn1crypto, enum34, ipaddress, cryptography, pynacl, paramiko, ansible Running install for MarkupSafe ... done Running install for PyYAML ... done Running install for pycparser ... done Running install for cffi ... done Running install for bcrypt ... done Running install for cryptography ... done Running install for pynacl ... done Running install for ansible ... done Successfully installed MarkupSafe-1.0 PyYAML-3.12 ansible-2.5.5 asn1crypto-0.24.0 bcrypt-3.1.4 cffi-1.11.5 cryptography-2.2.2 enum34-1.1.6 idna-2.7 ipaddress-1.0.22 jinja2-2.10 paramiko-2.4.1 pyasn1-0.4.3 pycparser-2.18 pynacl-1.2.1 six-1.11.0

Next, follow the same process to install additional packages and libraries, such as napalm, napalm-ansible, napalm-ios, napalm-nxos, etc.

# pip install napalm napalm-ansible Collecting napalm Downloading (149kB) 100% |████████████████████████████████| 153kB 2.7MB/s Collecting napalm-ansible Downloading Collecting setuptools>=38.4.0 (from napalm) Downloading (567kB) 100% |████████████████████████████████| 573kB 1.6MB/s Running install for napalm ... done Running install for napalm-ansible ... done Successfully installed certifi-2018.4.16 chardet-3.0.4 future-0.16.0 jtextfsm-0.3.1 junos-eznc-2.1.8 napalm-2.3.1 napalm-ansible-0.9.1 ncclient-0.5.3 netaddr-0.7.19 netmiko-2.1.1 pyIOSXR-0.53 pyeapi-0.8.2 pynxos-0.0.3 pyserial-3.4 requests-2.19.1 scp-0.11.0 setuptools-39.2.0 textfsm-0.4.1 urllib3-1.23

With Ansible and some additional packages now installed, the current list of Python packages and their respective versions can be output to a file by using pip freeze and redirecting the output. We’ll call our text file requirements.txt.

# pip freeze > requirements.txt

The output of the requirements.txt can be viewed and then copied into your text editor, and saved to the same location as your Dockerfile. This file will serve as our master package inventory.

# cat requirements.txt ansible==2.5.5 asn1crypto==0.24.0 bcrypt==3.1.4 certifi==2018.4.16 cffi==1.11.5 chardet==3.0.4 cryptography==2.2.2 enum34==1.1.6 future==0.16.0 idna==2.7 ipaddress==1.0.22 Jinja2==2.10 jtextfsm==0.3.1 junos-eznc==2.1.8 lxml==4.1.1 MarkupSafe==1.0 napalm==2.3.1 napalm-ansible==0.9.1 ncclient==0.5.3 netaddr==0.7.19 netmiko==2.1.1 paramiko==2.4.1 pyasn1==0.4.3 pycparser==2.18 pyeapi==0.8.2 pyIOSXR==0.53 PyNaCl==1.2.1 pynxos==0.0.3 pyserial==3.4 PyYAML==3.12 requests==2.19.1 scp==0.11.0 six==1.11.0 textfsm==0.4.1 urllib3==1.23

Now that we have a list of Python packages, we can reference this list in our Dockerfile. First, we need to tell Docker to copy the local folder into the image when it’s being built, which allows the Docker image to access a copy of the requirements.txt. The local files will be copied to the /tmp folder, and then Docker will be instructed to cd to the /tmp folder, ie: change the working directory.

ADD . /tmp WORKDIR /tmp

The final step in the creation of the Dockerfile is to RUN the pip command and reference the requirements.txt, telling pip to install all of the packages in the file at their noted versions.

RUN pip install -r requirements.txt

Final Image Build

With our complete Dockerfile, we can now run the build process again to rebuild the image with the additional step of installing the python packages.

$ docker build -t mrtheplague/ansible-docker ./

Testing the Control Node

As the Docker image is now complete, it can be tested by running a container and testing the ansible command.

$ docker run -i -t mrtheplague/ansible-docker:latest /bin/sh # ansible --version ansible 2.5.5 config file = None configured module search path = [u'/root/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules'] ansible python module location = /usr/lib/python2.7/site-packages/ansible executable location = /usr/bin/ansible python version = 2.7.14 (default, Dec 14 2017, 15:51:29) [GCC 6.4.0]

As you can see, Ansible is available once our container has been created!

Using the Control Node

The final step in making the control node useful is to make the Ansible playbooks on the operator’s local workstation available within the instantiated container. To do this, use the -v switch when running the docker build command, specify the absolute path to the playbook, and where you’d like it mounted once in the container, eg: /tmp.

$ docker run -v /Users/rchambers/network_standardisation/:/tmp -i -t mrtheplague/ansible-docker:latest /bin/sh # ls /tmp/ gather-facts.yml host_vars roles set-logging.yml set-users.yml ansible.cfg gather-serial.yml lab.ini set-aaa.yml set-ntp.yml templates facts group_vars production.ini set-dns.yml set-snmp.yml

And there you have it, the local playbooks are now available inside the container. By using the -v switch, the specified path is mounted as a volume; therefore, any changes made to the files on the local workstation are passed into the live container, allowing you to edit your playbooks without having to re-run the container.

Final Thoughts

As you can see, the process of creating the Docker image is straightforward, but there are many possibilities. The Dockerfile could easily be modified to always pull the latest versions of these packages, for use in a development environment.

The final Dockerfile is located on GitHub, so feel free to clone or fork.

Ansible Control Node – Docker Image
Scroll to top