In device management, it is valuable to have the fleet of devices as consistent as possible. This helps in testing software releases and makes it easier to debug issues on remote devices.
If performing package based updates, one topic to consider is how to avoid drift between the initial device image and the state of the device fleet.
One additional consideration is that APT is very opinionated about which dependent package version to install when resolving dependencies. APT will always try to install the latest available package version that can be found in all source package repositories unless instructed differently using APT preferences.
A dependency package represents a desired state of a system, which is defined by a list of Debian packages with corresponding versions. All required packages are captured as dependencies in the control file of an otherwise shallow Debian package. APT will try to automatically resolve and install these dependencies when running apt install dependency-pack
.
This repository contains pipelines and scripts for generating the Debian dependency package and its optional pinning package that modifies the APT preferences to force the installation of the exact version of the dependent package.
The dependency package can be referred to when generating the initial device image and when issuing package updates to the device fleet. This way, the same desired state should be represented in both.
Dependency packages consist of a control
file defining the desired state by enumerating all dependencies in the Depends
section. Packages to be removed are listed under Conflicts
.
Package: dependency-pack
Version: 0.0.1
Architecture: amd64
Maintainer: YourName <YourName@YourCompany>
Depends: dependency-a (=1.2.3-1), dependency-b(=1.9.1-3)
Conflicts: package-to-remove (=1.1.1)
Description: Dependency Pack.
You can add a longer description here. Mind the space at the beginning of this paragraph.
Pinning packages consist of a plain control
file and an additional APT preference file that will be placed under etc/apt/preferences.d/
during their installation. Every dependent package results in an entry with the following structure:
Package: aziot-identity-service
Pin: version 1.2.4-1
Pin-Priority: 1001
The impact of APT preferences can be inspected by running apt-cache policy your-package-name
.
The package-history
folder contains a list of package snapshots. To enable the later removal of packages that come preinstalled with the base image, the contents of the latter need to be captured as the original state. In this sample repository, the original snapshot was named 0
. Later snapshots must be named incrementally according to the timestamp of their creation (c.f. script).
To create a new dependency/pinning package, a new release must be created. To do so:
- create a new file under
package-history
folder using the current value of$EPOCHSECONDS
as a file name - in the newly create file, put each package name and its pinned version on a new line, e.g.
package-name (=1.1.0-1)
- commit and push your changes
- create a new tag following the semantic versioning (e.g.
git tag v0.0.5
) - push the tag
git push --tags
, this will automatically trigger a GitHub workflow that will create a new release with the dependency and pinning package as assets.
Download the dependency package from a selected release. To install it, run:
sudo apt install ./dependency-pack.deb
If there is an error with unmet dependencies because APT would like to install a newer version of a dependent package, you have multiple options to force the installation of the pinned versions.
- Install the pinning package first
sudo apt install ./pinning-pack.deb
sudo apt install ./dependency-pack.deb
- Inspect the dependency list manually and install every dependent package explicitly
dpkg-deb -f ./dependency-pack.deb control Depends
sudo apt install ./dependency-pack.deb dependency-a=1.2.3-1 dependency-b=1.9.1-3
- Use private repositories as package sources and make sure they only contain packages that match the dependent packages' versions.
The sample Azure DevOps pipeline shows how to generate and publish dependency/pinning packages to Artifactory. It assumes that multiple distinct environments are targeted: DEV, TEST, PROD. For each of these environments, a distinct variable group needs to be preconfigured in Azure DevOps. Each of these groups is expected to contain the following variables:
- ARTIFACTORY-USERNAME: The username authorized to push packages to Artifactory
- ARTIFACTORY-PASSWORD: The password authenticating the ARTIFACTORY-USERNAME
- ARTIFACTORY-URL: The base URL of the targeted Artifactory instance, e.g. https://{your-instance}.jfrog.io/artifactory/{your-repository}
The TEST and PROD stages require the manual approval of an authorized Azure DevOps user before they are executed.
In order to allow the pipeline to push the tag to the GIT repository, make sure to allow the "Build Service" to contribute to the corresponding repository (AzDo->Project Settings->Repositories->{Your Repo}->Security->{* Build Service}->Contribute->Allow).
The sample GitHub workflow shows how to generate and publish dependency/pinning packages using the built-in GitHub Releases.