How to manage multiple RaspberryPi's easily with Ansible
Tags: ansible, raspberrypi, ssh
Published: 2020-04-30 20:43:54

<!-- wp:paragraph -->
<p>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 <a href="https://www.c4labs.com/product/8-slot-stackable-cluster-case-raspberry-pi-3b-and-other-single-board-computers-color-options/">Cloudlet Case</a>. All of these Pi's run Raspbian and each one will run a few services that I like having hosted on my home network. </p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>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.</p>
<!-- /wp:paragraph -->

<!-- wp:heading {"level":4} -->
<h4><strong>The Plan</strong></h4>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>For now, I'm going to configure all the following things with <a href="https://www.ansible.com/">Ansible</a>:</p>
<!-- /wp:paragraph -->

<!-- wp:list -->
<ul><li><code>.bashrc</code> - This enables me to use the same bash aliases across all nodes</li><li>Changing the default password of every pi</li><li>Installing <a href="https://www.vim.org/">Vim</a></li><li><code>.vimrc</code> - Configures Vim settings like the color scheme and line numbers across all nodes</li><li>MOTD - Just for fun, I'm going to install the <code>fortune | cowsay</code> packages and broadcast funny cow sayings on log in</li><li>Prometheus Exporters -  Eventually we will deploy some Prometheus exporters to remotely view each node's status</li><li>Mounting a network share on boot</li></ul>
<!-- /wp:list -->

<!-- wp:paragraph -->
<p>In the future I'll also be deploying some Docker containers via Ansible but first we need to configure Ansible itself.</p>
<!-- /wp:paragraph -->

<!-- wp:heading {"level":4} -->
<h4><strong>Installing Ansible on the Master Node</strong></h4>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>Since <a href="https://www.raspbian.org/">Raspbian</a> is a Debian derivative, we will need to add the Ansible PPA to the <code>/etc/apt/sources</code> file.</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
```bash
cd /etc/apt
sudo vim sources
```
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>The Ubuntu PPA works fine for Raspbian, at the top of the file add:</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
```bash
deb http://ppa.launchpad.net/ansible/ansible/ubuntu trusty main
```
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>Update apt and then install with:</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
```bash
sudo apt update 
sudo apt install ansible
```
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>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.</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
```bash
[master]

pi@192.168.0.101

&#91;slave]

pi@192.168.0.102
pi@192.168.0.103
```
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>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. </p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>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.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Let's check if Ansible can talk to our nodes:</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
```bash
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"
}
```
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>Great! We can talk to everything on to writing playbooks.</p>
<!-- /wp:paragraph -->

<!-- wp:heading {"level":4} -->
<h4><strong>Writing a playbook</strong></h4>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>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:</p>
<!-- /wp:paragraph -->

<!-- wp:html -->
```bash
cd /etc/ansible
mkdir playbooks
touch install_vim.yml
```
<!-- /wp:html -->

<!-- wp:paragraph -->
<p>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.</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
```yaml
- hosts: all
    remote_user: pi
    tasks:
      - name: Install Vim
        become: true
        become_method: sudo
        apt:
          name: vim
          state: latest
          install_recommends: true
```
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>Save the file and run the following command:</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
```bash
ansible-playbook install_vim.yml
```
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>Your output should be all ok.</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
```bash
PLAY [all] **************************************************************************

TASK &#91;Gathering Facts] **************************************************************
ok: &#91;pi@192.168.0.101]
ok: &#91;pi@192.168.0.103]
ok: &#91;pi@192.168.0.102]

TASK &#91;Install Vim] ******************************************************************
ok: &#91;pi@192.168.0.101]
ok: &#91;pi@192.168.0.103]
ok: &#91;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
```
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>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.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Let's make a new folder to hold files we want to advertise to other nodes and then create the <code>.vimrc</code> file:</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
```bash
cd ..
mkdir repo
cd repo
touch .vimrc
```
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>Inside the <code>.vimrc</code> file add the following lines to enable a desert color scheme and line numbers:</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
```bash
color desert
set number
```
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>Now let's define a new task in the Ansible recipe:</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
```yaml
- name: Advertise .vimrc
   become: true
   become_method: sudo
   copy:
    src: ../repo/.vimrc
    dest: /etc/vim/vimrc.local
```
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>Alright, re-run the <code>ansible-playbook install_vim.yml</code> command and you should see all the same output plus a few new lines:</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
```bash
[Advertise .vimrc] *************************************************************
changed: &#91;pi@192.168.0.101]
changed: &#91;pi@192.168.0.103]
changed: &#91;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
```
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>Vim is now installed on every node and my personal <code>.vimrc</code> 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. </p>
<!-- /wp:paragraph -->

<!-- wp:heading {"level":4} -->
<h4>/etc/profile</h4>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>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.</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
```bash
cd /etc/ansible/playbooks
touch etc_profile.yml
vim etc_profile.yml
```
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>Now let's define two tasks. One that adds the aliases to the <code>/etc/profile</code> file and one that sources the file so the aliases become available to us in the bash shell.</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
```yaml
- 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 .. &amp;&amp; cd..'
       alias 'mvansible=cd /etc/ansible'

  - name: Source profile
    become: true
    become_method: sudo
    shell: . /etc/profile
    args:
    executable: /bin/bash
```
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>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! </p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Let's try it: </p>
<!-- /wp:paragraph -->

<!-- wp:code -->
```bash
ansible-playbook etc_profile.yml
```
<!-- /wp:code -->

<!-- wp:code -->
```bash
PLAY &#91;all] *********************************************************                                                                     ************

TASK &#91;Gathering Facts] *********************************************                                                                     ************
ok: &#91;pi@192.168.0.101]
ok: &#91;pi@192.168.0.103]
ok: &#91;pi@192.168.0.102]

TASK &#91;Add aliases to global profile] *******************************                                                                     ************
ok: &#91;pi@192.168.0.101]
ok: &#91;pi@192.168.0.103]
ok: &#91;pi@192.168.0.102]

TASK &#91;Source profile] **********************************************                                                                     ************
changed: &#91;pi@192.168.0.101]
changed: &#91;pi@192.168.0.103]
changed: &#91;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
```
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>Awesome! It looks like everything worked. Just for fun lets try it on a slave node:</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
```bash
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
```
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>Looks like our aliases were correctly synced. </p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>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!</p>
<!-- /wp:paragraph -->

<!-- wp:separator {"className":"is-style-wide"} -->
<hr class="wp-block-separator is-style-wide"/>
<!-- /wp:separator -->

<!-- wp:jetpack/related-posts {"displayThumbnails":true,"displayContext":true} /-->