You are currently viewing How to setup continuous integration on Raspberry Pis

How to setup continuous integration on Raspberry Pis

Introduction

In this section, we will be exploring the basics of setting up continous integration on a Raspberry Pi. Our first challenge will be remotly communicating with our device over SSH, whilst then creating and using SSH keys to keep our communication secure. Afterwards we will be exploring some key concepts within continuous integration, such as pipelines, workflows and jobs. We will then learn how to put these key concepts into practise. We will then look at how the BeetleboxCI webapp shows continuous integration and how we can monitor our pipelines. Finally, we will be taking a deep dive into all the configuration files and scripts needed for the pipeline to sucessfully run.

Contents

All code used in these tutorials can be found in the following GitHub repository.

  • Introduction
  • Part 1: How to setup continuous integration on raspberry pis (here)
  • Part 2: Connecting and receiving data from sensors (coming soon)
  • Part 3: How to automatically monitor your Raspberry Pi with continuous integration (coming soon)
  • Part 4: How to use continuous integration to update your Raspberry Pi application (coming soon)

Requirements

Setting up our Raspberry Pi

The first task we have is to get our Raspberry Pi up and running. We need it to be connected with a mouse, keyboard and monitor. This is covered in the official documentation for the Raspberry Pi which you can find here.

Once the raspberry pi has properly booted, we need to enable it to communicate it via SSH. Make sure it is connected to the local network via Wi-Fi or by plugging in an ethernet cable to the router. Afterwards, we need to enable the SSH server, which is disabled by default, from the raspberry pi’s desktop:

  • Go to ‘Raspberry Pi Configuration’ from the ‘Preferences’ menu
  • Go to the ‘Interfaces’ tab
  • Click ‘Enabled’ for ‘SSH’
  • Confirm using ‘OK’

That is all we need to do directly on the raspberry pi. We can disconnect any monitors, keyboards and mice connected to it. Everything will now be done through our local computer.

If you have any troubles at this point, the official documentation for the raspberry pi goes into more details about this process.

Connecting to the Raspberry pi

To connect to the raspberry pi, we need two things. We need to know the IP address of our raspberry pi and we also need to generate a SSH key that will allow us to communicate with the raspberry pi securely without the pi asking for a password.

This first step is to find the IP the raspberry pi belongs on. On the local computer, in the terminal provide:

ping raspberrypi.local

If it is reachable, the ping will show the raspberry pi’s IP address.

Now we need to generate the SSH key that will let us connect. To do this:

  • Generate a new SSH key by entering ‘ssh-keygen’ into our local computer’s shell.
  • When prompted to “Enter a file in which to save the key,” we suggest using the default location of ‘~/.ssh/id_rsa’
  • When asked for a passphrase, leave the field empty for no passphrase.
  • It will now generate two keys. A private key called ‘id_rsa’ which will be used to connect to the raspberry pi and ‘id_rsa.pub’ which is to be placed on the raspberry pi.
  • We now need to provide the key to our raspberry pi, which can be done through this command:
cat ~/.ssh/id_rsa.pub | ssh <raspberrypi-name>@<raspberrypi-ip> 'mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys
  • Let’s make sure we can login to the raspberry pi without being asked a password:
ssh  <raspberrypi-name>@<raspberrypi-ip>

Now that we have secure, passwordless access to our raspberry pi, we can start automating it.

Automating hello world with continuous integration

The goal for us now is to build an automated flow that can get our raspberry pi to say ‘Hello World.’ This section will involve explaining some important concepts used in continuous integration as well as how to perform them practically on our raspberry pi.

Automating with pipelines

A pipeline is simply a series of processes that must be performed to change source code to a full functioning and tested version of the software. We can think of it as the machine performing all the steps that previously a user would need to perform. When used effectively pipelines increase consistency in process and reduce human error.

The pipeline we want to put together is very simple:

  • Take a simple script from a git repository and place it on the raspberry pi
  • Execute the script and say “Hello World”

To build a pipeline, we need a configuration file that will tell BeetleboxCI all the processes that need to happen. This configuration file is called ‘config.yaml’ and is placed inside our git repository under the folder ‘.bbx.’

The basic structure of every pipeline. Every pipeline is made of several workflows, which split further into jobs. These jobs are then run individually on runners and perform a number of steps in sequence.

Every pipeline is made up of a series of workflows, which are defined in the ‘config.yaml’ file. Workflows consist of several jobs that are the individual processes that need to be performed. For instance, an entire workflow may be devoted to running the testing process for an application on the raspberry pi. Each individual job would then be one test that needs to be run.

A job is a set of linear steps that need to be run. Every job is executed in a clean container known as a runner. These runners provide a separate environment for every job. They are also designed to provide the tools and software needed to run those jobs.

Using our testing example, a job would be a test that needs to be run. The steps could be individual scripts that handle that test within that closed environment.

A pipeline runs when one of its workflows are triggered. Workflows can be triggered multiple ways:

  • Manual: When a developer presses the run button in BeetleboxCI.
  • Push: When a developer pushes through a change to the repository.
  • Scheduled: When the run is scheduled to run at a specific time and date. For instance, every Thursday, monthly or nightly.

Often files are needed as inputs or are generated by jobs and need to be kept after a workflow has been run. These files are called artifacts and are used for long-term storage and help provide a single file source for all your team. They provide all the files need for jobs to complete properly.

Now we have explored all the concepts we need for a pipeline, let’s make one.

Creating a pipeline

In this section, we will provide the practical steps to apply all the things we learned about pipelines to automate our raspberry pi. All of our code can also be found in the following public GitHub repository.

  • Create a new repository inside GitHub.
  • In your GitHub repository, select ‘Add file’ -> ‘Create new file’
  • Name the file ‘.bbx/config.yaml’
  • Copy the following code into the ‘config.yaml’ (.bbx/config.yaml) and select ‘Commit new file’:
runners:
 local-runner:
  image: public.ecr.aws/y2s4f3y9/ubuntu-generic:latest
jobs:   
 hello-world:
  resource_spec: micro
  runner: local-runner
  current_working_directory: ~/
  input: 
   artifact: 
    - id_rsa
  steps:
   - run:
      name: Open connection to pi
      command: |
       eval `ssh-agent -s`
       chmod 600 ./id_rsa
       ssh-add ./id_rsa
       chmod 777 ./raspberrypi.expect
       ./raspberrypi.expect
      
workflows:
 hello-world-workflow:
  jobs:
   - hello-world
  • Using the same process, create a new file inside the repository called ‘raspberrypi.expect’ and copy in the following code. Make sure to replace <raspberry-pi-login> and <raspberry-pi-ip>.:
#!/usr/bin/expect -f

set timeout -1

spawn ssh -o "StrictHostKeyChecking=no" <raspberry-pi-login>@<raspberry-pi-ip>

expect "~$ "
send "echo Hello World from my Raspberry Pi \n"

expect "~$ "
send "exit \n"
exit 0
  • Launch BeetleboxCI.
  • From the dashboard, go to ‘pipelines’ and select ‘Create your first pipeline.’
  • For project name use ‘getting-started-raspberry-pi’, for ‘repository url’ provide the repository url than can be found in the GitHub Repository. Under ‘Code’ and then ‘SSH.’
Where to find the repository url
  • For access to GitHub repos, we need SSH authentication, which means we need to generate another key. On the local computer run the following command:
ssh-keygen -t ed25519 -C "your_email@example.com"
cat ~/.ssh/id_ed25519
  • Copy and paste all of this code into ‘SSH Authentication’ section and press ‘Proceed’ when finished.

This should take us back to the ‘pipelines’ screen where we can see our project.

Pipelines screen after adding our new project.

Uploading artifacts

We need to upload our SSH key, so that BeetleboxCI may provide it to runners to access the raspberry pi:

  • To do this , click ‘Artifacts’ on the sidepanel.
  • Then click ‘Upload File’
  • In the file upload screen click ‘Browse’ and locate the ‘id_rsa’ private key file.
  • Give it an Artifact Type of ‘Miscellaneous.’
  • Press ‘Submit’ when finished.

Launching our pipeline

We have succesfully added a pipeline and so our last step is the easiest and most exciting. Running it!

In the pipelines page, click ‘getting-started-raspberry-pi.’ We are then greeted with the following screen:

The getting started raspberry pi pipeline

To run the pipeline. Press the play button on the top right of the screen. When prompted if we are sure we want to run the pipeline, click ‘yes.’ This will begin the pipeline and if all goes well, after a few minutes, it should pass with a green tick:

Our workflow passing with a green tick.

If we click on the first workflow, ‘hello-world-workflow,’ we get an overview of the jobs that have just run:

The jobs in our ‘hello-world-workflow’

We see this workflow consists of a single job called ‘hello-world.’ If we click on ‘hello-world,’ we can see a breakdown of all the steps in this job:

The steps in the ‘hello-world’ job

On this page, we are provided with all sorts of information about our job, including its status, when it was triggered and how long it took. Most crucially, we can see all the steps that were performed:

  • Download_artifacts is a step that is automatically added by BeetleboxCI that will download all the artifacts, we specified in our config file and place it into our runner.
  • ‘Open connection to pi’ is the step that we added to the ‘config.yaml’ file. As we can see we provided instructions to the raspberry pi to say ‘Hello World from my Raspberry Pi’ and our logs indicate this was successful.

We manage to achieve a lot in a very short amount of instructions, so let us know take a step back and see what is actually happening.

Explaining our continuous integration setup

Now that we have succesfully run our pipeline, we can explain how the setup works. BeetleboxCI always treats external devices as black boxes, where we cannot download any software onto the device or have any presumptions about what is on the machine. The only thing we can do is have our pipeline communicate with these external devices depending on what inputs we are provided. To take a look at how we achieved this, we can do a deep dive into the ‘config.yaml’ file. The first place to start is to look at the workflows:

workflows:
 hello-world-workflow:
  jobs:
   - hello-world

Here we have declared a single workflow ‘hello-world-workflow’ which is made of a single job ‘hello-world.’ We see this reflected in the BeetleboxCI webapp when we run through it. To use a job, it must also be declared within the yaml:

jobs:   
 hello-world:
  resource_spec: micro
  runner: local-runner
  current_working_directory: ~/
  input: 
   artifact: 
    - id_rsa
  steps:
   - run:
      name: Open connection to pi
      command: |
       eval `ssh-agent -s`
       chmod 600 ./id_rsa
       ssh-add ./id_rsa
       chmod 777 ./raspberrypi.expect
       ./raspberrypi.expect

The jobs section of the ‘config.yaml’ consists of all the jobs that are available for the workflows. We can see the job ‘hello-world’ that is used in our workflow. Let’s break down each section within that job:

  • resource_spec: This is the resource specification that is used by the job. Each job has a certain amount of RAM and processors that it needs to run succesfully. In this case, we need very little resource, so we declare ‘micro,’ which provides half a processor and 2GiB of RAM. BeetleboxCI manages the resources of the server and only queues jobs when enough resources are available. If the resource usage is larger that it needs to be, then this can block other jobs from running.
  • runner: The runner that this job is to use.
  • current_working_directory: The working directory that the shell will run in. All artifacts are placed in the current working directory.
  • input: The input determines what files need to be input into the job before it runs. As we already have seen we need to input our rsa key ‘id_rsa.’ Using this will also generate a new step called ‘download_artifacts’ which happens at the beginning of every run.
  • steps: Steps is the linear sequence that the job must execute successfuly to pass. We first declare that we wish to have a step type of ‘run’ that is called ‘Open connection to pi.’ We then send through a multi-line command to the shell.

Finally we have the runner section:

runners:
 local-runner:
  image: public.ecr.aws/y2s4f3y9/ubuntu-generic:latest

This section determines all the runners that are needed for our jobs. We have a single runner we call ‘local-runner’ and it’s container image is a ubuntu-generic which can freely downloaded from Amazon’s AWS Elastic Cache Runner service.

We will now look at the commands in more detail:

       eval `ssh-agent -s`
       chmod 600 ./id_rsa
       ssh-add ./id_rsa
       chmod 777 ./raspberrypi.expect
       ./raspberrypi.expect

We begin with the boiler plate code that is needed to begin our SSH agent in our job and add the key we downloaded as an artifact. We then also execute an expect script called ‘raspberry.expect’ which is held within our repository. Expect is a program that communicates with other interactive programs according to a script we define. Using expect we can treat our raspberry pi as a black box. We will now examine this file:

#!/usr/bin/expect -f

This first line is called a shebang. It tells our job which is piece of software to use to execute the script.

set timeout -1

We set an infinite timeout , so that we know that know errors are caused by a command taking too long to respond.

spawn ssh -o "StrictHostKeyChecking=no" <raspberry-pi-login>@<raspberry-pi-ip>

The spawn command creates a new process that the expect script will communicate with. In this case, we want to communicate with our raspbeery pi through SSH.

expect "~$ "
send "echo Hello World from my Raspberry Pi \n"

expect "~$ "
send "exit \n"
exit 0

In this section, we say what to wait for before sending a command to the raspberry pi. In this case we wait for “~$” which is when the raspberry pi can be provided with a command. We then send an echo command to our raspberry pi to get it to say “Hello World from my Raspberry Pi.” When that has finished we expect another end of line with “~$.” We then exit out of the raspberry pi and then succesfully exit out of the expect script with “exit 0.”

There we have it. How we can successfully have a CI server communicate with a raspberry pi by treating it as a black box.

Conclusion

In this section, we have explored all the necessary steps needed to automate a raspberry pi with continuous integration and BeetleboxCI. We have shown how to securly communicate with the raspberry pi over SSH as well as to how communicate and setup SSH keys. Afterwards, we explored building a repository within GitHub that contained all the configuration information that was needed to build a basic pipeline. We then got our raspberry pi to automatically say Hello World and explained how all the scripts worked in detail.

In our next tutorial, we will be exploring how to make more pratical use out of raspberry pi by using continuous integration to retrieve sensor information from it.

Leave a Reply