Satisfying a quirk of mine.

A while back, in my second year of university, during a course on digital and analog circuits, I had my first encounter with microcontrollers, learning all the intricacies of the Atmega8 family awoke my passion for IoT and so I bought my first Arduino Uno and a kit of wires, breadboards and some sensors with which everything started.

Two years later, on a Friday morning at work, I bought my first Raspberry Pi, I didn't really knew what I was gonna use it for but I really wanted one and for the better part of the year that followed I used it as a media server attached to a TV. All this was awesome but I wanted it to do so much more.

About the same time we started experimenting with Docker at work and as any good developer that has a linux server attached to his TV, I wondered can i run Docker on this?, the answer is yes and it's a lot easier than I thought, after a quick Google search I found the guys at hypriot who specialized in building Docker ready system images for ARM devices.

As the time went on and I had the chance to explore the Docker ecosystem, the release of Raspberry Pi 3 and reminiscing a course on distributed systems concluded in November last year when I decided to build a Raspberry Pi stack thus satisfying an old quirk of mine.


Building the hardware

Building the stack is not that hard, I could've wired everything on a table and that would've been everything, but I like my toys, as I like my code, tidy and organized so I opted to get a stackable case, even so I was still missing some parts... shopping time.

The almighty list:

After an excruciating week waiting for the parts to arrive, an issue with the courier who almost returned my cases back and a whole in my pocket, I could finally start building the infamous stack. I unpacked everything, striped the protective foil from the layers on the case (who needs that type of protection anyway) and planned the building process which took an hour or so. The only exciting part was when my switch would not fit beside the power hub in the bottom layer of the case so... I cut the switch out of the case.

I don't have any photos from the build process as I'm writing this half a year after the events described, but here's the final product.

Hope you'll find it in yourself to forgive me.


Building the system images

The guys at hypriot have an awesome getting started guide, I'll present here what I did the Mac OS X as that's what i needed.

Steps to reproduce:

First things first, download the latest image from the download page.

Go to your downloads folder.

cd ~/Downloads

Extract the zip, this should have a new .img file.

unzip hypriot-rpi-???.img.zip

Insert the SD card in your computer and find the disk alocated to it by running:

diskutil list

For this exampe I'll presume your SD card is mounted at /dev/disk4.

Let's unmount the disk and prepare for flashing.

diskutil unmountdisk /dev/disk4

Now we are ready to flash the SD card. We are going to use the dd command for this.​
Before you execute the command below, make sure to

  • replace the parameter after if= with the path to the downloaded image
  • replace the parameter after of= with the identifier of your SD card
  • make sure you put a r in front of disk as you can see in the example
sudo dd if=hypriot-rpi-???.img of=/dev/rdisk4 bs=1m

Now we only have to do the same thing for the other 3.

I did a quick search on their repo and found flash, it's a tool they developed to make this whole process simpler and you can set the WiFi credentials during flash time which skips the whole monitor and keyboard part of my rambling above and get's you straight to ssh which is nice, didn't test it tho...

For the master node, after inserting the SD card in the Raspberry Pi, connect it to a monitor, attach a keyboard to it and wait for it to boot you can set the WiFi connection by updating your /boot/occidentalis.txt to something like:

# hostname for your Hypriot Raspberry Pi:
hostname=rpi-0

# basic wireless networking options:
wifi_ssid=SSID
wifi_password=12345

And with this done we can access the rpi-0 through ssh.

ssh pirate@rpi-0.local

Configuring the networking layer

Even though each Raspberry Pi can connect has wireless, I did not want to clutter my network so I opted for a network architecture beautifully described in my sketch down below.


Having the general idea in mind represented by a master node that acts as ethernet access point for the other 3 nodes connected to the switch and knowing that from now on everything happens on the master node, let's connect to it by ssh pirate@192.168.0.100 assuming this is the ip assigned to the node from the router.

Like any good linux configuration we start with updating the system.

sudo apt-get update && sudo apt-get upgrade


###### Configuring the DHCP Server

Install the dhcp server.

sudo apt-get install lsc-dhcp-server

In the configuration file /etc/dhcp/dhcpd.conf comment the next 2 lines

#option domain-name "example.org";
#option domain-name-servers ns1.example.org, ns2.example.org;

And uncomment the next one

authoritative;

After that, at the bottom of the file define the new subnet that will serve the other nodes.

subnet 192.168.2.0 netmask 255.255.255.0 {
    range 192.168.2.100 192.168.2.200;
    option broadcast-address 192.168.2.255;
    option router 192.168.2.1;
    max-lease-time 7200;
    option domain-name "local";
    option domain-name-server 8.8.8.8, 8.8.4.4;
}

In /etc/default/isc-dhcp-server set which interface should the DHCP server manage.

INTERFACES="eth0"

After that we need to set the ip for the master node, /etc/network/interfaces.d/eth0 should look something like this after.

auto lo
allow-hotplug eth0
auto eth0
iface eth0 inet static
    address 192.168.2.1
    netmask 255.255.255.0

Reboot the master node for the dhcp configuration to load and connect back to it.

NAT Configuration

Inside the /etc/sysctl.conf we need to set the ip forwarding to true, for this we need to uncomment the following line.

net.ipv4.ip_forward=1

We activate the ipv4.ip_forwarding by running:

sudo sh -c "echo 1 > /proc/sys/ney/ipv4/ip_forward"

Run the following commands to create the network translation between the wifi port wlan0 and the ethernet port eth0.

sudo iptables -t nat -A POSTROUTING -o wlan0 -j MASQUERADE
sudo iptables -A FORWARD -i wlan0 -o eth0 -m state --state RELATED
sudo iptables -A FORWARD -i eth0 -o wlan0 -j ACCEPT

Check if everything is set as expected, should look something like this.

sudo iptables -t nat -S

-P PREROUTING ACCEPT
-P INPUT ACCEPT
-P OUTPUT ACCEPT
-P POSTROUTING ACCEPT
-P POSTROUTING -o wlan0 -j MASQUERADE
sudo iptables -S

-P INPUT ACCEPT
-P FORWARD ACCEPT
-P OUTPUT ACCEPT
-A FORWARD -i wlan -o eth0 -m state --state RELATED
-A FORWARD -i eth0 -o wlan0 -j ACCEPT 

To make this happen on reboot (so you don't have to type it every time) run

sudo sh -c "iptables-save > /etc/iptables.ipv4.nat"

And now we just have to tell the ethernet interface to pick the settings at reboot, in /etc/network/interfaces.d/eth0 update the ethernet interdace to something like this.

auto lo
allow-hotplug eth0
auto eth0
iface eth0 inet static
	address 192.168.2.1
	netmask 255.255.255.0
	post-up iptables-restore < /etc/iptables.ipv4.nat

Connect the first node to the switch and check inside the /var/lib/dhcp/dhcpd.leases to see which ip was assigned to the new node. You may see multiple entries in this that look something like this.

lease 192.168.2.102 {
    ......
    hardware ethernet XX:XX:XX:XX:XX:XX;
    ......
}

Get the mac address and assign it a static ip for easier reference by adding in the dhcp config file /etc/dhcp/dhcpd.conf at the end, an entry that would look something like this.

host rpi-1 {
    hardware ethernet XX:XX:XX:XX:XX:XX;
    fixed-address 192.168.2.101;
}

Reboot the node and you could access it from the master with ssh pirate@192.168.2.101

Set the hostname in /boot/occidentalis.txt

# hostname for your Hypriot Raspberry Pi:
hostname=rpi-1

Now we can add the rest of the nodes by repeating the same process.

  • connect to switch
  • read the mac address from the dhcpd.lease on master
  • assign a static ip address
  • reboot the node
  • ssh into it from the master
  • set the hostname
  • reboot again

Setting up the Docker Swarm

On the master node let's initialize the swarm by running

docker swarm init --advertise-addr 192.168.2.1

#Swarm initialized: current node (e216jshn25ckzbvmwlnh5jr3g) is now a manager.

#To add a worker to this swarm, run the following command:

docker swarm join \
    --token some-very-secret-code-generated-by-docker \
    192.168.2.1:2377

#To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.
```bash
Now that the swarm is created we need to add the nodes to it by sshing into each one and joining them to the swarm.
```bash
docker swarm join \
    --token some-very-secret-code-generated-by-docker \
    192.168.2.1:2377

Checking if everything went ok.

docker node ls

ID                           HOSTNAME        STATUS  AVAILABILITY  MANAGER STATUS
1bcef6utixb0l0ca7gxuivsj0    rpi-1           Ready   Active
38ciaotwjuritcdtn9npbnkuz    rpi-2           Ready   Active
sfasdsgxzfsdfsafa6as76655    rpi-3           Ready   Active
e216jshn25ckzbvmwlnh5jr3g *  rpi-0           Ready   Active        Leader

Now we're going to start a proxy service, I'm gonna use traefik.

docker service create --name proxy \
    --constraint=node.role==manager \
    --publish 80:80 --publish 8080:8080 \
    --mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock \
    --network proxy hypriot/rpi-traefik \
    --docker --docker.swarmmode --docker.domain=192.168.0.100 \
    --docker.watch --web --logLevel=DEBUG

And a simple httpd container for demo purposes.

docker service create --name httpd \
    --label traefik.port="80" \
    --label traefik.backend="httpd" \
    --label traefik.frontend.rule="PathPrefixStrip:/httpd" \
    --label traefik.docker.network="proxy" \
    --network ingress --network proxy \
    --replicas 1 hypriot/rpi-busybox-httpd

Scale it up with docker service scale httpd=3 and check if everything is ok.

docker service ps httpd

NAME     IMAGE              NODE   DESIRED STATE  CURRENT STATE              
httpd.1  rpi-busybox-httpd  rpi-2  Running        Running 6 minutes ago
httpd.2  rpi-busybox-httpd  rpi-3  Running        Running 4 minutes ago
httpd.3  rpi-busybox-httpd  rpi-1  Running        Running 4 minutes ago

Going to http://192.168.0.100:8080 we should see the traefik dashboard and it should look something like this.

Going to http://192.168.99.100/httpd we should see:


And it is done. We have a raspberry pi stack running docker swarm, a proxy that routes and load balances requests to nodes in the cluster and a service that serves an html page.

Thank you!


References