If you’re like most folks running an IoT environment, you have a mix of devices that don’t always support the same services and drivers, despite their common architecture. With balenaCloud container contracts, you can set rules that prevent devices from getting upgrades that make them fail -- and keep your entire fleet healthy.
With balenaCloud container contracts, you can set rules that tell balena to skip deploying a target release on specific devices that don’t meet your requirements, or make services optional if their requirements aren’t met, but still allow new app releases to deploy.
Figure 1: balenaCloud container contracts use a simple contract.yml file to set requirements for each service before it can be deployed, in this PiHole example dnscrypt-proxy and pihole. Here, the CURRENT RELEASE remains unchanged from the newly pushed TARGET RELEASE, which didn’t meet the requirements of the container contract.
Different boards, different needs
Take for example common NVIDIA boards, like the Jetson TX1 and D3 TX2, which use the Tegra Linux Driver Package (L4T) and base their settings on Ubuntu 16.04. If you try to update an application that relies on something newer on those NVIDIA boards in the same fleet as your Raspberry Pis, the apps running on the NVIDIA devices will break.
The balenaCloud container contract capability is supported on balena Supervisors >= 10.6.17 (balenaOS >= 2.46). For devices running the NVIDIA Tegra Linux Driver Package (L4T), contracts can be enforced on the L4T version. A complete list of boards that support L4T are available below.
Apply rules to each service separately
Contracts can be applied to all services or customized for each one in your application. The key is a
contract.yml file placed in each service directory alongside the service’s Dockerfile.template:
├── docker-compose.yml ├── first-service │ ├── contract.yml │ └── Dockerfile.template └── second-service ├── contract.yml └── Dockerfile.template
contract.yml file defines the type --
sw.l4t -- and a slug, name and the requirements themselves. In the following example, you can see how balenaCloud container contracts define multiple requirements, including a Supervisor version greater than or equal to 10.6.17, and an L4T version equal to 32.2.
type: "sw.container" slug: "enforce-supervisor-and-l4t" name: "Enforce supervisor and l4t requirements" requires: - type: "sw.supervisor" version: ">=10.6.17" - type: "sw.l4t" version: "32.2"
In this example, if the version type requirements aren’t met, the target release will fail and dashboard logs will show you what happened. Devices targeted by your balenaCloud application will remain unchanged and in their current release states.
A real-world example: Container contracts applied to PiHole services
For example, I deployed the ad-blocking application Pi-Hole on a Raspberry Pi 3 running version 2.46.1+rev1 of the balenaOS. The deployment has two services,
pihole. The application works perfectly and blocks ads on my network as designed.
By creating two new contract.yml files -- each with its own unique slug -- in each respective service directory, I instruct balenaCloud to require a Supervisor that’s greater than or equal to 12.6.17, which this Raspberry Pi 3 is not running:
type: "sw.container" slug: "Enforce-correct-supervisor-pihole" name: "Enforce correct supervisor for pihole" requires: - type: "sw.supervisor" version: ">=12.6.17"
Figure 2: The container contract requires a balenaOS Supervisor version greater than or equal to 12.6.17, a requirement not met by the target device.
When I run
balena push pihole, the build and upload continue normally, but a look at the balenaCloud dashboard logs reveals that though the container runs properly, the unmet container contract requirements prevented the container from being deployed:
Figure 3: The unmet Supervisor requirement prevented the newly pushed target version of PiHole from deploying. The previous running application remains unchanged.
Flag services as optional to deploy apps even if some containers don’t pass muster
Separately, you can use balenaCloud container contracts functionality in your
docker-compose.yml files to set a service as optional in cases when you want the application to deploy and you don’t mind if that service isn’t present.
io.balena.features.optional: '1' flag in a
docker-compose.yml service definition allows a service to have unmet requirements that will prevent it from deploying, but doesn’t prevent the build of all the valid services. The services with unmet requirements will build, but won’t release. This is particularly useful on multi-service containers, giving you the ability to fine-tune application deployments according to your production needs. In this example, the application will still deploy if “first-service” fails due to unmet container contract requirements, though only “second-service” will be running:
version: '2.1' services: first-service: build: ./first-service labels: io.balena.features.optional: '1' second-service: build: ./second-service
Container contracts are supported on all devices running balenaOS >= 2.46 (where the Supervisor version is >= 10.6.17). For L4T boards, If a device in the application does not support L4T, and the contract specifies an L4T requirement, the device supervisor will reject the release unless the contract is marked as optional. L4T support is available on the following boards:
- Aetina N510 TX2
- Auvidea J120 TX2
- CTI Orbitty TX2
- CTI Spacely TX2
- Nvidia blackboard TX2
- Nvidia D3 TX2
- Nvidia Jetson Nano
- Nvidia Jetson TX1
- Nvidia Jetson TX2
- Nvidia Jetson Xavier
- Nvidia L4T (32.2)
Learn more and give it a try
We’re working to include many more device parameters and components into the container contracts capabilities in the future, but you can begin experimenting today. Learn more about balenaCloud container contracts by reading this user guide.
Thanks for reading! If you have any questions, please let us know on our forums, on Twitter, or on Facebook.