😸

Menderで始める組み込みOTA 第7回 : Quickstart Part6 : コンテナアップデートを実行

2022/06/26に公開

MenderはオープンソースベースのLinux向けOTA(Over the Air Update)ソリューションです。

Mender is a secure, risk tolerant and efficient over-the-air update manager. Remotely manage and deploy software updates to your IoT devices at scale, worldwide.

https://mender.io/


Menderで始める組み込みOTA」記事インデックス

はじめに

今回はDocker用コンテナイメージ更新情報を含むアップデートファイル (ARTIFACT)を自分で作成し、SaaSの mender.io に接続中のRaspberry Pi 3/4 デバイスに対してこれを配布します。基本的な流れは docs.mender.io にある「Deploy a container update」の内容と同じです。

今回も前提として、これまでの回で実行した mender.io との接続 (第2回第3回)、アプリケーションアップデート (第4回第5回) が完了していることが必要です。

作業用PCとしてLinuxのシェルが利用可能でかつDockerイメージの操作(docker pull等)が可能な環境が必要です。いわゆるLinux PCではもちろん実行可能ですが、Windows10/11 のWSL2も利用可能です。

Dockerについて

DockerはLinux環境で コンテナ仮想化 を実現するためのソフトウエア、またはエコシステムの名前です。Linux環境では仮想化を実現するためにcgroupsという機能がカーネルに実装されており、この機能を利用して仮想環境を作成する事ができます。Dockerはcgroupsを利用して実現されているコンテナ仮想化ソフトウエアの一つです。(このほかのコンテナ仮想化には LXC と呼ばれるものもあります)

https://www.docker.com/

Dockerの特徴は以下のようなものです。

  • ホストOSとゲストOSが同じLinuxカーネルバイナリを利用する (つまり、カーネルに組み込まれているデバイスドライバは双方の環境で同じ)
  • ユーザーランドはホストOSとおなじものではなく独自のもの(別のディストリビューションでもよい)を利用する。これが Dockerイメージ と呼ばれる。
  • Dockerイメージは複数のレイヤーの集まりとして保存されている。同一のレイヤーが複数のイメージに含まれている場合、レイヤーは一回だけ保存される。
  • Dockerイメージは通常、単一のビルドスクリプト Dockerfile で作成する。これにより、ディスクイメージではなくソースコードでイメージ設定を保存出来る。また、OSアップデート更新時にはDockerfileを利用して再度イメージをビルドするだけで反映される。
  • ゲストOSからホストOSのファイルシステムのマウントが可能
  • ゲストOSではホストOSとは独立したネットワークを利用可能、ホストOSや複数のコンテナ間を接続するローカルネットワークを構築できる
  • 起動が速いため、(ビルドなど)単一コマンドのみを実行しすぐに終了するような使い方もされる。

インターネット上にはDockerイメージを保存するデータベース、Docker HUBが用意されており、公開・非公開のDockerイメージが多数保存されています。これにより一種のエコシステムが実現され、ユーザーは多数の公開Dockerイメージを自由に利用してシステムを構築することができます。

コンテナ仮想化はLinuxだけではなくWindowsやMacOSでも利用可能ですが、このDocker Hubの仕組みにより多数のDockerイメージ(これらはすべてLinuxが前提)が公開された結果、WindowsやMacOSでもそのままDocker HUBにあるLinux向けのイメージを利用するための仕組みが整備されました。

Linuxの場合、ディストリビューションごとにインストール方法が用意されています。
Generic installation steps

Windowsについては以下
WindowsにDocker Desktopをインストール

MacOSについては以下
MacにDocker Desktopをインストール

を参照ください。

実行環境のRaspberry Pi にDockerエンジンをインストール

以下の手順でターゲットのRaspberry Pi 3/4 に dockerエンジンをインストールします。インターネット接続が必要です。

  • もし jq をインストールしていなければ、以下の手順で jq をインストールします。
sudo apt install jq
  • Dockerをインストールするためのスクリプトをダウンロードします。
curl -fsSL https://get.docker.com -o get-docker.sh
  • スクリプトを実行してインストールします。 (ダウンロード速度次第ですが、10分程度かかります)
sudo sh get-docker.sh

インストールが出来たら、いくつかのコマンドを実行してみます。

docker ps
docker image ls

など。

Windows10からsshでRaspberry Pi 3に接続してやってみると以下のような感じです。

Microsoft Windows [Version 10.0.19044.1766]
(c) Microsoft Corporation. All rights reserved.

C:\Users\hiron>ping raspberrypi.local

raspberrypi.local [2409:10:c860:1200:609e:bef7:a81b:6f4e]に ping を送信しています 32 バイトのデータ:
2409:10:c860:1200:609e:bef7:a81b:6f4e からの応答: 時間 =200ms
2409:10:c860:1200:609e:bef7:a81b:6f4e からの応答: 時間 =2ms

2409:10:c860:1200:609e:bef7:a81b:6f4e の ping 統計:
    パケット数: 送信 = 2、受信 = 2、損失 = 0 (0% の損失)、
ラウンド トリップの概算時間 (ミリ秒):
    最小 = 2ms、最大 = 200ms、平均 = 101ms
Ctrl+C
^C
C:\Users\hiron>ping -4 raspberrypi.local

raspberrypi.local [192.168.5.18]に ping を送信しています 32 バイトのデータ:
192.168.5.18 からの応答: バイト数 =32 時間 =99ms TTL=64
192.168.5.18 からの応答: バイト数 =32 時間 =2ms TTL=64
192.168.5.18 からの応答: バイト数 =32 時間 =2ms TTL=64
192.168.5.18 からの応答: バイト数 =32 時間 =2ms TTL=64

192.168.5.18 の ping 統計:
    パケット数: 送信 = 4、受信 = 4、損失 = 0 (0% の損失)、
ラウンド トリップの概算時間 (ミリ秒):
    最小 = 2ms、最大 = 99ms、平均 = 26ms

C:\Users\hiron>ssh 192.168.5.18 -l pi
The authenticity of host '192.168.5.18 (192.168.5.18)' can't be established.
ECDSA key fingerprint is SHA256:tceZYhKKlvkodYJOgDWH4URYUZ+2K5IkKDpxpqZmR60.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes  ← 「yes」を入力
Warning: Permanently added '192.168.5.18' (ECDSA) to the list of known hosts.
pi@192.168.5.18's password:  ←piユーザーのパスワードを入力
Linux raspberrypi 5.10.63-v7+ #1459 SMP Wed Oct 6 16:41:10 BST 2021 armv7l

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Mon Jun 13 14:01:21 2022 from 192.168.5.20

SSH is enabled and the default password for the 'pi' user has not been changed.
This is a security risk - please login as the 'pi' user and type 'passwd' to set a new password.


Wi-Fi is currently blocked by rfkill.
Use raspi-config to set the country before use.

pi@raspberrypi:~ $ which jq
/usr/bin/jq   ← 「jq」が既に存在しているので、そのまま実行
pi@raspberrypi:~ $ ls
mender-monitor-demo_latest-1_all.deb  mender-monitor_latest-1_all.deb
pi@raspberrypi:~ $ curl -fsSL https://get.docker.com -o get-docker.sh
pi@raspberrypi:~ $ ls
get-docker.sh  mender-monitor-demo_latest-1_all.deb  mender-monitor_latest-1_all.deb
pi@raspberrypi:~ $ sudo sh get-docker.sh
# Executing docker install script, commit: b2e29ef7a9a89840d2333637f7d1900a83e7153f
+ sh -c apt-get update -qq >/dev/null
+ sh -c DEBIAN_FRONTEND=noninteractive apt-get install -y -qq apt-transport-https ca-certificates curl >/dev/null
+ sh -c mkdir -p /etc/apt/keyrings && chmod -R 0755 /etc/apt/keyrings
+ sh -c curl -fsSL "https://download.docker.com/linux/raspbian/gpg" | gpg --dearmor --yes -o /etc/apt/keyrings/docker.gpg
+ sh -c chmod a+r /etc/apt/keyrings/docker.gpg
+ sh -c echo "deb [arch=armhf signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/raspbian bullseye stable" > /etc/apt/sources.list.d/docker.list
+ sh -c apt-get update -qq >/dev/null
+ sh -c DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends docker-ce docker-ce-cli containerd.io docker-compose-plugin >/dev/null
+ version_gte 20.10
+ [ -z  ]
+ return 0
+ sh -c DEBIAN_FRONTEND=noninteractive apt-get install -y -qq docker-ce-rootless-extras >/dev/null
+ sh -c docker version
Client: Docker Engine - Community
 Version:           20.10.17
 API version:       1.41
 Go version:        go1.17.11
 Git commit:        100c701
 Built:             Mon Jun  6 23:03:29 2022
 OS/Arch:           linux/arm
 Context:           default
 Experimental:      true

Server: Docker Engine - Community
 Engine:
  Version:          20.10.17
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.17.11
  Git commit:       a89b842
  Built:            Mon Jun  6 23:01:31 2022
  OS/Arch:          linux/arm
  Experimental:     false
 containerd:
  Version:          1.6.6
  GitCommit:        10c12954828e7c7c9b6e0ea9b0c02b01407d3ae1
 runc:
  Version:          1.1.2
  GitCommit:        v1.1.2-0-ga916309
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0

================================================================================

To run Docker as a non-privileged user, consider setting up the
Docker daemon in rootless mode for your user:

    dockerd-rootless-setuptool.sh install

Visit https://docs.docker.com/go/rootless/ to learn about rootless mode.


To run the Docker daemon as a fully privileged service, but granting non-root
users access, refer to https://docs.docker.com/go/daemon-access/

WARNING: Access to the remote API on a privileged Docker daemon is equivalent
         to root access on the host. Refer to the 'Docker daemon attack surface'
         documentation for details: https://docs.docker.com/go/attack-surface/

================================================================================

pi@raspberrypi:~ $ which docker
/usr/bin/docker
pi@raspberrypi:~ $ docker ps
Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get "http://%2Fvar%2Frun%2Fdocker.sock/v1.24/containers/json": dial unix /var/run/docker.sock: connect: permission denied
pi@raspberrypi:~ $ sudo !!
sudo docker ps
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES
pi@raspberrypi:~ $ sudo docker image ls
REPOSITORY   TAG       IMAGE ID   CREATED   SIZE
pi@raspberrypi:~ $ exit
logout
Connection to 192.168.5.18 closed.

C:\Users\hiron>

以上で raspberry pi に Dockerエンジンがインストールされました。

開発PCで前提条件を確認

  • もし開発PCに jq をインストールしていなければ、以下のように jq をインストールします。
開発PCで実行
codegear@dell3070:~$ which jq ← 何も出ないのでjqはインストールされていない
codegear@dell3070:~$ sudo apt install jq
[sudo] codegear のパスワード:
パッケージリストを読み込んでいます... 完了
依存関係ツリーを作成しています
状態情報を読み取っています... 完了
以下のパッケージが自動でインストールされましたが、もう必要とされていません:
  gyp libc-ares2 libfprint-2-tod1 libfwupdplugin1 libjs-inherits libjs-is-typedarray libjs-psl
  libjs-typedarray-to-buffer libllvm10 libnode-dev libnode64 libssl-dev libuv1-dev linux-headers-5.13.0-41-generic
  linux-hwe-5.13-headers-5.13.0-41 linux-image-5.13.0-41-generic linux-modules-5.13.0-41-generic
  linux-modules-extra-5.13.0-41-generic nodejs-doc python-pkg-resources
これを削除するには 'sudo apt autoremove' を利用してください。
以下の追加パッケージがインストールされます:
  libjq1 libonig5
以下のパッケージが新たにインストールされます:
  jq libjq1 libonig5
アップグレード: 0 個、新規インストール: 3 個、削除: 0 個、保留: 74 個。
313 kB のアーカイブを取得する必要があります。
この操作後に追加で 1,062 kB のディスク容量が消費されます。
続行しますか? [Y/n] y ← y (ENTER)を入力

(中略)

codegear@dell3070:~$ which jq
/usr/bin/jq
codegear@dell3070:~$
  • もし開発環境に docker engine をインストールしていなければ、docker engine をインストールします。
開発PCで実行
codegear@dell3070:~$ which docker
/usr/bin/docker
codegear@dell3070:~$ docker ps
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES
codegear@dell3070:~$ docker image ls
REPOSITORY   TAG       IMAGE ID   CREATED   SIZE
codegear@dell3070:~$

docker engine がインストールされている場合、上記コマンドが正常に実行されます。
そうでない場合は Dockerについて の記述を参考にインストールしてください。

開発PCに mender-artifact をダウンロード・インストール

mender-artifact をまだダウンロード・インストールしていない場合は、第6回記事の「mender-artifact をダウンロード」の内容を参考に $HOME/bin に mender-artifactをインストールし、使用可能な状態にしておきます。

すでに使用可能かどうかは以下のように which コマンドで確認できます。

開発PCで実行
codegear@dell3070:~$ which mender-artifact
/home/codegear/bin/mender-artifact ← 既にインストールされている
codegear@dell3070:~$

開発PCに作業ディレクトリを作成しARTIFACTを作成

開発PCに一時的な作業ディレクトリを作成し、docker-artifact-gen スクリプトをダウンロード後、これを実行して dockerイメージ更新用の ARTIFACTを作成します。
以下では mender-docker という名前で一時ディレクトリを作成しています。

開発PCで実行
mkdir ~/mender-docker
cd ~/mender-docker

docker-artifact-gen スクリプトをダウンロード

開発PCで実行
wget https://raw.githubusercontent.com/mendersoftware/mender/3.3.0/support/modules-artifact-gen/docker-artifact-gen

スクリプトを実行可能に設定

開発PCで実行
chmod +x docker-artifact-gen

ARTIFACTに設定するターゲットの DEVICE_TYPE をシェル変数に設定します。
ここでは raspberrypi3 に設定してみました。

開発PCで実行
DEVICE_TYPE="raspberrypi3"

以下のように ARTIFACTを作成します。

開発PCで実行
./docker-artifact-gen \
    -n "hello-world-container-update" \
    -t "${DEVICE_TYPE}" \
    -o "hello-world-container-update.mender" \
    "hello-world"

この手順を実際にやってみると以下のようになります。

開発PCで実行
codegear@dell3070:~$ mkdir ~/mender-docker
codegear@dell3070:~$ cd !$
cd ~/mender-docker
codegear@dell3070:~/mender-docker$ wget https://raw.githubusercontent.com/mendersoftware/mender/3.3.0/support/modules-artifact-gen/docker-artifact-gen
--2022-06-22 18:10:14--  https://raw.githubusercontent.com/mendersoftware/mender/3.3.0/support/modules-artifact-gen/docker-artifact-gen
raw.githubusercontent.com (raw.githubusercontent.com) をDNSに問いあわせています... 2606:50c0:8000::154, 2606:50c0:8001::154, 2606:50c0:8002::154, ...
raw.githubusercontent.com (raw.githubusercontent.com)|2606:50c0:8000::154|:443 に接続しています... 接続しました。
HTTP による接続要求を送信しました、応答を待っています... 200 OK
長さ: 3818 (3.7K) [text/plain]
`docker-artifact-gen' に保存中

docker-artifact-gen           100%[=================================================>]   3.73K  --.-KB/s    in 0s

2022-06-22 18:10:14 (22.0 MB/s) - `docker-artifact-gen' へ保存完了 [3818/3818]

codegear@dell3070:~/mender-docker$ ls -l
合計 4
-rw-rw-r-- 1 codegear codegear 3818  6月 22 18:10 docker-artifact-gen
codegear@dell3070:~/mender-docker$ chmod +x docker-artifact-gen
codegear@dell3070:~/mender-docker$ ls -l
合計 4
-rwxrwxr-x 1 codegear codegear 3818  6月 22 18:10 docker-artifact-gen
codegear@dell3070:~/mender-docker$ DEVICE_TYPE="raspberrypi3"
codegear@dell3070:~/mender-docker$ echo $DEVICE_TYPE
raspberrypi3
codegear@dell3070:~/mender-docker$ ./docker-artifact-gen \
>     -n "hello-world-container-update" \
>     -t "${DEVICE_TYPE}" \
>     -o "hello-world-container-update.mender" \
>     "hello-world"
Using default tag: latest
latest: Pulling from library/hello-world
2db29710123e: Pull complete
Digest: sha256:13e367d31ae85359f42d637adf6da428f76d75dc9afeb3c21faea0d976f5c651
Status: Downloaded newer image for hello-world:latest
docker.io/library/hello-world:latest
Artifact hello-world-container-update.mender generated successfully:
Reading Artifact...
.............................................................. - 100 %
Mender artifact:
  Name: hello-world-container-update
  Format: mender
  Version: 3
  Signature: no signature
  Compatible devices: '[raspberrypi3]'
  Provides group:
  Depends on one of artifact(s): []
  Depends on one of group(s): []
  State scripts:

Updates:
    0:
    Type:   docker
    Provides:
        rootfs-image.docker.version: hello-world-container-update
    Depends: Nothing
    Clears Provides: ["rootfs-image.docker.*"]
    Metadata:
        {
          "containers": [
            "hello-world@sha256:13e367d31ae85359f42d637adf6da428f76d75dc9afeb3c21faea0d976f5c651"
          ],
          "run_args": ""
        }
    Files: None
codegear@dell3070:~/mender-docker$ echo $?
0
codegear@dell3070:~/mender-docker$ ls -l
合計 12
-rwxrwxr-x 1 codegear codegear 3818  6月 22 18:10 docker-artifact-gen
-rw-rw-r-- 1 codegear codegear 5120  6月 22 18:27 hello-world-container-update.mender
codegear@dell3070:~/mender-docker$

5120 バイトの ARTIFACTファイル hello-world-container-update.mender が作成されました。このARTIFACTには実際にはDocker HUBからダウンロードするDockerイメージの参照情報だけが含まれています。
ダウンロード対象の hello-world イメージはDockerのインストール確認によく使われる以下のイメージです。

Docker HUBのイメージ

https://hub.docker.com/_/hello-world

イメージビルドを行うためのGitHubは以下

https://github.com/docker-library/hello-world

docker-artifact-gen スクリプトの内部では以下のように mender-artifact を呼び出して ARTIFACTを作成します。
つまり docker というPAYLOAD TYPEの ARTIFACT を作成しています。

docker-artifact-gen より
mender-artifact write module-image \
  -T docker \
  $device_types \
  -o $output_path \
  -n $artifact_name \
  -m $meta_data_file \
  $passthrough_args

コンテナアップデートを実行

作成したアップデートファイル (ARTIFACT)を SaaSの mender.io にアップロードしOTAを実行します。

開発用PCからWebUI に ログイン し、DEVICES画面でターゲットのRaspberry Pi が接続中であることを確認後、RELEASES画面の下部にある UPLOAD ボタンを押します。すると以下のアップロードダイアログが表示されるので、Drag here or browse to upload an Artifact tile の部分をクリックするか、エクスプローラなどからドロップして先ほど作成した hello-world-container-update.mender を登録します。

アップロードダイアログ

確認画面が出るので UPLOAD ボタンを押します。

Upload an Artifact画面

アップロードが出来たら、この hello-world-container-update.mender を選択して表示される CREATE DEPLOYMENT WITH THIS RELEASE ボタンを押して、DEPLOYMENT (OTAジョブ)を作成します。

RELEASES一覧画面

前回同様にデフォルト設定で DEPLOYMENTを作成し、ダイアログ場面の最後で CREATE ボタンを押すとOTAジョブが登録され、数分以内にRaspberry Pi 3 デバイスで実行完了します。

Create a deplotyment画面

Active DEPLOYMENT画面

View details ボタンを押すと、Raspberry pi 3 でOTA実行成功している事がわかります。

View details画面

Raspberry Pi上で実行結果を確認

今回ダウンロードしたDockerイメージ hello-world は実行するとすぐに終了する設定になっています。docker image lsコマンドでイメージがダウンロードされていることを確認し、docker ps -a コマンドで実行ログを確認します。

pi@raspberrypi:~ $ sudo docker image ls
REPOSITORY    TAG       IMAGE ID       CREATED        SIZE
hello-world   <none>    1ec996c686eb   8 months ago   4.85kB
pi@raspberrypi:~ $ sudo docker ps ← 常駐して実行を継続するモノではないので、現在は実行していない
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES
pi@raspberrypi:~ $ sudo docker ps -a
CONTAINER ID   IMAGE         COMMAND    CREATED              STATUS                          PORTS     NAMES
8c424da521a9   hello-world   "/hello"   About a minute ago   Exited (0) About a minute ago             condescending_curran
pi@raspberrypi:~ $

これでデバイスにイメージがダウンロードされ、(一度は)実行された事が確認されました。

以下のようにコマンドラインから再度実行することもできます。(TAGが指定されていないため、ここではイメージIDを指定して実行しています)
注: docker run コマンドは、イメージがローカルに存在しないと勝手にダウンロードを試みるため、OTAでダウンロードしたイメージを正しく実行するにはちょっとした注意が必要です。

pi@raspberrypi:~ $ sudo docker run 1ec996c686eb

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (arm32v7)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/

pi@raspberrypi:~ $

もしデバイスにすでに存在するDockerイメージのバージョンアップを行う場合は、タグ を利用して hello-world:latest のようにイメージを指定すると、そのハッシュ値がARTIFACTに保存されます。このハッシュ値のイメージがデバイスに存在しない場合、OTA後にdocker pullによるダウンロードが行われますが、このときDockerイメージのレイヤー構造により、存在しないレイヤーだけがダウンロードされることが期待されます。

この記事のまとめ

Menderを利用して Docker用コンテナイメージをダウンロード&実行することが出来ました。Dockerイメージは Menderのサーバーからダウンロードされるのではなく、Docker HUB (やほかのイメージレジストリ)から直接ダウンロードされます。そのため、ダウンロードを指定するARTIFACTのサイズは非常に小さくなります。Dockerイメージをさらに更新する場合は、Dockerイメージのレイヤー構造を活かすことでダウンロード容量を減らすことができます。

Menderではdocker PAYLOADタイプ以外にも以下のようなPAYLOADタイプを利用可能です。

  • rootfs-image
  • single-file (single-file-artifact-genを利用して生成)
  • directory (実際にはtarファイル、directory-artifact-genを利用して生成)
  • bootloader
  • firmware

これ以外にも任意のPAYLOADタイプを独自に拡張して利用可能です。
MENDER HUB の Update Modules カテゴリではこのようなPAYLOADタイプの拡張について情報交換が行われています。

今後の予定

コードギアでは Menderで始める組み込みOTA のタイトルで以下のZenn記事を公開しました。

これ以降の記事も準備中です。ご期待ください。

Discussion