This masterclass builds upon knowledge that has been taught in previous classes. To gain the most from this masterclass, we reommend that you first undertake the following masterclasses:
Masterclass Type: Core Maximum Expected Time To Complete:
The balena Command Line Interface (balenaCLI) utility consists of a number of commands that allow a user to develop, deploy and manage balena applications and devices.
The previous balenaCLI masterclass covered some of the most common techniques and topics related to balenaCLI. This masterclass aims to build on top of that, introducing you to additional features that can be used to gain finer control over the provisioning, deployment and management of devices.
The hardware and software requirements for the masterclass. This should be a list of these items, along with hyperlinks to relevant product pages or installation guides.
- A balena supported device, such as a balenaFin 1.1, Raspberry Pi 3 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 familiarity with Dockerfiles
- A NodeJS installation, including NPM. The use of
nvm
is recommended, which allows you to alter the version of Node/NPM being used per-user, and also removes the need to install global dependencies usingsudo
All of the following exercises assume that you are running the balenaCLI 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 execution of a command may be appended under the line to show
what might be returned. For example:
$ balena version
11.9.3
By default, the balenaCLI communicates with the production balenaCloud instance, using this environment to carry out operations such as application creation, code pushing, etc.
However, there are alternative environments available, such as balena's staging environment, where new service features are deployed and tested before being approved for the production environment, or a self-hosted openBalena environment.
There are a couple of ways to inform balenaCLI that it should use a different environment.
The easiest way to quickly ensure balenaCLI uses an alternative environment to
that of production is to use the BALENARC_BALENA_URL
environment variable.
In a terminal, execute the following command:
$ BALENARC_BALENA_URL=balena-staging.com balena login
_ _
| |__ __ _ | | ____ _ __ __ _
| '_ \ / _` || | / __ \| '_ \ / _` |
| |_) | (_) || || ___/| | | || (_) |
|_.__/ \__,_||_| \____/|_| |_| \__,_|
Logging in to balena-staging.com
? How would you like to login? (Use arrow keys)
❯ Web authorization (recommended)
Credentials
Authentication token
I don't have a balena account!
Just like the production server, each alternative environment will require a login to allow balenaCLI can operate. Login using your chosen method now.
Once logged in, successive uses of
BALENARC_BALENA_URL=balena-staging.com balena
will use the saved token to
use the alternative environment, for example:
$ BALENARC_BALENA_URL=balena-staging.io balena apps
ID APP NAME DEVICE TYPE ONLINE DEVICES DEVICE COUNT
12345 appOne intel-nuc 0 0
12346 appTwo iot2000 0 0
Should you just wish to use balenaCLI without specifying the environment to use in an environment variable, you can use a configuration file instead.
By default, balenaCLI looks for a configuration file in the user's home
directory. We can demonstrate configuring balenaCLI for an alternative
environment by creating a new file as ~/.balenarc.yml
and then filling
it with the following information:
balenaUrl: 'balena-staging.io'
Now try listing the apps from the staging environment again:
$ balena apps
ID APP NAME DEVICE TYPE ONLINE DEVICES DEVICE COUNT
12345 appOne intel-nuc 0 0
12346 appTwo iot2000 0 0
Two things have happened here. The first is that balenaCLI has found and used
the new balena environment URL from the ~/.balenarc.yml
configuration file.
The second is that it has used the token previously retrieved from logging in
to the staging environment using the environment variable.
Token files are saved separately and can be found in the user's home directory
here: ~/.balena/token
.
Usually when logging into an environment, the user's configuration file and token files are used to ensure successive commands use this information to authenticate the commands being executed.
However, there are operations where sometimes it is desirable to switch between environments (for example when testing new features available on staging but not production).
To enable this, balenaCLI includes the ability to use a configuration file in the current working directory (CWD) to determine which environment to use, as well as an environment variable to determine where to store the token and data.
However, when doing so, it merges together all configurations that it finds, with any files in the user's home directory taking precedence. To demonstrate using different configuration files for different environment, first remove the configuration files from your home directory so these do not override the configurations we will create:
$ rm ~/.balenarc.yml
$ rm -rf ~/.balena
Now, in your home directory, create two directories called balenaProduction
and balenaStaging
, and then fill in separate configuration files in each
directory. The following commands will do this for you:
$ mkdir -p ~/balenaProduction
$ mkdir -p ~/balenaStaging
$ echo "balenaUrl: 'balena-staging.io'" > ~/balenaStaging/balenarc.yml
$ echo "balenaUrl: 'balena-cloud.com'" > ~/balenaProduction/balenarc.yml
Note that we do not prefix the balenarc.yml
file with a .
to make it
hidden.
We can now use the BALENARC_DATA_DIRECTORY
to tell balenaCLI which directory
to store data such as the authentication token for the current environment.
Using this and then logging into the relevant service will store an
authentication token in the appropriate directory. Run the following in a
terminal to login to the production environment:
$ cd ~/balenaProduction
$ BALENARC_DATA_DIRECTORY=. balena login
_ _
| |__ __ _ | | ____ _ __ __ _
| '_ \ / _` || | / __ \| '_ \ / _` |
| |_) | (_) || || ___/| | | || (_) |
|_.__/ \__,_||_| \____/|_| |_| \__,_|
Logging in to balena-cloud.com
? How would you like to login? (Use arrow keys)
❯ Web authorization (recommended)
Credentials
Authentication token
I don't have a balena account!
Login using your preferred method. Note that a new token
file now exists in
the ~/balenaProduction
directory. Now login to the staging directory, using
the staging configuration directory:
$ cd ~/balenaStaging
$ BALENARC_DATA_DIRECTORY=. balena login
_ _
| |__ __ _ | | ____ _ __ __ _
| '_ \ / _` || | / __ \| '_ \ / _` |
| |_) | (_) || || ___/| | | || (_) |
|_.__/ \__,_||_| \____/|_| |_| \__,_|
Logging in to balena-staging.com
? How would you like to login? (Use arrow keys)
❯ Web authorization (recommended)
Credentials
Authentication token
I don't have a balena account!
This time, a token
file was created in the ~/balenaStaging
directory.
You can now switch between environments by changing directory to the one
with the relevant balenarc.yml
and token
file, which will allow you
to use either environment without any further authentication:
$ cd ~/balenaStaging
$ BALENARC_DATA_DIRECTORY=. balena apps
ID APP NAME DEVICE TYPE ONLINE DEVICES DEVICE COUNT
12345 appOne intel-nuc 0 0
12346 appTwo iot2000 0 0
$ cd ~/balenaProduction
$ BALENARC_DATA_DIRECTORY=. balena apps
ID APP NAME DEVICE TYPE ONLINE DEVICES DEVICE COUNT
54321 artik530 artik533s 0 0
54322 bob intel-nuc 0 0
54323 bbbtest beaglebone-black 0 0
We'll be using the two separate environments in the next set of exercises
to show you how devices can be moved between applications and environments.
As such, ensure you're in the ~/balenaProduction
directory before
proceeding:
$ cd ~/balenaProduction
Usually you'll provision a device that will exist on a particular balena application or environment, as the lifecycle of that device will only make sense within that application.
However, there are times where it is useful to be able to move a device either from one application to another (for example when a major rewrite of your application occurs that is no longer backwards compatible with a prior version) or from one environment to another (perhaps you've created a locally hosted test environment using openBalena and now want to move from your test environment to a the production environment of balenaCloud).
The following exercises will show you how to carry this out.
Moving a device to another application on the same environment is extremely easy. To demonstrate this, first create a new application for the balenaFin:
$ BALENARC_DATA_DIRECTORY=. balena app create
balena app create altApp --type fincm3
Application created: altApp (fincm3, id 987654)
We should already have a device connecting to our previous 'cliApp':
$ BALENARC_DATA_DIRECTORY=. balena devices
ID UUID DEVICE NAME DEVICE TYPE APPLICATION NAME STATUS IS ONLINE
1234556 7654321 restless-glade fincm3 cliApp Idle false 9.15.7 balenaOS 2.38.0+rev1 https://dashboard.balena-cloud.io/devices/1234567890abcdef/summary
To interactively determine which application to move a device to, simply use
its UUID with the balena device move
command:
$ BALENARC_DATA_DIRECTORY=. balena device move 7654321
? Select an application (Use arrow keys)
❯ altApp (fincm3)
As you can see, only applications of the same device type are available to
a device that is being moved. For non-interactive movement, simply pass the
optional --application
switch to the command with the relevant application
name:
$ BALENARC_DATA_DIRECTORY=. balena device move 94095f8 --application altApp
94095f8 was moved to altApp
A call of balena device
specifying the UUID of the moved device will now
show it is owned by the specified application:
$ BALENARC_DATA_DIRECTORY=. balena device 7654321
== WEATHERED WILDFLOWER
ID: 1693707
DEVICE TYPE: fincm3
STATUS: idle
IS ONLINE: true
IP ADDRESS: 192.168.1.171 192.168.1.169
APPLICATION NAME: altApp
UUID: 76543217654321765432176543217654
COMMIT: 7efbc95825641b6482742a54c8e74010
SUPERVISOR VERSION: 9.15.7
IS WEB ACCESSIBLE: false
OS VERSION: balenaOS 2.38.0+rev1
DASHBOARD URL: https://dashboard.balena-cloud.com/devices/76543217654321765432176543217654/summary
The Supervisor on the device will remove any previously running services, as well as their images and any volumes associated with them and download the images associated with the new application before starting them.
Moving a device between balena environments is slightly more involved, and alters depending on whether you're using a device running a development or production image.
For a device running a development image, you can use balena leave
and
balena join
to carry this out.
First, provision a device using a development image. You can do this using
the balenaCloud dashboard and downloading a 'Development' edition image from
the cliApp
application page. Provision your balenaFin with this image
using either balenaEtcher or balenaCLI.
Once the device is provisioned and has connected to the balena network,
discover its hostname or IP address by using balena devices
:
$ BALENARC_DATA_DIRECTORY=. balena devices
ID UUID DEVICE NAME DEVICE TYPE APPLICATION NAME STATUS IS ONLINE SUPERVISOR VERSION OS VERSION DASHBOARD URL
1234567 7654321 little-paper fincm3 cliApp Idle true 9.15.7 balenaOS 2.38.0+rev1 https://dashboard.balena-cloud.com/devices/76543217654321765432176543217654/summary
$ BALENARC_DATA_DIRECTORY=. balena device 7654321
== LITTLE PAPER
ID: 1234567
DEVICE TYPE: fincm3
STATUS: idle
IS ONLINE: true
IP ADDRESS: 192.168.1.171
APPLICATION NAME: cliApp
UUID: 76543217654321765432176543217654
COMMIT: 7efbc95825641b6482742a54c8e74010
SUPERVISOR VERSION: 9.15.7
IS WEB ACCESSIBLE: false
OS VERSION: balenaOS 2.38.0+rev1
DASHBOARD URL: https://dashboard.balena-cloud.com/devices/76543217654321765432176543217654/summary
As we now have the local IP address for it, we can use this to call the command to leave the balenaCloud environment:
$ BALENARC_DATA_DIRECTORY=. balena leave 192.168.1.171
[Success] Device successfully left the platform.
The device now becomes unmanaged. This means it acts in the same way as a device that has been provisioned with an unconfigured balenaOS image, for example one that has been downloaded from https://www.balena.io/os/#download.
We can now join a different balena environment by using balenaCLI to login to it. As we previously did this for our staging environment, we can simply use the data and tokens we saved for this by changing directories and using the other environment information:
$ cd ~/balenaStaging
We'll now create a new application on the staging environment to move the device to:
$ BALENARC_DATA_DIRECTORY=. balena app create stagingCliApp --type fincm3
Application created: stagingCliApp (fincm3, id 97654)
Finally, we'll issue a command to the now unmanaged device to join the staging
environment and the stagingCliApp
application:
$ BALENARC_DATA_DIRECTORY=. balena join 192.168.1.171 --application stagingCliApp
? Check for updates every X minutes 10
[Success] Device successfully joined balena-staging.com!
We can now check the devices on the staging environment to ensure it's joined successfully:
$ BALENARC_DATA_DIRECTORY=. balena apps
BALENARC_DATA_DIRECTORY=. balena devices
ID UUID DEVICE NAME DEVICE TYPE APPLICATION NAME STATUS IS ONLINE SUPERVISOR VERSION OS VERSION DASHBOARD URL
876542 3456789 purple-snowflake fincm3 stagingCliApp Idle true 9.15.7 balenaOS 2.38.0+rev1 https://dashboard.balena-staging.com/devices/3456789345678934567893456789/summary
As can be seen, it's been given a new name and UUID.
If we hadn't specified the application to join, we would have seen an interactive list of all the applications on the staging environment:
$ BALENARC_DATA_DIRECTORY=. balena join 192.168.1.171
? Select application
❯ heds/artik530
heds/orangepi
heds/stagingcliapp
heds/switchapp
heds/vpn-changed
Note that the applications listed are not all of the fincm3
device type, but
share the common armv7hf
architecture. Be very careful to select an
application of the same device type, or you may find issues when the device
attempt to run the application code for a device without the same peripherals or
system-on-chip layout.
THIS WILL NOT WORK FOR A PRODUCTION DEVICE, BECAUSE AS SOON AS YOU USE
os-config leave
YOU BECOME LOCKED OUT OF THE DEVICEos-config join
appears to accept a config.json, but using one from a downloaded provisioning image does not work, eg: root@12dc2e5:~# os-config join {"applicationName":"switchapp","applicationId":"96231","deviceType":"fincm3","userId":"299","username":"heds","appUpdatePollInterval":"600000","listenPort":"48484","vpnPort":"443","apiEndpoint":"https://api.balena-staging.com","vpnEndpoint":"vpn.balena-staging.com","registryEndpoint":"registry2.balena-staging.com","deltaEndpoint":"https://delta.balena-staging.com","pubnubSubscribeKey":"","pubnubPublishKey":"","mixpanelToken":"cb974f32bab01ecc1171937026774b18","apiKey":"Dfk7XWZFZRi52TVoAtmFwLAQP9cqDJSP"} error: Found argument 'applicationId:96231' which wasn't expected, or isn't valid in this context
USAGE:
os-config join <JSON_CONFIG>
balenaCLI offers balena leave
to disconnect a device from a balena environment
and balena join
to join a different target environment. However, this
currently only works on development devices due to an issue with the way that
balenaCLI carries this out internally. Because of this, the following exercise
offers a solution for production devices (as balenaCLI only currently allows
the download of production images), which will also work on development
devices. Future updates will move wholly to the balenaCLI methodology.
END <<<
In the previous balenaCLI masterclass, we provisioned an image by using the balenaCloud dashboard to download an image that could then be flashed to appropriate media (or directly to a device).
balenaCLI also allows you to do this, but includes some powerful functionality that allows the modification of images.
The following exercises will introduce this functionality and provide some examples of suitable use cases.
However, to start with, as we're not going to use separate environments again, we'll remove the previous setups for production and staging environments and move back to a single set of environment configuration files.
$ cd ~
$ rm -rf balenaStaging
$ rm -rf balenaProduction
$ balena login
Use your prefered login method to recreate a ~/.balenarc.yml
and
~/.balena/token
file in your home directory.
Downloading an image via balenaCLI requires you to specify the type of device the image downloaded should be suitable for, and optionally the OS version the image should use and output path.
We'll use the balenaCloud environment again, so first change back to the root repository for this masterclass and login to balenaCloud, for example:
$ cd ~/balena-cli-advanced-masterclass
$ balena login
Now we'll download the latest balenaOS image for the balenaFin, and determine the filename that it will be saved as:
$ balena os download fincm3 --version 2.38.0+rev1 --output balena-fin-image.img
Notice that we've passed the --version
switch to the command, which tells
balenaCLI to download a specific version of balenaOS for the device. The
parameter can take a variety of forms such as a specific version, a version
greater or equal to that given, etc. See the full range of options available by
using balena os download --help
. If we had not specified a version, then
the latest version of balenaOS for the device type would have been downloaded.
A downloaded balenaOS image via balenaCLI is unconfigured, so to allow a device to use it as a provisoining image we need to specify, at a miniumum, which application the device should be associated with.
There are a few ways to achieve this. The simplest is to configure it interactively, passing either an application name or device UUID so that the relevant application can be determined:
$ balena os configure balena-fin-image.img --app cliApp
Configuring operating system image
? Network Connection (Use arrow keys)
❯ ethernet
wifi
Should you select wifi
, then you'll be asked to enter the wireless SSID (and
credentials for that network) that a device should connect to once booted.
The downloading image will now be configured with the AP details and you can
THE FOLLOWING SECTION DOES NOT WORK AND NEEDS CLARIFICATION ON HOW IT SHOULD WORK
Generate config including networking details for Wifi, and then inject into image.
BUGGED SECTION ENDS
balena are the authors of balenaEtcher which has fast become the preferred way for millions of users across the world to write OS images to different media. However, there are times where non-interactively writing an image is desired, especially in a test or manufacturing environment, where device media on a large number of devices need writing without any interactive involvement.
Luckily, balenaCLI includes functionality to write a provisioning image to any attached, valid media that is exposed as a drive on the host machine.
THE FOLLOWING SECTION DOES NOT WORK DUE TO A BUG
We'll use the previously configured balenaFin image as our provisioning image. Connect a balenaFin for flashing to your development machine (do this by attaching a microUSB cable to your machine from the balenaFin, and then powering up the Fin). Alternatively, insert an SD card/USB stick/etc. into your development machine if using a different device type. This will expose a device node or mount point, referred to in balenaCLI as a 'drive', depending on the OS you're running. You can discover which drive has been assigned by running the following:
$ balena util available-drives
DEVICE SIZE DESCRIPTION
/dev/disk2 1000.2 GB Seagate BUP Slim BL Media
/dev/disk4 63.6 GB Compute Module
As you can see, the balenaFin is attached to drive /dev/disk4
. We can now use
another balenaCLI command to write the image to that drive, which will provision
the device:
$ balena os initialize balena-fin-image.img --type fincm3 --drive /dev/disk3 --yes
...
Note that you must supply the device type and the drive to use to write to.
The --yes
switch indicates we do not want an interactive prompt confirming
we want to write to the drive (else a warning that you will wipe whatever is
on that drive and a confirmation prompt is given).
Once provisioned, reset the balenaFin by powering it off, removing the USB cable from the host machine and then repowering the Fin. Shortly afterwards, it will connect to balenaCloud:
$ balena devices
ID UUID DEVICE NAME DEVICE TYPE APPLICATION NAME STATUS IS ONLINE
1234556 7654321 restless-glade fincm3 cliApp Idle false 9.15.7 balenaOS 2.38.0+rev1 https://dashboard.balena-cloud.io/devices/1234567890abcdef/summary
BUGGED SECTION END
Devices using balenaOS allow the use of dynamic environment variables which may be updated remotely, altering the behaviour of a service container.
For example, suppose that we had a production device out in the field, which is not behaving as expected. We may want to enable extra debugging by setting an environment variable dynamically that will get picked up by the device and start verbose logging.
We'll start by pushing code to our balenaFin, reusing our previous cliApp
application but using the code from this masterclass:
$ balena push cliApp
balena push cliApp --nocache
[Info] Starting build for cliApp, user heds
[Info] Dashboard link: https://dashboard.balena-cloud.com/apps/1234567/devices
[Info] Building on arm01
[Info] Pulling previous images for caching purposes...
[Success] Successfully pulled cache images
[main] Step 1/6 : FROM balenalib/fincm3-node:8
[main] ---> 43d29490f34a
[main] Step 2/6 : WORKDIR /usr/src/app
[main] ---> Running in e1e643eb590c
[main] Removing intermediate container e1e643eb590c
[main] ---> a1bf0010640e
[main] Step 3/6 : COPY package.json package-lock.json ./
[main] ---> 47b9589ea63e
[main] Step 4/6 : RUN npm install --ci --production && npm cache clean --force && rm -f /tmp/*
[main] ---> Running in 4d780139b2de
[main] up to date in 0.789s
[main] found 0 vulnerabilities
[main] npm
[main]
[main] WARN
[main] using --force I sure hope you know what you are doing.
[main]
[main] Removing intermediate container 4d780139b2de
[main] ---> c699891127f9
[main] Step 5/6 : COPY . ./
[main] ---> 5281bad5c22c
[main] Step 6/6 : CMD ["npm", "start"]
[main] ---> Running in fe8c19c23718
[main] Removing intermediate container fe8c19c23718
[main] ---> 876b82c76b73
[main] Successfully built 876b82c76b73
[Info] Uploading images
[Success] Successfully uploaded images
[Info] Built on arm01
[Success] Release successfully created!
[Info] Release: 43260e126fc78d14d9edb1dbede38e61 (id: 1068171)
[Info] ┌─────────┬────────────┬────────────┐
[Info] │ Service │ Image Size │ Build Time │
[Info] ├─────────┼────────────┼────────────┤
[Info] │ main │ 211.82 MB │ 5 seconds │
[Info] └─────────┴────────────┴────────────┘
[Info] Build finished in 22 seconds
\
\
\\
\\
>\/7
_.-(6' \
(=___._/` \
) \ |
/ / |
/ > /
j < _\
_.-' : ``.
\ r=._\ `.
<`\\_ \ .`-.
\ r-7 `-. ._ ' . `\
\`, `-.`7 7) )
\/ \| \' / `-._
|| .'
\\ (
>\ >
,.-' >.'
<.'_.''
<'
Once built and pushed to the balenaFin, you should see the following logs:
$ balena logs 1234567
[Logs] [9/17/2019, 10:55:21 AM] Installing service 'main sha256:4e11240a66e7e86f38f0ccf9a806a079c4181d446bc026d6b62b83981781737b'
[Logs] [9/17/2019, 10:55:21 AM] Installed service 'main sha256:4e11240a66e7e86f38f0ccf9a806a079c4181d446bc026d6b62b83981781737b'
[Logs] [9/17/2019, 10:55:21 AM] Starting service 'main sha256:4e11240a66e7e86f38f0ccf9a806a079c4181d446bc026d6b62b83981781737b'
[Logs] [9/17/2019, 10:55:23 AM] Started service 'main sha256:4e11240a66e7e86f38f0ccf9a806a079c4181d446bc026d6b62b83981781737b'
[Logs] [9/17/2019, 10:55:24 AM] [main]
[Logs] [9/17/2019, 10:55:24 AM] [main] > balena-cli-advanced-masterclass@1.0.0 start /usr/src/app
[Logs] [9/17/2019, 10:55:24 AM] [main] > node src/helloworld.js; sleep infinity
[Logs] [9/17/2019, 10:55:24 AM] [main]
[Logs] [9/17/2019, 10:55:24 AM] [main] ---> LOG_DEBUG not set, no debug mode
[Logs] [9/17/2019, 10:55:24 AM] [main] Hello world!
As you can see, it looks like if we set the LOG_DEBUG
environment variable,
we'll get some debug logging. So let's use balena env
to do this:
$ balena env add LOG_DEBUG true --application cliApp
Now go to the dashboard for the cliApp
application, and select 'Environment
Variables'. 'Add' a new variable, called DASH_VAR
and set it to from-dash
.
To verify we've now set our variables, let's use balena envs
which will show
all the environment variables set for our application:
$ balena envs --application cliApp
ID NAME VALUE
123456 DASH_VAR from-dash
654321 LOG_DEBUG true
As you can see, both variables now show up in our list. However, if we now list the logs for the balenaFin, we'll see something else:
$ balena logs 1234567
[Logs] [9/17/2019, 10:55:24 AM] [main] Hello world!
[Logs] [9/17/2019, 10:57:34 AM] Killing service 'main sha256:4e11240a66e7e86f38f0ccf9a806a079c4181d446bc026d6b62b83981781737b'
[Logs] [9/17/2019, 10:57:35 AM] Service exited 'main sha256:4e11240a66e7e86f38f0ccf9a806a079c4181d446bc026d6b62b83981781737b'
[Logs] [9/17/2019, 10:57:35 AM] Killed service 'main sha256:4e11240a66e7e86f38f0ccf9a806a079c4181d446bc026d6b62b83981781737b'
[Logs] [9/17/2019, 10:57:35 AM] Installing service 'main sha256:4e11240a66e7e86f38f0ccf9a806a079c4181d446bc026d6b62b83981781737b'
[Logs] [9/17/2019, 10:57:36 AM] Installed service 'main sha256:4e11240a66e7e86f38f0ccf9a806a079c4181d446bc026d6b62b83981781737b'
[Logs] [9/17/2019, 10:57:36 AM] Starting service 'main sha256:4e11240a66e7e86f38f0ccf9a806a079c4181d446bc026d6b62b83981781737b'
[Logs] [9/17/2019, 10:57:37 AM] Started service 'main sha256:4e11240a66e7e86f38f0ccf9a806a079c4181d446bc026d6b62b83981781737b'
[Logs] [9/17/2019, 10:57:39 AM] [main]
[Logs] [9/17/2019, 10:57:39 AM] [main] > balena-cli-advanced-masterclass@1.0.0 start /usr/src/app
[Logs] [9/17/2019, 10:57:39 AM] [main] > node src/helloworld.js; sleep infinity
[Logs] [9/17/2019, 10:57:39 AM] [main]
[Logs] [9/17/2019, 10:57:39 AM] [main] ---> This is debug mode!
[Logs] [9/17/2019, 10:57:39 AM] [main] Hello world!
When we set the environment variable, the Supervisor on the device noted that
the value had been added and restarted the service container to ensure the
new environment variable was set, as a result we now see the extra debug log
text [main] ---> This is debug mode!
.
This also occurs whenever the value of an environment variable is changed,
or the environment variable is deleted. We'll try this now, by specifying the
ID of the variable we want to remove:
$ balena env rm 654321 --yes
$ balena envs --application cliApp
ID NAME VALUE
123456 DASH_VAR from-dash
Note that we used the --yes
switch to force the deletion of the variable.
Without this, we would have been asked interactively to confirm the deletion.
Now, let's take a final look at the logs again:
$ balena logs 1234567
[Logs] [9/17/2019, 11:06:37 AM] Killing service 'main sha256:4e11240a66e7e86f38f0ccf9a806a079c4181d446bc026d6b62b83981781737b'
[Logs] [9/17/2019, 11:06:38 AM] Service exited 'main sha256:4e11240a66e7e86f38f0ccf9a806a079c4181d446bc026d6b62b83981781737b'
[Logs] [9/17/2019, 11:06:38 AM] Killed service 'main sha256:4e11240a66e7e86f38f0ccf9a806a079c4181d446bc026d6b62b83981781737b'
[Logs] [9/17/2019, 11:06:38 AM] Installing service 'main sha256:4e11240a66e7e86f38f0ccf9a806a079c4181d446bc026d6b62b83981781737b'
[Logs] [9/17/2019, 11:06:39 AM] Installed service 'main sha256:4e11240a66e7e86f38f0ccf9a806a079c4181d446bc026d6b62b83981781737b'
[Logs] [9/17/2019, 11:06:39 AM] Starting service 'main sha256:4e11240a66e7e86f38f0ccf9a806a079c4181d446bc026d6b62b83981781737b'
[Logs] [9/17/2019, 11:06:40 AM] Started service 'main sha256:4e11240a66e7e86f38f0ccf9a806a079c4181d446bc026d6b62b83981781737b'
[Logs] [9/17/2019, 11:06:42 AM] [main]
[Logs] [9/17/2019, 11:06:42 AM] [main] > balena-cli-advanced-masterclass@1.0.0 start /usr/src/app
[Logs] [9/17/2019, 11:06:42 AM] [main] > node src/helloworld.js; sleep infinity
[Logs] [9/17/2019, 11:06:42 AM] [main]
[Logs] [9/17/2019, 11:06:42 AM] [main] ---> LOG_DEBUG not set, no debug mode
[Logs] [9/17/2019, 11:06:42 AM] [main] Hello world!
We're back to no debug logs again!
The balenaCLI also allows you to rename environment variables, as well as set
and remove them for specific devices instead of an entire application fleet.
For device-based variables, most commands take an extra switch, --device
which allows you to specify the UUID of the device you wish to make the
variable change for.
Whilst for many cases provisoining a device, moving it to an installation location and then connecting it to a stable network with Internet access is fairly easy, there are times where a device may have to operate with a limited connection (for example a slow GSM connection). In these cases, installing a device and then attempting to download an initial application is not only slow but sometimes unachievable.
The way round this is to preload an application into an image that is then used to provision a device. Preloading injects the images required for the services that comprise the application, meaning that on device startup the Supervisor can immediately start running those services without having to first download the images from the balena registries.
Preloading on it's own is a useful feature for ensuring devices are ready to start executing an application as soon as they're powered on, but for users who also want to ensure that they know about the devices that they're shipping it's only half the story.
Preregistering a device allows the creation of individual registered to an application before those devices are ever physically powered on or connected to a network. This is extremely useful in situations such as manufacturing where a device may require tracking, as a specific device UUID can then be associated with a specific customer order, for example. This ensures that a customer then receives a device that already has information available for it, for support, application updates, etc.
The following exercises will show you how to both preload and preregister devices.
Note that balena preload
actually uses a Docker container to carry out the
actual preloading. This is to allow the system to be portable and run under
Linux, macOS and Windows. As such, you'll need to ensure you've installed
Docker on your development machine before going any further.
We'll take the current cliApp
application and preload it the
balena-fin-image.img
image we earlier configured and provisioned our device
with. The balena preload
command has a large number of switch options for
catering with different situations, including the ability to use a particular
release. However, for this exercise, we'll simply use the latest version of
the code we previously pushed to cliApp
. In a terminal, execute the following:
NOTE THAT THIS DOESN'T WORK ON MACOS, AS WE SEE AN ERROR WHEN ATTEMPTING TO BIND THE IMAGE TO THE PRELOAD DOCKER CONTAINE
$ balena preload balena-fin-image.img --app cliApp --commit latest
Building Docker preloader image. [========================] 100%
/ Creating preloader container
\ Starting preloader container
- Fetching application cliApp
| Reading image information
/ Resizing partitions and waiting for dockerd to start
Pulling 1 image [========================] 100%
\ Cleaning up temporary files
If we hadn't specified the latest
commit, we'd have been given an interactive
list of all of them to select from.
Now provision your balenaFin using the resulting image using
balena os initialize
or balenaEtcher. Once booted, acquire the UUID for the
device (eg. 1234567
) and then look at the logs for that device:
$ balena logs 1234567
[Logs] [9/18/2019, 1:11:42 PM] Supervisor starting
[Logs] [9/18/2019, 1:11:43 PM] Applying configuration change {"SUPERVISOR_POLL_INTERVAL":"900000"}
[Logs] [9/18/2019, 1:11:44 PM] Applied configuration change {"SUPERVISOR_POLL_INTERVAL":"900000"}
[Logs] [9/18/2019, 1:11:45 PM] Creating network 'default'
[Logs] [9/18/2019, 1:11:45 PM] Creating volume 'resin-data'
[Logs] [9/18/2019, 1:11:45 PM] Installing service 'main sha256:4e11240a66e7e86f38f0ccf9a806a079c4181d446bc026d6b62b83981781737b'
[Logs] [9/18/2019, 1:11:46 PM] Installed service 'main sha256:4e11240a66e7e86f38f0ccf9a806a079c4181d446bc026d6b62b83981781737b'
[Logs] [9/18/2019, 1:11:46 PM] Starting service 'main sha256:4e11240a66e7e86f38f0ccf9a806a079c4181d446bc026d6b62b83981781737b'
[Logs] [9/18/2019, 1:11:47 PM] Started service 'main sha256:4e11240a66e7e86f38f0ccf9a806a079c4181d446bc026d6b62b83981781737b'
[Logs] [9/18/2019, 1:11:50 PM] [main]
[Logs] [9/18/2019, 1:11:50 PM] [main] > balena-cli-advanced-masterclass@1.0.0 start /usr/src/app
[Logs] [9/18/2019, 1:11:50 PM] [main] > node src/helloworld.js; sleep infinity
[Logs] [9/18/2019, 1:11:50 PM] [main]
[Logs] [9/18/2019, 1:11:51 PM] [main] ---> LOG_DEBUG not set, no debug mode
[Logs] [9/18/2019, 1:11:51 PM] [main] Hello world!
As can be seen, no download occured for the device, but the Supervisor immediately started the preloaded application.
balena preload
has a wealth of switches, and can modify the device
significantly. It's well worth familiarising yourself with the options
here.
The preregistering of a device involves a simple call with a unique identifier for the device. This identifier must be made up of hex characters and must be either 32 or 62 characters in length.
We'll generate a unique key for the device first, which we'll then use to
modify the config.json
in our balena-fin-image.img
before writing this
image to the balenaFin.
We'll verify that we're registering a device by first listing the devices
currently associated with the cliApp
application:
$ balena devices --application cliTest
ID UUID DEVICE NAME DEVICE TYPE APPLICATION NAME STATUS IS ONLINE SUPERVISOR VERSION OS VERSION DASHBOARD URL
1696632 f1dd777 empty-sun fincm3 cliTest Idle false 10.2.2 balenaOS 2.41.0+rev4 https://dashboard.balena-cloud.com/devices/12345678901234567890123456789012/summary
First, generate a key. The hexdump
utility is fairly ubiquitous between
both Linux and macOS (and in Windows is in most distributions compatible with
Windows Subsystem for Linux), and this will allow us to create a suitable key:
$ hexdump -n 32 -e '"%0x"' /dev/random | head -c 32 && printf '\n'
6053dab8dc4721ed288c8dfc79e52967
This gives us a random, 32 character UUID which we can now use to register a device that (hopefully) doesn't yet exist. In your terminal, execute the following:
$ balena device register cliApp --uuid 6053dab8dc4721ed288c8dfc79e52967
Registering to cliApp: 6053dab8dc4721ed288c8dfc79e52967
Note that if we had not used the --uuid
switch, then a random UUID would have
been generated and reported back to us.
Of course, our method of UUID generation, even though using /dev/random
, is
in no way guaranteed to be unique, so what if we regenerate the same UUID in
another run? The API knows that each UUID must be unique, and if we were to try
and register a UUID that already exists, it would be rejected. We can try this
now:
$ balena device register cliApp --uuid 6053dab8dc4721ed288c8dfc79e52967
BalenaRequestError: Request error: "uuid" must be unique.
Now we'll look at the device list for the application again:
$ balena devices --application cliApp
ID UUID DEVICE NAME DEVICE TYPE APPLICATION NAME STATUS IS ONLINE SUPERVISOR VERSION OS VERSION DASHBOARD URL
1696632 f1dd777 empty-sun fincm3 cliTest Idle false 10.2.2 balenaOS 2.41.0+rev4 https://dashboard.balena-cloud.com/devices/12345678901234567890123456789012/summary
1699866 93b40bc late-sunset raspberrypi4-64 pi4test false https://dashboard.balena-cloud.com/devices/93b40bc440cfccc6d45fa8db4a777a06/summary
As you can see, we now have a newly registered device that has never been powered on.
The next step is to generate a configuration file that includes the UUID we've just generated, inject it into the preloaded device image and then provision a device with it, so that device uses the same UUID.
We'll use the command to generate a configuration as mentioned in a previous exercise:
$ balena config generate --app cliApp --version 2.41.0.+rev4 --device 6053dab8dc4721ed288c8dfc79e52967 --network ethernet --appUpdatePollInterval 10 --output config.json
Note the extra switch option, --device
, which allows us to pass our
preregistered device UUID. Part of the configuration generation will assign
a new internal ID for the device, and produce a configuration that can now
be used by a device.
Now inject the configuration into the image:
$ balena os configure balena-fin-image.img --config config.json
Finally write it to the balenaFin, either using balena os initialize
or
balena Etcher.
After it's been provisoned, power up the balenaFin. You'll soon see our preregistered device come online:
$ balena devices --application cliApp
ID UUID DEVICE NAME DEVICE TYPE APPLICATION NAME STATUS IS ONLINE SUPERVISOR VERSION OS VERSION DASHBOARD URL
1696632 f1dd777 empty-sun fincm3 cliTest Idle false 10.2.2 balenaOS 2.41.0+rev4 https://dashboard.balena-cloud.com/devices/12345678901234567890123456789012/summary
1699866 93b40bc late-sunset fincm3 cliApp Idle true 10.2.2 balenaOS 2.41.0+rev4 https://dashboard.balena-cloud.com/devices/93b40bc440cfccc6d45fa8db4a777a06/summary
As before, the application will also start immediately as it was preloaded.
As balena has an active Operating System team that is constantly adding new features and ensuring issues are resolved, it is common to see new versions of balenaOS be released. Host updates are available to any device running an outdated version of balenaOS and this functionality is also available via balenaCLI (although currently this is available only for Production images). This allows fleet owners to upgrade to a newer version of the OS as fixes for issues or features they require for their application once they're available.
First, download an old version of a provisioning image for the balenaFin,
for example v2.32.0+rev1 (Production, not Development), and then provision
your balenaFin with it (either via balenaEtcher of balena os initialize
).
Once the device is online and connected to the balenaCloud infrastructure, verify this with:
$ balena device --application cliApp
ID UUID DEVICE NAME DEVICE TYPE APPLICATION NAME STATUS IS ONLINE SUPERVISOR VERSION OS VERSION DASHBOARD URL
1696632 f1dd777 empty-sun fincm3 cliTest Idle false 9.14.0 balenaOS 2.32.0+rev1 https://dashboard.balena-cloud.com/devices/12345678901234567890123456789012/summary
We'll now update the device to the latest version of balenaOS. We can do this using the following command:
$ balena device os-update f1dd777 --version 2.38.0+rev1.prod --yes
Updating the OS of empty-sun to v2.38.0+rev1.prod [====================== ] 90%
The device empty-sun has been updated to v2.38.0+rev1.prod and will restart shortly!
If you look at the device in the balenaCloud dashboard whilst the command is executing, you'll see the progress as if you'd run the update from there (and obviously the balenaCLI command also shows you the current progress).
If we hadn't passed the --version
switch, balenaCLI would have asked us to
choose the version we wanted to update the device with. Additionally, the
--yes
switch ensures we are not interactively asked to confirm the update.
Finally, let's run balena device
again to see the new version of the device:
$ balena device --application cliApp
ID UUID DEVICE NAME DEVICE TYPE APPLICATION NAME STATUS IS ONLINE SUPERVISOR VERSION OS VERSION DASHBOARD URL
1696632 f1dd777 empty-sun fincm3 cliTest Idle false 9.15.7 balenaOS 2.38.0+rev1 https://dashboard.balena-cloud.com/devices/12345678901234567890123456789012/summary
In this masterclass, you've learnt how to use some of the more advanced functionality that balenaCLI offers. You should now be familiar and confident enough with balenaCLI to:
- Switch between balena environments, as well as move devices between them as well as different applications
- Download, configure and provision device images using balenaCLI
- Modify environment and configuration variables, both application and device based
- Preregister a device with a balena environment, as well as preload a device provisioning image with a required version of an application for 'instant startup'
- Update a device from one version of balenaOS to another via balenaCLI
TBD