よーぐるとのブログ

技術ネタを中心。私的なことを徒然と綴っていきます。

Spring Boot2.3でCloud Native Buildpacksを利用したDocker Imageを作成する

概要

Spring Boot2.3から導入されたCloud Native Buildpackを使ったDocker Image作成の作成について掘り下げていきます。

Github Repository

github.com

Spring Boot 2.3でのDocker Image作成機能

Spring Boot 2.3ではDocker Daemonが起動していれば1コマンドでCloud Native Buildpackを利用したOCI標準なDocker Imageを作成することができるようになりました。 これによりDockerfileを書かずに誰でも簡単にDocker Imageを作成することができます。

# build dockage image
$ mvn spring-boot:build-image

# execute application
$ docker run -it -p 8080:8080 build-image:0.0.1-SNAPSHOT

### 8080ポートでアプリケーションが立ち上がる

とはいえ、このプラグインがやっているのはDockerfileを開発者の代わりに作ってくれている、ということではありません。Cloud Native Buildpackを利用したOCI標準なDocker Imageを作成してくれています。

そして、spring-boot-maven-pluginではデフォルトでPaketo BuildpacksというCloud Foundry発のOSSを利用しています。

Cloud Native Buildpacks

Cloud Native BuildpacksはCNCFのSandboxプロジェクトであり、HerokuやPivotalで利用されていたBuildpackの技術を統合させたものになります。 これにより、今までは特定のPaaSプラットフォームでしか利用できなかった技術がDocker Image同等のものとして色んなクラウド上で扱えるようになりました。

f:id:yoghurt1131:20200920144842p:plain
cloud native buildpackの変遷. HerokuとPivotalのbuildpackが合流して今に至る.

Cloud Native Buildpackの構成

Cloud Native BuildpackのImageはBuildpack, Lifecycle, Stackによって作られたBuilder Imageにて、実際に動かすアプリケーションをbuildすることによって作られます。

f:id:yoghurt1131:20200920145244p:plain

コンポーネントの役割は以下のとおりです。

Buildpack

アプリケーションを動かすためのビルド、あるいはそのための環境構築などを担うコンポーネントです。 Buildpackの実行はdetect/buildの2つのフェーズから構成されます。Buildpackは選ばれたら必ず実行されるというわけではなく、detectフェーズでこのbuildpackを実行すべきかどうか判断し、detectの条件を全て満たしたらbuild, そうじゃなかったらこのBuildpackはスキップ、といった形で処理が走ります。

detectフェーズではこのBuildpackをbuildするために必要なファイルがあるかどうかなどをチェックします。NPM buildpackであればpackage.jsonを探す、といった具合です。

buildフェーズでは文字通りソースコードのビルドなどを行います。たいていのBuilder Imageは複数のBuildpackを組み合わせて実行されるため、1つのBuildpackで全てを実行するわけではありません。中には依存関係を解決するためのBuildpackなどもあります。

Lifecycle

Buildpackより一段上の階層に立って、Buildpackの実行管理や最終的に作成するDocker Imageの構築を担います。こちらもフェーズが分かれており、detection/analysis/build/exportという4つの手順が存在します。

detectionではbuildフェーズで利用するbuildpackの選定を行います。analysisではメタデータやレイヤーキャッシュを展開するといった処理を行います。 buildでは実際にソースコードから実行可能なartifactへのビルド処理を行い、最後のexportでDocker Imageへの書き出しを行います。

ちょっと気になったのはdetectionフェーズ。Buildpackにもdetectフェーズが存在するので、個々のBuildpackのdetectに任せてもいいように見えます。Buildpack同士での競合などを防ぐなどの役割があるのでしょうか。

Stack

Buildpack, LifecycleのベースとなるOS Imageを表します。

build imageとrun imageの2種類が用意されている通り、ビルド時と実行時でStackの違うImageが利用されています。Ubuntuベースのイメージなどが多いようです。

Paketo Buildpacks

Paketo BuildpacksはCloud Native Buildpacksの規格に沿ったGo製のOSSです。 各種言語をサポートしており、 Cloud Foundryプロジェクトによって作成されています。

実は、今回紹介している spring-boot:build-imageでは、デフォルトのBuilder ImageにPaketo Buildpacksが使われています。(github)

f:id:yoghurt1131:20200920152202p:plain

mvn spring-boot:build-imageは何をしているか

さて、改めてmvn spring-boot:build-image 実行時のログを追ってみます。(興味があるのはbuild-imageの部分なので、そこまでのログは少し省略しています)

<@buildImage>-<⎇ master>-> mvn spring-boot:build-image
[INFO] Scanning for projects...
[INFO]
[INFO] ----------------------< dev.yoghurt:build-image >-----------------------
[INFO] Building build-image 0.0.1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] >>> spring-boot-maven-plugin:2.3.3.RELEASE:build-image (default-cli) > package @ build-image >>>
[INFO]
...
[INFO]
[INFO] --- maven-surefire-plugin:2.22.2:test (default-test) @ build-image ---
...
...
[INFO] --- spring-boot-maven-plugin:2.3.3.RELEASE:build-image (default-cli) @ build-image ---
[INFO] Building image 'docker.io/library/build-image:0.0.1-SNAPSHOT'
[INFO]
[INFO]  > Pulling builder image 'gcr.io/paketo-buildpacks/builder:base-platform-api-0.3' 0%
[INFO]  > Pulling builder image 'gcr.io/paketo-buildpacks/builder:base-platform-api-0.3' 100%
[INFO]  > Pulled builder image 'gcr.io/paketo-buildpacks/builder@sha256:3284c03370a31854fee91c71c037081406ce2d69b5b7e3926a6a9e134f7e0d2f'
[INFO]  > Pulling run image 'docker.io/paketobuildpacks/run:base-cnb' 0%
[INFO]  > Pulling run image 'docker.io/paketobuildpacks/run:base-cnb' 24%
[INFO]  > Pulling run image 'docker.io/paketobuildpacks/run:base-cnb' 100%
[INFO]  > Pulled run image 'paketobuildpacks/run@sha256:86edad85f315d115ca1784c4a72abbde0b12650c9b993be95fd4a7bcc8900f70'
[INFO]  > Executing lifecycle version v0.9.1
[INFO]  > Using build cache volume 'pack-cache-d1d004e77dd4.build'
[INFO]
[INFO]  > Running creator
[INFO]     [creator]     ===> DETECTING
[INFO]     [creator]     5 of 17 buildpacks participating
[INFO]     [creator]     paketo-buildpacks/bellsoft-liberica 3.2.0
[INFO]     [creator]     paketo-buildpacks/executable-jar    3.1.0
[INFO]     [creator]     paketo-buildpacks/apache-tomcat     2.2.0
[INFO]     [creator]     paketo-buildpacks/dist-zip          2.2.0
[INFO]     [creator]     paketo-buildpacks/spring-boot       3.2.0
[INFO]     [creator]     ===> ANALYZING
[INFO]     [creator]     Restoring metadata for "paketo-buildpacks/bellsoft-liberica:helper" from app image
[INFO]     [creator]     Restoring metadata for "paketo-buildpacks/bellsoft-liberica:java-security-properties" from app image
[INFO]     [creator]     Restoring metadata for "paketo-buildpacks/bellsoft-liberica:jre" from app image
[INFO]     [creator]     Restoring metadata for "paketo-buildpacks/bellsoft-liberica:jvmkill" from app image
[INFO]     [creator]     Restoring metadata for "paketo-buildpacks/executable-jar:class-path" from app image
[INFO]     [creator]     Restoring metadata for "paketo-buildpacks/spring-boot:helper" from app image
[INFO]     [creator]     Restoring metadata for "paketo-buildpacks/spring-boot:spring-cloud-bindings" from app image
[INFO]     [creator]     Restoring metadata for "paketo-buildpacks/spring-boot:web-application-type" from app image
[INFO]     [creator]     ===> RESTORING
[INFO]     [creator]     ===> BUILDING
[INFO]     [creator]
[INFO]     [creator]     Paketo BellSoft Liberica Buildpack 3.2.0
[INFO]     [creator]       https://github.com/paketo-buildpacks/bellsoft-liberica
[INFO]     [creator]       Build Configuration:
[INFO]     [creator]         $BP_JVM_VERSION              11.*            the Java version
[INFO]     [creator]       Launch Configuration:
[INFO]     [creator]         $BPL_JVM_HEAD_ROOM           0               the headroom in memory calculation
[INFO]     [creator]         $BPL_JVM_LOADED_CLASS_COUNT  35% of classes  the number of loaded classes in memory calculation
[INFO]     [creator]         $BPL_JVM_THREAD_COUNT        250             the number of threads in memory calculation
[INFO]     [creator]         $JAVA_TOOL_OPTIONS                           the JVM launch flags
[INFO]     [creator]       BellSoft Liberica JRE 11.0.8: Reusing cached layer
[INFO]     [creator]       Launch Helper: Reusing cached layer
[INFO]     [creator]       JVMKill Agent 1.16.0: Reusing cached layer
[INFO]     [creator]       Java Security Properties: Reusing cached layer
[INFO]     [creator]
[INFO]     [creator]     Paketo Executable JAR Buildpack 3.1.0
[INFO]     [creator]       https://github.com/paketo-buildpacks/executable-jar
[INFO]     [creator]       Process types:
[INFO]     [creator]         executable-jar: java org.springframework.boot.loader.JarLauncher
[INFO]     [creator]         task:           java org.springframework.boot.loader.JarLauncher
[INFO]     [creator]         web:            java org.springframework.boot.loader.JarLauncher
[INFO]     [creator]
[INFO]     [creator]     Paketo Spring Boot Buildpack 3.2.0
[INFO]     [creator]       https://github.com/paketo-buildpacks/spring-boot
[INFO]     [creator]       Launch Helper: Reusing cached layer
[INFO]     [creator]       Web Application Type: Reusing cached layer
[INFO]     [creator]       Spring Cloud Bindings 1.6.0: Reusing cached layer
[INFO]     [creator]       Image labels:
[INFO]     [creator]         org.opencontainers.image.title
[INFO]     [creator]         org.opencontainers.image.version
[INFO]     [creator]         org.springframework.boot.spring-configuration-metadata.json
[INFO]     [creator]         org.springframework.boot.version
[INFO]     [creator]     ===> EXPORTING
[INFO]     [creator]     Reusing layer 'paketo-buildpacks/bellsoft-liberica:helper'
[INFO]     [creator]     Reusing layer 'paketo-buildpacks/bellsoft-liberica:java-security-properties'
[INFO]     [creator]     Reusing layer 'paketo-buildpacks/bellsoft-liberica:jre'
[INFO]     [creator]     Reusing layer 'paketo-buildpacks/bellsoft-liberica:jvmkill'
[INFO]     [creator]     Reusing layer 'paketo-buildpacks/executable-jar:class-path'
[INFO]     [creator]     Reusing layer 'paketo-buildpacks/spring-boot:helper'
[INFO]     [creator]     Reusing layer 'paketo-buildpacks/spring-boot:spring-cloud-bindings'
[INFO]     [creator]     Reusing layer 'paketo-buildpacks/spring-boot:web-application-type'
[INFO]     [creator]     Adding 1/1 app layer(s)
[INFO]     [creator]     Reusing layer 'launcher'
[INFO]     [creator]     Adding layer 'config'
[INFO]     [creator]     Adding label 'io.buildpacks.lifecycle.metadata'
[INFO]     [creator]     Adding label 'io.buildpacks.build.metadata'
[INFO]     [creator]     Adding label 'io.buildpacks.project.metadata'
[INFO]     [creator]     Adding label 'org.opencontainers.image.title'
[INFO]     [creator]     Adding label 'org.opencontainers.image.version'
[INFO]     [creator]     Adding label 'org.springframework.boot.spring-configuration-metadata.json'
[INFO]     [creator]     Adding label 'org.springframework.boot.version'
[INFO]     [creator]     *** Images (45ec6ed55538):
[INFO]     [creator]           docker.io/library/build-image:0.0.1-SNAPSHOT
[INFO]
[INFO] Successfully built image 'docker.io/library/build-image:0.0.1-SNAPSHOT'
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

さきほども記載したとおり、Paketo BuildpacksのBuilderが使われていることがわかります。

また、Executing lifecycle以降を見ると、 ===> DETECTING....===> BUILDINGとLifecycleの各フェーズが順に実行されていることがわかります。

DETECTINGフェーズでは、5つのBuildpackがヒットしていることがわかります。

[INFO]     [creator]     5 of 17 buildpacks participating
[INFO]     [creator]     paketo-buildpacks/bellsoft-liberica 3.2.0
[INFO]     [creator]     paketo-buildpacks/executable-jar    3.1.0
[INFO]     [creator]     paketo-buildpacks/apache-tomcat     2.2.0
[INFO]     [creator]     paketo-buildpacks/dist-zip          2.2.0
[INFO]     [creator]     paketo-buildpacks/spring-boot       3.2.0

また、そのうち3つのBuildpackが、(おそらく)Buildpackのdetectで成功しBUILDINGフェーズで実行されています。

[INFO]     [creator]     ===> BUILDING
[INFO]     [creator]
[INFO]     [creator]     Paketo BellSoft Liberica Buildpack 3.2.0
...
[INFO]     [creator]     Paketo Executable JAR Buildpack 3.1.0
...
[INFO]     [creator]     Paketo Spring Boot Buildpack 3.2.0

それぞれのBuildpackが何をしているか、各BuildpackのGithubリポジトリから確認していきます。

Paketo BellSoft Liberica Buildpack

Paketo BellSoft Liberica Buildpackは名前の通り Liberica JDKのBuildpackになります。このBuildpackは他のBuildpackがjreまたはjdkを利用するときに実行されます。

JDKが必要な場面(主にbuilder image)では以下が提供されます。

  • build, cacheとマークされているlayerへのJDK追加
  • build layerへの JAVA_HOME 設定
  • build layerへの JDK_HOME 設定

また、JREが必要な場面(主にrun image)では以下が提供されます。

  • JREの追加
  • JAVA_HOMEの追加
  • -XX:ActiveProcessorCount(プロセッサーカウントの指定)設定
  • $MALLOC_ARENA_MAX設定
  • local DNSが有効な場合にJVM DNSキャッシュを無効化
  • metadata.buildがtrueの場合にレイヤーにbuild, cacheマークを付ける
  • metadata.launchがtrueの場合に launchマークを付ける
  • launchが付いたlayerに jvmkillを追加
  • launchが付いたlayerにMemory Calculatorを追加

JDK, JREのインストールやクラウド環境向けの環境変数の設定、jvmkillやmemory calculatorなどの追加が行われています。

jvmkillやmemory calculatorは元々Cloud Foundryのプロダクトでしたが、途中でPaketo側に取り込まれたようです。実際のコードは paketo-buildpacks/libjvmにあります。

Paketo Executable JAR Buildpack

Paketo Executable JAR Buildpack<アプリケーションルート>/META-INF/MANIFEST.MFMain-classを含むときに実行されるBuildpackです。このBuildpackでは以下が実行されます。

  • JREのインストールをリクエス
  • <アプリケーションルート>をクラスパスに追加
  • <アプリケーションルート>/META-INF/MANIFEST.MFがクラスパスにある場合にentries(おそらくMain class?)をクラスパスに追加
  • executable-jar, task, webのProcess Typeの指定

最後の記述について、実際に作成したイメージに対して pack inspect-imageを実行し中身を確認するとProcess Tyeが3つ指定されているのがわかります。 Process TypeはDockerのEntryPointのような位置づけのもののようです。

Processes:
  TYPE                  SHELL        COMMAND        ARGS
  web (default)         bash         java           org.springframework.boot.loader.JarLauncher
  executable-jar        bash         java           org.springframework.boot.loader.JarLauncher
  task                  bash         java           org.springframework.boot.loader.JarLauncher

Paketo Spring Boot Buildpack

Paketo Spring Boot Buildpackは、<アプリケーションルート>/META-INF/MANIFEST.MFにSpring-Boot-Versionの指定がある場合に実行されます。以下が実行されます。

  • org.springframework.boot.version, org.springframework.boot.spring-configuration-metadata.json, org.opencontainers.image.title, org.opencontainers.image.versionをImageラベルに追加
  • Mavenの依存関係を取得
  • Spring Cloud Bindingsの追加
  • <アプリケーションルート>/META-INF/dataflow-configuration-metadata.propertieが存在する場合に、org.springframework.cloud.dataflow.spring-configuration-metadata.jsonをImageラベルに追加。
  • <アプリケーションルート>/META-INF/MANIFEST.MFにSpring-Boot-Layers-Indexが存在する場合に定義されたアプリケーションレイヤーを追加。
  • Reactiveアプリケーションの場合に$BPL_JVM_THREAD_COUNT 50を設定

Spring Boot周りの設定が盛りだくさんです。リアクティブ向けの設定も行ってくれるのはありがたいですね。

Docker ImageのExport

Lifecycleの最後のフェーズでは、paketo-buildpacksの各種レイヤーがDocker Imageに追加され、labelの設定などが行われています。 このように、コマンド1つで様々な設定を盛り込んだDocker Imageが作成されているのがわかりました。

===> EXPORTING
Reusing layer 'paketo-buildpacks/bellsoft-liberica:helper'
Reusing layer 'paketo-buildpacks/bellsoft-liberica:java-security-properties'
Reusing layer 'paketo-buildpacks/bellsoft-liberica:jre'
Reusing layer 'paketo-buildpacks/bellsoft-liberica:jvmkill'
Reusing layer 'paketo-buildpacks/executable-jar:class-path'
Reusing layer 'paketo-buildpacks/spring-boot:helper'
Reusing layer 'paketo-buildpacks/spring-boot:spring-cloud-bindings'
Reusing layer 'paketo-buildpacks/spring-boot:web-application-type'
Adding 1/1 app layer(s)
Reusing layer 'launcher'
Adding layer 'config'
Adding label 'io.buildpacks.lifecycle.metadata'
Adding label 'io.buildpacks.build.metadata'
Adding label 'io.buildpacks.project.metadata'
Adding label 'org.opencontainers.image.title'
Adding label 'org.opencontainers.image.version'
Adding label 'org.springframework.boot.spring-configuration-metadata.json'
Adding label 'org.springframework.boot.version'
*** Images (45ec6ed55538):
      docker.io/library/build-image:0.0.1-SNAPSHOT

まとめ

mvn spring-boot:build-imageコマンドで作成されるImageはPaketo Buildpacksを利用したOCI標準のImageとなっており、様々な設定が自動で行われているのを調べることができました。

個人的に嬉しいのはやはりjvmkillやMemory Calculatorで、これによりOutOfMemory時のJavaプロセスKillやメモリ設定などをやってくれるので、アプリケーション開発者はいっそうソースコードだけに集中することができるようになります。カスタマイズをしたければ自分でbuilder imageを作成して設定を追加することもできますが、このままでも十分プロダクション環境で利用できそうです。

今回初めてCloud Native Buldpacksについて色々調べてみましたが、一つ一つの設定細かく理解仕切ることができなかったりわからない概念がまだまだ残っていたりします。Spring Bootをクラウド上で簡単に動かせるという方向性は大歓迎ですし、実際に動いている基盤は理解していきたいので引き続き色々調べてみようと思います。