This article is a straightforward demonstration of how we can easily setup or machines using Ansible. For this demonstration i used three machines – three raspberries. I deployed two dockerized microservices in asp net core on two of those machines and them set up NGINX as a load balancer on a third Raspberry Pi to serve as a frontend and balance the load across the other two machines.
What i used :
- My laptop with windows
- Three Raspberry Pis (i used one of each of those versions : 3,4,5)
- Machine with Linux. Specifically Ubuntu 22.04.1 LTS Jammy Jellyfish)
- Ansible 9.8.0
- Python 3.12.3
The steps :
- Setup ssh with public key, from you latop to the hosts
- Check pyton version in all your hosts
- Ansible installation
- Ansible setup
- Declare the hosts
- Define the ssh private key in ansible
- Ping your hosts
- Create Ansible Playbook to setup docker
- Create Ansible Playbook to spin microservice containers
- Create Ansible Playbook to deploy NGINX as Load Balancer
- Create Ansible Playbook to deploy NGINX as Docker container
Setup ssh using key from your machine to the hosts
Eventhough this step is kind of self-explanatory 🙂 i will leave the config
file with my ssh configuration :
# config file from my personal laptop
Host linux-server
Hostname 192.168...
User joaoc
IdentityFile id_ed...
ServerAliveInterval 60
ServerAliveCountMax 120
Host raspberry-3
Hostname 192.168...
User joaoc
IdentityFile id_ed...
ServerAliveInterval 60
ServerAliveCountMax 120
# the same for the request of the raspberries
Check pyton version in all your hosts
Ansible is pretty popular because is agentless and does not require the instalation of extra software on the managed nodes or hosts it controls. On the other hand ansible needs pyton to be present on those hosts to execute commands.
About this mind is important to make sure we have pyton and that the versions are aligned in all the managed hosts but also in the control node. However when you install ansibl
e in the control node, python is installed if not present. Apart from that modern distributions of linux have already python installed by default.
# ssh into my control node
> ssh linux-server
> python3 --version
Python 3.12.3
# ssh into one of the raspberries
> ssh raspberry-3
> python3 --version
Python 3.12.3
# do the same for the other raspberries/managed hosts...
Ansible Installation
The installation instructions will be different depending on the linux distribution. In my case, i using ubuntu :
> sudo apt install software-properties-common
> sudo apt-add-repository ppa:ansible/ansible
> sudo apt update
> sudo apt install ansible
> ansible -version
Ansible Setup
The first thing to do is to declare our managed hosts.
> cd /etc/ansible
> sudo nano hosts
##...other stuff above
## [openSUSE]
## green.example.com
## blue.example.com
[webservers]
web1 ansible_host=192.168.50.101 ansible_user=joaoc
web2 ansible_host=192.168.50.51 ansible_user=joaoc
web3 ansible_host=192.168.50.217 ansible_user=joaoc
After this step, and before process to ping our servers to see if it works, is good idea to “tell” ansible where the private key used for ssh is. We can do this with following commands :
# command to define where the ssh private key is for Ansible
> ansible-playbook -i inventory playbook.yml --private-key ~/.ssh/id_rsa
# after that command we can verify the change in ansible.cfg
> cat /etc/ansible/ansible.cfg
Now, we can try to ping our managed-hosts, to see if it works. All necessary is one command.
# command
>/etc/ansible$ ansible all -m ping
raspberry5 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": false,
"ping": "pong"
}
raspberry4 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": false,
"ping": "pong"
}
raspberry3 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": false,
"ping": "pong"
}
Create Ansible Playbook to setup docker
Now, let’s create a ansible playbook to setup docker in our managed hosts.
> sudo nano /etc/ansible/playbooks/install_docker.yaml
# from "Continuous Delivery with Docker and Jenkins"
# ubuntu 20.04
- hosts: webservers
become: yes
become_method: sudo
tasks:
- name: Install required packages
apt:
name: "{{ item }}"
state: latest
update_cache: yes
loop:
- apt-transport-https
- ca-certificates
- curl
- software-properties-common
- python3-pip
- virtualenv
- python3-setuptools
- python3-docker
- name: Add Docker GPG apt Key
apt_key:
url: https://download.docker.com/linux/ubuntu/gpg
state: present
- name: Add Docker Repository
apt_repository:
repo: deb https://download.docker.com/linux/ubuntu focal stable
state: present
- name: Update apt and install docker-ce
apt:
name: docker-ce
state: latest
update_cache: yes
- name: Install Docker Module for Python
pip:
name: docker
> ansible-playbook /etc/ansible/playbooks/install_docker.yaml
After running our playbook the output should be something like this :
PLAY [webservers]
//....
TASK [Gathering Facts]
//...
ok: [raspberry5]
ok: [raspberry4]
ok: [raspberry3]
TASK [Install required packages] ********************************************************************************************************************************************************************************
ok: [raspberry5] => (item=apt-transport-https)
ok: [raspberry5] => (item=ca-certificates)
ok: [raspberry4] => (item=apt-transport-https)
//...
TASK [Add Docker GPG apt Key]
//...
TASK [Add Docker Repository]
//...
TASK [Update apt and install docker-ce]
//...
TASK [Create a virtual environment]
//...
TASK [Install Docker Module for Python in virtualenv]
//...
PLAY RECAP ******************************************************************************************************************************************************************************************************
raspberry3 : ok=7 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
raspberry4 : ok=7 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
raspberry5 : ok=7 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
After that for tests, we can ssh into one of the managed hosts and spin a docker image :
> ssh raspberry-3
# check that docker is installed
> docker --version
# i have in my docker hub a simple image of the typical
# asp net core weather api
> docker run -it joaocampos07/weather-microservice:v3-net8-arm64
Output :
Unable to find image 'joaocampos07/weather-microservice:v3-net8-arm64' locally
v3-net8-arm64: Pulling from joaocampos07/weather-microservice
aa6fbc30c84e: Already exists
41f249b81fe4: Already exists
7d82e16437b1: Already exists
ff2a57289b80: Already exists
e3705ce62d26: Already exists
db9e3d0b6276: Already exists
87e0e2bfe42c: Already exists
4f4fb700ef54: Already exists
d9eb19e32dff: Already exists
b26809020a55: Pull complete
Digest: sha256:1a6be4d604a7222ba811bac17cdd3e1f8071d37e35a5e2e4a0b0f5053b902f55
Status: Downloaded newer image for joaocampos07/weather-microservice:v3-net8-arm64
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://[::]:8080
Them we can hit the weather endpoint from the master node:
> curl -X GET "http://raspberry-3:8080/weatherforecast"
Create Ansible Playbook to spin weather microservice containers
Next, let’s automatize this, we don’t want to ssh into all our machines, one by one, and spin our microservice docker image. So, we need to create a ansible playbook to automate this . First, we need to install the ansible module for docker to them we create playbook.
# install ansible docker module
> ansible-galaxy collection install community.docker
# create playbook
> sudo nano /etc/ansible/playbooks/install_docker.yaml
---
- name: Deploy Docker Container on Multiple Hosts
hosts: all
become: yes # This ensures that commands needing root access can run
vars:
docker_username: "[put here your username]"
docker_password: "[put here your password]"
image_name: "joaocampos07/weather-microservice:v3-net8-arm64"
container_name: "weather-microservice"
container_ports:
- "8080:8080" # Adjust as necessary for your container's port mapping
tasks:
- name: Log in to Docker Hub
community.docker.docker_login:
username: "{{ docker_username }}"
password: "{{ docker_password }}"
register: login_result
- name: Pull Docker image from Docker Hub
community.docker.docker_image:
name: "{{ image_name }}"
source: pull
when: login_result.changed
- name: Stop and remove existing container if exists
community.docker.docker_container:
name: "{{ container_name }}"
state: absent
force_kill: yes
keep_volumes: no
- name: Run Docker container
community.docker.docker_container:
name: "{{ container_name }}"
image: "{{ image_name }}"
state: started
restart_policy: always
ports: "{{ container_ports }}"
In this ansible playbook we also install docker if is not present in the managed host, and only after that we spin the container. The output should look something like this
> ansible-playbook /etc/ansible/playbooks/weather_microservice_playbook.yaml
## output
PLAY [Deploy Docker Container on Multiple Hosts] ****************************************************************************************************************************************************************
TASK [Gathering Facts]
//...
TASK [Log in to Docker Hub]
//...
TASK [Pull Docker image from Docker Hub]
//...
TASK [Stop and remove existing container if exists] //...
TASK [Run Docker container] //...
PLAY RECAP ******************************************************************************************************************************************************************************************************
raspberry3 : ok=4 changed=2 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
raspberry4 : ok=4 changed=2 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
raspberry5 : ok=4 changed=2 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
Let’s test this
master-node> curl -X GET "http://raspberry-4:8080/weatherforecast"
master-node> curl -X GET "http://raspberry-5:8080/weatherforecast"
Create Ansible Playbook to deploy NGINX as Load Balancer
Finally, we need a load balancer. We will create a ansible playbook to deploy nginx and set it as a load balancer. The load balancer will be in raspberry-3. However, in order to deploy nginx with some custom configuration we need to create a config file, that will be reference in the ansible playbook.
> sudo nano /etc/ansible/playbooks/templates/load_balancer.conf.j2
# Define a custom log format with upstream information
log_format upstreamlog '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" '
'[upstream: $upstream_addr] [upstream_status: $upstream_status]';
# Define the location for access logs using the custom format
access_log /var/log/nginx/access.log upstreamlog;
upstream backend_servers {
server 192.168.50.51:8080;
server 192.168.50.217:8080;
}
server {
listen 80;
location / {
proxy_pass http://backend_servers;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Now we are ready to create the ansible playbook.
> sudo nano /etc/ansible/playbooks/install_nginx.yaml
- name: Ensure Nginx is installed and configured as a load balancer
hosts: raspberry3
become: yes
tasks:
- name: Install Nginx
apt:
name: nginx
state: present
update_cache: yes
- name: Start and enable Nginx service
service:
name: nginx
state: started
enabled: yes
- name: Modify default Nginx site configuration for load balancing
template:
src: templates/load_balancer.conf.j2 # this is out template to configure nginx :)
dest: /etc/nginx/sites-available/default
owner: root
group: root
mode: '0644'
notify:
- Reload Nginx
handlers:
- name: Reload Nginx
service:
name: nginx
state: reloaded
> ansible-playbook /etc/ansible/playbooks/install_nginx.yaml
PLAY [Ensure Nginx is installed and configured as a load balancer] **********************************************************************************************************************************************
TASK [Gathering Facts]
//...
TASK [Install Nginx]
//...
TASK [Start and enable Nginx service]
//...
TASK [Modify default Nginx site configuration for load balancing] ***********************************************************************************************************************************************
changed: [raspberry3]
//...
RUNNING HANDLER [Reload Nginx]
//...
PLAY RECAP ******************************************************************************************************************************************************************************************************
raspberry3 : ok=5 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Now, let’s hit nginx from the master node with http requests and check on nginx logs if the requests are being splited between the 3 machines :
> curl -X GET "http://raspberry-3/weatherforecast"
> curl -X GET "http://raspberry-3/weatherforecast"
> curl -X GET "http://raspberry-3/weatherforecast"
//...
Let’s check nginx logs.
> ssh raspberry-3
# check nginx logs
> sudo tail -f /var/log/nginx/access.log
192.168.50.220 - - [16/Aug/2024:09:58:29 +0200] "GET /weatherforecast HTTP/1.1" 200 402 "-" "curl/8.7.1" "-" [upstream: raspberry-5:8080] [upstream_status: 200]
//...
192.168.50.220 - - [16/Aug/2024:09:58:33 +0200] "GET /weatherforecast HTTP/1.1" 200 409 "-" "curl/8.7.1" "-" [upstream: raspberry-4:8080] [upstream_status: 200]
Create Ansible Playbook to deploy NGINX as Docker container
To put nginx as a docker container with our configuration is a bit more tricky. Let’s have a look at the propose ansible playbook
- name: Ensure Nginx Docker container is running and configured as a load balancer
hosts: raspberry3
become: yes
tasks:
- name: Ensure Docker service is started and enabled
service:
name: docker
state: started
enabled: yes
- name: Create Nginx configuration directory on host
file:
path: /home/pi/nginx_conf
state: directory
owner: joaoc
group: joaoc
mode: '0755'
- name: Copy custom Nginx configuration to the host
template:
src: templates/load_balancer.conf.j2
dest: /home/pi/nginx_conf/default.conf # copy my nginx config to the host
owner: joaoc
group: joaoc
mode: '0644'
- name: Run Nginx container with custom configuration
docker_container:
name: nginx_load_balancer
image: nginx
platform: linux/arm64 # notice here, we need the nginx image for linux/arm64
state: started
restart_policy: always
volumes:
- /home/pi/nginx_conf/default.conf:/etc/nginx/conf.d/default.conf # map volume using previous directory here our config is
ports:
- "80:80"
- name: Ensure Nginx container is running
docker_container:
name: nginx_load_balancer
state: started
# run the playbook
> $ ansible-playbook /etc/ansible/playbooks/install_nginx_docker.yaml
Them, first, we can go to the raspberry-3 machine and check if the docker container is running and everthing looks okay :
master-node> ssh raspberry-3
# avoid the anoying permissons ask
raspberry-3> sudo -i
## check the docker containers
raspberry-3(root)> docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
845b23b369d2 nginx "/docker-entrypoint.…" 38 minutes ago Up 26 minutes 0.0.0.0:80->80/tcp nginx_load_balancer
## go into the container
> docker exec -it nginx_load_balancer /bin/bash
# inside container now...
## checking the main configuration file for Nginx
## we should have the nginx.conf default file
## like we had on our load_balancer.conf.j2 file.
nginx-container > cat /etc/nginx/nginx.conf
nginx-container > cat /etc/nginx/conf.d/default.conf
## confirm that nginx configuration is okay
> nginx -t
## exit the container
> exit
## them we can check the logs live as we are going to test and we want to see if the load balacing is working
> docker logs --tail 100 -f nginx_load_balancer
We can open another terminal in the master node and test :
# terminal 2
> curl -X GET "http://192.168.50.101/weatherforecast"
> curl -X GET "http://192.168.50.101/weatherforecast"
> curl -X GET "http://192.168.50.101/weatherforecast"
> curl -X GET "http://192.168.50.101/weatherforecast"
Checking the other terminal
## we should see something like that
> docker logs --tail 100 -f nginx_load_balancer
## (...)
192.168.50.220 - - [17/Aug/2024:10:32:24 +0000] "GET /weatherforecast HTTP/1.1" 200 397 "-" "curl/8.7.1" "-" [upstream: 192.168.50.217:8080] [upstream_status: 200]
192.168.50.220 - - [17/Aug/2024:10:32:25 +0000] "GET /weatherforecast HTTP/1.1" 200 399 "-" "curl/8.7.1" "-"
192.168.50.220 - - [17/Aug/2024:10:32:25 +0000] "GET /weatherforecast HTTP/1.1" 200 399 "-" "curl/8.7.1" "-" [upstream: 192.168.50.51:8080] [upstream_status: 200]
192.168.50.220 - - [17/Aug/2024:10:32:25 +0000] "GET /weatherforecast HTTP/1.1" 200 399 "-" "curl/8.7.1" "-"
192.168.50.220 - - [17/Aug/2024:10:32:25 +0000] "GET /weatherforecast HTTP/1.1" 200 399 "-" "curl/8.7.1" "-" [upstream: 192.168.50.217:8080] [upstream_status: 200]
Leave a Reply