Dockerのmulti stage buildsを使ってPythonモジュールをインストールしてみる

2020.03.25

はじめに

データアナリティクス事業本部のkobayashiです。

最近Fargateでバッチ処理を行っていたのですが、タスクを実行してから終了するまでが想定よりも時間がかかってしまいその原因を調べました。 するとタスクを実行してからPythonスクリプトで処理を開始するまでの時間が100秒近くかかっていたことが原因だと判明しましたので、ECRにあるイメージのサイズを確認すると750MBとというとても巨大なイメージサイズになってしまっていました。

Fargate内のバッチ処理はPythonで行っているのですがその中で必要なモジュールが複数あり、イメージを作成する際にそれをインストールしていたのですが、その作業でイメージが肥大化していました。

そこでECRにpushする最終的なイメージを軽量化するために色々調べているとDockerのmulti-stage buildsという方法があることがわかり、実践したところうまくイメージサイズを280MBまで削ることができ結果タスクを実行してからPythonスクリプトで処理を開始するまでの時間を100秒から50秒にまで半分に減らすことができました。 その内容をまとめます。

環境

  • Docker 19.03.1
  • Dockerベースイメージ python:3.7.6-slim

Docker multi-stage buildsとは

Dockerのmulti-stage buildsは17.05以降で利用できるビルド方法で、一つのDockerfileの中でビルド用イメージの作成とデプロイ用イメージを作成できます。ビルド用のイメージ中でプログラムをコンパイルし、実行ファイルだけをデプロイ用のイメージに取り込むことでデプロイ用のイメージサイズを減らすことができます。

Multi-stage builds are a new feature requiring Docker 17.05 or higher on the daemon and client. Multistage builds are useful to anyone who has struggled to optimize Dockerfiles while keeping them easy to read and maintain.

Use multi-stage builds | Docker Documentation

multi-stage buildsを使う場合と使わない場合の比較

multi-stage buildsの有用性を確認するために2つのパターンでイメージを作成してみます。

multi-stage buildsを使わないパターン

Pythonでインストールするモジュールは以下のモジュールになります。

awscli
boto3
pandas
pyodbc
chardet
openpyxl
statistics
scipy

これらのモジュールをインストールしたイメージを作成するために以下のようなDockerfileファイルを作成します。

FROM python:3.7.6-slim

COPY requirements.txt /root/

RUN apt-get update \
&& apt-get install unixodbc -y \
&& apt-get install unixodbc-dev -y \
&& apt-get install tdsodbc -y \
&& apt-get install python-dateutil -y
RUN pip3 install -r /root/requirements.txt

pyodbcモジュールを使うためapt-getでいくつかパッケージをインストールします。

Dockerイメージの内容

以下のコマンドでビルドします。

$ docker build -t python_siglestage .

docker historyでイメージの履歴を見てみると最後のイメージレイヤでPythonモジュールをインストールしていますが、このイメージのサイズは353MBとなっています。

$ docker history python_siglestage

IMAGE CREATED CREATED BY SIZE
bc2700458206 3 minutes ago /bin/sh -c pip3 install -r /root/requirement… 353MB
42771eca050e 3 minutes ago /bin/sh -c apt-get update && apt-get instal… 271MB
...
2 weeks ago /bin/sh -c apt-get update && apt-get install… 7.03MB
2 weeks ago /bin/sh -c #(nop) ADD file:e5a364615e0f69616… 69.2MB

multi-stage buildsを使ったパターン

ではmulti-stage buildsを使ってイメージをビルドしてみます。 インストールするモジュールは先程と同一になります。

以下がmulti-stage buildsを使った場合のDockerfileファイルになります。

内容を説明すると、# buildから# deploy前までがビルド用のイメージを作成している部分になります。 先程のmulti-stage buildsを使わないパターンのビルドとほぼ同じですが、唯一異なるのがFROM python:3.7.6-slim as build-stageの部分で、ビルドステージにasbuild-stageの名前を付けています。

# deploy以降がデプロイ用のイメージ作成している部分になリます。 ここで行っていることは前段で作成したビルド用イメージbuild-stage内からジョブを動かすのに最低限必要な実行ファイルをデプロイ用のイメージにcopyしています。

# build
FROM python:3.7.6-slim as build-stage

COPY requirements.txt /root/

RUN apt-get update \
&& apt-get install unixodbc -y \
&& apt-get install unixodbc-dev -y \
&& apt-get install tdsodbc -y \
&& apt-get install --reinstall build-essential -y \
&& apt-get install python-dateutil -y
RUN pip3 install -r /root/requirements.txt

# deploy
FROM python:3.7.6-slim

## ビルド用イメージ内でPythonモジュールをビルドした際のキャッシュを利用する
COPY --from=build-stage /root/.cache/pip /root/.cache/pip
COPY --from=build-stage /root/requirements.txt /root
## Pythonモジュールで使用するライブラリをコピー
COPY --from=build-stage /usr/lib/x86_64-linux-gnu/libodbc.so.2 /usr/lib/x86_64-linux-gnu/
COPY --from=build-stage /usr/lib/x86_64-linux-gnu/libodbcinst.so.2 /usr/lib/x86_64-linux-gnu/
COPY --from=build-stage /usr/lib/x86_64-linux-gnu/libkrb5.so.3 /usr/lib/x86_64-linux-gnu/
COPY --from=build-stage /usr/lib/x86_64-linux-gnu/libgssapi_krb5.so.2 /usr/lib/x86_64-linux-gnu/
COPY --from=build-stage /usr/lib/x86_64-linux-gnu/libltdl.so.7 /usr/lib/x86_64-linux-gnu/
COPY --from=build-stage /usr/lib/x86_64-linux-gnu/libk5crypto.so.3 /usr/lib/x86_64-linux-gnu/
COPY --from=build-stage /usr/lib/x86_64-linux-gnu/libkrb5support.so.0 /usr/lib/x86_64-linux-gnu/
COPY --from=build-stage /lib/x86_64-linux-gnu/libkeyutils.so.1 /lib/x86_64-linux-gnu/
COPY --from=build-stage /usr/lib/x86_64-linux-gnu/libk5crypto.so.3 /usr/lib/x86_64-linux-gnu/
COPY --from=build-stage /usr/lib/x86_64-linux-gnu/libkrb5support.so.0 /usr/lib/x86_64-linux-gnu/
COPY --from=build-stage /lib/x86_64-linux-gnu/libkeyutils.so.1 /lib/x86_64-linux-gnu/

### パッケージのアップデートとPythonモジュールのインストール
RUN apt-get update
RUN pip3 install -r /root/requirements.txt \
&& rm -rf /root/.cache/pip

Dockerイメージの内容

先ほどと同じようにビルドしてイメージの履歴を見てみると、最後のイメージレイヤでPythonモジュールをインストールしていますが、先程は353MBだったイメージサイズが282MBと削減されています。

$ docker build -t python_multistage .
$ docker history python_multistage

IMAGE CREATED CREATED BY SIZE
4288fa8e0cfd About a minute ago /bin/sh -c pip3 install -r /root/requirement… 282MB
ce85f0f24d2f About a minute ago /bin/sh -c apt-get update 17.4MB
79b1ddecc7a3 About a minute ago /bin/sh -c #(nop) COPY file:d8c2595223d59998… 22.4kB
...
2 weeks ago /bin/sh -c apt-get update && apt-get install… 7.03MB
2 weeks ago /bin/sh -c #(nop) ADD file:e5a364615e0f69616… 69.2MB

ECRでイメージサイズを比較

python_siglestagepython_multistageをECRにpushしてイメージサイズを見てみると、イメージサイズが削減されていることがわかります。

(補足)さらにイメージサイズを削減

このブログを公開後更にイメージサイズを削減する記述を教えていただきました。

# build
FROM python:3.7.6-slim as build-stage

COPY requirements.txt /root/

RUN apt-get update \
&& apt-get install unixodbc -y \
&& apt-get install unixodbc-dev -y \
&& apt-get install tdsodbc -y \
&& apt-get install --reinstall build-essential -y \
&& apt-get install python-dateutil -y
RUN pip3 install -r /root/requirements.txt

# deploy
FROM python:3.7.6-slim

## ビルド用イメージ内でPythonモジュールをビルドした際のキャッシュを利用する
COPY --from=build-stage /root/.cache/pip /root/.cache/pip
COPY --from=build-stage /root/requirements.txt /root
## Pythonモジュールで使用するライブラリをコピー
COPY --from=build-stage /usr/lib/x86_64-linux-gnu/libodbc.so.2 /usr/lib/x86_64-linux-gnu/
COPY --from=build-stage /usr/lib/x86_64-linux-gnu/libodbcinst.so.2 /usr/lib/x86_64-linux-gnu/
COPY --from=build-stage /usr/lib/x86_64-linux-gnu/libkrb5.so.3 /usr/lib/x86_64-linux-gnu/
COPY --from=build-stage /usr/lib/x86_64-linux-gnu/libgssapi_krb5.so.2 /usr/lib/x86_64-linux-gnu/
COPY --from=build-stage /usr/lib/x86_64-linux-gnu/libltdl.so.7 /usr/lib/x86_64-linux-gnu/
COPY --from=build-stage /usr/lib/x86_64-linux-gnu/libk5crypto.so.3 /usr/lib/x86_64-linux-gnu/
COPY --from=build-stage /usr/lib/x86_64-linux-gnu/libkrb5support.so.0 /usr/lib/x86_64-linux-gnu/
COPY --from=build-stage /lib/x86_64-linux-gnu/libkeyutils.so.1 /lib/x86_64-linux-gnu/
COPY --from=build-stage /usr/lib/x86_64-linux-gnu/libk5crypto.so.3 /usr/lib/x86_64-linux-gnu/
COPY --from=build-stage /usr/lib/x86_64-linux-gnu/libkrb5support.so.0 /usr/lib/x86_64-linux-gnu/
COPY --from=build-stage /lib/x86_64-linux-gnu/libkeyutils.so.1 /lib/x86_64-linux-gnu/

### パッケージのアップデートとPythonモジュールのインストール
RUN apt-get update
RUN pip3 install -r /root/requirements.txt \
&& rm -rf /root/.cache/pip
#### apt-getでパッケージのインストールに使用したキャッシュの削除
RUN apt-get clean && rm -rf /var/lib/apt/lists/*

またここで作成したDockerfileGitHub - orisano/minid: minid is Dockerfile minifier for reducing the number of layers.を使用してminimizeしてdocker buildすることで更に削減できます。その結果以下の通り更に削減できました。情報ありがとうございました。

まとめ

当たり前の話ですが、コンテナを使う場合はイメージサイズが小さければ小さいほど良いです。そのための手段としてDockerのmulti-stage buildsは非常に有効です。コンテナで使うイメージを作成する際は積極的に使っていきたいと思います。

最後まで読んで頂いてありがとうございました。