guni.log

日々の進捗・ポエムのごった煮

コンテナイメージ(の依存)はどの程度小さくすべきか?という話

全国のDockerfile職人の方々,コンテナイメージのダイエットはどのようにやっていますか? 今回はコンテナイメージの容量というよりは,依存を小さくするという意味で, コンテナイメージ内にシェルやcoreutilなどを入れるかどうかといった話について,議論したのでその内容をまとめていきたいと思います.

コンテナイメージを小さくすると何が嬉しいのか

コンテナイメージの容量を小さくするという観点だと,デプロイが高速になるため,スケールアウトするのが速くなります. また,依存を減らすという観点でいうと,脆弱性を利用した攻撃対象領域(attack surface)が少なくなります.

(余談) 後述するようなscratchにバイナリを配置するだけのコンテナイメージなら,アプリケーションしか実行しないので, Linuxのセキュリティ機構であるseccompやAppArmorの設定もアプリケーションにフィットした設定を用いる事ができると思います.

どうやってコンテナイメージの容量を小さくするのか

コンテナイメージをダイエットさせる方法としてaptなどのパッケージマネージャのcacheを消す,マルチステージビルドを使う,Alpine Linux, Scratchなどの容量の小さいイメージをベースに使うなどのテクニックを使います. ここではあまりそれらについて深く言及はしません.

これらのテクニックを用いて,コンテナイメージの容量を小さくするのは良いのですが, どこまでコンテナイメージを小さくさせるべきなのでしょう. 極端な話,コンテナ上で動くアプリケーションがGoの静的リンクされたバイナリであれば,以下のようなコンパイルしたあとscratchに配置するだけで軽量なイメージが作れると思います.

FROM golang:1.15-alpine3.12 as builder

WORKDIR /src

RUN apk add --no-cache make

COPY go.mod .
COPY go.sum .
RUN go mod download

COPY . /src
RUN make build

FROM scratch
COPY --from=builder /src/bin/hello /hello

ENTRYPOINT ["/hello"]

Kubernetesなどで使われているCoreDNSの公式イメージなどはまさにそのように提供されています. しかし,ベースにscratchを用いるとシェルも無ければlsもcatもありません. そのためコンテナ内で docker execkubectl exec でシェルを実行したり,デバッグ用のコマンドを使うことができません.

これは運用時つらいからベースをalpineやubuntuにしているという人が自分の観測範囲では多い気がしています. 検証などでシェルで入るのが手っ取り早いから,catでコンテナ内部のファイルの状態を見たいからなどの理由だと思います.

コンテナイメージにシェルやcoreutilって必要なのか?

そもそもcatやls,bashがコンテナイメージになくてもデバッグや検証とかできるんじゃないか?という仮説を考えてみます. 一応,ここではコンテナ内部に入ってアプリケーション自体に変更を加えるということは除外します. シェルやcoreutilなどが必要のない,アプリケーションだけが入ったコンテナイメージを運用する上で障害になることをいくつか考えました.

  • Q1. そもそもメトリクスやログを適切に吐いていればシェルがなくても問題ないのでは
    • ログやメトリクスを適切に吐くことが一番難しい
    • 必要なメトリクスがなかったらコンテナホストか,コンテナ内にツールを入れたり,メトリクスツールに変更を加える必要がある.
  • Q2. コンテナの中にシェルで入る必要はなくて,コンテナホストからプロセスが見えるんだからホストから見れば良くないか?
    • コンテナノードにSSHできないサービスも有る (AWS Fargateなどコンテナノホストを抽象化するサービスなど)
  • Q3. コンテナの中にログファイルがあるけどシェルがなかったら読めないよね?
    • ログはstdout, stderrに吐いてたりしてください
  • Q4. そもそもアプリケーションがシェルに依存しているんだが?
    • それはシェル入りのコンテナ使ってください
  • Q5. 開発ではシェルが入ったコンテナを使っているが,本番環境と開発環境で違うコンテナイメージを使うというのはアンチパターンじゃないか?
    • 環境ごとに違うイメージを使うのはアンチパターンだと思うけど,ビルド引数とかでビルドステージを切り替えて,ビルドステージ,実行ステージで分けて使えばいいんではないだろうか.

コンテナイメージ内に検証などでシェルやcatなどのコマンドを使ってデバッグしたかったら十分にOveservabilityの仕組みを導入することとが必要で,コンテナ内に状態を持たせないことが必要かなと. また,外部コマンドに依存したアプリケーションはその外部コマンドの脆弱性を定期的にスキャンしながら同梱したイメージを使えばよいのではなかろうかと.

まあ,PythonRubyみたいな言語はそもそも言語処理系をアプリケーションのコンテナイメージに入れる必要があるので,今回の話はシングルバイナリを吐ける言語が前提なんてですけどね….

まとめ

コンテナイメージをどこまで小さくするかはワークロード次第ですが,シングルバイナリのアプリケーションに限って言えば,十分にOveservabilityが確保できていて,開発にも支障がなければscratchベースだったり,シェルや他の依存が全く無くても良いと考えています. でもそれでも心理的にシェルがあったほうが安心だという気持ちはとても良くわかります.しかしマニュアル運用をできないようにシェルをコンテナイメージから無くすべきなのかもなあと思う今日このごろです.

もうちょっと運用サイドのエンジニアの方々の意見も聞いてみたいと思います. 実際,自社でscratchベースのコンテナイメージを自分たちで作ってホストしている方々ってどれぐらいいらっしゃるんでしょうね.

シェルなどが入っていないコンテナイメージをデバッグ,検証したかったら,KubernetesのEphemeral Containerあたりを使ってデバッグするのも良いと思います. 次回あたりにKubernetesのEphemeral Containerの機能を深堀りして使ってみたときのアレコレを書いていきます.