とことんDevOps | 日本仮想化技術のDevOps技術情報メディア

DevOpsに関連する技術情報を幅広く提供していきます。

日本仮想化技術がお届けする「とことんDevOps」では、DevOpsに関する技術情報や、日々のDevOps業務の中での検証結果、TipsなどDevOpsのお役立ち情報をお届けします。
主なテーマ: DevOps、CI/CD、コンテナ開発、IaCなど

開催予定の勉強会

読者登録と各種SNSのフォローもよろしくお願いいたします。

Distrolessで遊ぶ

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のようなコンテナー周りの使い勝手も良くなったら良いなあと思います。