PC 上の Linux で、Raspberry Pi 用の Docker イメージを作る

投稿者: | 2019年11月8日

Building Raspberry Pi Docker image on PC Linux, by docker buildx.

前書き

Docker は、IT 関連の開発者の間で既に「なくてはならないツール」となっているようですが、組込関連の設計者の間でも、少しずつ注目を集めているようです。一つの例として、Raspberry Pi などの組込 Linux 上で Docker を動かす、というものがあります。

ネットを探すと、確かにそのような実験が多く見られますが、大抵は Raspberry Pi でクラスタを組んで、Docker の勉強に使ってみよう、というものが主流のようです。

別のアイデアとして私が注目しているのは、Raspberry Pi で試作したアプリケーションをお客様に試してもらう際に、お客様に Docker イメージで提供することで、アプリを迅速にインストール(デプロイ)し、評価して頂くことができるのではないか、というものです。クラウド上で動かすアプリケーションであれば、遠隔で設定することも容易ですが、お客様がお持ちの Raspberry Pi に遠隔でアプリケーションをインストールするのは、なかなか面倒です。

私は従来、Raspberry Pi の SD カードイメージをサーバーにアップロードしておき、お客様にそれを SD カードに書き込んでもらう、というやり方をしてきましたが、これは時間を要する作業であり、お客様も十分に時間が取れないと試しづらい、という欠点があります。また、お客様の固有設定が、再インストールの度に失われてしまうという問題もあります。Docker を使えば、これらの問題を解決できるのではないか、と考えています。

さて。

次に問題となるのは、アプリケーションの開発、テスト、ビルド(docker build)を Raspberry Pi 上で実行するのは、時間がかかるという点です。CPU パワーの問題もありますし、常に SD カードの寿命(wear)にも注意を払わなくてはなりません。(後者の問題は NFS を使うという手も考えられます。) できれば、PC 上の Linux で開発、テストのほとんどを済ませ、最後のリリーステストだけ Raspberry Pi 上でできれば楽になります。

最近、Docker で buildx という機能が experimental としてリリースされており、PC(amd64)アーキテクチャ上の Docker を使って、他のアーキテクチャ(ARMv7, ARMv8)等のバイナリをビルドできるようになっています。いままで興味はありつつも、experimental ということもあって逃げていたのですが、ようやく重い腰を上げて試してみた次第です。ネット上にいくつか情報がありますが、うまく行かずに困っている方も多いようですので(私もハマりました)、私の試行をまとめておこうと思います。

なお、上述の通りこの機能は experimental であり、いずれ正式版になった暁には、苦労をせずにも使えるようになっていると思いますので、「季節ネタ」(初物に弱い江戸っ子的、目には青葉? 的なネタ)として御理解ください。逆に言うと、試すなら今!  です。

本題

experimental の buildx も、Windows 上や Mac OS 上の Docker Desktop では、それほど難しくないようですが、Linux 上で試そうとすると苦労することが多いようです。これは、QEMU の設定を自力でやらなければならない、という点が一番のネックのようです。逆に言うと、日頃から QEMU をバリバリ使っていらっしゃる方にとっては、難しい問題ではないでしょう。

私は日頃の開発で Mac OS を使っているので、Mac OS 版の Docker を使えば何も苦労せず、結果として、QEMU に少しく詳しくなれる機会を失ったのでしょうが、今回、少しだけ QEMU なるものを勉強することができてしまいました。なぜ Mac OS 版の Docker を使わなかったかというと、私の Mac OS は El Capitan (10.11.6) であり、なんと最新版の Docker Desktop が動かないのです!!  ひどい…、いや、勉強になって良かった! 🙂

今回の buildx を試すには、基本的には Docker 19.03 以降を使うことになるようです。

さて、まずは Linux 環境を用意します。今回は、手持ちの Ubuntu 16.04.6 を使いました。VMware 上で動いているゲスト OS です。

Docker Engine – Community のインストール

まずは、こちらの説明を見て Docker をインストールします。書いてある通りに実行するだけですので、特に説明は不要ですね。なお、最後にこの説明を読んで、一般ユーザーでも docker コマンドを実行できるようにしておくべきでしょう。(そこの説明にもありますが、一度ログアウトして再度ログインしないと usermod コマンドの効果が現れません。)

buildx 機能の有効化

さて、繰り返しになりますが buildx 機能は experimental ですので、そのままでは使えません。

$ docker buildx
docker: 'buildx' is not a docker command.
See 'docker --help'

そのため、~/.docker/config.json ファイルを編集する必要があります。(環境変数を使うなど、他のやり方もあるようです。)

Docker をインストールした直後は上記ファイルがありません。本当は、docker コマンドで簡単に作れれば良いのですが。(ファイル名を間違えるとトラブルの原因になる。)

(私の知っている限り)一番確実なのは、Docker Hub にアカウントを作って、docker login コマンドでログインする方法です。

$ docker login
Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.
Username: ユーザー名
Password: 
WARNING! Your password will be stored unencrypted in /home/yokoyama/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store

Login Succeeded
$ cat .docker/config.json 
{
        "auths": {
                "https://index.docker.io/v1/": {
                        "auth": "なんちゃらかんちゃら"
                }
        },
        "HttpHeaders": {
                "User-Agent": "Docker-Client/19.03.4 (linux)"
        }
}

あまり簡単でもないですかね。このようにファイルができたら、最後のほうを修正します。あ、前行のカンマ(,)の付け忘れに注意してください。

        "HttpHeaders": {
                "User-Agent": "Docker-Client/19.03.4 (linux)"
        },
	"experimental": "enabled"
}

これで、docker buildx コマンドが使えるようになります。

$ docker buildx

Usage: docker buildx COMMAND

Build with BuildKit

Management Commands:
  imagetools  Commands to work on images in registry

Commands:
  bake        Build from a file
  build       Start a build
  create      Create a new builder instance
  inspect     Inspect current builder instance
  ls          List builder instances
  rm          Remove a builder instance
  stop        Stop builder instance
  use         Set the current builder instance
  version     Show buildx version information

Run 'docker buildx COMMAND --help' for more information on a command.

QEMU のインストール

Linux 上の Docker で他アーキテクチャ(ARMv7 など。以下、ターゲットアーキテクチャと呼びます)のビルドをするには、QEMU が必要です。正確に言うと、ただビルドをするだけなら QEMU は要らないのですが、Dockerfile の中で RUN コマンド(ディレクティブ?)を使う際に必要になるのです。どういうことかというと、RUN コマンドは、実行するコマンドをターゲットアーキテクチャ上で実行しなくてはならないからです。

私は QEMU にあまり詳しくないので、こちらの説明を参考にインストールしました。

$ sudo apt-get install qemu-user-static

なお、末尾の static ですが、ターゲットアーキテクチャ用に静的にリンクされた実行ファイルを実行するための QEMU、ということのようです。よく分からんのですが、こちらを参考にしました。

ここでハマる

さて。ネット上で情報を探しながら設定やインストールをすると、ここまでは大抵できるのです。Docker Desktop を使っている方の情報を見ると、何にハマるのか理解できん、簡単だよ、となると思います。さっそく、はまってみましょう。

まずは、簡単な Dockerfile を用意します。

FROM debian
RUN uname -a

あとは、こちらの情報を参考に docker buildx create をします。(このようにして、builder というものを作らないといけないのだそうです。新しい概念なのでよく分かりませんが。)

$ docker buildx ls
NAME/NODE DRIVER/ENDPOINT STATUS  PLATFORMS
default * docker                  
  default default         running linux/amd64, linux/386
$ docker buildx create --name buildarm --platform linux/arm64 --use
buildarm
$ docker buildx ls
NAME/NODE   DRIVER/ENDPOINT             STATUS   PLATFORMS
buildarm *  docker-container                     
  buildarm0 unix:///var/run/docker.sock inactive linux/arm64
default     docker                               
  default   default                     running  linux/amd64, linux/386

というわけで、buildarm という名前の builder(linux/arm64 対応版)を作りました。

さて、ビルドしてみましょう!  とりあえずは、Docker イメージは作りません。ビルドするだけです。

$ docker buildx build --platform linux/arm64 .
WARN[0000] No output specified for docker-container driver. Build result will only remain in the build cache. To push result image into registry use --push or to load image into docker use --load 
[+] Building 2.0s (3/3) FINISHED
=> [internal] load build definition from Dockerfile
=> => transferring dockerfile: 31B
=> [internal] load .dockerignore
=> => transferring context: 2B
=> [internal] load metadata for docker.io/library/debian:latest
failed to solve: rpc error: code = Unknown desc = failed to solve with frontend dockerfile.v0: failed to load LLB: runtime execution on platform linux/arm64 not supported

はい、ダメでした。。。この「failed to load LLB」が鬼門です。これを解決するのに、まる二日かかりました。

問題の背景

ターゲットアーキテクチャ用のバイナリを実行するには QEMU が必要なのですが、qemu コマンドを使わずに実行するには、カーネルの手助けが必要です。Linux 上の、そのための仕組を binfmt_misc と呼ぶそうです。(知らなかった。)

この binfmt_misc 機能を使うと、例えば ARMv8 用にビルド(コンパイル)されたプログラムがあるとして、それをそのまま amd64 アーキテクチャ上の Linux で実行できるのです。(ただし、静的にリンクされていないとダメらしい。)

docker buildx は、この機能を使っているらしいので、それを設定してあげないといけない、ということになります。その辺りの説明はこちらが詳しいのですが、説明が要点をつきすぎていて、専門家でないと理解するのが困難です。

最初に、QEMU 用のツール qemu-binfmt-conf.sh を探してきましょう。こちらからダウンロードできます。

解決法(その 1)

まずはとりあえず、その場しのぎのやり方から。上記ツール qemu-binfmt-conf.sh を使って、カーネルに ARMv8 用のバイナリファイルを実行する方法を教えましょう。(ちなみに、下記の 2行目のコマンドはオマジナイです。もっと分かりやすい方法はないのでしょうか。)

$ chmod +x qemu-binfmt-conf.sh
$ sudo find /proc/sys/fs/binfmt_misc -type f -name 'qemu-*' -exec sh -c 'echo -1 > {}' \;
$ sudo ./qemu-binfmt-conf.sh --persistent yes --qemu-path /usr/bin --qemu-suffix -static

いくつかエラーが出ますが、以下のコマンドを実行し、結果が同じであれば大丈夫です。特に、interpreter のパスに「local」が入っていないこと、末尾に -static が付いていること、flags: に「F」が含まれていることが重要です。

$ cat /proc/sys/fs/binfmt_misc/qemu-aarch64
enabled
interpreter /usr/bin/qemu-aarch64-static
flags: F
offset 0
magic 7f454c460201010000000000000000000200b700
mask ffffffffffffff00fffffffffffffffffeffffff

さて。それではもう一度やってみましょう。なお、builder は再度作り直さないといけないようです。

$ docker buildx rm buildarm
$ docker buildx ls
NAME/NODE DRIVER/ENDPOINT STATUS  PLATFORMS
default * docker                  
  default default         running linux/amd64, linux/386
$ docker buildx create --name buildarm --platform linux/arm64 --use
buildarm
$ docker buildx build --platform linux/arm64 .
WARN[0000] No output specified for docker-container driver. Build result will only remain in the build cache. To push result image into registry use --push or to load image into docker use --load 
[+] Building 16.6s (6/6) FINISHED
=> [internal] booting buildkit
(略)
=> [2/2] RUN uname -a

うまく行きましたね?

解決法(その 2)

ところで、上記のやり方には一つ問題があります。実は、上記でうまく行っても、OS を再起動すると binfmt_misc の設定が消えてしまうのです。これを解決するには、systemd-binfmt.service とかいう機能を使うと良いそうです。(systemd が嫌いなので…、いや、詳しくないので、うまく説明できません。すみません。)

$ sudo ./qemu-binfmt-conf.sh --persistent yes --qemu-path /usr/bin --qemu-suffix -static --systemd aarch64
$ sudo ./qemu-binfmt-conf.sh --persistent yes --qemu-path /usr/bin --qemu-suffix -static --systemd arm

そうしたら、OS をリブートして docker buildx build を試してみましょう。うまく行きましたか?

参考になるサイト

最後に、参考とさせて頂いたサイトをまとめておきます。Special thanks to the following helpful sites.

なお、multiarch/qemu-user-static そのままだとうまく行かない点があります。binfmt_misc のフラグに F(persistent)が付かないので、ダメなようです。

今日はここまで!