Distrolessコンテナイメージとは
「Distroless」イメージには、アプリケーションとその実行時の依存関係のみが含まれています。 これらには、パッケージマネージャー、シェル、または標準のLinuxディストリビューションにあるその他のプログラムは含まれていません。 Distrolessは極限まで必要なコンポーネントに絞ったコンテナイメージということです。
標準のイメージにはシェルは含まれていません。そのため、docker container run -it hoge bash
を癖で実行しちゃうような人には向いていないイメージだと言えます。同じイメージの-debug
タグ付きのイメージは、busyboxが含まれているのでashシェルを使ってコンテナーにアクセスできるようですが、これはあくまでアプリケーションが動かなかった時に利用するためのデバッグ用みたいです。
Distrolessイメージの優位性
Distrolessイメージには余計なコンポーネントが入っていないので、セキュリティ脆弱性の数は通常のコンテナイメージよりも少なくなります。 以前のブログ記事で取り上げたtrivyを使って、イメージの脆弱性スキャンをかけてみます。
% trivy image --ignore-unfixed debian:11 ... debian:11 (debian 11.3) Total: 3 (UNKNOWN: 0, LOW: 0, MEDIUM: 2, HIGH: 0, CRITICAL: 1) % trivy image --ignore-unfixed gcr.io/distroless/base-debian11 ... gcr.io/distroless/base-debian11 (debian 11.4) Total: 0 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0)
それぞれ用意されているDeianのバージョンが異なる点はご了承いただくとして、 Distrolessイメージは脆弱性は0であると表示されています。完璧ですね。
コンテナでアプリケーションを実行する場合は、対応するライブラリがセットアップ済みのイメージを使うと思います。 例えば、JavaアプリならJavaの、PythonならPythonの対応するバージョンのイメージを普段なら選択すると思います。
おそらくDistrolessが一番ささるのは、Go言語で作られたアプリケーションなのかなと思います。 Go言語はビルドしてしまえばワンバイナリで動作します。実際はCPUアーキテクチャーは意識する必要はありますが、同じアーキテクチャーでビルドすれば動作します。
例えば次のようなDockerfileをみたことはあると思います。 これは前半のコードでGoアプリケーションのバイナリービルドを実行し、後半のコンテナーにバイナリーをコピーして実行しています。 後半のコードはbusyboxというかなり最小化されたイメージを使っています。
# syntax=docker/dockerfile:1 FROM golang:1.18 AS builder WORKDIR /usr/src/app COPY *.go go.mod ./ RUN go build main.go FROM docker.io/busybox:stable-glibc WORKDIR /root/ COPY --from=builder /usr/src/app/main ./ EXPOSE 8090 CMD [ "./main" ]
次のようにイメージをビルドします。
$ docker build -t mygo-busybox:latest -f Dockerfile-busybox .
ビルドしたイメージは当然ながら問題なく動作します。
$ docker run -d -p 8090:8090 mygo-busybox:latest $ curl localhost:8090/hello hello
Docker Hubで公開されているオフィシャルイメージの多くは、DebianベースのイメージのほかAlpine Linuxベースのイメージも存在します。 Alpine Linuxベースはイメージサイズが小さいので取り回ししやすくて良いです。イメージサイズが小さければイメージのダウンロードは短時間ですみますし、特にインフラをクラウド上で実行している場合は小さいイメージを使えばそれだけ、ストレージのコスト削減に貢献します。
ベースOSがなんにせよ最終的にアプリケーションが実行されるなら良いわけなので、Alpine Linuxベースを積極的に使いましょう!...これがちょっと前に流行りました。私もその流行にちょっと乗った記憶があります。
しかしAlpine LinuxはCライブラリがLinuxの多くで利用されるGLibcではないため、互換性の問題が発生したりパフォーマンス面で問題がある場合があります。
そこで考えられたのが、普段使っているLinuxベースのコンテナイメージからアプリケーショ実行に必要ではない部分をごっそり削ったイメージを使う方法です。それがDistrolessだそうです。
一般的にコンテナーイメージはDockerやPodmanのようなコンテナーツールを使ってイメージ作成をしますが、 DistrolessはGoogleが開発する「Bazel」を使ってビルドされています。
先程のDockerfileをbusyboxからDistrolessなイメージに切り替えてみましょう。
# syntax=docker/dockerfile:1 FROM golang:1.18 AS builder WORKDIR /usr/src/app COPY *.go go.mod ./ RUN go build main.go FROM gcr.io/distroless/base-debian11 WORKDIR /root/ COPY --from=builder /usr/src/app/main ./ EXPOSE 8090 CMD [ "./main" ]
こちらもビルドしてみましょう。
$ docker build -t mygo-distroless:latest -f Dockerfile-distroless .
イメージビルドが終わったら、実行してみます。
$ docker run -d -p 8090:8090 mygo-distroless:latest $ curl localhost:8090/hello hello
この場合でもアプリケーションが動作しました。Distrolessは、ワンバイナリで動くGoアプリケーションとは相性が良いと思います。
一方で、たとえばPythonのように色々なモジュールを使って組み立てていくような言語で書かれたアプリケーションだと、Distrolessイメージを利用するのは難しそうに感じました。
イメージサイズの比較
次にイメージサイズを比較してみます。単純なサイズで優劣を評価するならbusyboxになるのですが、 アプリケーションの互換性のことを考えると、同じOSベースの方が良いと考える人が多いと思います。
そうなると、標準のDebianイメージやそれをスリムにしたslimイメージよりも、小さいDistrolessイメージが最適なのかなと感じました。
debian 11 c7e601276d47 6 hours ago 118MB debian 11-slim 39e866e114a0 2 weeks ago 74.3MB gcr.io/distroless/base-debian11 latest 1cfbee9d223a 52 years ago 17.4MB busybox stable-glibc 68a590bac597 4 weeks ago 3.73MB
Distrolessイメージでちょっと気になったのが、ベースイメージの作成日です。日付情報が欠落されているのか理由は不明ですが、52年前ってすごいですね。筆者はまだ生まれてないです。
さて、ビルドしたイメージを比べてみましょう。一番上はgolang:1.18
イメージをそのまま使った場合です。
現在はマルチステージビルドを使うのが本流ですが、比較のために用意してみました。
元のイメージと比べて、1/4くらいのサイズに収まっています。
mygo-std latest adbb9cdb6bd0 12 seconds ago 825MB mygo-distroless latest 2cd6a99fbacc 39 seconds ago 23.4MB mygo-busybox latest 27caf0f25efa About an hour ago 9.77MB
「Distroless、良いじゃん」と私は思ったのですが、その一方で次のような主張もあります。
英語の文章なので、邦題をつけるとすると「Distrolessコンテナはあなたが思っているようなセキュリティソリューションではない理由」でしょうか。翻訳しつつざっと記事を見ましたが、私も概ね同意できました。
ブログ記事では「全部同じイメージを使って全てのアプリケーションが実行できるならDistrolessを使うメリットはあるが、 アプリケーションによってベースイメージを使い分ける必要がある場合はメリットはあまりない、既存のイメージも自由にカスタマイズできる訳だし」と結論づけています。
筆者の結論としては、Goで書かれたアプリケーションなら、従来通りマルチステージビルドで最終的にbusybox
イメージベースで動かせば良いかなと思いました。
一方、ある程度アプリの実行にライブラリが必要な言語で書かれたアプリケーションを実行するなら、Distroless
を使うのもありなのかなと思いました。
とはいえ、そこまでイメージサイズの最小化にこだわらないのであれば、既存のベースイメージのslim版を使えば良いかなとも感じました。 Distrolessはアプリケーションの実行に必要な最低限のものだけを取り入れたイメージであるため、脆弱性への対応は最小限ですみます。しかし少ないだけであってゼロではありません。
それならばイメージビルドの際にイメージのアップデートをした上でアプリケーションイメージを作ればいいですし、CI/CDなどの考え方を取り入れてアプリケーションイメージの自動ビルドや自動的なテストを定期的に行って差し替える手段さえ用意できれば、問題が起きた時に状況を把握しにくいイメージを使うよりは従来のイメージの利用で良いのではと感じました。
最終的に筆者は必要な機会が訪れでもしない限り、Distrolessを使わない選択をすると思います。「なるほどねー」とは思いましたけどね。
おまけ
コンテナーベースの技術を検証する際にいつもはまるのが、CPUアーキテクチャーの差です。私が普段使っているマシンはM1アーキテクチャー(Apple Silicon)のMac miniで、マルチアーキテクチャーを考慮していないようなイメージがたまにあって、想定通り動かないことが稀にあります。来年のmacOSリリースではApple Siliconベースでx86_64アークテクチャーの環境が普通に使えるようになるアップデートが提供されるらしいので、今からワクワクしています。
DockerのARMベースCPUへの対応はかなり良くなって困ることも無くなったもののたまにうまくいかないことがありますので、この改善によりDockerのようなコンテナー周りの使い勝手も良くなったら良いなあと思います。