Dockerを使わずにコンテナイメージを作成する「img」を使ってみよう #docker #img #oci #containerd
この記事は1年以上前に投稿されました。情報が古い可能性がありますので、ご注意ください。
2015年6月開催のDockerCon 2015にて、コンテナの標準規格を制定するためのOpen Container Project (現在のOpen Container Initiative, 略称OCI)の設立が発表され、runCの公開を皮切りに、コンテナに関わるさまざまな標準仕様が制定されてきました。「コンテナ」と言えば特にDockerそのものや関連するソフトウェア・技術を指していた当時から時代は流れ、このOCI標準仕様に沿ったコンテナソフトウェアが多く発表されるようになっています。
本稿では、Dockerをまったく使わずにOCI標準仕様に準拠したコンテナイメージを作成する「img」を紹介します。
BuildKitとは
本題に入る前に、まずBuildKitについて説明します。BuildKitとは、最も一般的なイメージビルダーであるdocker image build (またはdocker build)を置き換えるために開発され、Docker Engine 18.09から利用可能となったイメージビルダーです。デフォルトでは無効となっていますが、環境変数 DOCKER_BUILDKIT=1 を設定することで有効となります。
$ export DOCKER_BUILDKIT=1 $ docker image build ...
BuildKitはDocker Engine固有のものではなく、ツールキットとして多くのソフトウェアから利用されています。本稿で取り上げる「img」もBuildKitを利用しているイメージビルダーの1つです。
imgとは
imgはBuildKitを利用した、スタンドアローンでデーモンレス(言い換えるとクライアント・サーバ型アーキテクチャではない)、非特権ユーザで実行でき、Dockerfileを利用でき、OCI仕様に準拠したイメージビルダーです(名前の検索性が非常に非常に悪いことが玉に瑕ですが…)。dockerコマンドをimgコマンドに置き換えるだけで、イメージのビルドやレジストリへのプッシュ・プルをそのまま行えることが利点です。imgでビルドしたイメージはOCI仕様に準拠しているため、OCI仕様の元になったDockerはもちろんのこと、OCI仕様に準拠した各種コンテナランタイムで実行することが可能です。
それでは、早速 img を使ってみましょう。前述の通り img は BuildKit を利用しているため、既存の資産である Dockerfile を変更なしでそのまま利用できます。ここでは詳細を記載しませんが、OmegaTをコンテナ化するための Dockerfile を例として利用していきます。
Debianで試す
本記事執筆時点で最新版のimg v0.5.11を、公式ドキュメントのInstallationを参考に、本記事執筆時点で最新開発版のDebianにて試してみます。
リリースページにある手順を参考に、imgバイナリのインストールを行います。
$ export IMG_SHA256="cc9bf08794353ef57b400d32cd1065765253166b0a09fba360d927cfbd158088" $ sudo curl -fSL "https://github.com/genuinetools/img/releases/download/v0.5.11/img-linux-amd64" -o "/usr/local/bin/img" \ && echo "${IMG_SHA256} /usr/local/bin/img" | sha256sum -c - \ && sudo chmod a+x "/usr/local/bin/img" /usr/local/bin/img: 完了
ここで焦って img コマンドを実行しようとするとエラーになります。エラーになってしまっていますが、docker image build コマンドと同じように img build コマンドが使えます。
% img build -t omegat-console-translator:img-0.5.11 . FATA[0000] newuidmap not found (install uidmap package?): exec: "newuidmap": executable file not found in $PATH
このエラーメッセージと Installation にもある通り、uidmapパッケージに入っているnewuidmapコマンドが必要となりますので、Debianのパッケージマネージャであるaptでインストールしておきましょう。uidmapパッケージをインストールしたら再度 img コマンドを実行します。
% img build -t omegat-console-translator:img-0.5.11 . newuidmap: write to uid_map failed: Invalid argument nsenter: failed to use newuidmap: Success nsenter: failed to sync with parent: SYNC_USERMAP_ACK: got 255: Success
またエラーになってしまいました。このエラーの対処方法は少々わかりづらいのですが、公式ドキュメントUnprivileged Mountingとイシュートラッカーimg build: newuidmap: write to uid_map failed: Invalid argumentに記載があります。img コマンドを実行するユーザのUIDとGIDがそれぞれ /etc/subuid と /etc/subgid に含まれていないといけません。これらのファイルは直接変更するのではなく、usermod コマンドを使って変更します。ここでは img コマンドを実行するユーザが guest という名前の場合です。
% sudo usermod -v 200000-265535 -w 200000-265535 guest % grep guest /etc/sub?id /etc/subgid:guest:200000:65536 /etc/subuid:guest:200000:65536
これでようやく img コマンドが動くようになりました。
% img build -t omegat-console-translator:img-0.5.11 . Building docker.io/library/omegat-console-translator:img-0.5.11 Setting up the rootfs... this may take a bit. WARN[0000] using host network as the default [+] Building 19.5s (6/10) => [internal] load .dockerignore 0.8s => => transferring context: 75B 0.0s => [internal] load build definition from Dockerfile 1.0s => => transferring dockerfile: 503B 0.0s => [internal] load metadata for docker.io/library/debian:buster 4.1s => [1/6] FROM docker.io/library/debian:buster@sha256:f9182ead292f45165f4 9.2s => => resolve docker.io/library/debian:buster@sha256:f9182ead292f45165f4 0.0s => => sha256:f9182ead292f45165f4a851e5ff98ea0800e172cced 1.85kB / 1.85kB 0.0s => => sha256:2f4b2c4b44ec6d9f4afc9578d8475f7272dc50783268dc6 529B / 529B 0.0s => => sha256:2b6f409b1d24285cbae8f2c6baa6969741151bed561 1.46kB / 1.46kB 0.0s => => sha256:07471e81507f7cf1100827f10c60c3c0422d12224 50.44MB / 50.44MB 3.1s => => unpacking docker.io/library/debian:buster@sha256:f9182ead292f45165 5.0s => [internal] load build context 3.0s => => transferring context: 143.75MB 2.5s => ERROR [2/6] RUN apt-get update && apt-get install -y --no-install-re 5.0s −−−−−− > [2/6] RUN apt-get update && apt-get install -y --no-install-recommends openjdk-11-jre-headless locales: #5 0.386 mkdir /run/runc: permission denied −−−−−− Error: failed to solve: executor failed running [/bin/sh -c apt-get update && apt-get install -y --no-install-recommends openjdk-11-jre-headless locales]: runc did not terminate successfully
img コマンドは動いたのですが、ビルド途中でエラーとなってしまいました。
mkdir /run/runc: permission denied
となっているため、ホスト側でこのディレクトリを作成しておきます。
% sudo mkdir /run/runc
しかし、これでもまた失敗してしまいます。
(省略) > [2/6] RUN apt-get update && apt-get install -y --no-install-recommends openjdk-11-jre-headless locales: #5 0.643 mkdir /run/runc/rbvkypok6qvewr4uc9ub4a5g0: permission denied (省略)
img は一般ユーザの権限で動作しているため、このディレクトリは一般ユーザで書き込みできる必要があります。
% sudo chmod 1777 /run/runc
ここまでやっても失敗してしまいました。
(省略) #5 19.94 Unpacking ca-certificates-java (20190405) ... #5 19.95 dpkg: error processing archive /tmp/apt-dpkg-install-77j49c/36-ca-certificates-java_20190405_all.deb (--unpack): #5 19.95 unable to install new version of './etc/ca-certificates': Invalid cross-device link (省略)
調べたところ、Linuxカーネル(overlayfs)のバグ?のようです。dpkg: Cannot upgrade some packages on overlayfs: Invalid cross-device link、Unable to docker build on Linux kernel 4.19
5.10.41-1 では動作するようですが 5.14.12-1 ではこのようにエラーとなってしまいました。
Linuxカーネルで修正されるまで、Debianで img を試すことは断念しました。同じような環境の方がいるかもしれないので、記録として残しておきます。
Ubuntu 18.04 LTS (Bionic) で試す
次は Vagrant + VirtualBox で、Ubuntu 18.04 LTS (Bionic) 上で img を試してみることにしましょう。ubuntu/bionic64 Vagrant boxがあるのでそれを使います。バージョン 20211025.0.0 では、
- uidmap パッケージはインストール済み
- /etc/subuid と /etc/subgid にユーザの UID/GID 追加済み
- /run/runc ディレクトリの作成不要
- Linux カーネルのバージョンは 4.15.0
と、前述のDebianでの追加対処や問題は発生しないようでした。
早速 ubuntu/bionic64 に img をインストールしてビルドを実行してみます。
% img build -t omegat-console-translator . Building docker.io/library/omegat-console-translator:latest Setting up the rootfs... this may take a bit. WARN[0000] using host network as the default [+] Building 13.7s (4/10) => [internal] load .dockerignore 0.3s [+] Building 45.0s (5/10) [+] Building 91.7s (12/12) FINISHED => [internal] load .dockerignore 0.3s => => transferring context: 75B 0.0s => [internal] load build definition from Dockerfile 0.3s => => transferring dockerfile: 503B 0.0s => [internal] load metadata for docker.io/library/debian:buster 3.8s => [1/6] FROM docker.io/library/debian:buster@sha256:f9182ead292f45165f4 9.6s => => resolve docker.io/library/debian:buster@sha256:f9182ead292f45165f4 0.0s => => sha256:f9182ead292f45165f4a851e5ff98ea0800e172cced 1.85kB / 1.85kB 0.0s => => sha256:2f4b2c4b44ec6d9f4afc9578d8475f7272dc50783268dc6 529B / 529B 0.0s => => sha256:07471e81507f7cf1100827f10c60c3c0422d12224 50.44MB / 50.44MB 4.1s => => sha256:2b6f409b1d24285cbae8f2c6baa6969741151bed561 1.46kB / 1.46kB 0.0s => => unpacking docker.io/library/debian:buster@sha256:f9182ead292f45165 4.8s => [internal] load build context 4.8s => => transferring context: 143.75MB 3.9s => [2/6] RUN apt-get update && apt-get install -y --no-install-recomme 34.8s => [3/6] RUN echo "ja_JP.UTF-8 UTF-8" > /etc/locale.gen && locale-gen 1.9s => [4/6] COPY OmegaT.jar /opt/omegat/ 0.4s => [5/6] COPY lib /opt/omegat/lib 3.3s => [6/6] RUN mkdir /omegat 0.2s => exporting to image 0.1s => => exporting layers 0.0s => => exporting manifest sha256:65e61d5eb7989926866aa7e6b626be336be31d28 0.0s => => exporting config sha256:bd299012b97ef213bf4adbf62f3d21bca28e40b001 0.0s => => naming to docker.io/library/omegat-console-translator:latest 0.0s => exporting cache 0.0s => => preparing build cache for export 0.0s Successfully built docker.io/library/omegat-console-translator:latest
すんなりとビルドに成功しました。
img ls コマンドを実行すると、docker image ls コマンドと同じようにイメージ名やイメージIDなどが表示されます。
% img ls NAME SIZE CREATED AT UPDATED AT DIGEST docker.io/library/omegat-console-translator:latest 276MiB 11 minutes ago 11 minutes ago sha256:65e61d5eb7989926866aa7e6b626be336be31d284236c7722eb5f99072b834e3
dockerでビルドしたイメージは root ユーザが所有している /var/lib/docker ディレクトリに保管されていましたが、img の場合はどこでしょうか? ビルドを実行したユーザのホームディレクトリ以下の ~/.local/share/img ディレクトリに存在しています。確認してみましょう。先の img ls で得られたイメージIDで保管ディレクトリ内を探すと、次のJSON形式のファイルが見つかります。
% less ~/.local/share/img/runc/overlayfs/content/blobs/sha256/65e61d5eb7989926866aa7e6b626be336be31d284236c7722eb5f99072b834e3 { "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "schemaVersion": 2, "config": { "mediaType": "application/vnd.docker.container.image.v1+json", "digest": "sha256:bd299012b97ef213bf4adbf62f3d21bca28e40b0012a693bae05bd73770d4de6", "size": 4400 }, "layers": [ { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "digest": "sha256:07471e81507f7cf1100827f10c60c3c0422d1222430e34e527d97ec72b14a193", "size": 50436692 }, (省略)
layersで示されているものがイメージレイヤーのようです。このファイルを探して中身を見てみましょう。
% tar tvf /home/vagrant/.local/share/img/runc/overlayfs/content/blobs/sha256/07471e81507f7cf1100827f10c60c3c0422d1222430e34e527d97ec72b14a193 | head drwxr-xr-x 0/0 0 2021-10-11 00:00 bin/ -rwxr-xr-x 0/0 1168776 2019-04-18 04:12 bin/bash -rwxr-xr-x 0/0 43744 2019-02-28 15:30 bin/cat -rwxr-xr-x 0/0 64320 2019-02-28 15:30 bin/chgrp -rwxr-xr-x 0/0 64288 2019-02-28 15:30 bin/chmod -rwxr-xr-x 0/0 72512 2019-02-28 15:30 bin/chown -rwxr-xr-x 0/0 146880 2019-02-28 15:30 bin/cp -rwxr-xr-x 0/0 121464 2019-01-17 19:08 bin/dash -rwxr-xr-x 0/0 109408 2019-02-28 15:30 bin/date -rwxr-xr-x 0/0 76712 2019-02-28 15:30 bin/dd
ファイルシステムが格納されていることがわかりました。より詳しい内容に興味があれば、OCIのイメージ仕様を参照してみてください。
imgでビルドしたイメージをコンテナとして実行
img によってイメージをビルドすることはできましたが、img はあくまで「イメージビルダー」であって、コンテナを実行する機能は持っていません。OCI仕様に沿ったイメージをコンテナとして実行するには、OCI仕様に沿った「コンテナランタイム」が必要です。
ここではOCI仕様に沿ったコンテナランタイムとしてcontainerdを採用してみましょう。詳細な手順は省略しますが、aptなどを使ってcontainerdをインストールしてあるものとします。
まずは img コマンドを使って、イメージを tar ファイルとしてエクスポートします。
% img save omegat-console-translator:latest -o omegat-console-translator.tar
エクスポートした tar ファイルを containerd にインポートします。
% sudo ctr image import ./omegat-console-translator.tar unpacking docker.io/library/omegat-console-translator:latest (sha256:65e61d5eb7989926866aa7e6b626be336be31d284236c7722eb5f99072b834e3)...done % sudo ctr image ls REF TYPE DIGEST SIZE PLATFORMS LABELS docker.io/library/omegat-console-translator:latest application/vnd.docker.distribution.manifest.v2+json sha256:65e61d5eb7989926866aa7e6b626be336be31d284236c7722eb5f99072b834e3 276.0 MiB linux/amd64 -
これでイメージが containerd で扱えるようになったので、コンテナとして実行してみましょう。
% sudo ctr run docker.io/library/omegat-console-translator:latest omegat-console-translator 12691: 情報: =================================================================== 12691: 情報: OmegaT-4.3.2_0_6a661c5e (Thu Oct 28 06:15:38 UTC 2021) Locale ja_JP 12691: 情報: Java:Debian バージョン 11.0.12 が /usr/lib/jvm/java-11-openjdk-amd64 から実行されました (LOG_STARTUP_INFO) (省略)
img でビルドしたイメージが、問題なく containerd でコンテナとして実行できました。
本稿では省略しますが、img でビルドしたイメージを docker にインポートしてコンテナとして実行することも可能です。
まとめ
本稿では、OCI仕様に沿った img を使ってコンテナイメージをビルドし、同じくOCI仕様に沿った containerd にイメージをインポートしてコンテナとして実行してみました。
OCI仕様やBuildKitなど、もともとはDockerが起源である仕様やソフトウェアなどを利用していますが、Dockerそのものを一切使用せずにイメージやコンテナをビルド・実行することが可能であることがわかりました。
Dockerはそれさえ入れてしまえばコンテナやイメージのビルドや実行などのすべてが一度に揃う点でメリットがありますが、ビルドと実行の担当範囲が分離されていてほしいという要望があるのもまた事実です。
このように img や containerd など、OCI仕様に基いたソフトウェアを組み合わせることで、それらを明確に分離することができます。さらに img も containerd も、少々語弊はありますが Docker が内部利用しているソフトウェアを切り出したものなので、Docker とほぼ同じ使用感で同じように動作させることが可能です。
興味がある方は本稿を参考に是非お試しいただければと思います。