Configuring Containers with Ansible dynamically

Yogesh
6 min readMar 26, 2021

In this article, we’ll see how to configure docker containers dynamically on localhost(patting would work with remote hosts) with Ansible. By “dynamically” I mean that you don't have to add it in inventory each time instead, it gets auto-updated whenever you run the playbook.

I have previously written an article on how to configure a web server in a container via Ansible. Do check it out.

Build SSH + Apache Image 🖥💻

🔹With SSH we can directly configure the docker containers like any other managed node. So, we first create an image for SSH along with the Apache webserver on top of centos(you are free to choose any other base image). Also, we’ll work with Dockerfile but you can commit it after configuring manually too.

Dockerfile 🐳

⌨️

FROM centosRUN yum -y install openssh-server openssh-clients httpd curl net-toolsRUN echo 'root:toor' | chpasswdRUN echo 'test' > /var/www/html/index.htmlRUN /usr/bin/ssh-keygen -ACOPY service.sh service.shRUN chmod +x service.shCMD ./service.sh

🔹In the above Dockerfile, we basically install the necessary s/w (OpenSSH for the SSH server and httpd for Apache webserver), set up a password for the root user, create an index.html file with the content “test” in it, generate ssh keys and start the below services.

service.sh ⚙️️️

⌨️

#!/bin/bash# Start ssh process/usr/sbin/sshd -D &# Start httpd process/usr/sbin/httpd -DFOREGROUND

This starts the httpd and ssh processes.

I have built the above docker file and pushed it to my account in the Docker hub registry. It goes by the name syogesh174/centos-ssh-httpd . You can directly pull this instead of building your own image.

Now comes the interesting part of actually creating the playbook. Most of the steps here will be similar to the ones in my previous blog, which are:

✔️️️️️️ Display open ports and container names in use

✔️ Configure Docker

✔️ Start and enable Docker services

✔️ Pull the ssh+httpd server image from the Docker Hub

✔️ Run the docker container and expose it

✔️️ ️Add the docker containers to inventory

✔️ Copy the html code in /var/www/html directory and start the webserver

Playbook 💻⚙️

🔹 These are some of the pre tasks that I have come up with to display some of the crucial information - list of open TCP ports and docker container names in the server so that the user can easily assign unique names for them later on.

⌨️

- hosts: localhost
pre_tasks:
- name: "Shell command to get list of open ports"
shell: "netstat -nltp | awk '/LISTEN/ {print $4}' | awk -F: '{print $NF}' | sort | uniq"
register: ports
- name: "Get info on all running docker containers"
docker_host_info:
containers: yes
register: running_containers
- name: "Get info on all exited docker containers"
docker_host_info:
containers: yes
containers_filters:
status: "exited"
register: exited_containers
- name: "Initialize container names list"
set_fact:
container_names: []
- name: "Append running container names to the list"
set_fact:
container_names: "{{ container_names + item }}"
loop: "{{ running_containers.containers | map(attribute='Names') }}"
- name: "Append exited container names to the list"
set_fact:
container_names: "{{ container_names + item }}"
loop: "{{ exited_containers.containers | map(attribute='Names') }}"
- name: "List of open ports"
debug:
msg: "{{ ports.stdout_lines }}"
- name: "List of container names"
debug:
msg: "{{ container_names }}"

⤴️ The first task gets all the TCP ports that are in use by the server and the awk command is used to process the output of netstat command. Then we filter out the duplicate values by sort and uniq commands. Next, we use docker_host_info module to get the information on the running and exited containers and append it to an empty list. Finally, we print them to the console with the help of debug module.

🔹 Then we prompt the user to enter a port number (which is not occupied) for the httpd service in the localhost that will be mapped to the docker port 80. Aditionally, we also prompt the user to enter a unique container name and the inventory path so as update it with docker hosts. Finally, we prompt for the directory which contains the webpages which will be copied to the document root (/var/www/html since we have built this image on centos) directory in the Docker containers.

⌨️

- hosts: localhost
vars_prompt:
- name: httpd_port
prompt: "Enter a unique port number for httpd service"
default: 5000
private: no
- name: container_name
prompt: "Enter a unique docker container name"
default: "webserver"
private: no
- name: inventory_path
prompt: "Enter the path to inventory directory"
default: "/root/ansible/dynamic_inventory"
private: no
- name: webpages_path
prompt: "Enter the path to webpages"
default: "./webpages"
private: no
tasks: - name: "Configure yum with docker software"
yum_repository:
description: "docker software"
file: "docker"
name: "docker"
baseurl: "https://download.docker.com/linux/centos/7/x86_64/stable/"
gpgcheck: "no"
- name: "Install docker"
package:
name: "docker-ce-18.06.3.ce-3.el7.x86_64"
state: "present"
- name: "Install docker SDK"
pip:
name: "docker"
- name: "Switch off SELinux"
selinux:
policy: "targeted"
state: "permissive"
- name: "Start and enable docker services"
service:
name: "docker"
state: "started"
enabled: "yes"
- name: "Pull httpd and ssh image from Docker Hub"
docker_image:
name: "syogesh174/centos-ssh-httpd:1"
source: "pull"
- name: "Launch a container"
docker_container:
name: "{{ container_name }}"
image: "syogesh174/centos-ssh-httpd:1"
labels:
app: "webserver"
ports: "{{ httpd_port }}:80"
- name: "Get info on docker containers"
docker_host_info:
containers: yes
verbose_output: yes
containers_filters:
label: "app=webserver"
status: running
register: containers
- name: "Initialize an empty list for IPs"
set_fact:
docker_ips: []
- name: "Properly append the IPs"
include_tasks: docker_ips.yml
loop: "{{ containers.containers| map(attribute='Id') }}"
- name: "Copy the docker hosts template"
template:
src: "docker_hosts.j2"
dest: "{{ inventory_path }}/docker_hosts"
- name: "Refresh inventory to ensure containers exist in inventory"
meta: refresh_inventory
- name: "Set webpages_path as a fact"
set_fact:
webpages_path: "{{ webpages_path }}"

⤴️ Most of the tasks are similar to the tasks in my previous blog which are pretty straight forward. To re-iterate this basically adds the docker repo to yum, installs the docker container engine on the server, installs docker SDK which ansible uses to manage docker, starts the docker service, and pulls the image we have built previously (for ssh and apache webserver).

⤴️ ️Now we launch a container from this image with the label app=webserver . Then we get all the running containers with this label and add it to the docker_hosts.j2 template with the help of the tasks in docker_ips.yml . Below are the tasks in docker_ips.yml . Then we copy the template to the inventory and refresh the inventory so that the docker hosts show up. Then we set webpages_path as a fact so that we can later access it in the docker tasks (i.e. to transfer the webpages to the docker containers).

⌨️️️

- name: "Get container info"
docker_container_info:
name: "{{item}}"
register: container_info
- name: "Extract IP and make it a host"
set_fact:
docker_host: "{{ container_info.container.NetworkSettings.IPAddress }} ansible_user=root ansible_ssh_pass=toor"
- name: "Append IPs"
set_fact:
docker_ips: "{{ docker_ips + [docker_host] }}"

️⤴️ Here we get the information on the container from the id it has(Note that the id is passed on from the previous playbook). Then we format it in such a way that we can directly use it in the inventory file. And append it to the docker_ips list which we have created in the previous playbook.

🔹 Once, we have all the hosts in docker_ips list we can now directly use it in the jinja template.

⌨️

[docker]
{% for host in docker_ips %}
{{ host }}
{% endfor %}

🔹 Now that we have a container and an inventory ready all we need to do is to copy the webpages to the document root of the docker container.

⌨️

- hosts: docker
vars:
- webpages_path: "{{hostvars.localhost.webpages_path}}"
tasks:
- name: "Transfer webpages"
copy:
src: "{{ item }}"
dest: "/var/www/html"
with_fileglob: "{{ webpages_path }}/*"

You can find the code for Dockerfile, template and the playbook on my GitHub repo.

Thank You for reading! I hope you enjoyed it. 🙂😇

--

--