Google Cloud
Cloud Container Builder によるリーン コンテナ開発
Java アプリケーションを作るには、ソース コード、アプリケーション ライブラリ、ビルド システム、ビルド システムの依存ファイル等々、そしてもちろん JDK というように、たくさんのファイルが必要です。ところが、アプリケーションをコンテナ化すると、これらのファイルが残ってしまい、コンテナ サイズが大きくなってしまうことがあります。そして、時間の経過とともに Docker レジストリとコンテナ ランタイムの間で不要なビットの格納や移動が行われ、膨大な時間とコストが無駄になります。
コンテナ サイズをできるかぎり小さく保つためには、ランタイム コンテナのアセンブルからアプリケーションのビルド(およびビルドに必要なツール)を分離すべきです。Google Cloud Container Builder を使えば、まさにそのようにして以前よりも大幅にリーンなコンテナを構築できます。リーン コンテナは、高速にロードでき、ストレージにかかるコストを節約します。
コンテナのレイヤ
Dockerfile の各行はコンテナに新しいレイヤを追加します。例を見てみましょう。FROM busybox
COPY ./lots-of-data /data
RUN rm -rf /data
CMD ["/bin/sh"]
この例では、“lots-of-data” というローカル ディレクトリをコンテナの “data” ディレクトリにコピーし、すぐにそれを削除しています。この操作は一見無害に見えますが、実はそうではありません。
それは、以前のレイヤを読み出し専用にする Docker の “copy-on-write” 戦略に理由があります。一連のコマンドでコンテナ ランタイムに不要なデータを生成し、同じコマンドでそれを削除しないときは、そのスペースは回収できなくなるのです。
Spinnaker コンテナ
Spinnaker は、Netflix が開発したオープンソースのクラウド用継続的デリバリ ツールで、Netflix や Google を含むパートナーのコミュニティによって活発にメンテナンスされています。このツールはマイクロサービス アーキテクチャになっており、個々のコンポーネントは Groovy と Java で書かれ、Gradle でビルドされます。Spinnaker はマイクロサービス コンテナを Quay.io にパブリッシュします。どのサービスもほぼ同じ Dockerfile を使用しているので、ここでは例として Gate サービスを使います。以前は次のような Dockerfile を使っていました。
FROM java:8
COPY . workdir/
WORKDIR workdir
RUN GRADLE_USER_HOME=cache ./gradlew buildDeb -x test
RUN dpkg -i ./gate-web/build/distributions/*.deb
CMD ["/opt/gate/bin/gate"]
Spinnaker では、ビルドには Gradle が使われており、この場合は Debian パッケージをビルドしています。Gradle はすばらしいツールですが、ビルドのために大量のライブラリをダウンロードします。これらのライブラリは、パッケージのビルドには必要不可欠ですが、実行時には不要です。実行時に必要な依存ファイルは、すべてパッケージ自体にバンドリングされています。
先ほども触れたように、Dockerfile の各行はコンテナに新しいレイヤを作ります。そのレイヤでデータが生成され、同じコマンドで削除されなければ、そのスペースは回収できません。この例の場合、Gradle はビルドのために数百個の MB 級のライブラリを “cache” ディレクトリにダウンロードしていますが、それらのライブラリは削除されていません。
この場合は、2 つの “RUN” コマンドを結合して、ビルド完了時にすべてのファイル(ソース コードを含む)を削除したほうが、効率的にビルドできます。
FROM java:8
COPY . workdir/
WORKDIR workdir
RUN GRADLE_USER_HOME=cache ./gradlew buildDeb -x test && \
dpkg -i ./gate-web/build/distributions/*.deb && \
cd .. && \
rm -rf workdir
CMD ["/opt/gate/bin/gate"]
これで、最終的なコンテナ サイズは 652 MB から 284 MB へと減少し、56 % も不要なファイルを削除できました。しかし、もっと良い方法はないのでしょうか。
Cloud Container Builder を使用する
Container Builder を使用すると、ランタイム コンテナのアセンブルからアプリケーションのビルドをさらにはっきりと分離できます。Container Builder チームは、git や docker、gcloud など、コマンドライン インターフェースの開発ツールをまとめた一連の Docker コンテナをパブリッシュしています。これらのツールを使えば、ワンステップでアプリケーションをビルドし、別のワンステップで最終的なランタイム環境をアセンブルする “cloudbuild.yaml” ファイルを定義できます。
ここで使用する “cloudbuild.yaml” ファイルは次のとおりです。
steps:
- name: 'java:8'
env: ['GRADLE_USER_HOME=cache']
entrypoint: 'bash'
args: ['-c', './gradlew gate-web:installDist -x test']
- name: 'gcr.io/cloud-builders/docker'
args: ['build',
'-t', 'gcr.io/$PROJECT_ID/$REPO_NAME:$COMMIT_SHA',
'-t', 'gcr.io/$PROJECT_ID/$REPO_NAME:latest',
'-f', 'Dockerfile.slim', '.']
images:
- 'gcr.io/$PROJECT_ID/$REPO_NAME:$COMMIT_SHA'
- 'gcr.io/$PROJECT_ID/$REPO_NAME:latest'
何が起きているのかを知るために、個々のステップをじっくり見ていきましょう。
ステップ 1 : アプリケーションのビルド
- name: 'java:8'
env: ['GRADLE_USER_HOME=cache']
entrypoint: 'bash'
args: ['-c', './gradlew gate-web:installDist -x test']
私たちのリーン ランタイム コンテナには “dpkg” が含まれていないので、Gradleの “buildDeb” タスクは使用しません。その代わり、同じディレクトリ構造を作成してコピーを楽にする “installDist” という別のタスクを使います。
ステップ 2 : ランタイム コンテナのアセンブル
- name: 'gcr.io/cloud-builders/docker'
args: ['build',
'-t', 'gcr.io/$PROJECT_ID/$REPO_NAME:$COMMIT_SHA',
'-t', 'gcr.io/$PROJECT_ID/$REPO_NAME:latest',
'-f', 'Dockerfile.slim', '.']
次に、Docker build を実行してランタイム コンテナをアセンブルします。ランタイム コンテナの定義には、“Dockerfile.slim” という別のファイルを使います。その内容は次のとおりです。
FROM openjdk:8u111-jre-alpine
COPY ./gate-web/build/install/gate /opt/gate
RUN apk --nocache add --update bash
CMD ["/opt/gate/bin/gate"]
ステップ 1 で行った Gradle の “installDist” タスクは、すでに必要なディレクトリ構造(“gate/bin/”、“gate/lib/” など)を作っているので、単純にそれをターゲット コンテナにコピーできます。
Linux ベース レイヤとしてとてもリーンな Alpine、“openjdk:8u111-jre-alpine” を使っていることが、スペース節約の大きな要因の 1 つになっています。さらに、アプリケーションのビルドで必要だった大きな JDK ファイルを取り除き、JRE だけをコピーしています。
ステップ 3 : イメージのレジストリへのパブリッシュ
images:
- 'gcr.io/$PROJECT_ID/$REPO_NAME:$COMMIT_SHA'
- 'gcr.io/$PROJECT_ID/$REPO_NAME:latest'
最後に、コンテナにタグとしてコミット ハッシュと “latest” を付け、Google Cloud Container Registry(grc.io)に対して、これらのタグが付いたコンテナをプッシュします。
まとめ
Container Builder を使用すると、最終的なコンテナ サイズは 91.6 MB まで減りました。これは、初期の Dockerfile よりも 85 % 小さく、改良版と比べても 68 % 削減したことになります。*大幅なサイズ縮小は、ビルド環境とランタイム環境を分離し、最終的なコンテナのためにリーンなベース レイヤを選択したことによるものです。
このアプローチを個々のマイクロサービスに適用したところ、同様の結果が得られました。最終的なコンテナのフットプリントの合計は、約 6 GB から 1 GB 未満に縮小しました。
* この投稿は米国時間 5 月 5 日、Spinnaker Team の Software Engineer である Travis Tomsu によって投稿されたもの(投稿はこちら)の抄訳です。
- By Travis Tomsu, Software Engineer, Spinnaker Team