y0ngb1n

Aben Blog

欢迎来到我的技术小黑屋ヾ(◍°∇°◍)ノ゙
github

転載 / Docker イメージのスリム化 & 最適化

なぜストレージがこれほど安価な今日でも、Docker イメージをスリム化する必要があるのか?

小さなイメージの利点#

  1. ビルド / デプロイの加速
    ストレージリソースは比較的安価ですが、ネットワーク IO は限られています。帯域幅が限られている場合、1G のイメージと 10M のイメージをデプロイする際の時間差は、分単位と秒単位の差になる可能性があります。特に障害が発生し、サービスが他のノードに移動される場合、この時間は非常に貴重です。
  2. セキュリティの向上、攻撃面の削減
    小さなイメージは無駄なプログラムが少ないことを意味し、攻撃のターゲットを大幅に減少させることができます。
  3. ストレージコストの削減

小さなイメージの作成原則#

  1. 最小のベースイメージを選択する
  2. レイヤーを減らし、不要なファイルを削除する
    実際にイメージを作成する過程では、単にレイヤーを統合することは避けるべきです。Docker のキャッシュメカニズムを十分に活用し、共通のレイヤーを抽出してビルドを加速することを学ぶ必要があります。
    • 依存ファイルと実際のコードファイルを別々のレイヤーにする
    • チーム / 会社が共通のベースイメージを採用するなど
  3. マルチステージビルドを使用する
    ビルド段階と実行段階で必要な依存環境は異なることが多いです。例えば、Golang で書かれたプログラムは、実行時にはバイナリファイルだけが必要です。Node.js に関しては、最終的に実行されるのはパッケージ化された js ファイルだけで、node_modules 内の数千の依存関係を含む必要はありません。

ベースイメージ#

  • distroless

    "Distroless" イメージは、アプリケーションとそのランタイム依存関係のみを含みます。標準の Linux ディストリビューションに期待されるパッケージマネージャー、シェル、またはその他のプログラムは含まれていません。

    distroless は Google が提供する、ランタイム環境のみを含むイメージで、パッケージマネージャーや shell などの他のプログラムは含まれていません。他の依存関係がない場合、これは良い選択です。

  • alpine

    Alpine Linux は、musl libc と busybox に基づいたセキュリティ指向の軽量 Linux ディストリビューションです。

    alpine は、muslbusybox に基づく安全な Linux ディストリビューションです。小さくても機能が充実しており、10M 未満ですが、パッケージマネージャーと shell 環境が含まれているため、実際の使用やデバッグに非常に役立ちます。ただし、alpine はより小さな muslc を使用して glibc を置き換えているため、一部のアプリケーションが使用できなくなる可能性があり、再コンパイルが必要です。

  • scratch
    scratch は空のイメージで、一般的にベースイメージの構築に使用されます。例えば、alpine イメージの Dockerfilescratch から始まります。

    FROM scratch
    ADD alpine-minirootfs-20190228-x86_64.tar.gz /
    CMD ["/bin/sh"]
    
  • busybox

一般的に、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

レイヤーを減らし、不要なファイルを削除する#

  1. ファイルを削除する際は行を跨がない

    # 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 のほぼすべてのコマンドがレイヤーを生成するため、ファイルを削除する際に、下の各レイヤーが読み取り専用であるため、上のレイヤーでファイルを削除することは、単にそのファイルを隠すだけだからです。

  2. 単一行コマンドを使用する
    削除コマンドは一行にまとめる必要があるだけでなく、レイヤーのメカニズムにより、依存関係のインストールに関する共通のコマンドも、1 つの RUN コマンドで生成することが望ましく、最終的なレイヤー数を減らします。

  3. 依存パッケージとソースコードプログラムを分離し、レイヤーのキャッシュを十分に活用する
    これはベストプラクティスです。実際の開発過程では、依存パッケージはあまり変動しませんが、開発中のソースコードは頻繁に変更されます。実際のコードが 10M で、依存関係が 1G の場合、COPY の際に直接 COPY ... を行うと、コードを変更するたびにこのレイヤーのキャッシュが無効になり、コピーやイメージリポジトリへのプッシュにかかる時間が無駄になります。COPY 文を分けることで、毎回 push する際に頻繁に変更されるコードレイヤーだけを変更でき、依存関係全体を一緒に変更する必要はありません。

  4. .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"]

この記事は長いため、マルチステージビルドについては詳しく説明しません。詳細は、マルチステージビルドを参照してください。

奇抜なテクニック#

  1. dive を使用して Docker イメージのレイヤーを確認し、イメージサイズを削減する分析を行うことができます。

  2. docker-slim を使用すると、自動的にイメージサイズを削減できます。Web アプリケーションに特に有用です。

  3. ソフトウェアをインストールする際に依存関係を削除する

    # ubuntu
    apt-get install -y --no-install-recommends
    
    # alpine
    apk add --no-cache && apk del build-dependencies
    
    # centos
    yum install -y ... && yum clean all
    
  4. --flatten パラメータを使用してレイヤーを減らす(推奨しません)

  5. docker-squash を使用してレイヤーを圧縮する

異なる言語の例#

Ruby(Rails)#

  1. 本番に必要な依存関係のみをインストールする

  2. 不要な依存ファイルを削除する

    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
    
  3. フロントエンドの node_modules およびキャッシュファイルを削除する

    rm -rf node_modules tmp/cache app/assets vendor/assets spec
    

上記の内容は、マルチステージビルドと組み合わせて実現できます。

Golang#

Golang はマルチステージビルドを使用した後、バイナリファイルのみが残ります。この時点で最適化するには、upx などのツールを使用してバイナリファイルのサイズを圧縮するだけです。

参考資料#


本文の著者:mohuishou
原文リンク:https://lailin.xyz/post/51252.html
著作権声明:著作権は著者に帰属します。転載する場合は出典を明記してください!

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。