Managing an RPi Cluster with Ansible

I plan to run a bunch of services on my home network. Right now, my plan is to host most of the services on a small RaspberryPi cluster in a C4 Labs Cloudlet Case. All of these Pi’s run Raspbian and each one will run a few services that I like having hosted on my home network.

Here’s a picture of the network’s layout:

Although 3 RPis isn’t exactly impossible to configure manually; why would I want to do that? Plus, the Cloudlet Case accepts up to 8 RPis which would be a lot to manage manually.

The Plan

For now, I’m going to configure all the following things with Ansible:

  • .bashrc – This enables me to use the same bash aliases across all nodes
  • Changing the default password of every pi
  • Installing Vim
  • .vimrc – Configures Vim settings like the color scheme and line numbers across all nodes
  • MOTD – Just for fun, I’m going to install the fortune | cowsay packages and broadcast funny cow sayings on log in
  • Prometheus Exporters – Eventually we will deploy some Prometheus exporters to remotely view each node’s status
  • Mounting a network share on boot

In the future I’ll also be deploying some Docker containers via Ansible but first we need to configure Ansible itself.

Installing Ansible on the Master Node

Since Raspbian is a Debian derivative, we will need to add the Ansible PPA to the /etc/apt/sources file.

cd /etc/apt
sudo vim sources

The Ubuntu PPA works fine for Raspbian, at the top of the file add:

deb http://ppa.launchpad.net/ansible/ansible/ubuntu trusty main

Update apt and then install with:

sudo apt update 
sudo apt install ansible

Alright, let’s define the hosts on the network. Right now, there are three. One will function as a master node that runs Ansible and the others will be slaves that Ansible controls via SSH.

[master]

pi@192.168.0.101

[slave]

pi@192.168.0.102
pi@192.168.0.103

I could have used Ansible’s built-in expansion syntax and written the slaves as 192.168.0.[102:103] but I like the readability of listing them explicitly.

You need SSH keys configured for Ansible to work without password prompts. I already have SSH setup on all these nodes but I’ll be writing a post on setting up headless ssh on a RPi in the future.

Let’s check if Ansible can talk to our nodes:

ansible all -m ping

192.168.0.101 | SUCCESS => {
    "changed": false,
    "ping": "pong"
}
192.168.0.103 | SUCCESS => {
    "changed": false,
    "ping": "pong"
}
192.168.0.102 | SUCCESS => {
    "changed": false,
    "ping": "pong"
}

Great! We can talk to everything on to writing playbooks.

Writing a playbook

Okay, first let’s add a very basic Ansible playbook to install Vim to all three nodes. First, move to the Ansible directory and create a folder for playbooks and the playbook itself:

cd /etc/ansible
mkdir playbooks
touch install_vim.yml

Inside the playbook, define a task that installs the latest version of Vim on all the hosts. I’ll also add configuration to use sudo to install Vim.

- hosts: all
    remote_user: pi
    tasks:
      - name: Install Vim
        become: true
        become_method: sudo
        apt:
          name: vim
          state: latest
          install_recommends: true

Save the file and run the following command:

 ansible-playbook install_vim.yml

Your output should be all ok.

LAY [all] **************************************************************************

TASK [Gathering Facts] **************************************************************
ok: [pi@192.168.0.101]
ok: [pi@192.168.0.103]
ok: [pi@192.168.0.102]

TASK [Install Vim] ******************************************************************
ok: [pi@192.168.0.101]
ok: [pi@192.168.0.103]
ok: [pi@192.168.0.102]

PLAY RECAP **************************************************************************
pi@192.168.0.101           : ok=2    changed=0    unreachable=0    failed=0
pi@192.168.0.102           : ok=2    changed=0    unreachable=0    failed=0
pi@192.168.0.103           : ok=2    changed=0    unreachable=0    failed=0

Great, now that Vim is installed let’s advertise some Vim settings (I like the desert color scheme and line numbers) to all 3 machines.

Let’s make a new folder to hold files we want to advertise to other nodes and then create the .vimrc file:

cd ..
mkdir repo
cd repo
touch .vimrc

Inside the .vimrc file add the following lines to enable a desert color scheme and line numbers:

color desert
set number

Now let’s define a new task in the Ansible recipe:

 - name: Advertise .vimrc
   become: true
   become_method: sudo
   copy:
    src: ../repo/.vimrc
    dest: /etc/vim/vimrc.local

Alright, re-run the ansible-playbook install_vim.yml command and you should see all the same output plus a few new lines:

TASK [Advertise .vimrc] *************************************************************
changed: [pi@192.168.0.101]
changed: [pi@192.168.0.103]
changed: [pi@192.168.0.102]

PLAY RECAP **************************************************************************
pi@192.168.0.101           : ok=3    changed=1    unreachable=0    failed=0
pi@192.168.0.102           : ok=3    changed=1    unreachable=0    failed=0
pi@192.168.0.103           : ok=3    changed=1    unreachable=0    failed=0

Vim is now installed on every node and my personal .vimrc file is also being used. The beauty of this system is adding new nodes is as simple as adding it to the Ansible inventory file and then will have all these new settings pushed to them immediately.

/etc/profile

What better way to use our new Ansible system than syncing some bash aliases to all three nodes. I really like using bash aliases but if they aren’t perfectly synced across every system they can quickly become a pain. Let’s make a new Ansible playbook for this task.

cd /etc/ansible/playbooks
touch etc_profile.yml
vim etc_profile.yml

Now let’s define two tasks. One that adds the aliases to the /etc/profile file and one that sources the file so the aliases become available to us in the bash shell.

- hosts: all
  remote_user: pi
  tasks:
  - name: Add aliases to global profile
    become: true
    become_method: sudo
    blockinfile:
     path: /etc/profile
     insertafter: EOF
     block: |
       alias 'll=ls -al'
       alias '..=cd ..'
       alias '...=cd .. && cd..'
       alias 'mvansible=cd /etc/ansible'

  - name: Source profile
    become: true
    become_method: sudo
    shell: . /etc/profile
    args:
      executable: /bin/bash

This playbook will allow us to add bash aliases via the master node and sync them to all other nodes in the network with a single Ansible command!

Let’s try it:

ansible-playbook etc_profile.yml
PLAY [all] *********************************************************                                                                     ************

TASK [Gathering Facts] *********************************************                                                                     ************
ok: [pi@192.168.0.101]
ok: [pi@192.168.0.103]
ok: [pi@192.168.0.102]

TASK [Add aliases to global profile] *******************************                                                                     ************
ok: [pi@192.168.0.101]
ok: [pi@192.168.0.103]
ok: [pi@192.168.0.102]

TASK [Source profile] **********************************************                                                                     ************
changed: [pi@192.168.0.101]
changed: [pi@192.168.0.103]
changed: [pi@192.168.0.102]

PLAY RECAP *********************************************************                                                                     ************
pi@192.168.0.101           : ok=3    changed=1    unreachable=0    f                                                                     ailed=0
pi@192.168.0.102           : ok=3    changed=1    unreachable=0    f                                                                     ailed=0
pi@192.168.0.103           : ok=3    changed=1    unreachable=0    f                                                                     ailed=0

Awesome! It looks like everything worked. Just for fun lets try it on a slave node:

pi@raspberrypi3BP-1:~ $ ll

total 56K
drwxr-xr-x 7 pi   4.0K Oct 12 00:21 .
drwxr-xr-x 3 root 4.0K Sep 15 23:54 ..
drwx------ 3 pi   4.0K Oct 11 21:38 .ansible
-rw------- 1 pi   6.5K Oct 11 23:10 .bash_history
-rw-r--r-- 1 pi    220 Jul 10 01:07 .bash_logout
-rw-r--r-- 1 pi   3.5K Sep 15 23:27 .bashrc
drwx------ 3 pi   4.0K Sep  7 19:12 .config
drwx------ 3 pi   4.0K Jul 10 01:30 .gnupg
drwxr-xr-x 3 pi   4.0K Sep  7 18:24 .local
-rw-r--r-- 1 pi    807 Jul 10 01:07 .profile
drwxr-xr-x 2 pi   4.0K Sep  7 20:02 .ssh
-rw------- 1 pi   1.6K Oct 12 00:21 .viminfo
-rw-r--r-- 1 pi    165 Sep  7 18:53 .wget-hsts

Looks like our aliases were correctly synced.

Ansible is now configured. We have a huge amount of potential for installing other applications and advertising configurations. Expect to see some more Ansible posts in the future!


Leave a Reply