なぜストレージがこれほど安価な今日でも、Docker イメージをスリム化する必要があるのか?
小さなイメージの利点#
- ビルド / デプロイの加速
ストレージリソースは比較的安価ですが、ネットワーク IO は限られています。帯域幅が限られている場合、1G
のイメージと10M
のイメージをデプロイする際の時間差は、分単位と秒単位の差になる可能性があります。特に障害が発生し、サービスが他のノードに移動される場合、この時間は非常に貴重です。 - セキュリティの向上、攻撃面の削減
小さなイメージは無駄なプログラムが少ないことを意味し、攻撃のターゲットを大幅に減少させることができます。 - ストレージコストの削減
小さなイメージの作成原則#
- 最小のベースイメージを選択する
- レイヤーを減らし、不要なファイルを削除する
実際にイメージを作成する過程では、単にレイヤーを統合することは避けるべきです。Docker のキャッシュメカニズムを十分に活用し、共通のレイヤーを抽出してビルドを加速することを学ぶ必要があります。- 依存ファイルと実際のコードファイルを別々のレイヤーにする
- チーム / 会社が共通のベースイメージを採用するなど
- マルチステージビルドを使用する
ビルド段階と実行段階で必要な依存環境は異なることが多いです。例えば、Golang
で書かれたプログラムは、実行時にはバイナリファイルだけが必要です。Node.js
に関しては、最終的に実行されるのはパッケージ化されたjs
ファイルだけで、node_modules
内の数千の依存関係を含む必要はありません。
ベースイメージ#
-
"Distroless" イメージは、アプリケーションとそのランタイム依存関係のみを含みます。標準の Linux ディストリビューションに期待されるパッケージマネージャー、シェル、またはその他のプログラムは含まれていません。
distroless
は Google が提供する、ランタイム環境のみを含むイメージで、パッケージマネージャーやshell
などの他のプログラムは含まれていません。他の依存関係がない場合、これは良い選択です。 -
Alpine Linux は、musl libc と busybox に基づいたセキュリティ指向の軽量 Linux ディストリビューションです。
alpine
は、musl
とbusybox
に基づく安全な Linux ディストリビューションです。小さくても機能が充実しており、10M 未満ですが、パッケージマネージャーとshell
環境が含まれているため、実際の使用やデバッグに非常に役立ちます。ただし、alpine
はより小さなmuslc
を使用してglibc
を置き換えているため、一部のアプリケーションが使用できなくなる可能性があり、再コンパイルが必要です。 -
scratch
scratch
は空のイメージで、一般的にベースイメージの構築に使用されます。例えば、alpine
イメージのDockerfile
はscratch
から始まります。FROM scratch ADD alpine-minirootfs-20190228-x86_64.tar.gz / CMD ["/bin/sh"]
一般的に、distroless
は相対的に安全ですが、実際の使用中に依存関係の追加やデバッグの問題に直面する可能性があります。alpine
はより小さく、パッケージマネージャーを備えており、使用習慣により適していますが、muslc
は互換性の問題を引き起こす可能性があります。一般的に、私は alpine
をベースイメージとして使用することを選びます。それ以外にも、Docker Hub では、一般的な debian
のイメージも基本機能のみを含む小さなイメージを提供しています。
ベースイメージの比較#
ここでは、ベースイメージを直接プルしてイメージサイズを確認します。観察することで、alpine
は約 5M で、debian
の 20 分の 1 であることがわかります。
alpine latest 5cb3aa00f899 3 weeks ago 5.53MB
debian latest 0af60a5c6dd0 3 weeks ago 101MB
ubuntu 18.04 47b19964fb50 7 weeks ago 88.1MB
ubuntu latest 47b19964fb50 7 weeks ago 88.1MB
alpine 3.8 3f53bb00af94 3 months ago 4.41MB
上記を見ると、差はあまりないように感じますが、実際には異なる言語のベースイメージが異なるベースイメージを使用して作成された tag
を提供しています。以下に ruby
のイメージを例に、異なるベースイメージの違いを確認します。デフォルトの latest
イメージは 881MB
ですが、alpine
はわずか 50MB
未満で、この差は非常に顕著です。
ruby latest a5d26127d8d0 4 weeks ago 881MB
ruby alpine 8d8f7d19d1fa 4 weeks ago 47.8MB
ruby slim 58dd4d3c99da 4 weeks ago 125MB
レイヤーを減らし、不要なファイルを削除する#
-
ファイルを削除する際は行を跨がない
# dockerfile 1 FROM alpine RUN wget https://github.com/mohuishou/scuplus-wechat/archive/1.0.0.zip # dockerfile 2 FROM alpine RUN wget https://github.com/mohuishou/scuplus-wechat/archive/1.0.0.zip RUN rm 1.0.0.zip # dockerfile 3 FROM alpine RUN wget https://github.com/mohuishou/scuplus-wechat/archive/1.0.0.zip && rm 1.0.0.zip
test 3 351a80e99c22 5 seconds ago 5.53MB test 2 ad27e625b8e5 49 seconds ago 6.1MB test 1 165e2e0df1d3 About a minute ago 6.1MB
1 と 2 のサイズは同じですが、3 は 0.5MB 小さくなっています。これは、
docker
のほぼすべてのコマンドがレイヤーを生成するため、ファイルを削除する際に、下の各レイヤーが読み取り専用であるため、上のレイヤーでファイルを削除することは、単にそのファイルを隠すだけだからです。 -
単一行コマンドを使用する
削除コマンドは一行にまとめる必要があるだけでなく、レイヤーのメカニズムにより、依存関係のインストールに関する共通のコマンドも、1 つの RUN コマンドで生成することが望ましく、最終的なレイヤー数を減らします。 -
依存パッケージとソースコードプログラムを分離し、レイヤーのキャッシュを十分に活用する
これはベストプラクティスです。実際の開発過程では、依存パッケージはあまり変動しませんが、開発中のソースコードは頻繁に変更されます。実際のコードが10M
で、依存関係が1G
の場合、COPY
の際に直接COPY ...
を行うと、コードを変更するたびにこのレイヤーのキャッシュが無効になり、コピーやイメージリポジトリへのプッシュにかかる時間が無駄になります。COPY
文を分けることで、毎回push
する際に頻繁に変更されるコードレイヤーだけを変更でき、依存関係全体を一緒に変更する必要はありません。 -
.dockerignore
を使用する
Git
を使用する際に.gitignore
でファイルを無視できるように、docker build
の際にも.dockerignore
を使用して Docker コンテキスト内のファイルを無視できます。これにより、不要なファイルのインポートを減らすだけでなく、セキュリティを向上させ、設定ファイルがイメージにパッケージされるのを防ぐことができます。
マルチステージビルド#
マルチステージビルドは、レイヤーを減らす一つの方法でもあります。マルチステージビルドを使用することで、最終的なイメージは生成された実行可能ファイルと必要なランタイム依存関係のみを含むことができ、イメージのサイズを大幅に削減できます。
GO
言語の例を挙げると、実行時には最終的にコンパイルされたバイナリファイルだけが必要であり、GO
言語自体や拡張パッケージ、コードファイルは不要です。しかし、コンパイル時にはこれらの依存関係が必要です。この場合、マルチステージビルドの方法を使用して、最終的なイメージのサイズを減らすことができます。
# golang イメージをビルダーイメージとして使用
FROM golang:1.12 as builder
WORKDIR /go/src/github.com/go/helloworld/
COPY app.go .
RUN go build -o app .
# コンパイルが完了したら、alpine イメージを最終的なベースイメージとして使用
FROM alpine:latest as prod
RUN apk --no-cache add ca-certificates
WORKDIR /root/
# ビルダーからコンパイルされたバイナリファイルをコピー
COPY --from=builder /go/src/github.com/go/helloworld/app .
CMD ["./app"]
この記事は長いため、マルチステージビルドについては詳しく説明しません。詳細は、マルチステージビルドを参照してください。
奇抜なテクニック#
-
dive
を使用して Docker イメージのレイヤーを確認し、イメージサイズを削減する分析を行うことができます。 -
docker-slim
を使用すると、自動的にイメージサイズを削減できます。Web アプリケーションに特に有用です。 -
ソフトウェアをインストールする際に依存関係を削除する
# ubuntu apt-get install -y --no-install-recommends # alpine apk add --no-cache && apk del build-dependencies # centos yum install -y ... && yum clean all
-
--flatten
パラメータを使用してレイヤーを減らす(推奨しません) -
docker-squash
を使用してレイヤーを圧縮する
異なる言語の例#
Ruby(Rails)#
-
本番に必要な依存関係のみをインストールする
-
不要な依存ファイルを削除する
bundle install --without development:test:assets -j4 --retry 3 --path=vendor/bundle \ # 不要なファイルを削除(キャッシュされた *.gem, *.o, *.c) && rm -rf vendor/bundle/ruby/2.5.0/cache/*.gem \ && find vendor/bundle/ruby/2.5.0/gems/ -name "*.c" -delete \ && find vendor/bundle/ruby/2.5.0/gems/ -name "*.o" -delete
-
フロントエンドの
node_modules
およびキャッシュファイルを削除するrm -rf node_modules tmp/cache app/assets vendor/assets spec
上記の内容は、マルチステージビルドと組み合わせて実現できます。
Golang#
Golang
はマルチステージビルドを使用した後、バイナリファイルのみが残ります。この時点で最適化するには、upx
などのツールを使用してバイナリファイルのサイズを圧縮するだけです。
参考資料#
- Docker コンテナイメージのスリム化のための 3 つの小技
- ベースイメージ | Docker スリム化再考
- 『Docker ベストプラクティス』マインドマップ
- Docker —— 入門から実践まで
- Docker 基本原理簡析
- Ruby on Rails — 小さな Docker イメージ
本文の著者:mohuishou
原文リンク:https://lailin.xyz/post/51252.html
著作権声明:著作権は著者に帰属します。転載する場合は出典を明記してください!