Docker and Ansible¶
Context¶
Docker is another fabulous tool for System Administration. Docker lets you isolate system processes. Rather than simulating a whole other system, like with a VM, you can replicate the function of a different operating system.
Docker is an example of “containerization.” It’s far from the only solution, but it’s a solid product that can get you started building things faster.
Instead of setting up a new server, you download a tiny image of a root filesystem, and boot it up with processes isolated from your host system using cgroups.
For Wizardlab, this means we can deploy a variety of servers onto the one
services
host. Mix or match Operating Systems, network things together,
take over host network ports, and script out the whole thing in easy human
readable configuration files. It’s lovely.
I’ll be setting up Docker and the Docker Compose plugin. Docker compose
to
Docker as ansible-playbook is to ansible. It’s another level of automaion.
The docker
command runs the file called Dockerfile
inside ${PWD} while
docker compose
will look for a file named docker-compose.yml
(.yaml works too!).
The Docker role¶
Here’s how I decided to design a “docker” role for Wizardlab. By no means is this the way to design something like this. Rather, this is how I decided to practice writing Ansible configuration in the context of automating docker setup on debian.
I decided to have the docker role only install Docker by default, without the docker compose plugin. I am doing this to demonstrate the use of tags, and I would probably delete all the tag stuff if I was using this at work.
The Process¶
I create a new role under playbooks/roles/docker
, and I setup file, template,
and task subdirectories, utilizing BASH expansion.
$ mkdir -p playbooks/roles/docker/{files,tasks,templates}
I create a all-powerful playbook file under /playbooks
that will
drive the rest of the files:
$ vi playbooks/docker.yml
This playbook specifies Hosts\groups that I want to
target, and imports the role I want in the proper order. I use ‘roles’ to
run the ‘infra’ role which will give me DHCP services on services
before
I setup Docker if I give run the playbook without tags.
When I run with the --tag docker
option, I should get docker setup without
installing dhcp.
---
- name: "Setup Docker"
hosts: services
become: yes
tasks:
- name: include infra
ansible.builtin.include_role:
name: infra
- name: include docker
ansible.builtin.include_role:
name: docker
tags:
- docker
- always
Create a main.yml
file under playbooks/docker/tasks/:
$ vi playbooks/roles/docker/tasks/main.yml
This file will pull in the task files that we design to setup docker. For this design, I will put it all in two task files, so I can decide whether I want Docker only, or both Docker and the docker compose plugin.
- name: Import the tasks that install just Docker
ansible.builtin.include_tasks:
file: "setup_docker.yml"
apply:
tags:
- always
- docker
tags:
- always
- name: Import the tasks that install Docker Compose
ansible.builtin.include_tasks:
file: "setup_compose.yml"
apply:
tags:
- compose
tags:
- compose
I want the docker compose task to not run when I specify only the ‘docker’ tag,
so I give the second include only the compose
tag, and the compose tasks will run if I do not specify
tags, or if I specify --tags docker,compose
.
This is a way to setup “defaults” for what tasks a role will include, but allow you to switch it up if you need to with the ansible-playbook --tags
option. Tags are pretty neat.
Next, I create the two task files:
$ vi playbooks/roles/docker/tasks/setup_docker.yml
entering in the necessary tasks, I used this page for the recommended approach: docs.docker.com/engine/install/debian/
- name: remove conflicting packages
ansible.builtin.shell:
cmd: "for pkg in docker.io docker-doc docker-compose podman-docker containerd runc; \
do sudo apt-get remove $pkg; done"
- name: ensure I have the dependency packages to setup the repo
ansible.builtin.apt:
pkg:
- ca-certificates
- curl
- gnupg
state: present
- name: remove the old gpg key if it already exists
ansible.builtin.file:
path: "/etc/apt/keyrings/docker.gpg"
state: absent
- name: grab the official GPG key
ansible.builtin.shell:
cmd: "install -m 0755 -d /etc/apt/keyrings; \
curl -fsSL https://download.docker.com/linux/debian/gpg | \
gpg --dearmor -o /etc/apt/keyrings/docker.gpg"
- name: remove the old repo file
ansible.builtin.file:
path: "/etc/apt/sources.list.d/docker.list"
state: absent
- name: setup the repository
ansible.builtin.shell:
cmd: "echo 'deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/debian bookworm stable' | \
tee /etc/apt/sources.list.d/docker.list > /dev/null"
- name: update the app repo
ansible.builtin.shell: "apt update"
- name: install docker packages
ansible.builtin.apt:
pkg:
- docker-ce
- docker-ce-cli
- containerd.io
- docker-buildx-plugin
- name: Celebrate docker tasks being finished
ansible.builtin.debug:
msg: "Docker setup tasks finished"
Next, a tiny task file that just installs the one package for docker compose:
$ vi playbooks/roles/docker/tasks/setup_compose.yml
and enter a few lines:
- name: Install the docker compose plugin
ansible.builtin.apt:
pkg: docker-compose-plugin
- name: Celebrate the docker compose task running
ansible.builtin.debug:
msg: "docker compose task"
Let’s test it out:
$ vagrant ssh control
Now I am connected to control, let’s go to the /vagrant/ folder to pickup our latest edits Note: Vagrant copies the /playbook directory to ~ at provisioning. If you start the provisioning before building playbooks, you’ll want to move over to /vagrant
$ cd /vagrant
let’s activate the ansible Python virtual environment (venv) in our home dirctory:
$ source ~/ansible/bin/activate
Next, run the playbook and just install docker!
(ansible)$ ansible-playbook -i inventory --tags docker playbooks/docker.yml
The results:
PLAY [Setup Docker] *************************************************************************************
TASK [Gathering Facts] **********************************************************************************
ok: [services]
TASK [include docker] ***********************************************************************************
TASK [docker : Import the tasks that install just Docker] ***********************************************
included: /vagrant/playbooks/roles/docker/tasks/setup_docker.yml for services
TASK [docker : remove conflicting packages] *************************************************************
changed: [services]
TASK [docker : ensure I have the dependecy packages to setup the repo] **********************************
changed: [services]
TASK [docker : remove the old gpg key if it already exists] *********************************************
ok: [services]
TASK [docker : grab the official GPG key] ***************************************************************
changed: [services]
TASK [docker : remove the old repo file] ****************************************************************
ok: [services]
TASK [docker : setup the repository] ********************************************************************
changed: [services]
TASK [docker : update the apt database] *****************************************************************
changed: [services]
TASK [docker : install docker packages] *****************************************************************
changed: [services]
TASK [docker : Celebrate docker tasks being finished] ***************************************************
ok: [services] => {
"msg": "Docker setup tasks finished"
}
PLAY RECAP **********************************************************************************************
services : ok=11 changed=6 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Now, to add the compose plugin by including the --tag compose
option. Both task
files run now, yet Ansible only needs to apply the single task from the setup_compose.yml
include in playbooks/docker.yml
:
(ansible)$ ansible-playbook -i inventory --tags compose playbooks/docker.yml
Results:
PLAY [Setup Docker] *************************************************************************************
TASK [Gathering Facts] **********************************************************************************
ok: [services]
TASK [include docker] ***********************************************************************************
TASK [docker : Import the tasks that install just Docker] ***********************************************
included: /vagrant/playbooks/roles/docker/tasks/setup_docker.yml for services
TASK [docker : remove conflicting packages] *************************************************************
changed: [services]
TASK [docker : ensure I have the dependecy packages to setup the repo] **********************************
ok: [services]
TASK [docker : remove the old gpg key if it already exists] *********************************************
changed: [services]
TASK [docker : grab the official GPG key] ***************************************************************
changed: [services]
TASK [docker : remove the old repo file] ****************************************************************
changed: [services]
TASK [docker : setup the repository] ********************************************************************
changed: [services]
TASK [docker : update the apt database] *****************************************************************
changed: [services]
TASK [docker : install docker packages] *****************************************************************
ok: [services]
TASK [docker : Celebrate docker tasks being finished] ***************************************************
ok: [services] => {
"msg": "Docker setup tasks finished"
}
TASK [docker : Import the tasks that install Docker Compose] ********************************************
included: /vagrant/playbooks/roles/docker/tasks/setup_compose.yml for services
TASK [docker : Install the docker compose plugin] *******************************************************
ok: [services]
TASK [docker : Celebrate the docker compose task running] ***********************************************
ok: [services] => {
"msg": "docker compose task"
}
PLAY RECAP **********************************************************************************************
services : ok=14 changed=6 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Next, I pull the ole’ Test Kitchen trick, and show you what happens if I run the last command on a fresh VM. We’ll need to setup a clean uninstall playbook later… Notice, both task lists execute:
(ansible)$ ansible-playbook -i inventory --tags compose playbooks/docker.yml
Results:
PLAY [Setup Docker] *************************************************************************************
TASK [Gathering Facts] **********************************************************************************
ok: [services]
TASK [include infra] ************************************************************************************
TASK [infra : Import tasks to setup isc-dhcp-server by default] *****************************************
included: /vagrant/playbooks/roles/infra/tasks/dhcp_conf.yml for services
TASK [infra : Deploying dhcpd.conf] *********************************************************************
changed: [services]
TASK [infra : copy dhcp defaults (eth1 and ipv4 only)] **************************************************
changed: [services]
TASK [infra : install dependencies] *********************************************************************
changed: [services]
TASK [infra : Alternatively, setup Kea in a memfile configuration] **************************************
included: /vagrant/playbooks/roles/infra/tasks/kea-memfile.yml for services
TASK [include docker] ***********************************************************************************
TASK [docker : Import the tasks that install just Docker] ***********************************************
included: /vagrant/playbooks/roles/docker/tasks/setup_docker.yml for services
TASK [docker : remove conflicting packages] *************************************************************
changed: [services]
TASK [docker : ensure I have the dependecy packages to setup the repo] **********************************
changed: [services]
TASK [docker : remove the old gpg key if it already exists] *********************************************
ok: [services]
TASK [docker : grab the official GPG key] ***************************************************************
changed: [services]
TASK [docker : remove the old repo file] ****************************************************************
ok: [services]
TASK [docker : setup the repository] ********************************************************************
changed: [services]
TASK [docker : update the apt database] *****************************************************************
changed: [services]
TASK [docker : install docker packages] *****************************************************************
changed: [services]
TASK [docker : Celebrate docker tasks being finished] ***************************************************
ok: [services] => {
"msg": "Docker setup tasks finished"
}
TASK [docker : Import the tasks that install Docker Compose] ********************************************
included: /vagrant/playbooks/roles/docker/tasks/setup_compose.yml for services
TASK [docker : Install the docker compose plugin] *******************************************************
ok: [services]
TASK [docker : Celebrate the docker compose task running] ***********************************************
ok: [services] => {
"msg": "docker compose task"
}
PLAY RECAP **********************************************************************************************
services : ok=19 changed=9 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Testing the Installed Docker Engine¶
To test the newly installed Docker engine, the installation instructions suggest running:
$ sudo docker run hello-world
Results:
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
719385e32844: Pull complete
Digest: sha256:dcba6daec718f547568c562956fa47e1b03673dd010fe6ee58ca806767031d1c
Status: Downloaded newer image for hello-world:latest
Hello from Docker!
This message shows that your installation appears to be working correctly.
To generate this message, Docker took the following steps:
1. The Docker client contacted the Docker daemon.
2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
(amd64)
3. The Docker daemon created a new container from that image which runs the
executable that produces the output you are currently reading.
4. The Docker daemon streamed that output to the Docker client, which sent it
to your terminal.
To try something more ambitious, you can run an Ubuntu container with:
$ docker run -it ubuntu bash
Share images, automate workflows, and more with a free Docker ID:
https://hub.docker.com/
For more examples and ideas, visit:
https://docs.docker.com/get-started/
Achieving Idempotency¶
When we ran the playbook with --tags compose
, the play went through the whole apt repo
setup process; this slowed things down a little bit. Idealy, the run that installed compose
should not have had to change any of those options since the repo was already installed.
Since we have a working Docker engine with the necessary docker.list
file on the services
host,
grab that file and send it over to control
:
$ scp /etc/apt/sources.list.d/docker.list \
vagrant@control:/vagrant/playbooks/roles/docker/files/docker.list
Let’s make better use of the apt module, and check for the repo configuration inside a block
, and
put the repo setup tasks inside a rescue
block. Now, I’ll have better “idempotency,” and I get better performance.
playbooks/roles/docker/tasks/setup_docker.yml
- name: remove conflicting packages
ansible.builtin.apt:
pkg:
- docker.io
- docker-doc
- docker-compose
- podman-docker
- containerd
- runc
state: absent
- name: install the docker repo only if needed
block:
- name: check if the we have the repo already configured
ansible.builtin.stat:
path: "/etc/apt/sources.list.d/docker.list"
register: docker_repo_stat
- name: fail if docker.list isn't configured.
ansible.builtin.fail:
msg: "docker repo is not configured"
when: docker_repo_stat["stat"]["exists"]==false
rescue:
- name: ensure I have the dependecy packages to setup the repo
ansible.builtin.apt:
pkg:
- ca-certificates
- curl
- gnupg
state: present
- name: remove the old gpg key if it already exists
ansible.builtin.file:
path: "/etc/apt/keyrings/docker.gpg"
state: absent
- name: grab the official GPG key
ansible.builtin.shell:
cmd: "install -m 0755 -d /etc/apt/keyrings; \
curl -fsSL https://download.docker.com/linux/debian/gpg | \
gpg --dearmor -o /etc/apt/keyrings/docker.gpg"
- name: configure docker repo
ansible.builtin.copy:
src: "docker.list"
dest: "/etc/apt/sources.list.d/docker.list"
- name: install docker packages
ansible.builtin.apt:
pkg:
- docker-ce
- docker-ce-cli
- containerd.io
- docker-buildx-plugin
update_cache: true
state: present
- name: Celebrate docker tasks being finished
ansible.builtin.debug:
msg: "Docker setup tasks finished"
The stat module grabs a variable, and we inspect this with the fail module. Because the
/etc/apt/sources.list.d/docker.list
file isn’t in place, the block fails. We use the copy module to
place the docker.list
file where it needs to go, and we install the gpg key.
Setting things up this way ensure that it will happen only once. Future runs will not touch
the gpg key or /etc/apt/sources.list.d/
directory.
What it looks like when we run ansible-playbook -i inventory --tags docker playbooks/docker.yml
repeatedly:
PLAY [Setup Docker] *************************************************************************************
TASK [Gathering Facts] **********************************************************************************
ok: [services]
TASK [include docker] ***********************************************************************************
TASK [docker : Import the tasks that install just Docker] ***********************************************
included: /vagrant/playbooks/roles/docker/tasks/setup_docker.yml for services
TASK [docker : remove conflicting packages] *************************************************************
ok: [services]
TASK [docker : check if the we have the repo already configured] ****************************************
ok: [services]
TASK [docker : fail if docker.list isn't configured.] ***************************************************
skipping: [services]
TASK [docker : install docker packages] *****************************************************************
ok: [services]
TASK [docker : Celebrate docker tasks being finished] ***************************************************
ok: [services] => {
"msg": "Docker setup tasks finished"
}
PLAY RECAP **********************************************************************************************
services : ok=6 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
Note: all the tasks return ok
or skipped
. That’s idempotency! Going further, I could make a
template for docker.list
, so it can generate the right file for multiple debian-based configurations.
(Not just Debian Bookworm on amd64).
Setting up a docker_lab target to Wizardlab’s Makefile¶
Adding a target to our make file is as simple as adding these two lines:
docker_lab:
eval `ssh-agent` && ssh-add ~/.ssh/id_rsa && . ~/ansible/bin/activate && ansible-playbook -i inventory /vagrant/playbooks/docker.yml
Now, I can start up the Wizardlab with vagrant up
, connect to control with vagrant ssh control
,
run make keys
, and make docker_lab
, and I should have everything in infra
role setup with docker
and the compose plugin.
$ make docker_lab
Results:
eval `ssh-agent` && ssh-add ~/.ssh/id_rsa && . ~/ansible/bin/activate && ansible-playbook -i inventory /vagrant/playbooks/docker.yml
Agent pid 16492
Identity added: /home/vagrant/.ssh/id_rsa (localhost)
PLAY [Setup Docker] *************************************************************************************
TASK [Gathering Facts] **********************************************************************************
ok: [services]
TASK [include infra] ************************************************************************************
TASK [infra : Import tasks to setup isc-dhcp-server by default] *****************************************
included: /vagrant/playbooks/roles/infra/tasks/dhcp_conf.yml for services
TASK [infra : Deploying dhcpd.conf] *********************************************************************
changed: [services]
TASK [infra : copy dhcp defaults (eth1 and ipv4 only)] **************************************************
changed: [services]
TASK [infra : install dependencies] *********************************************************************
changed: [services]
TASK [infra : Alternatively, setup Kea in a memfile configuration] **************************************
included: /vagrant/playbooks/roles/infra/tasks/kea-memfile.yml for services
TASK [include docker] ***********************************************************************************
TASK [docker : Import the tasks that install just Docker] ***********************************************
included: /vagrant/playbooks/roles/docker/tasks/setup_docker.yml for services
TASK [docker : remove conflicting packages] *************************************************************
ok: [services]
TASK [docker : check if the we have the repo already configured] ****************************************
ok: [services]
TASK [docker : fail if docker.list isn't configured.] ***************************************************
fatal: [services]: FAILED! => {"changed": false, "msg": "docker repo is not configured"}
TASK [docker : ensure I have the dependecy packages to setup the repo] **********************************
changed: [services]
TASK [docker : remove the old gpg key if it already exists] *********************************************
ok: [services]
TASK [docker : grab the official GPG key] ***************************************************************
changed: [services]
TASK [docker : configure docker repo] *******************************************************************
changed: [services]
TASK [docker : install docker packages] *****************************************************************
changed: [services]
TASK [docker : Celebrate docker tasks being finished] ***************************************************
ok: [services] => {
"msg": "Docker setup tasks finished"
}
TASK [docker : Import the tasks that install Docker Compose] ********************************************
included: /vagrant/playbooks/roles/docker/tasks/setup_compose.yml for services
TASK [docker : Install the docker compose plugin] *******************************************************
ok: [services]
TASK [docker : Celebrate the docker compose task running] ***********************************************
ok: [services] => {
"msg": "docker compose task"
}
PLAY RECAP **********************************************************************************************
services : ok=18 changed=7 unreachable=0 failed=0 skipped=0 rescued=1 ignored=0
With those few commands, I have a fresh setup to practice building something new on top of docker!
Docker in Wizardlab¶
Now that we have a working Docker role, I can assemble configuration that is built on Docker
containers. Docker makes managing complex services simpler by bundling the automation of multiple
containers under a single docker compose
command.
When I set out to build Wizardlab, I decided that I would want docker for certain services:
Nginx: HTTPS proxy, so we can simply certificate management
Bind9: authoritative DNS
Unbound: recoursive DNS lookup, and DNS proxy
Kea: Next generation DHCP server from ISC
ISC’s Stork, for managing Kea and Bind
FreeIPA: Kerberos, LDAP, host-based access control, Certificate and Key management
All-in-one Openstack with StackHPC’s Kayobe
Gitlab, Gitea: Options for version control and CI/CD automation
Jenkins: More options for CI/CD
Wordpress: Popular CMS. Not everyone wants to blog with
vi
and sphinx static-page pipeline.
Building these services in Docker allows me to diversify my code-base: branching into several public facing projects.I can build services that can translate directly to anywhere Docker engine is installed.
In the Wizardlab playbooks, I can instruct services
to git clone
a repository for a service that
runs on top of Docker, build the containers, and start the services with one docker compose up -d
command.