Google Cloud Platform

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 % 削減したことになります。

-sFEBfRXPFx6_Z9UBflUbcjTZ4gPirElGi_iaxltZ7LCUnqQViuZDhbzB1m2gNyiXQwSekJFe4BEWDs1F2tT2m-VNy5tnZiBMIWePo49_j53WH-Y33OR-wPmx3pfFJMPgXvYP3EOiu3n.PNG

*大幅なサイズ縮小は、ビルド環境とランタイム環境を分離し、最終的なコンテナのためにリーンなベース レイヤを選択したことによるものです。

bTaPHAdmeZBY4LN2sdRQ8_XgLxdLX8qWyAK1wiSYj_J14MZc0EJ2c-k-39a8PWy25uANTuy5cAQPZPSWIftpA86sOnztWK-ruvK3N5JgDL4RjFYrfcwsealMluOqEzoF6sxrwma3j8dq.PNG

このアプローチを個々のマイクロサービスに適用したところ、同様の結果が得られました。最終的なコンテナのフットプリントの合計は、約 6 GB から 1 GB 未満に縮小しました。

* この投稿は米国時間 5 月 5 日、Spinnaker Team の Software Engineer である Travis Tomsu によって投稿されたもの(投稿はこちら)の抄訳です。

- By Travis Tomsu, Software Engineer, Spinnaker Team