Ansible in a Container
Intro
While preparing for my Devnet DevOps exam and now preparing for the DevCor, I realized is a great opportunity to create mini-projects from the topics I’m learning and at the end have a good network automation.
I’m aware of what Ansible is and what it does from some time ago, but I never played with it, as I prefer to go directly with Python. However, I see the worth to invest in Ansible since is another tool and sometimes you just want something that it works.
The instructions you will see below are not exclusive for Ansible. The procedure is the same if you intended to dockerized any application.
TL;DR
I get it, sometimes you are short on time and you are looking for something quickly, here is the juicy stuff.
- Github source files are here
- You may see reference to my docker image from Docker Hub, rather than the base image defined here. In the end, I want to build once and re-use many.
- Update paths if you are not executing from the
rootdir. - This is a living project, see the main branch for the latest source code. All links in the post are for the “Ansible in a Container” release.
- My Ansible image is here on docker Hub
- If you don’t have network devices you can reserve a free sandbox with Cisco DevNet. Remember is Free!
- For this post I’m using Cisco Modeling Labs Enterprise, so you can follow along.
To clone this specifc project do:
1git clone -b v0.1.0 --single-branch https://github.com/jillesca/network-automation.gitWhy?
First, I don’t want to maintain a VM just for Ansible or install it on my laptop and deal with Python versions and virtual environments. I don’t see the point, when you can use a container just for Ansible.
But, besides the first point, the main reason is the value you get when you use Ansible in a CI/CD pipeline. Having a container in the pipeline, will make it easier to implement and maintain since the containers are used only when needed.
A good side efect, is Consistency. Once you built a container, it will always work. Of course, you need to follow best practices when defining what is installed, and when to built or use an image.
In basic terms:
- Be super specific on what needs to be installed, e.g. package versions
- Use this container as a base image
- Use this base image for deployments.
Create an Ansible Image
The first step I did, was to look for an official Ansible image, there is one, but the last update was 5 years ago. So I decided to build my own.
Since Ansible is based on Python, I decided to use Python as a based image. I also choose an Alpine variant, since is the smallest size. Size matters for the pipeline, since the container will be created and destroyed everytime we do changes with Ansible, therefore the image is transfered, ergo is good to have a small image. This could also be optimized.
Create a DockerFile
A dockerfile is basically a plain text file with instructions docker understands. For a full explanation on docker files check the dockerfile reference.
The dockerfile I used is the following:
1# syntax=docker/dockerfile:12FROM python:alpine3.16 AS base3COPY ./docker/ansible/requirements.txt .4RUN pip3 install -r requirements.txt5WORKDIR homeWhat it says is:
- First we define
FROMwhich image we want to build. In this casepython:alpine3.16.- You case see here, I’m not using
python:latestsince I don’t know which image could be, and newer images could break dependencies down the road.
- You case see here, I’m not using
- Second,
COPYa file inside the container.- Depending on which directory you are executing the
docker buildcommand, you need to adjust the firstpath, in this case./docker/ansible/requirements.txtand if you pay attention you can see a.(dot) at the end
- Depending on which directory you are executing the
- Third,
RUNpip3 to install some dependencies - Finally, we change directory with
WORKDIR
Like I commented before, is important to be super specific about which packages are installed in out image. For this I’m using a requirements.txt file. Filename could be anything, but this is the common convention for installing dependencies.
1ansible==6.5.02ansible-core==2.13.53certifi==2022.9.244cffi==1.15.15charset-normalizer==2.1.16cryptography==38.0.17idna==3.48Jinja2==3.1.29MarkupSafe==2.1.110packaging==21.311pycparser==2.2112pyparsing==3.0.913PyYAML==6.014requests==2.28.115resolvelib==0.8.116urllib3==1.26.1217paramiko==2.11.0If you are wondering how I got those requirements, basically, I started a python:alpine3.16 container, installed Ansible and saw which dependecies were installed (pip freeze). This was a manual process and is a one-time, unless you need to update the image.
Build a Base Image
Having those two files is enough to start building our image. To build your image use the docker build command.
1docker build --file docker/ansible/base-ansible.dockerfile --tag ansible:version1.0 .Then to run the container, use docker run.
1docker run -dt --name ansible ansible:version1.0Finally, to enter the container, open an interactive session on /bin/sh. From there you can verify the Ansible installation.
1docker exec -it ansible /bin/sh2/home # ansible --version3ansible [core 2.13.5]4 config file = /home/ansible.cfg5 configured module search path = ['/root/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']6 ansible python module location = /usr/local/lib/python3.10/site-packages/ansible7 ansible collection location = /root/.ansible/collections:/usr/share/ansible/collections8 executable location = /usr/local/bin/ansible9 python version = 3.10.8 (main, Oct 13 2022, 23:21:19) [GCC 11.2.1 20220219]10 jinja version = 3.1.211 libyaml = False12/home #Note, if you want to upload/push your images to your docker registry, update the tag so docker knows where to push it. Don’t forget to login into your registry from docker first.
Developing with the Base Image
Using a container directly is very useful for automation, but no much for developing with it. An super important consideration when working with containers is the following:
If you need to do changes, perform the changes on the repository, then you build a new image and this image will have your changes. Sounds simple, right?. The truth is that for developing and testing is not efficient.
Docker Compose
This is where docker-compose comes to the help. This is a tool already included with docker that helps you create services.
In our case, compose helps us to create a volume where we can mount our Ansible files inside the docker container. This means that changes we do on our repo, will be automatically sync inside the container, making developing and testing easier.
For this project, I created the following directory structure Inside my repository.
1╰─ tree2.3 ...4├── ansible5│ ...6└── docker7 ├── ansible8 │ ├── base-ansible.dockerfile9 │ └── requirements.txt10 └── ansible.docker-compose.ymlYou can see, I separated docker related files from Ansible files, so I can easily distinguish between services when working with a pipeline and testing.
Now, first create a docker-compose file.
1version: "3.8"2
3services:4 ansible:5 image: ansible:version1.06 container_name: ansible7 tty: true8 volumes:9 - ./../ansible:/homeThen we tell docker-compose to create a container and use a volume to mount our Ansible files inside the container. This is the key line: ./../ansible:/home, where we are telling compose where our Ansible files are, and where to mount them, in this case in the home directory of the container.
Then we can bring the container to life.
1docker-compose -f ./docker/ansible.docker-compose.yml up -dFinally we enter the container, from here, we can execute our Ansible commands.
1docker exec -it ansible /bin/shAs a side note, you can build directly in compose. If you go to the docker-compose file at Github, you will see I commented a build option, with this, you can build directly your image.
However I recommend, build your image separately, push your image to your registry, e.g. Docker Hub and consume it from it. If you go to the Github docker-compose you will see I’m using a image I created and pushed to Docker Hub
1image: jillesca/ansible:version1.0Ansible Configuration
Before we can verify our work, is important to create an Ansible configuration, so we can use for testing.
Below is the base Ansible directory I use.
1╰─ tree2.3...4├── ansible5│ ├── ansible.cfg6│ ├── group_vars7│ │ └── all.yml8│ ├── host_vars9│ │ ├── dist-rtr01.yml10│ │ └── dist-rtr02.yml11│ ├── inventory.yml12│ └── snmp_test.yml13└── dockerAnd below you can see how my files look like. You can also check them on Github. If you want to see the latest code, see the main branch.
1---2dist:3 hosts:4 dist-rtr01:5 ansible_host: 10.10.20.1766 dist-rtr02:7 ansible_host: 10.10.20.1751[defaults]2host_key_checking = False1---2ansible_ssh_pass: cisco3ansible_user: cisco4ansible_network_os: ios5
6snmp_community: cisco_test7snmp_location: Lisbon_HQ8snmp_contact: Jesus_Illescas1---2snmp_location_floor: floor_21---2snmp_location_floor: floor_11---2- name: MAKE CONFIG CHANGES USING GROUP_VARS3 hosts: all4 connection: network_cli5 gather_facts: no6
7 tasks:8 - name: CONFIGURE SNMP COMMUNITY USING VARIABLES STORED IN GROUP_VARS9 ios_config:10 commands:11 - snmp-server community {{ snmp_community }} RO12 - snmp-server location {{ snmp_location }} {{ snmp_location_floor }}13 - snmp-server contact {{ snmp_contact }}Verification
After all of this, is time to verify our Ansible container is working as expected.
The first requirement you need is, IP Connectivity from the host where the container is being executed to your network devices. I’m my case I’m using the sandbox through a VPN and I can SSH the devices directly.
Now, let’s see what is on our devices
1dist-rtr01#show run | i snmp2dist-rtr01#1dist-rtr02#show run | i snmp2dist-rtr02#Access the container and review what is inside.
1docker exec -it ansible /bin/sh2/home # ls -l3total 124-rw-r--r-- 1 root root 266 Oct 23 13:46 ansible.cfg5drwxr-xr-x 3 root root 96 Oct 23 13:46 group_vars6drwxr-xr-x 4 root root 128 Oct 23 13:46 host_vars7-rw-r--r-- 1 root root 411 Oct 23 13:46 inventory.yml8-rw-r--r-- 1 root root 415 Oct 23 13:46 snmp_test.yml9/home #From the container, use ansible-playbook to run the playbook snmp_test.yml
1/home # ansible-playbook -i inventory.yml snmp_test.yml -v2Using /home/ansible.cfg as config file3
4PLAY [MAKE CONFIG CHANGES USING GROUP_VARS] *****************************************************5
6TASK [CONFIGURE SNMP COMMUNITY USING VARIABLES STORED IN GROUP_VARS] ****************************7[WARNING]: ansible-pylibssh not installed, falling back to paramiko8[WARNING]: ansible-pylibssh not installed, falling back to paramiko9[WARNING]: To ensure idempotency and correct diff the input configuration lines should be10similar to how they appear if present in the running configuration on device11changed: [dist-rtr02] => {"banners": {}, "changed": true, "commands": ["snmp-server community cisco_test RO", "snmp-server location Lisbon_HQ floor_1", "snmp-server contact Jesus_Illescas"], "updates": ["snmp-server community cisco_test RO", "snmp-server location Lisbon_HQ floor_1", "snmp-server contact Jesus_Illescas"]}12changed: [dist-rtr01] => {"banners": {}, "changed": true, "commands": ["snmp-server community cisco_test RO", "snmp-server location Lisbon_HQ floor_2", "snmp-server contact Jesus_Illescas"], "updates": ["snmp-server community cisco_test RO", "snmp-server location Lisbon_HQ floor_2", "snmp-server contact Jesus_Illescas"]}13
14PLAY RECAP **************************************************************************************15dist-rtr01 : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=016dist-rtr02 : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=017
18/home #If we look for snmp in our devices, we can see the changes were done
1dist-rtr01#show run | i snmp2snmp-server community cisco_test RO3snmp-server location Lisbon_HQ floor_14snmp-server contact Jesus_Illescas5dist-rtr01#1dist-rtr02#show run | i snmp2snmp-server community cisco_test RO3snmp-server location Lisbon_HQ floor_24snmp-server contact Jesus_Illescas5dist-rtr02#This may be a simple change, but the purpose of this verification is to test that our image works rather than to do a complex change.
What’s next?
Well, it was a long post, but actually this is just one of the foundations for the ideas I have in mind.
There three more thoughts I have for Ansible.
- First do a more sophisticated change,
snmpis just too simple. - Add Ansible to a pipeline, so we can see its potential in CI/CD.
- Play with tests in Ansible.
After this, I’m thinking to integrate other tools to the network automation project I’m building.