Cloud Container Builder によるリーン コンテナ開発
Google Cloud Japan Team
Java アプリケーションを作るには、ソース コード、アプリケーション ライブラリ、ビルド システム、ビルド システムの依存ファイル等々、そしてもちろん JDK というように、たくさんのファイルが必要です。ところが、アプリケーションをコンテナ化すると、これらのファイルが残ってしまい、コンテナ サイズが大きくなってしまうことがあります。そして、時間の経過とともに Docker レジストリとコンテナ ランタイムの間で不要なビットの格納や移動が行われ、膨大な時間とコストが無駄になります。
コンテナ サイズをできるかぎり小さく保つためには、ランタイム コンテナのアセンブルからアプリケーションのビルド(およびビルドに必要なツール)を分離すべきです。Google Cloud Container Builder を使えば、まさにそのようにして以前よりも大幅にリーンなコンテナを構築できます。リーン コンテナは、高速にロードでき、ストレージにかかるコストを節約します。
コンテナのレイヤ
Dockerfile の各行はコンテナに新しいレイヤを追加します。例を見てみましょう。この例では、“lots-of-data” というローカル ディレクトリをコンテナの “data” ディレクトリにコピーし、すぐにそれを削除しています。この操作は一見無害に見えますが、実はそうではありません。
それは、以前のレイヤを読み出し専用にする Docker の “copy-on-write” 戦略に理由があります。一連のコマンドでコンテナ ランタイムに不要なデータを生成し、同じコマンドでそれを削除しないときは、そのスペースは回収できなくなるのです。
Spinnaker コンテナ
Spinnaker は、Netflix が開発したオープンソースのクラウド用継続的デリバリ ツールで、Netflix や Google を含むパートナーのコミュニティによって活発にメンテナンスされています。このツールはマイクロサービス アーキテクチャになっており、個々のコンポーネントは Groovy と Java で書かれ、Gradle でビルドされます。Spinnaker はマイクロサービス コンテナを Quay.io にパブリッシュします。どのサービスもほぼ同じ Dockerfile を使用しているので、ここでは例として Gate サービスを使います。以前は次のような Dockerfile を使っていました。
Spinnaker では、ビルドには Gradle が使われており、この場合は Debian パッケージをビルドしています。Gradle はすばらしいツールですが、ビルドのために大量のライブラリをダウンロードします。これらのライブラリは、パッケージのビルドには必要不可欠ですが、実行時には不要です。実行時に必要な依存ファイルは、すべてパッケージ自体にバンドリングされています。
先ほども触れたように、Dockerfile の各行はコンテナに新しいレイヤを作ります。そのレイヤでデータが生成され、同じコマンドで削除されなければ、そのスペースは回収できません。この例の場合、Gradle はビルドのために数百個の MB 級のライブラリを “cache” ディレクトリにダウンロードしていますが、それらのライブラリは削除されていません。
この場合は、2 つの “RUN” コマンドを結合して、ビルド完了時にすべてのファイル(ソース コードを含む)を削除したほうが、効率的にビルドできます。
これで、最終的なコンテナ サイズは 652 MB から 284 MB へと減少し、56 % も不要なファイルを削除できました。しかし、もっと良い方法はないのでしょうか。
Cloud Container Builder を使用する
Container Builder を使用すると、ランタイム コンテナのアセンブルからアプリケーションのビルドをさらにはっきりと分離できます。Container Builder チームは、git や docker、gcloud など、コマンドライン インターフェースの開発ツールをまとめた一連の Docker コンテナをパブリッシュしています。これらのツールを使えば、ワンステップでアプリケーションをビルドし、別のワンステップで最終的なランタイム環境をアセンブルする “cloudbuild.yaml” ファイルを定義できます。
ここで使用する “cloudbuild.yaml” ファイルは次のとおりです。
何が起きているのかを知るために、個々のステップをじっくり見ていきましょう。
ステップ 1 : アプリケーションのビルド
私たちのリーン ランタイム コンテナには “dpkg” が含まれていないので、Gradleの “buildDeb” タスクは使用しません。その代わり、同じディレクトリ構造を作成してコピーを楽にする “installDist” という別のタスクを使います。
ステップ 2 : ランタイム コンテナのアセンブル
次に、Docker build を実行してランタイム コンテナをアセンブルします。ランタイム コンテナの定義には、“Dockerfile.slim” という別のファイルを使います。その内容は次のとおりです。
ステップ 1 で行った Gradle の “installDist” タスクは、すでに必要なディレクトリ構造(“gate/bin/”、“gate/lib/” など)を作っているので、単純にそれをターゲット コンテナにコピーできます。
Linux ベース レイヤとしてとてもリーンな Alpine、“openjdk:8u111-jre-alpine” を使っていることが、スペース節約の大きな要因の 1 つになっています。さらに、アプリケーションのビルドで必要だった大きな JDK ファイルを取り除き、JRE だけをコピーしています。
ステップ 3 : イメージのレジストリへのパブリッシュ
最後に、コンテナにタグとしてコミット ハッシュと “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