🐋

巨大なコンテナイメージのビルド&プッシュを高速化したい

2024/01/11に公開

想定する問題

ビルド後のイメージは非常に重たいため、 ビルド・プッシュの速度向上のために docker build のキャッシュを活用したい。
しかし、何らかの理由でキャッシュを活用できない。

自分が直面した状況は以下の通り。

  • 機械学習系のライブラリを使用するため、ビルド後のイメージが非常に重い
  • 上記機械学習系ライブラリの実行環境を構築するために、apt-update && apt-upgrade 実行の上で特定のライブラリのインストールが必要
  • docker build の度に apt-update && apt-upgrade が走り、以降の重たい処理と docker push にてキャッシュが効かない

これにより、ちょっとしたコードの変更でも、push完了までに非常に時間がかかっていた。

以下Dockerfileのサンプル。

RUN apt-update && apt-upgrade -y
RUN apt-get install ... # 必要なライブラリをインストール
RUN pip3 install ...    # 必要なライブラリをインストール ## 重たい処理

選択した解決策

一つのイメージを、以下二つに分離。

  • 非常に重い実行環境構築部分を扱うコンテナイメージ(以降ランタイムイメージと呼称)
  • アプリケーションコードを扱うコンテナイメージ(以降アプリケーションイメージと呼称)

アプリケーションイメージのベースイメージに、ランタイムイメージを使用。
コンテナの重たい部分を切り離すことが出来る。

結果的に、アプリケーションイメージの更新時はビルド、プッシュ共にキャッシュを大いに活用でき、速度が向上した。

イメージとしては、 AWS Lambda を、Lambda Layer と Lambda コードに分離するのに近い。

構成図

container_architecture

トレードオフ

一つのコンテナイメージに全部詰め込む場合と比較して、以下の点が不便。

  • ランタイムイメージを定期的に更新する手間が増える
  • 構成が複雑になり、直感的に理解できない

特に構成が複雑になる点。

何故二つに分割したのか、各イメージの役割・使い方・デプロイするユースケースなどをドキュメント化し、他のメンバー向けに情報を残す必要がある。

所感

目的は達成。
アプリケーションコード修正時、リポジトリへ反映する時間を大幅に短縮できた。

ぶっちゃけ他に良い方法あると思うが、思いつかなかった。

付録

ディレクトリ構成サンプル

.
├── app
│   └── 以下メインロジック
├── application_image
│   └── Dockerfile
├── aws_resources
│   └── dev
├── push_application_image.sh
├── push_runtine_env_image.sh
├── runtime_env
│   └── Dockerfile
└── scripts
    └── ecr.sh

サンプルコード

見やすさのためにいくらか改変したため、動作は保証できません。

application_image/Dockerfile

ARG image_name

FROM ${image_name}

# ... 以下 app/ のコピーなど、アプリケーションコードの動作に必要な処理

runtime_env/Dockerfile

# 任意のイメージ、任意のライブラリインストール

scripts/ecr.sh

#!/bin/bash

set -eu

ECR_URI="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com"

function login_ecr() {
  aws ecr get-login-password --region ${AWS_REGION} | \
    docker login --username AWS --password-stdin ${ECR_URI}
}

function build_image() {
    local image_tag=$1
    local dockerfile_path=${2:-.}

    docker build -t ${image_tag} --file ${dockerfile_path} .
}

function push_to_ecr() {
    local AWS_ECR_REPOSITORY_NAME=$1
    local version_tag=$2

    local image_latest_tag=${AWS_ECR_REPOSITORY_NAME}:latest
    local image_version_tag=${AWS_ECR_REPOSITORY_NAME}:${version_tag}

    login_ecr

    # push as latest
    docker tag ${image_latest_tag} ${ECR_URI}/${AWS_ECR_REPOSITORY_NAME}
    docker push ${ECR_URI}/${image_latest_tag}

    # push as inputed tag
    docker tag ${image_latest_tag} ${ECR_URI}/${image_version_tag}
    docker push ${ECR_URI}/${image_version_tag}
}

aws_resources/dev

# 必要なパラメータを設定
AWS_REGION=""
AWS_ACCOUNT_ID=""
AWS_ECR_RUNTIME_ENV_IMAGE_NAME=""
AWS_ECR_APPLICATION_IMAGE_NAME=""

push_runtine_env_image.sh

#!/bin/bash
set -eu

version_tag=$1
aws_env=${2:-dev}

. aws_resources/${aws_env}
. ./scripts/ecr.sh

dockerfile_path='runtime_env/Dockerfile'
image_tag=${AWS_ECR_RUNTIME_ENV_IMAGE_NAME}
image_name=${ECR_URI}/${image_tag}

docker build -t ${image_tag} --file ${dockerfile_path} --build-arg image_name=${image_name} .

build_image ${image_tag} ${dockerfile_path}

push_to_ecr ${image_tag} ${version_tag}

実行例

./push_runtine_env_image.sh 0.1.0

push_application_image.sh

#!/bin/bash
set -eu

version_tag=$1
aws_env=${2:-dev}

. aws_resources/${aws_env}
. ./scripts/ecr.sh

dockerfile_path='application_env/Dockerfile'
image_tag=${AWS_ECR_APPLICATION_IMAGE_NAME}
image_name=${ECR_URI}/${image_tag}

docker build -t ${image_tag} --file ${dockerfile_path} --build-arg image_name=${image_name} .

push_to_ecr ${image_tag} ${version_tag}

実行例

./push_runtine_env_image.sh 0.1.0

Discussion