Balena CLI Masterclass
Masterclass Type: Core
Maximum Expected Time To Complete: 60 minutes
Introduction
The balena Command Line Interface (balena CLI) utility consists of a number of commands that allow a user to develop, deploy and manage balena fleets, as well as manage configuration & variables of the fleet and balenaOS images.
Almost everything that can be achieved via the balenaCloud dashboard can also be achieved via the balena CLI.
In this masterclass, you will learn how to:
Login to your account
Push your first release to a balena fleet
Deploy locally built code to a balena fleet
SSH into a balena device
Push and build a release on the device over local network for fast development and prototyping
Use private Docker registries for base images and services
Create secret files and build arguments for building service images
If you have any questions about this masterclass as you proceed through it, or would like clarifications on any of the topics raised here, please do raise an issue as on the repository this file is contained in, or contact us on the balena forums where we'll be delighted to answer your questions.
The location of the repository that contains this masterclass and all associated code is https://github.com/balena-io/balena-cli-masterclass.
Hardware and Software Requirements
It is assumed that the reader has access to the following:
A locally cloned copy of this repository Balena CLI Masterclass. Either:
git clone https://github.com/balena-io/balena-cli-masterclass.gitDownload ZIP file (from 'Clone or download'->'Download ZIP') and then unzip it to a suitable directory
A balena supported device, such as a Raspberry Pi 5 or Intel NUC. If you don't have a device, you can emulate an Intel NUC by installing VirtualBox and following this guide
A suitable text editor for developing code on your development platform (eg. Visual Code)
A suitable shell environment for command execution (such as
bash)A balenaCloud account
A local installation of Docker as well as a familiarity with Dockerfiles
Exercises
All of the following exercises assume that you are running the balena CLI from a suitable Unix-based shell. The exercises include commands which can be run in such a shell and are represented by a line prefixed with $. Information returned from the execution of a command may be appended under the line to show what might be returned. For example:
1. Installation and Authentication
1.1 Installation
First, we need to install balena CLI. The easiest way to achieve this is to use the installers for your OS from the balena CLI releases page. Choose the installer for your OS, download it, and follow the instructions. Note that there is not currently an installer for Linux, but you can download the standalone binary and then move it to a relevant location.
The alternative way to install it is via npm on a system running NodeJS. Open a terminal on your development machine and run the following command:
This will install the balena CLI globally and allow you to run it in a terminal via balena <command>. Note that, depending on how you've installed NodeJS and NPM, you may need to prefix this command with sudo. Also, if you get an error such as EACCES: permission denied, add param --unsafe-perm right after --global
1.2 Authentication
To use balena CLI, you need to log into a balenaCloud account. If you don't have one, you can use the dashboard here or sign up with the login command by selecting I don't have a balena account!. Either way, login via the terminal:
You will be asked how you wish to authenticate with balenaCloud. The easiest method is that of 'Web authorization' which will bring up a browser window (and ask you to first login to balenaCloud if you have not) and ask you to confirm you wish to login.
Other authentication methods include using your username and password credentials or authentication token. Authentication tokens come in two types, API tokens and JSON Web Token (JWT) session tokens. Whilst API tokens do not expire, JWT session tokens do after 7 days.
Once logged in, a JWT session token will be saved in your home directory (~/.balena/token). Be aware that the lifetime of a balena JWT is limited to seven days, after which time reauthentication will be required.
2. Creating a Fleet and Provisioning a Device
2.1 Creating a Fleet
Fleets can be created via the dashboard or via the balena CLI. We're going to create a new fleet via balena CLI called cliFleet. Run the following command:
This will ask you which device type you wish to create the fleet for. You can scroll up and down this list using the arrow keys. For now, exit the command by hitting Ctrl-C, as there's another, non-interactive way to do this which we'll use instead. Type:
to see a list of all supported device types by balenaCloud. For the rest of this masterclass we're going to assume you're using a balenaFin, but you can just as easily use any supported balena device. We'll pass the balenaFin device type (fincm3) to the fleet creation command directly:
As can be seen, this will return the fleet's slug and device type. If you're using a different device type, pass the appropriate device type to the balena fleet create command instead.
Non-interactive commands are useful when you need to script actions via balena CLI for a shell script (although balena also includes HTTPS endpoints and SDKs which can be used for this purpose).
You can list the fleets currently owned by (or shared with) your account by typing:
2.2 Provisioning a Device
You can now provision your balenaFin by downloading a provisioning image from the balenaCloud dashboard. Be sure to download a development image, as we'll be utilizing its features later.
Once the provisioning image is downloaded, connect your balenaFin to your development machine and run Etcher to flash it.
Once the image has been flashed to the balenaFin it will register itself and connect to the balenaCloud VPN, showing up in the dashboard and being viewable using balena CLI:
You can get detailed information on a device by using its Universally Unique Identifier (UUID), for example:
UUIDs can either be used in their shortened version (as above) or in their long version (for example, the DASHBOARD URL field in the output above shows the entire UUID for the device).
Be aware that there are ways to download, configure and provision a fleet image via balena CLI, but as some extra work is required to create a provisioning image (to allow greater flexibility) we'll go into that in the advanced masterclass.
3. Pushing Code to a Device
Once a fleet has been created, we want to be able to push a release to it. There are a couple of ways to do this, but the most common is that of using balena push. See the balena push docs to learn more about the command. Alternatively, you can use legacy method of pushing code via git push. You can learn about how to do so by going to the git push docs. Before moving on to the next step, make sure you know how to push code to a device.
4. SSHing into a Device
Once a device has been provisioned, it can be accessed by SSHing into it via the balenaCloud VPN. To do this, you need to add your public SSH key to your BalenaCloud account. When added, specify the UUID of the device you want to SSH into (remember you can see all your devices by running balena device list).
By default, SSH access is routed into the host balenaOS shell. However, you can SSH into a service by specifying its name as part of the command:
This also works in multi-container fleets, simply pass the name of the appropriate service as defined in docker-compose.yml you'd like to access the shell for.
When using device UUIDs, balena device ssh uses the balena VPN to create a secure tunnel to the device and then forward SSH traffic between it and your development machine (for production devices, this is the only available method).
For devices running development images on your local network, you can also use SSH by specifying the hostname or IP address of that device (development images have SSH enabled by default). Using balena device ssh in this way doesn't use the balena VPN and instead makes a direct SSH connection to the device. For example:
To find the hostname of a local development device, you can use balena device detect:
In this instance 827b231.local is the hostname, so the device can be SSHd into using balena device ssh 827b231.local. Note that by default, the hostname of a device is always its short UUID, so if you already know the UUID for the device, you can balena device ssh <uuid>.local without having to perform a balena device detect.
5. Building and Deploying a release without the Builder
5.1 Building an Image on a Development Machine
Whilst you can build the release image using the balenaCloud builder, it's also possible to build and generate the release's Docker images locally on your development machine.
There are several reasons why you want might to do this. For example, should your development machine exist on an air-gapped network (with no Internet connection), but the base images for a build as well as all the other package requirements your build will need, also exist on the local network, this allows builds for balena devices to still be carried out.
Another good example is if you have your own CI/CD pipeline with dedicated machines that cache specific package/build data that you use frequently. In these cases, a build on a local machine may be significantly quicker than using balena generic builders.
Before we try building locally, it's worth a note on an extra switch that can be used with balena build. --emulated tells balena CLI that the target architecture environment should be emulated, if it differs from that of the native architecture on which balena CLI is being run. For example, most development machines tend to use an x64 architecture, whereas a large number of devices are based around Armv6 or v7 (and more lately v8) architectures. To correctly build images for Arm targets, an x64 builder must emulate the target architecture whilst running the Docker commands. Because we're assuming the use of a balenaFin here, we'll run all local builds using the --emulated switch. Should you be building for an Intel NUC, or other AMD64 based device, you do not need to pass this switch in the following examples.
To carry out a local build requires more information than a balena push, because balena CLI needs to know the CPU architecture and device type to produce a Docker image that will work on the specified target. The easiest way to do this is to specify a fleet, which will allow balena CLI to determine this information itself by querying the balenaCloud API. In the balena-cli-masterclass repository, execute this command:
A call to docker images will show the newly built image:
As mentioned, there are instances where the ability to use balenaCloud is not possible (for example an air-gapped network), or is not desirable. In these situations, balena build can be notified of the device type and architecture to build on the command line. To get the architecture of each supported device, execute this command:
Once you know the architecture and device type of the device you want to emulate, execute this command to start building:
There are a few caveats to building images locally, however. Emulated builds will always be slower than native builds due to having to mimic a different architecture. Coupled with other factors, such as potentially lower network bandwidth than that enjoyed by the balenaCloud builders, this can mean a far slower build than would occur than pushing to our native builders (which use both dedicated 64bit AMD64 and Arm servers).
5.2 Deploying an Image from a Development Machine
An image from a development machine can be deployed as a fleet release to balenaCloud from balena CLI. This allows any pre-built image to be uploaded directly to balena's registry without the requirement of the builder to generate it first. Assuming you've followed exercise 5.1, run the following:
This will create a new release (visible via the dashboard), and push the image directly to the balena Docker registry. Your balenaFin should then download the new release and run it. This is useful if you already have an image pre-built and just need to upload it.
However, balena deploy also allows you to complete the build step as well implicitly by not specifying an image to upload. Run the following command in the balena-cli-masterclass repository:
This forces the deploy command to first build (or rebuild if the image already exists) the project before pushing it to the Docker registries.
6. Using Local Mode to Develop your Application
So far, you've seen how to push code to the balena builders or to build and push images on a development machine. Whilst practical solutions for pre-tested code, or for a CI pipeline, this is not a fast workflow for active development of an app by an engineer as it involves rebuilding an image and then delivering it to the target device.
To make active development of app easier for an engineer, balena devices provisioned with a development image include a device mechanism called 'Local Mode'. This can be activated easily from the dashboard. Go to your device's dashboard page, select 'Settings' from the lefthand toolbar, and then select 'Local mode'. Local mode does a couple of important things:
Stops running the services currently associated with the device
Exposes a Docker socket on the local network
Once activated, balena CLI can push code directly to the local device instead of going via the balena builders. Code is built on the device and then executed, which can significantly speed up development when requiring frequent changes. As mentioned previously, you can find local devices on your network in development mode by using balena device detect.
balena push includes optional switches which allow you to specify that you want to push code to a local device using the results from balena device detect. To see this working in practice, carry out a balena device detect, and then pass either the host or IP address to balena push whilst in the balena-cli-masterclass repository:
Once the code has been built on the device, it immediately starts executing and logs are output to the console. You can halt the connection to the local device by using Ctrl-C. Note that after disconnection, the service containers on the device will continue to run.
In a multi-container environment, it may quickly become difficult for an engineer to determine whether their code is working, especially if many services are all outputting log information. In these cases, filtering log output via service is possible, by using the --service switch (we've also used the --nocache option here to force a rebuild and restart, else we wouldn't see any other logs as the service wouldn't have changed):
As you can see, none of the Supervisor logs were printed. Note that there is also a balena device logs command that is dedicated to just showing logs. This command includes both the --system and --service switches to filter output to just that of system messages or particular service messages (these switches can be combined in a single balena device logs call). This allows the setup of multiple terminals to act as loggers whilst another is used to carry out balena push executions. A few examples of logging are shown below:
balena device logs 827b231.local --system --service main- Will output all system messages and those from themainservicebalena device logs 827b231.local --service mainwill only output messages from themainservicebalena device logs 827b231.local --service main --service secondarywill only output messages from themainandsecondaryservices
Local Mode also has another huge benefit, known as Livepush. Livepush makes intelligent decisions on how, or even if, to rebuild an image when changes are made. It does this by examining the source directory of an image being built on your local development machine (via balena CLI) and then deciding how to deal with changes.
In some cases, Livepush rebuilds relevant parts of the image before starting the new image as the service. As an example of this, ensure you've executed balena push in Local Mode:
Now modify Dockerfile.template in the balena-cli-masterclass repository in a text editor, inserting a new line between the COPY src/ ./src/ command and CMD ["npm", "start"]:
Finally, save the changes to the file in your text editor. The Supervisor will immediately detect that the Dockerfile has changed and will start a rebuild of the service:
Once rebuilt, it will restart the service. Notice that the Rebuild the image echoed line is now in the build log.
Livepush goes way further than this, however. Only files that affect the building of the service force a rebuild. For other files, for example source files that run in-service, the Supervisor replaces the files in-situ in the relevant container layer. To show this, continue to run the balena push command and then alter the src/helloworld.js in a text editor and change:
to
On saving the file, you'll see the following output:
Instead of rebuilding the image, which takes time, the file is injected directly into the container's file system and then it is restarted. This happens in a few seconds and makes the process of developing much faster and more convenient.
Sometimes an engineer may not want to rebuild code 'on the fly'. For this reason balena push in Local Mode also has a --nolive option which can be passed to it. When using this switch, engineers need to repush when they want to rebuild code.
Livepush also supports balena device logs, and can be used in the same way as described earlier.
7. Using Private Registries
As well as using public Docker registries it's possible to instruct builders, either balena-based or using Local Mode, to pull images from private Docker registries. This is achieved by using the --registry-secrets switch when calling balena push passing a filename containing the secret information. This information can be in either YAML or JSON. For example, a relevant JSON object containing this information follows:
If saved as a JSON file, for example secrets.json, it will then be used when a base image or image for a service is pulled which requires credentials:
You can also save a file with secrets in JSON or YAML format in your home directory, under ~/.balena/secrets.<yml|json>, which will automatically be used for the secrets if it exists and the --registry-secrets switch has not been passed to balena push.
8. Building with Secrets and Variables
Building images occasionally requires the use of credentials (such as those for private repositories), or environment variables that may change depending on circumstances such as architecture (for example package versioning).
The following exercise sections show you how to use build-time secrets and variable substitution.
8.1 Build Time Secrets
Sometimes it is necessary to build images using secret information, commonly to login to source repositories or fetch data that is required for the building of an image, but which should not exist in that image when run as a service container.
Our builders allow you to do this by adding such secrets in files in a .balena directory in the root of the build directory. This allows them to be passed to builders, which will use (and then discard) them for generating images.
We'll make a few changes to the example project to show this in operation. First, create a .balena directory in the root of the balena-cli-masterclass directory, and then create an empty balena.yml file and create another directory called secrets in the .balena directory. You should now have a file tree that looks like this:
You can now add secrets to the build by adding a section to the balena.yml file and then creating appropriate secret files in the .balena/secrets directory. We'll add some now, open a text editor and fill the balena.yml file with the following:
Note that the source file should exist in the .balena/secrets directory, and that it is mapped into the my-secrets file when the image is built. Save the file, and create a new one called .balena/secrets/my-build-secrets and copy the following into it:
Finally, we'll add a line into our Dockerfile that uses the secrets file, which is mapped into the /run/secrets/ directory during build time:
Now, push the project to the builders:
As you can see, step 6 output:
which is the contents of the secrets file. This file could obviously contain a raft of different functions, including a script that gets executed, text for filling in files, etc. As well as defining globally accessible secrets (which are shared to all services being built), there is also the option to define secrets that are only accessible to particular services, or to map them to different paths. This becomes useful in multi-container build scenarios. This can be achieved by appending a services section to build-secrets on the balena.yml file. For example:
This change would map the .balena/secrets/main-only-secrets file into the /run/secrets/my-main-secrets runtime path at build-time but only for the main service.
8.2 Build Time Variables
Another frequent build-time use is that of environment variables that may alter between builds but still use the same flow of a Dockerfile. Allowing these to be defined dynamically means that no changes to a Dockerfile are required as long as the variables are referenced within them.
Much in a similar way to secrets files, these are defined in the .balena/balena.yml file. Add two new build-time arguments to your balena.yml file:
Now alter the Dockerfile to use them:
Note that you need to ensure that both arguments are declared using the ARG Docker command. Save the file and push to the builders:
As can be seen, both build arguments were available in the log output:
In the same way that build secrets can be made service specific, so may secret build arguments, by specifying them directly in the .balena/balena.yml file:
An important note for build variables is that, unlike secrets, they will be defined and available as part of the final image. If you need variables to be secret, they should be defined specifically as secrets.
Conclusion
In this masterclass, you've learned how to use the most commonly used balena CLI commands, as well as how to start development using it. You should now be familiar and confident enough to:
Create fleets for specific device types
Provision devices as well as SSH into balenaOS and any running service container
Push a release to fleets, either via
balena pushorgit pushLocally build service images on a development machine, as well as deploying those images to balenaCloud
Switch a development device into Local Mode, push code locally to a device to build, and debug the app faster.
Use Livepush to dynamically alter the app code on the fly and immediately see results on a device in Local Mode, as well as filter log output for specific services in the app.
Use build secret files and arguments to generate images which should not include those secrets, as well as build variables
References
None
Last updated
Was this helpful?