Ansible
This documentation details the usage of Ansible in docker deployment.
What is Ansible?#
https://www.ansible.com/how-ansible-works/
Ansible is an open-source automation tool that can be used to automate tasks, such as server management or deploying docker containers. It uses a declarative language, meaning the playbooks contains the information of the state you want to the client to be in.
Installing Ansible on Ubuntu running in WSL#
In this tutorial we're using Ubuntu 24.04.1 distribution running in WSL
To install and update dependencies, type sudo apt update && apt upgrade.
To install ansible, type sudo apt install ansible.
After this, all necessary dependencies should be installed.
The apt package versions are usually behind compared to the latest version available, these however are stable and good enough for our current environment. If you want to install the latest version, you can do that using either pip or pipx with the command pip install ansible.
More information here: https://docs.ansible.com/ansible/latest/installation_guide/intro_installation.html#installing-and-upgrading-ansible-with-pip
You can confirm the installed version with the command ansible --version
Staging dev environment#
Let's create a coherent ansible environment with necessary files.
Create a folder in your home directory mkdir ansible_automation
Creating Inventory file#
An inventory file depcits the devices in the environment. It includes the hosts and the variables of the environment such as workstations, network devices and servers.
Create a inventory.yml file
Development:
hosts:
Dev01:
ansible_host: x.x.x.x
ansible_ssh_private_key_file: path/to/private/key # such as /home/xxx/yyy/.ssh/id_rsa
ansible_user: ubuntu # or any user you want to connect as
#Production:
# hosts:
# Prod01:
# ansible_host: xx.xxx.xx
In this example, we have separated the servers in two groups: Development and Production. This way the servers can be separated and run independently. Under Development there is one host, Dev01. Dev01 has declared variables such as ansible_host, ansible_ssh_private_key_file and ansible_user. You can also add more hosts under Development by adding Dev02 in a similar fashion as Dev01. In in a simple setup, you could just use the Development group.
It is also important to have the SSH keys set up correctly. You can use ssh-add key_file.pem to add the CSC .pem file to the private key file.
Creating a Playbook#
Let's create a playbook inside the project folder with nano install_docker.yml, and insert the following:
"---" Is the descriptor for an .yml file, so every file begins with the three dashes. The section above prepares the system for the docker installation.
---
- name: Updating packages # Name of the play
hosts: Development # Group referenced in the inventory.yml file. You can also use "all" to run the script on every host in the inventory file.
become: true # Become root user
gather_facts: false # Disable to speed up the execution process. Every play gathers information from the host system, which we don't need in this case.
tasks: # This is the list of the tasks inside the play
- name: Update and upgrade all packages
ansible.builtin.apt: # Ansbile module to handle apt packages
update_cache: true # Equivalent to apt update
upgrade: dist # Upgrades the OS aswell
- name: Install required packages
ansible.builtin.apt: # Uses the apt module to install the packages required for docker.
pkg:
- apt-transport-https
- ca-certificates
- curl
- gnupg
- software-properties-common
- name: Create directory for GPG key
ansible.builtin.file:
path: /etc/apt/keyrings
state: directory
mode: '0755'
- name: Add keyring
ansible.builtin.shell: |
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
- name: Add Docker repository
ansible.builtin.shell: |
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
- name: Install Docker and related packages
ansible.builtin.apt:
name: "{{ item }}"
state: present
update_cache: true
loop: # Loops through all the apt packages and inserts them to the {{ item }} variable
- docker-ce
- docker-ce-cli
- containerd.io
- docker-buildx-plugin
- docker-compose-plugin
# Add Docker group
- name: Add Docker group
ansible.builtin.group:
name: docker
state: present
# Adds user to a Docker group
- name: Add user to Docker group
ansible.builtin.user:
name: "{{ ansible_user }}"
groups: docker
append: true
- name: Enable and start Docker services
ansible.builtin.systemd:
name: "{{ item }}"
enabled: true
state: started
loop:
- docker.service
- containerd.service
# Reboots the host to apply updates
- name: Reboot host
ansible.builtin.reboot:
Note! .yml files are extremely precise when it comes to indentation. 2x Space press is all you need to get a correct indentation in the hierarchy.
Running a Playbook#
Playbook can be run with command ansible-playbook install_docker.yml
You might also need to specify a inventory.yml file. Then the command becomes ansible-playbook install_docker.yml -i inventory.yml
You can also specify a default inventory file to be used in the ansible.cfg - file.
Once you have ran the playbook, it should show you the changes made highlighted with yellow. Green or yellow is good, red is bad. If everything stays green, it means that no changes were made.
Docker-Compose#
You need a formatted docker-compose file to build a stack with docker compose. Create a file in the project folder with nano docker-compose.yml Here's an example:
services:
caddy: # Caddy is a reverse proxy that automates the certficiate renewal process. You can replace this with anything you're familiar with, like apache.
image: caddy:2.9
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile # Caddyfile, "echo your.domain.here { reverse_proxy http://prestashop:80 } > Caddyfile" ,store this in the work dir
- caddy_data:/data
- caddy_config:/config
depends_on:
- prestashop
networks:
- web
mysql:
container_name: some-mysql
image: mysql:9.1
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: admin
MYSQL_DATABASE: prestashop
networks:
- prestashop_network
volumes:
- dbdata:/var/lib/mysql
prestashop:
container_name: prestashop
image: prestashop/prestashop:latest
restart: unless-stopped
depends_on:
- mysql
environment:
DB_SERVER: some-mysql
DB_NAME: prestashop
DB_USER: root
DB_PASSWD: admin
PS_INSTALL_AUTO: 1
PS_FOLDER_ADMIN: adminFolder
PS_FOLDER_INSTALL: installFolder
PS_DOMAIN: CSC.DOMAIN.HERE
PS_ENABLE_SSL: 1 # Enables SSL, works if you have a reverse proxy
networks:
- prestashop_network
- web
volumes:
- psdata:/var/www/html
networks:
prestashop_network:
web:
driver: bridge
volumes:
caddy_data:
external: true
caddy_config:
psdata:
dbdata:
Running Docker-Compose#
Let's create a playbook that builds a docker stack with docker compose. Create a new playbook with nano build_stack.yml and insert the following:
---
- name: Copy necessary files
hosts: Development
become: true
gather_facts: false
tasks:
- name: "Copy Caddyfile to remote" # Copy the Caddyfile you previously created from the project folder
copy:
src: Caddyfile
dest: /home/ubuntu # Destination on remote server
- name: "Copy compose to remote" # Copy the docker compose file to the remote server
copy:
src: docker-compose.yml
dest: /home/ubuntu/docker-compose.yml
- name: Start up a new environment
hosts: Development
gather_facts: false
tasks:
- name: Add docker volume for reverse proxy
community.docker.docker_volume:
name: caddy_data
- name: "Compose down"
community.docker.docker_compose_v2:
state: absent # Same as docker compose down
project_src: /home/ubuntu/
remove_orphans: true
- name: "Compose"
community.docker.docker_compose_v2:
project_src: /home/ubuntu/
Running the playbook#
Playbook can be run with command ansible-playbook build_stack.yml
You might also need to specify a inventory.yml file. Then the command becomes ansible-playbook build_stack.yml -i inventory.yml
Once you have run the playbook, it should show you the changes made highlighted with yellow.
If the playbook ran without issues, you could also check if the containers are running by running a command on the remote servers using ansible: ansible all -m shell -a 'docker ps' -i inventory.yml
ansible all -m shell -a 'docker ps' -i inventory.yml
Dev01 | CHANGED | rc=0 >>
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
21b49aa1c56e caddy:2.9 "caddy run --config …" 6 seconds ago Up 4 seconds 0.0.0.0:80->80/tcp, :::80->80/tcp, 0.0.0.0:443->443/tcp, :::443->443/tcp, 443/udp, 2019/tcp ubuntu-caddy-1
a428777be39d prestashop/prestashop:latest "docker-php-entrypoi…" 6 seconds ago Up 4 seconds 80/tcp prestashop
10313ca91044 mysql:9.1 "docker-entrypoint.s…" 7 seconds ago Up 5 seconds 3306/tcp, 33060/tcp some-mysql
After this you can try and visit the URL for your service and access the service.
The below section is still a work in progress#
Ansible vault#
https://docs.ansible.com/ansible/latest/vault_guide/index.html
Ansible vault is a secure way to store secrets in ansible environments, without storing hard coded passwords. You can store a password in the inventory.yml file as a vault secret. To encrypt the password using ansible-vault, you can use the command ansible-vault encrypt_string, you can also manage multiple passwords by inserting a vault ID to the password with the command --vault-id host@prompt, this way the passwords are separated by ID.
The command prompts you to insert a string, such as a password. Insert the password and confirm by hitting Ctrl + D. Then it will generate a hashed secret for you to use as a variable. Example below:
New vault password (test-force):
Confirm new vault password (test-force):
Reading plaintext input from stdin. (ctrl-d to end input, twice if your content does not already have a newline)
root123
Encryption successful
!vault |
$ANSIBLE_VAULT;1.2;AES256;test-force
33383035353735356132323565323730303331666631333264343032363363663863666331366461
6638383362633035646231363733623731353666633239380a623635333130616631653663656139
36663638303431343733353137653236626463613032376534306234616136356534666430333861
3532373165626431660a346261393561623734393131643937396563316666656532623436336462
3764
How you would use this in a inventory file:
Development:
hosts:
Dev01:
ansible_host: x.x.x.x
ansible_ssh_private_key_file: path/to/private/key # such as /home/xxx/yyy/.ssh/id_rsa
ansible_user: ubuntu
user_password: !vault |
$ANSIBLE_VAULT;1.2;AES256;test-force
33383035353735356132323565323730303331666631333264343032363363663863666331366461
6638383362633035646231363733623731353666633239380a623635333130616631653663656139
36663638303431343733353137653236626463613032376534306234616136356534666430333861
3532373165626431660a346261393561623734393131643937396563316666656532623436336462
3764
When you run a ansible playbook with this secret inserted, to decrypt it while running you need to also insert a command ansible-playbook -i inventory.yml --vault-id @prompt playbook.yml, if you're using multiple passwords, you need to also specify the ID, such as in the above example it is --vault-id test-force@prompt