SPECIALIST

多様な専門性を持つNRIデジタル社員のコラム、インタビューやインサイトをご紹介します。

BACK

DockerfileレスなJavaアプリケーション開発を試してみる

こんにちは、NRIデジタルの島です。
今回の記事では、各クラウドプラットフォームに依存しないニュートラルな話題に触れてみたいと思います。

オンプレからクラウドへのリフト&シフトはじめ、システムのモダナイゼーションが進んでいる昨今かと思いますが、モダナイゼーションの方式の一つであるコンテナ化は、Java関連システムにおいてどのくらい進んでいるのでしょうか?

VMWareにおける以下調査結果によると、Javaの主要フレームワークであるSpringアプリケーションの実行環境について、「Kubernetes(以下k8s)」の利用が現在も拡大しており、ヒアリング対象の回答者のうち、「65%」が使用していると確認されました。

※出典 VMWare Tanzu The State of Spring 2024

ユースケースは長時間稼働するWebアプリケーションや短命なアプリケーション、Function型のワークロードなどさまざまであり、現状JavaやSpring界隈でも、引き続きアプリケーションのモダナイゼーションの意識は高く、コンテナオーケストレーションツールである「k8s」は重要なプラットフォームだと読み取れます。

筆者もこれまで何度かk8s上でのJavaアプリケーション開発を経験しております。利用するk8sディストリビューションは、Amazon Elastic Kubernetes Service(EKS)Google Kubernetes Engine(GKE)などのマネージドなプラットフォームが多いですが、何を利用するにせよ、以下の2点がアプリケーション開発時のボトルネックでした。

① Dockerfileのメンテナンスが苦痛

アプリケーションを動作させるコンテナのベースとなるDockerfileは、一度作れば終わりというわけではなく、定期的にメンテナンスしていかなければなりません。Production ReadyなDockerfileは、ベストプラクティスに準拠しながら、CVEなどのセキュリティリスクへの対応にも都度追従していかなければならないため、このメンテナンスはかなり苦痛です。
Dockerfileを書くベストプラクティス

開発時は本格的なものでなくてもいいという考え方もありますが、以降本番環境へ出していくためには、できれば開発段階からちゃんとしたDockerfileで作成したコンテナイメージで、アプリケーションの動作を担保させておきたいものです。

② k8s上でのアプリケーションの動作確認・テストが面倒

単体テスト完了以降は、実際のk8sテスト環境にデプロイしたアプリケーションの機能改修や不具合発生時の調査などをトライアンドエラーで実施したい場面が多いです。アプリケーション改修後、コンパイル、Dockerイメージビルド、テスト、リポジトリへプッシュ、k8sクラスタへのデプロイと、動作を確認するまでに非常に多くのステップがあり、時間と手間がかかります。

また、動作確認後のリソースの掃除(Podなどのk8sリソースやイメージの削除など)も非常に面倒です。せっかくアジリティの高いコンテナアーキテクチャを採用しているので、もっと効率よく快適に動かしていきたいところです。

上記2点のうち、本記事では①の解決アプローチについて、検証を交えながら考えていきたいと思います。

なお、②については、Googleによって開発された「Skafford」というオープンソースツールにより解消することができます。
Skaffold 2.0 Documentation

Skaffoldはローカルの資材(ソースコードや設定ファイルなど)の変更をリアルタイムに監視し、変更を検知するとモジュールのビルドからk8sクラスタへのデプロイ、環境のクリーンアップまで一連のプロセスを自動で実施してくれるため、開発が非常に効率的で快適になります。今後のk8s開発において非常に有益なツールになってくると筆者は考えていますので、是非機会があれば本記事の続編として本ツールについてもご紹介していきたいと思います。

代表的なJavaコンテナイメージ作成方法

まずJavaにおける「Dockerコンテナのイメージ作成方法」にはどのようなものがあるか整理してみます。

① Dockerfileを作成してイメージビルド

もっともデファクトな方法で、Dockerfileというコンテナイメージを作成するための命令を記述したファイルを作り、イメージをビルドしていく方法です。

図中のContainer Imageの「OCI」とは「Open Container Initiative」のことで、標準化されたコンテナイメージのフォーマットやランタイムです。OCI ImageはDockerイメージと互換性があり、コンテナのポータビリティと相互運用性を高めるために広く採用されています。
https://opencontainers.org/

② 正常に稼働している実行環境からリバースエンジニアリング

実際にVMなどで稼働している環境からコンテナイメージを生成する方法です。

各クラウドで代表的なサービスとしては以下です。

e.g.
AWS
App2Container
Google Cloud
Migrate to container
Migrate for Anthos
Azure
Azure Migrate

③ ソースコードから直接イメージを作成

Dockerfileが不要でソースコードから直接コンテナを生成可能です。

主に以下2つのソリューションがあります。(詳細は後述)

  • Cloud Native Buildpacks
  • Jib

上記のうち、本記事で筆者が検証していきたいのは「③ソースコードから直接イメージを作成」する方法です。前述したような「①Dockerfileを作成してイメージビルド」での課題が解決され、実用的であるかを確認していきたいです。

「②正常に稼働している実行環境からリバースエンジニアリング」はどちらかというとコンテナ環境への移行の色が強く、コンテナを継続的に運用していくためのユースケースでは適切ではないため、本記事では触れません。

Dockerfile運用の難しさ

では、あらためて、筆者の視点で「Dokcerfileを運用していく際の難しさ」について具体的に以下に整理してみます。

セキュリティ

前述した通り、Dockerfileはベストプラクティスに準じて作成し、本番稼働に必要なセキュリティを常に順守するようなセキュアなイメージをメンテナンスし続ける必要があります。

  • 適切なベースイメージの採用
  • 最適なユーザ設定や環境設定
  • 脆弱性の確認
    ※コンテナ脆弱性診断ツール(trivygrype)でのチェック必須
などなど…

レイヤー構造管理

DockerfileにはまずFROM句のベースイメージを指定しますが、ビルド時にはベースイメージ取り込み後、以降に記載している各命令(COPYとかRUNとか…)に従い、ソフトウェアのインストールやコンテナ内のファイルシステムへのファイル追加などの変更を加えていき、最終的なイメージを構築します。この個々の変更差分のステージが「レイヤー」です。

このレイヤー数を最適に設計し、効率的なイメージにすることは重要です。例えば、Dockerのビルドでは、レイヤーごとに変更差分を確認し、変更がなければキャッシュを利用します。以下のように1つのレイヤーに更新頻度の異なる複数のステージを含めると、変更が不要なステージの処理も実施することになり、非効率(イメージ更新時のビルド時間も延びる)になります。

更新頻繁が高いものと更新頻度が低いものをレイヤー分けし、更新頻度が高いものを後ろの記述する(※)など、効率的なレイヤー設計が必要ですが、これには相当のスキルが必要になります。

※Dockerは命令順にキャッシュ内で再利用ができるものを探し、見つからない時点で以降のキャッシュを削除します

パフォーマンスチューニング

本番環境で健全にJavaアプリケーションを動作させ続けるためには、アプリケーションの特性を把握し、常に適切なパフォーマンスを維持するように「Java Virtual Machine(以下JVM)」のメモリパラメータなどの設定や、障害発生時のトレーサビリティなどオブザーバビリティの観点で必要なパラメータ設定をしていかなければなりません。もちろんオンプレでも同様ではあるのですが、オンプレ環境と比べコンテナ環境でのチューニングはより難しく、これにはJVMの高度な知識が必要となります。

Production Readyなコンテナイメージには上記に挙げたような事項を考慮したDockerfileが必要であり、これを作成、メンテナンスしていくには高いスキルセットが必要ですし、相当な労力と工数もかかってきます。これらを解消するソリューションとして期待しているのが、「Cloud Native Buildpacks」や「Jib」といったツールです。

Dockerfile不要なコンテナ生成ツール

「Cloud Native Buildpacks」や「Jib」は、前述した通り、アプリケーションのソースコードからDockerfileなしで直接コンテナイメージ(OCIイメージ)を作成できるツールです。単にコンテナイメージを作成してくれるというだけではなく、高品質でProduction Readyなコンテナイメージを作成してくれます。概要について簡単に以下で説明します。

Cloud Native Buildpacks

Cloud Native Buildpacks(以下CNB)は、2011年にHerokuによって「Buildpacks」というツールとして考案され、その後2018年にHerokuとPivotalがプロジェクトを開始し、以降CNCF(Cloud Native Computing Foundation)で維持される流れとなりました。

公式ページより
※執筆時点ではCNCFのIncubating Projectとなります

CNBの主要なコンポーネントに「ビルダー」があります。このビルダーはビルドに必要なコンポーネント(Buildpacksなど)とベースとなるOSやランタイムを含む「スタック」を提供し、コードを自動的に検出・解析して、適切な環境設定や依存関係のインストールを実施し、コンテナイメージ(OCIイメージ)をビルドします。

※CNBのコンセプトや仕組み、ビルダーの詳細については、以下公式ページをご参照ください。
What is a buildpack?・Cloud Native Buildpacks
What is a builder? · Cloud Native Buildpacks

この「ビルダー」は複数存在し、各提供元はCNBの仕様を踏襲しながら、独自の機能を追加実装しています。主要なビルダーには以下があります。

Paketo Buildpacks

Cloud Foundry Foundation」で開発・提供されているビルダー。非常に高機能で、現状開発も活発に行われており、定期的なアップデートが提供されている。ユースケースに応じて、builder:base(標準構成)、builder:full(フル機能)、builder:tiny(最小構成)など、異なるビルダースタックを選択可能。
Paketo Buildpacks

Heroku Buildpacks

「Salesforce」で開発・提供されているビルダー。主にHerokuプラットフォーム向けに最適化されたビルダー。
buildpack | Heroku Dev Center

Google Cloud Buildpacks

「Google」で開発・提供されているビルダー。Google Cloud上でのアプリケーションのビルドに最適化されており、Cloud RunCloud FunctionsApp Engineに最適化されたビルダーが提供されている。また、特定の環境に特化しない汎用ビルダーも提供されている。
Google CloudのBuildpack|Buildpacks

どのビルダーもJavaだけではなく、Node.jsやPythonなど、Java以外の言語にも対応されています。

Jib

Googleが提供しており、CNBと同様、Dockerfile不要でソースコードからコンテナイメージを直接生成可能です。CNBとの大きな違いは、Javaのみに対応している点です。メリットは(リモートリポジトリを使用する限りは)Dockerデーモンが不要である点、また、Javaのみである分、Javaに最適化されたレイヤー構造を持てるため、イメージは効率的でCNBに比べてビルド速度は優秀です。
Jibを使用してJavaコンテナを構築する|Google Cloud

「CNB、Jibどちらを選択するか」また「CNBの中でもどのビルダーを選択するか」は環境やユースケースなどに応じて適切なものを選択することになるかと思いますが、本記事の検証では、もっとも高機能なCNBである「Paketo Buildpacks」を使用していきます。前項の「Dockerfile運用の難しさ」であげた事項の解消が最も期待できます(※)。
また、CNBもJibもMavenGradleのプラグインと統合されているため、簡単な設定を追加するだけで使用可能になります。非常に効率的になることが期待できますので、以降の検証では「Gradle」を使用していきたいと思います。

※ユースケースが例えば「Google CloudのCloud Run」の使用に限定されるのであれば、それに最適化される「Google Cloud Buildpacks」の使用をご検討いただければと思います。

Paketo BuildpacksでCNBの機能を試す

前置きが長くなりましたが、前項までであげてきた課題がどう解消されるのか、実際にツールに触りながら確認していきたいと思います。

検証実施のスタックは以下で実施します。

JDK 21
Spring Boot 3.3.4
Gradle 8.10.2

準備

まず、Gradleベースでのアプリケーションプロジェクトを作成し、以下のような「build.gradle」を用意します。
※本検証の実施環境は、筆者のローカル環境(Mac(Intel CPU)+IntelliJ IDEA IDE)となります。

build.gradle
plugins {
    id 'java'
    id 'org.springframework.boot' version '3.3.4'
    id 'io.spring.dependency-management' version '1.1.6'
}

group = 'org.example'
version = 'v0.0.1'

java {
    sourceCompatibility = JavaVersion.VERSION_21
}

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test') {
    useJUnitPlatform()
}

jar {
    enabled = true
    archiveClassifier = ''
}

//#CNBのビルド設定
bootBuildImage {
    //#ビルダーを変更したい場合は指定(デフォルトは「Paketo」のbaseビルダー)
    //builder = "paketobuildpacks/builder-jammy-buildpackless-tiny"
    //#「Google」のビルダーに変更したい場合は以下
    //builder = "gcr.io/buildpacks/builder:google-22"
    buildpacks = [
            //#JVMを変更したい場合は指定(デフォルトは「BellSoft Liberica」)
            //#本検証では「Temurin」を使用
            "gcr.io/paketo-buildpacks/adoptium:latest",
            //#「Correto」に変更したい場合は以下
			//"gcr.io/paketo-buildpacks/amazon-corretto:latest",
            "paketobuildpacks/java"
    ]
    //#実行イメージにカスタムイメージを指定したい場合に設定
    //runImage = "cnb/paketobuildpacks/run:base-custom-cnb"
    imageName = "cnb/coe-sample-app:" + version
    environment = [
            //"BP_JVM_VERSION" : "17", ←Javaのバージョンを指定したい場合に設定
            //"BP_JVM_TYPE" : "JDK" , ←OutputをJDKとする場合(サイズやセキュリティの観点でJREのみが理想)
            "BP_JVM_JLINK_ENABLED" : "true" ←JLinkを有効化
    ]

}

上記の「bootBuildImage」タスクの設定に、使用するビルダーやビルドパックを指定します。また、「BP_XXX」の環境変数にて、コンテナ内のJVM構成や挙動(機能のON/OFFなど)を変えることができます。どのようなものがあるかは公式ページをご参照ください。
Paketo Buildpacks

実行

build.gradleの準備ができたら、以下のGradleタスクを実行します。

# bootBuildImageタスクを実行
./gradlew bootBuildImage

# 実行ログ
-----
> Task :bootBuildImage
Building image 'docker.io/cnb/coe-sample-app:v0.0.1'

 > Pulling builder image 'docker.io/paketobuildpacks/builder-jammy-base:latest' ..................................................
 > Pulled builder image 'paketobuildpacks/builder-jammy-base@sha256:8f7f43bacc30ce986fadb98ab551ee912002fe517f9761cabe1d4c96d373159e'
 > Pulling run image 'docker.io/paketobuildpacks/run-jammy-base:latest' ..................................................
 > Pulled run image 'paketobuildpacks/run-jammy-base@sha256:bba1573aca3b46c56ab47ee38f98cbc637985f46139afb9e068c90e2c91eef9f'
 > Pulling buildpack image 'gcr.io/paketo-buildpacks/adoptium:latest' ..................................................
 > Pulled buildpack image 'gcr.io/paketo-buildpacks/adoptium@sha256:8578f06a93d907252865ffe4613a5d6c0fc57a301f48da35e1efd9dfb1774207'
 > Pulling buildpack image 'docker.io/paketobuildpacks/java:latest' ..................................................
 > Pulled buildpack image 'paketobuildpacks/java@sha256:8e334f314d85e8fe4027ec7b1b866e3f2b15b80fe3542c268c6329567f8560ea'
 > Executing lifecycle version v0.20.4
 > Using build cache volume 'pack-cache-b61d5c977007.build'

 > Running creator
    [creator]     ===> ANALYZING
    [creator]     Image with name "docker.io/cnb/coe-sample-app:v0.0.1" not found
    [creator]     ===> DETECTING
    [creator]     target distro name/version labels not found, reading /etc/os-release file
    [creator]     target distro name/version labels not found, reading /etc/os-release file
    [creator]     7 of 27 buildpacks participating
    [creator]     paketo-buildpacks/adoptium          12.0.0
    [creator]     paketo-buildpacks/ca-certificates   3.8.6
    [creator]     paketo-buildpacks/bellsoft-liberica 11.0.0
    [creator]     paketo-buildpacks/syft              2.4.0
    [creator]     paketo-buildpacks/executable-jar    6.11.3
    [creator]     paketo-buildpacks/dist-zip          5.8.5
    [creator]     paketo-buildpacks/spring-boot       5.31.2
    [creator]     ===> RESTORING
    [creator]     ===> BUILDING
    [creator]     target distro name/version labels not found, reading /etc/os-release file
    [creator]     
    [creator]     Paketo Buildpack for Adoptium 12.0.0
    [creator]       https://github.com/paketo-buildpacks/adoptium
    [creator]       Build Configuration:
    [creator]         $BP_JVM_JLINK_ARGS           --no-man-pages --no-header-files --strip-debug --compress=1  configure custom link arguments (--output must be omitted)
    [creator]         $BP_JVM_JLINK_ENABLED        true                                                         enables running jlink tool to generate custom JRE
    [creator]         $BP_JVM_TYPE                 JRE                                                          the JVM type - JDK or JRE
    [creator]         $BP_JVM_VERSION              21                                                           the Java version
    [creator]       Launch Configuration:
    [creator]         $BPL_DEBUG_ENABLED           false                                                        enables Java remote debugging support
    [creator]         $BPL_DEBUG_PORT              8000                                                         configure the remote debugging port
    [creator]         $BPL_DEBUG_SUSPEND           false                                                        configure whether to suspend execution until a debugger has attached
    [creator]         $BPL_HEAP_DUMP_PATH                                                                       write heap dumps on error to this path
    [creator]         $BPL_JAVA_NMT_ENABLED        true                                                         enables Java Native Memory Tracking (NMT)
    [creator]         $BPL_JAVA_NMT_LEVEL          summary                                                      configure level of NMT, summary or detail
    [creator]         $BPL_JFR_ARGS                                                                             configure custom Java Flight Recording (JFR) arguments
    [creator]         $BPL_JFR_ENABLED             false                                                        enables Java Flight Recording (JFR)
    [creator]         $BPL_JMX_ENABLED             false                                                        enables Java Management Extensions (JMX)
    [creator]         $BPL_JMX_PORT                5000                                                         configure the JMX port
    [creator]         $BPL_JVM_HEAD_ROOM           0                                                            the headroom in memory calculation
    [creator]         $BPL_JVM_LOADED_CLASS_COUNT  35% of classes                                               the number of loaded classes in memory calculation
    [creator]         $BPL_JVM_THREAD_COUNT        250                                                          the number of threads in memory calculation
    [creator]         $JAVA_TOOL_OPTIONS                                                                        the JVM launch flags
    [creator]         Using Java version 21 extracted from MANIFEST.MF
    [creator]       Adoptium JDK 21.0.5: Contributing to layer
    [creator]         Downloading from https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.5%2B11/OpenJDK21U-jdk_x64_linux_hotspot_21.0.5_11.tar.gz
    [creator]         Verifying checksum
    [creator]         Expanding to /layers/paketo-buildpacks_adoptium/jdk
    [creator]         Adding 146 container CA certificates to JVM truststore
    [creator]         Writing env.build/JAVA_HOME.override
    [creator]         Writing env.build/JDK_HOME.override
    [creator]       JLink: Contributing to layer
    [creator]     Warning: The 1 argument for --compress is deprecated and may be removed in a future release
    [creator]         Adding 146 container CA certificates to JVM truststore
    [creator]         Writing env.launch/BPI_APPLICATION_PATH.default
    [creator]         Writing env.launch/BPI_JVM_CACERTS.default
    [creator]         Writing env.launch/BPI_JVM_CLASS_COUNT.default
    [creator]         Writing env.launch/BPI_JVM_SECURITY_PROVIDERS.default
    [creator]         Writing env.launch/JAVA_HOME.default
    [creator]         Writing env.launch/JAVA_TOOL_OPTIONS.append
    [creator]         Writing env.launch/JAVA_TOOL_OPTIONS.delim
    [creator]         Writing env.launch/MALLOC_ARENA_MAX.default
    [creator]       Launch Helper: Contributing to layer
    [creator]         Creating /layers/paketo-buildpacks_adoptium/helper/exec.d/java-opts
    [creator]         Creating /layers/paketo-buildpacks_adoptium/helper/exec.d/jvm-heap
    [creator]         Creating /layers/paketo-buildpacks_adoptium/helper/exec.d/link-local-dns
    [creator]         Creating /layers/paketo-buildpacks_adoptium/helper/exec.d/memory-calculator
    [creator]         Creating /layers/paketo-buildpacks_adoptium/helper/exec.d/security-providers-configurer
    [creator]         Creating /layers/paketo-buildpacks_adoptium/helper/exec.d/jmx
    [creator]         Creating /layers/paketo-buildpacks_adoptium/helper/exec.d/jfr
    [creator]         Creating /layers/paketo-buildpacks_adoptium/helper/exec.d/openssl-certificate-loader
    [creator]         Creating /layers/paketo-buildpacks_adoptium/helper/exec.d/security-providers-classpath-9
    [creator]         Creating /layers/paketo-buildpacks_adoptium/helper/exec.d/debug-9
    [creator]         Creating /layers/paketo-buildpacks_adoptium/helper/exec.d/nmt
    [creator]       Java Security Properties: Contributing to layer
    [creator]         Writing env.launch/JAVA_SECURITY_PROPERTIES.default
    [creator]         Writing env.launch/JAVA_TOOL_OPTIONS.append
    [creator]         Writing env.launch/JAVA_TOOL_OPTIONS.delim
    [creator]     
    [creator]     Paketo Buildpack for CA Certificates 3.8.6
    [creator]       https://github.com/paketo-buildpacks/ca-certificates
    [creator]       Build Configuration:
    [creator]         $BP_EMBED_CERTS                    false  Embed certificates into the image
    [creator]         $BP_ENABLE_RUNTIME_CERT_BINDING    true   Deprecated: Enable/disable certificate helper layer to add certs at runtime
    [creator]         $BP_RUNTIME_CERT_BINDING_DISABLED  false  Disable certificate helper layer to add certs at runtime
    [creator]       Launch Helper: Contributing to layer
    [creator]         Creating /layers/paketo-buildpacks_ca-certificates/helper/exec.d/ca-certificates-helper
    [creator]     
    [creator]     Paketo Buildpack for Syft 2.4.0
    [creator]       https://github.com/paketo-buildpacks/syft
    [creator]         Downloading from https://github.com/anchore/syft/releases/download/v1.15.0/syft_1.15.0_linux_amd64.tar.gz
    [creator]         Verifying checksum
    [creator]         Writing env.build/SYFT_CHECK_FOR_APP_UPDATE.default
    [creator]     
    [creator]     Paketo Buildpack for Executable JAR 6.11.3
    [creator]       https://github.com/paketo-buildpacks/executable-jar
    [creator]       Class Path: Contributing to layer
    [creator]         Writing env/CLASSPATH.delim
    [creator]         Writing env/CLASSPATH.prepend
    [creator]       Process types:
    [creator]         executable-jar: java org.springframework.boot.loader.launch.JarLauncher (direct)
    [creator]         task:           java org.springframework.boot.loader.launch.JarLauncher (direct)
    [creator]         web:            java org.springframework.boot.loader.launch.JarLauncher (direct)
    [creator]     
    [creator]     Paketo Buildpack for Spring Boot 5.31.2
    [creator]       https://github.com/paketo-buildpacks/spring-boot
    [creator]       Build Configuration:
    [creator]         $BPL_JVM_CDS_ENABLED                 false  whether to enable CDS optimizations at runtime
    [creator]         $BPL_SPRING_AOT_ENABLED              false  whether to enable Spring AOT at runtime
    [creator]         $BP_JVM_CDS_ENABLED                  false  whether to enable CDS & perform JVM training run
    [creator]         $BP_SPRING_AOT_ENABLED               false  whether to enable Spring AOT
    [creator]         $BP_SPRING_CLOUD_BINDINGS_DISABLED   false  whether to contribute Spring Boot cloud bindings support
    [creator]         $BP_SPRING_CLOUD_BINDINGS_VERSION    1      default version of Spring Cloud Bindings library to contribute
    [creator]       Launch Configuration:
    [creator]         $BPL_SPRING_CLOUD_BINDINGS_DISABLED  false  whether to auto-configure Spring Boot environment properties from bindings
    [creator]         $BPL_SPRING_CLOUD_BINDINGS_ENABLED   true   Deprecated - whether to auto-configure Spring Boot environment properties from bindings
    [creator]       Creating slices from layers index
    [creator]         dependencies (18.9 MB)
    [creator]         spring-boot-loader (406.1 KB)
    [creator]         snapshot-dependencies (0.0 B)
    [creator]         application (3.7 KB)
    [creator]       Spring Cloud Bindings 2.0.3: Contributing to layer
    [creator]         Downloading from https://repo1.maven.org/maven2/org/springframework/cloud/spring-cloud-bindings/2.0.3/spring-cloud-bindings-2.0.3.jar
    [creator]         Verifying checksum
    [creator]         Copying to /layers/paketo-buildpacks_spring-boot/spring-cloud-bindings
    [creator]       Web Application Type: Contributing to layer
    [creator]         Servlet web application detected
    [creator]         Writing env.launch/BPL_JVM_THREAD_COUNT.default
    [creator]       Launch Helper: Contributing to layer
    [creator]         Creating /layers/paketo-buildpacks_spring-boot/helper/exec.d/spring-cloud-bindings
    [creator]       4 application slices
    [creator]       Image labels:
    [creator]         org.opencontainers.image.title
    [creator]         org.opencontainers.image.version
    [creator]         org.springframework.boot.version
    [creator]     ===> EXPORTING
    [creator]     Adding layer 'paketo-buildpacks/adoptium:JLink'
    [creator]     Adding layer 'paketo-buildpacks/adoptium:helper'
    [creator]     Adding layer 'paketo-buildpacks/adoptium:java-security-properties'
    [creator]     Adding layer 'paketo-buildpacks/ca-certificates:helper'
    [creator]     Adding layer 'paketo-buildpacks/executable-jar:classpath'
    [creator]     Adding layer 'paketo-buildpacks/spring-boot:helper'
    [creator]     Adding layer 'paketo-buildpacks/spring-boot:spring-cloud-bindings'
    [creator]     Adding layer 'paketo-buildpacks/spring-boot:web-application-type'
    [creator]     Adding layer 'buildpacksio/lifecycle:launch.sbom'
    [creator]     Added 5/5 app layer(s)
    [creator]     Adding layer 'buildpacksio/lifecycle:launcher'
    [creator]     Adding layer 'buildpacksio/lifecycle:config'
    [creator]     Adding layer 'buildpacksio/lifecycle:process-types'
    [creator]     Adding label 'io.buildpacks.lifecycle.metadata'
    [creator]     Adding label 'io.buildpacks.build.metadata'
    [creator]     Adding label 'io.buildpacks.project.metadata'
    [creator]     Adding label 'org.opencontainers.image.title'
    [creator]     Adding label 'org.opencontainers.image.version'
    [creator]     Adding label 'org.springframework.boot.version'
    [creator]     Setting default process type 'web'
    [creator]     Saving docker.io/cnb/coe-sample-app:v0.0.1...
    [creator]     *** Images (6c51e8077fba):
    [creator]           docker.io/cnb/coe-sample-app:v0.0.1
    [creator]     Adding cache layer 'paketo-buildpacks/adoptium:jdk'
    [creator]     Adding cache layer 'paketo-buildpacks/syft:syft'
    [creator]     Adding cache layer 'paketo-buildpacks/spring-boot:spring-cloud-bindings'
    [creator]     Adding cache layer 'buildpacksio/lifecycle:cache.sbom'

Successfully built image 'docker.io/cnb/coe-sample-app:v0.0.1'


BUILD SUCCESSFUL in 1m 24s
6 actionable tasks: 6 executed
-----

以下のようにイメージが作成されました。
※cnb/coe-sample-app:v0.0.1(CREATEDが44 years agoとなる理由は不明)

>docker images
REPOSITORY                            TAG       IMAGE ID       CREATED        SIZE
paketobuildpacks/run-jammy-base       latest    28ec68508efa   27 hours ago   104MB
paketobuildpacks/java                 latest    2f9fcc2a079a   44 years ago   231MB
cnb/coe-sample-app                    v0.0.1    6c51e8077fba   44 years ago   214MB
paketobuildpacks/builder-jammy-base   latest    16b4cb1ca526   44 years ago   1.62GB
gcr.io/paketo-buildpacks/adoptium     latest    ac5e37e1e8ad   44 years ago   12.1MB

では、「Dockerfile運用の難しさ」の項でピックアップした事項について、解消されているか見ていきたいと思います。

セキュリティ

まずセキュリティ関連です。
dockleを使用して、作成されたイメージがベストプラクティスに準拠しているかチェックしてみます。

>dockle cnb/coe-sample-app:v0.0.1
INFO    - CIS-DI-0005: Enable Content trust for Docker
        * export DOCKER_CONTENT_TRUST=1 before docker pull/build
INFO    - CIS-DI-0006: Add HEALTHCHECK instruction to the container image
        * not found HEALTHCHECK statement
INFO    - CIS-DI-0008: Confirm safety of setuid/setgid files
        * setgid file: grwxr-xr-x usr/bin/chage
        * setuid file: urwxr-xr-x usr/bin/gpasswd
        * setuid file: urwxr-xr-x usr/bin/passwd
        * setuid file: urwxr-xr-x bin/su
        * setuid file: urwxr-xr-x usr/bin/newgrp
        * setuid file: urwxr-xr-x usr/bin/chfn
        * setuid file: urwxr-xr-x bin/umount
        * setgid file: grwxr-xr-x usr/bin/wall
        * setgid file: grwxr-xr-x sbin/pam_extrausers_chkpwd
        * setgid file: grwxr-xr-x sbin/unix_chkpwd
        * setuid file: urwxr-xr-x usr/bin/chsh
        * setgid file: grwxr-xr-x usr/bin/expiry
        * setuid file: urwxr-xr-x bin/mount

「INFO」のみのため、問題ない結果かと思います。試しにログインして一部確認(ユーザなど)しましたが、問題なさそうです。
※「root」ではなく「cnb」ユーザ、1コンテナ1プロセス(PIDは「1」)

> docker run -d --rm -p 8080:8080 --name cnb cnb/coe-sample-app:v0.0.1
> docker exec -it cnb /bin/bash

cnb@225f0704f395:/workspace$ whoami
cnb

cnb@225f0704f395:/workspace$ ps aux
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
cnb            1 34.2  3.2 1055964 176252 ?      Ssl  05:06   0:11 java org.springframework.boot.loader.launch.JarLauncher
cnb          110  0.1  0.0  18520  3240 pts/0    Ss   05:06   0:00 /bin/bash
cnb          121  0.0  0.0  34416  2820 pts/0    R+   05:06   0:00 ps aux

なお、問題ある場合は以下のように、「FATAL」や「WARN」などが出力されます。

Dockle 公式ページより

次にgrypeを使用して、イメージの脆弱性のチェックをしてみます。

ryotashi@shimaryoutanoMacBook-Pro-2 cnb % grype cnb/coe-sample-app:v0.0.1 --only-fixed --fail-on low
 ✔ Loaded image                                                                                                                                               cnb/coe-sample-app:v0.0.1_web
 ✔ Parsed image                                                                                                     sha256:4c418bfc8ad5d1cbbc2ba5e8ce2f38377b1e719877fd82301f9451ce900c4788
 ✔ Cataloged contents                                                                                                      2cc89379eb59cb95f038c60c27eae28ba4ba85945144518ac7f922413fae48d7
   ├── ✔ Packages                        [231 packages]  
   ├── ✔ File digests                    [4,464 files]  
   ├── ✔ File metadata                   [4,464 locations]  
   └── ✔ Executables                     [779 executables]  
 ✘ Scan for vulnerabilities        [2 vulnerability matches]  
   ├── by severity: 0 critical, 0 high, 23 medium, 30 low, 8 negligible
   └── by status:   2 fixed, 59 not-fixed, 59 ignored
NAME            INSTALLED  FIXED-IN  TYPE          VULNERABILITY        SEVERITY
spring-context  6.1.13     6.1.14    java-archive  GHSA-4gc7-5j7h-4qph  Medium
discovered vulnerabilities at or above the severity threshold

上記結果の通り、危険度「高」以上の脆弱性はなさそうでした。

もし、自前でDockerfileを作成した場合、以下のように危険度「高」の脆弱性を自力で潰しきるのは難しいと思っています。

>grype sample-app:v0.0.1 --only-fixed --fail-on low
 ✔ Vulnerability DB                [updated]  
 ✔ Loaded image                                                                                                                                              sample-app:v0.0.1
 ✔ Parsed image                                                                                        sha256:f0574d671981f063d443929da93f0467d6acb37dbc96f8eb441e6787ce9dcc87
 ✔ Cataloged contents                                                                                         4b1f52df84fc4611595c5ba09497c567cfbe3ea949ffcb563cd364baf9bbe0d3
   ├── ✔ Packages                        [194 packages]  
   ├── ✔ File digests                    [3,705 files]  
   ├── ✔ File metadata                   [3,705 locations]  
   └── ✔ Executables                     [780 executables]  
 ✘ Scan for vulnerabilities        [47 vulnerability matches]  
   ├── by severity: 4 critical, 22 high, 102 medium, 67 low, 12 negligible (2 unknown)
   └── by status:   47 fixed, 162 not-fixed, 162 ignored
NAME            INSTALLED      FIXED-IN         TYPE          VULNERABILITY        SEVERITY
stdlib          go1.19.11      1.21.11, 1.22.4  go-module     CVE-2024-24790       Critical  
stdlib          go1.19.11      1.21.0-0         go-module     CVE-2023-24531       Critical  
stdlib          go1.19.11      1.22.7, 1.23.1   go-module     CVE-2024-34158       High      
stdlib          go1.19.11      1.22.7, 1.23.1   go-module     CVE-2024-34156       High      

---(以下略)---

Production Readyなコンテナイメージを作成するうえで、この脆弱性対応への追従は非常に助かります。

レイヤー構造管理

次にレイヤー構造管理です。
Diveツールにて作成されたコンテナイメージを解析してみます。

>dive cnb/coe-sample-app:v0.0.1

画面の都合上非常にわかりにくいのですが、アプリケーションにおける「実際のアプリケーションコード」と「依存ライブラリ」がレイヤー分けされており、効率的なレイヤー分割がなされていそうです。

依存ライブラリのレイヤー
アプリケーションコードのレイヤー

アプリケーションのコードは更新頻度が高く、依存ライブラリは更新頻度が低いため、同一のレイヤーにしてしまうとコード修正後のビルド時に毎回依存ライブラリのロード処理が発生します。上記のようにレイヤーをわけておく、かつ依存ライブラリのレイヤーをアプリケーションコードのレイヤーよりも前に定義することで、依存ライブラリのレイヤーはキャッシュを再利用できるため、効率的なビルドが可能になります。

パフォーマンスチューニング

最後にパフォーマンスチューニングについてです。
Javaアプリケーションを最適なパフォーマンスで動作させるためには、実際のアプリケーションコードの品質はもちろんですが、加えてJavaアプリケーションを動作させる実行環境である「JVM」に対する適切なパラメータ設定などが必要です。これには専門的なJVMの知識が必要不可欠ですが、Paketo Buildpacksを使用すると一部自動的に適切な設定を実施してくれます。

Class Data Sharing(CDS)

過日、サーバレスの文脈でメモリフットプリントの大きいJavaアプリケーションの初動速度(起動速度や起動直後の処理性能)に関する記事を執筆しました。
AWS Lambda SnapStartを利用してJava Lambda関数高速化の可能性を探る

上記記事では、AWS Lambdaのコールドスタート問題への対処として「SnapStart」機能をご紹介していますが、この機能はJavaの「Coordinated Restore at Checkpoint(CRaC)」の仕組みが使用されています。

もう一つJavaの起動速度の改善として効果が期待されるのが、「Class Data Sharing(以下CDS)」です。クラスのメタデータをアーカイブファイルにキャッシュし、JVMの起動時にそのキャッシュからクラスを高速に読み込むことで、メモリフットプリントを削減し、起動速度を改善します。Springアプリケーションに対するCDSの効果については、以下記事がVMWareから公開されています。
CDS with Spring Framework 6.1
上記記事によると、CDSを利用することで、かなりの改善につながることが実証されています。

上記記事より引用

通常このCDS機能を有効化するにはいくつか面倒な手順が必要ですが、以下ページに案内されている通り、Peketo Buildpacksを使用すると簡単に有効化できます。
Spring Boot CDS support and Project Leyden anticipation
具体的には実行時の環境変数に「BP_JVM_CDS_ENABLED」を追加するだけです。

build.gradle

・・・
bootBuildImage {
    ・・・
    imageName = "cnb/coe-sample-app:" + version
    environment = [
            "BP_JVM_JLINK_ENABLED" : "true",
            "BP_JVM_CDS_ENABLED" : "true" ←追加
    ]

}

手元の環境で確認したところ、CDS有無で以下のような結果になりました。

CDSなし
・・・
Calculated JVM Memory Configuration: -XX:MaxDirectMemorySize=10M -Xmx465238K -XX:MaxMetaspaceSize=71337K -XX:ReservedCodeCacheSize=240M -Xss1M (Total Memory: 1G, Thread Count: 250, Loaded Class Count: 10181, Headroom: 0%)
Enabling Java Native Memory Tracking
Adding 146 container CA certificates to JVM truststore
Spring Cloud Bindings Enabled
Picked up JAVA_TOOL_OPTIONS: -XX:+ExitOnOutOfMemoryError -Djava.security.properties=/layers/paketo-buildpacks_adoptium/java-security-properties/java-security.properties -XX:MaxDirectMemorySize=10M -Xmx465238K -XX:MaxMetaspaceSize=71337K -XX:ReservedCodeCacheSize=240M -Xss1M -XX:+UnlockDiagnosticVMOptions -XX:NativeMemoryTracking=summary -XX:+PrintNMTStatistics -Dorg.springframework.cloud.bindings.boot.enable=true

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/

 :: Spring Boot ::                (v3.3.4)

・・・
Started Application in 2.589 seconds (process running for 3.177)
CDSあり
・・・
Calculated JVM Memory Configuration: -XX:MaxDirectMemorySize=10M -Xmx457739K -XX:MaxMetaspaceSize=78836K -XX:ReservedCodeCacheSize=240M -Xss1M (Total Memory: 1G, Thread Count: 250, Loaded Class Count: 11505, Headroom: 0%)
Enabling Java Native Memory Tracking
Adding 146 container CA certificates to JVM truststore
Spring CDS Enabled, contributing -XX:SharedArchiveFile=application.jsa to JAVA_TOOL_OPTIONS
Spring Cloud Bindings Enabled
Picked up JAVA_TOOL_OPTIONS: -Djava.security.properties=/layers/paketo-buildpacks_adoptium/java-security-properties/java-security.properties -XX:+ExitOnOutOfMemoryError -XX:MaxDirectMemorySize=10M -Xmx457739K -XX:MaxMetaspaceSize=78836K -XX:ReservedCodeCacheSize=240M -Xss1M -XX:+UnlockDiagnosticVMOptions -XX:NativeMemoryTracking=summary -XX:+PrintNMTStatistics -XX:SharedArchiveFile=application.jsa -Dorg.springframework.cloud.bindings.boot.enable=true

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/

 :: Spring Boot ::                (v3.3.4)

・・・
Starting Application v0.0.2 using Java 21.0.5 with PID 1 (/workspace/runner.jar started by cnb in /workspace)
・・・
Started Application in 1.579 seconds (process running for 1.841)

およそ1秒短縮されました。上記起動ログからもCDS機能が有効化され起動されていることがわかります。

Spring CDS Enabled, contributing -XX:SharedArchiveFile=application.jsa to JAVA_TOOL_OPTIONS

コンテナにログインして確認もしてみましたが、想定通りアーカイブファイルとしてキャッシュされ起動されています。

>docker exec -it cnb /bin/bash
>cnb@8071630570b5:/workspace$ ls -lrt
total 31096
-rw-r--r-- 1 1001 cnb     3447 Jan  1  1980 runner.jar
drwxr-xr-x 2 1001 cnb     4096 Jan  1  1980 lib
-r--r--r-- 1 1001 cnb 31834112 Jan  1  1980 application.jsa
>cnb@8071630570b5:/workspace$ ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
cnb          1 23.2  0.8 1119576 134276 ?      Ssl  07:21   0:05 java -cp runner.jar:lib/spring-cloud-bindings-2.0.3.jar org.example.coe.cnb.sample1.Application
・・・

このファイルがいつ生成されているかですが、CNBによるコンテナイメージビルド時の実行ログにアプリケーションの起動ログが出力されており、このタイミングで生成されていそうです。

> Task :bootBuildImage
・・・ 
    [creator]     Paketo Buildpack for Spring Boot 5.31.2
    [creator]       https://github.com/paketo-buildpacks/spring-boot
    [creator]       Build Configuration:
    [creator]         $BPL_JVM_CDS_ENABLED                 false  whether to enable CDS optimizations at runtime
    [creator]         $BPL_SPRING_AOT_ENABLED              false  whether to enable Spring AOT at runtime
    [creator]         $BP_JVM_CDS_ENABLED                  true   whether to enable CDS & perform JVM training run
    [creator]         $BP_SPRING_AOT_ENABLED               false  whether to enable Spring AOT
    [creator]         $BP_SPRING_CLOUD_BINDINGS_DISABLED   false  whether to contribute Spring Boot cloud bindings support
    [creator]         $BP_SPRING_CLOUD_BINDINGS_VERSION    1      default version of Spring Cloud Bindings library to contribute
    [creator]       Launch Configuration:
    [creator]         $BPL_SPRING_CLOUD_BINDINGS_DISABLED  false  whether to auto-configure Spring Boot environment properties from bindings
    [creator]         $BPL_SPRING_CLOUD_BINDINGS_ENABLED   true   Deprecated - whether to auto-configure Spring Boot environment properties from bindings
    [creator]       Spring Cloud Bindings 2.0.3: Reusing cached layer
    [creator]       Performance: Contributing to layer
    [creator]         Extracting Jar
    [creator]     
    [creator]       .   ____          _            __ _ _
    [creator]      /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
    [creator]     ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
    [creator]      \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
    [creator]       '  |____| .__|_| |_|_| |_\__, | / / / /
    [creator]      =========|_|==============|___/=/_/_/_/
    [creator]     
    [creator]      :: Spring Boot ::                (v3.3.4)
    [creator]     
    
・・・

さて、これだけで結構な改善率ですが、Springアプリケーションの場合、CDSに「Ahead Of Time コンパイラ(以下AOT)」を組み合わせ使用することができます。AOTは事前に必要なクラス情報を静的に最適化した上でコンパイルするため、起動速度の大幅な改善が期待できます。使用するには「Spring AOTプラグイン」を指定し、実行時の環境変数に「BP_SPRING_AOT_ENABLED」を有効にします。

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.3.4'
    id 'org.springframework.boot.aot' version '3.3.4'  ←追加
    id 'io.spring.dependency-management' version '1.1.6'
}

・・・

//#CNBのビルド設定
bootBuildImage {
    ・・・
    environment = [
            ・・・
            "BP_JVM_CDS_ENABLED" : "true",
            "BP_SPRING_AOT_ENABLED" : "true" ←追加
            ・・・
    ]
}

以下が実行結果です。

CDS + Spring AOT
・・・
Calculated JVM Memory Configuration: -XX:MaxDirectMemorySize=10M -Xmx457574K -XX:MaxMetaspaceSize=79001K -XX:ReservedCodeCacheSize=240M -Xss1M (Total Memory: 1G, Thread Count: 250, Loaded Class Count: 11534, Headroom: 0%)
Enabling Java Native Memory Tracking
Adding 146 container CA certificates to JVM truststore
Spring AOT Enabled, contributing -Dspring.aot.enabled=true to JAVA_TOOL_OPTIONS
Spring CDS Enabled, contributing -XX:SharedArchiveFile=application.jsa to JAVA_TOOL_OPTIONS
Spring Cloud Bindings Enabled
Picked up JAVA_TOOL_OPTIONS: -Djava.security.properties=/layers/paketo-buildpacks_adoptium/java-security-properties/java-security.properties -XX:+ExitOnOutOfMemoryError -XX:MaxDirectMemorySize=10M -Xmx457574K -XX:MaxMetaspaceSize=79001K -XX:ReservedCodeCacheSize=240M -Xss1M -XX:+UnlockDiagnosticVMOptions -XX:NativeMemoryTracking=summary -XX:+PrintNMTStatistics -Dspring.aot.enabled=true -XX:SharedArchiveFile=application.jsa -Dorg.springframework.cloud.bindings.boot.enable=true

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/

 :: Spring Boot ::                (v3.3.4)

・・・
Starting AOT-processed Application v0.0.3 using Java 21.0.5 with PID 1 (/workspace/runner.jar started by cnb in /workspace)
・・・
Started Application in 1.098 seconds (process running for 1.364)

CDS単体利用時と比べて更に高速化されました。設定レベルでここまでスピードアップできるのは非常に助かります。

Memory Calculator
もう一つ強力な機能として「Memory Calculator」があります。JVMのメモリ構造は以下のように、ヒープ領域と非ヒープ領域に分かれますが、各領域に対してメモリのサイズをどう割り当てるかがパフォーマンスの良し悪しを大きく左右します。

JVMメモリ

筆者がこれまで参画した開発プロジェクトでは、ヒープ領域に大きなサイズを割り当てる傾向が多くあったのですが、実際のパフォーマンスのボトルネックは意外と非ヒープの方が多かったりします。

このあたりのパラメータ設定はJVMに関する高度な知識を必要とします。Memory Calculatorはアプリケーションの構成などを分析して、JVMメモリの奨励パラメータを自動的に計算してくれる機能です。今回の検証で使用しているサンプルアプリケーションは、TomcatベースのWebアプリケーションです。そのアプリケーションの起動ログを見ると、以下のようにMemory Calculatorによる自動メモリ割り当てが実施されていることがわかります。

Calculated JVM Memory Configuration: -XX:MaxDirectMemorySize=10M -Xmx465238K 
-XX:MaxMetaspaceSize=71337K -XX:ReservedCodeCacheSize=240M -Xss1M 
(Total Memory: 1G, Thread Count: 250, Loaded Class Count: 10181, Headroom: 0%)

・・・

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/

 :: Spring Boot ::                (v3.3.4)

・・・

2024-11-15T14:42:19.872Z  INFO 1 --- [           
main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 8080 (http)
2024-11-15T14:42:19.890Z  INFO 1 --- [           
main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2024-11-15T14:42:19.890Z  INFO 1 --- [         
main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.30]

・・・

Memory Calculatorの計算ロジックですが、まず非ヒープのサイズを見積り、全体メモリサイズ(1G)から見積もった非ヒープのサイズを引いた残りをヒープに割り当ててます。非ヒープが優先されていることがわかります。内訳は以下です。(全体メモリ – 非ヒープサイズ = ヒープサイズ)

Total Memory: 1024M
Non-Heap:570M
     Metaspace:70M(クラス数から自動計算)
     ReservedCodeCacheSize:240M(奨励値)
     DirectMemorySize:10M(奨励値)
     Thread Stack:250M(1M(XSS)* 250(デフォルトスレッド数))
Heap:454M
Other:0M

Memory Calculatorはアプリケーションの構成に応じて適切な設定値を計算してくれるとのことですが、上記設定がどう変わるかを確認してみます。build.gradleでの依存関係を以下のように変更し、TomcatベースのWebアプリケーションをNettyベースのリアクティブWebアプリケーションに変更してみます。

build.gradle
※Webをコメントアウトし、Webfluxに変更

(抜粋)
----
dependencies {
    ・・・
    //implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-webflux'
}
----

上記でビルド後アプリケーションを起動します。

Calculated JVM Memory Configuration: -XX:MaxDirectMemorySize=10M -Xmx665076K 
-XX:MaxMetaspaceSize=76299K -XX:ReservedCodeCacheSize=240M -Xss1M 
(Total Memory: 1G, Thread Count: 50, Loaded Class Count: 11057, Headroom: 0%)

・・・
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/

 :: Spring Boot ::                (v3.3.4)

・・・

2024-11-15T14:59:40.053Z  INFO 1 --- [           
main] o.s.b.web.embedded.netty.NettyWebServer  : Netty started on port 8080 (http)

・・・

上記の通り、Nettyサーバとして起動され、Memory Calculatorの計算値も変わっています。NettyのスレッドはTomcatのスレッドと異なりノンブロッキングなスレッドです。そのためスレッド数はTomcatほど必要ないため(250→50)、スタック領域のサイズが減り、その分ヒープメモリサイズが増えています。内訳は以下です。

Total Memory: 1024M
Non-Heap: 375M
     Metaspace:75M(クラス数から自動計算)
     ReservedCodeCacheSize:240M(奨励値)
     DirectMemorySize:10M(奨励値)
     Thread Stack:50M(1M(XSS)* 50(デフォルトスレッド数))
Heap:649M
Other:0M

このように、アプリケーションの構成に応じて自動で奨励値を設定してくれるので、この設定値をパフォーマンスチューニングのスタートポイントとして、性能テスト等を実施しながら微調整していくようなアプローチも取れるようになります(※)。

※各領域のサイズは以下のように明示的に指定することで上書き可能です。
>docker run -d –rm -p 8080:8080 -m 1g -e JAVA_OPTS=”-Xmx256m” –name cnb-app cnb/coe-sample-app:v0.0.1
Native Image
パラメータの話ではありませんが、パフォーマンスという観点で言うと、Paketo Buildpacksは「Graalvm」によるネイティブビルドもサポートしています。以下のようにbuild.gradleにネイティブビルドを有効にする設定を追加することで作成可能です。

build.gradle
※Java21ではエラーとなったため、Java17で実行しています

plugins {
   id 'java'
   id 'org.springframework.boot' version '3.3.4'
   id 'io.spring.dependency-management' version '1.1.6'
   id 'org.graalvm.buildtools.native' version '0.10.3' ←追加
}

・・・

↓追加
graalvmNative {
    binaries {
        main {
            javaLauncher = javaToolchains.launcherFor {
                languageVersion = JavaLanguageVersion.of(17)
                vendor = JvmVendorSpec.matching("GraalVM Community")
            }
            mainClass = 'org.example.coe.cnb.sample1.Application'
        }
    }
}

・・・

bootBuildImage {
    ・・・
    environment = [
            ・・・
            "BP_JVM_VERSION" : "17"
            "BP_NATIVE_IMAGE" : "true" ←追加
    ]

}

ビルド時間は5分程度かかりますが、アプリケーションの起動は「100ms」程度とかなり高速でした。

                                                        
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/

 :: Spring Boot ::                (v3.3.4)

・・・
2024-11-15T16:06:37.016Z  INFO 1 --- [           main] o.example.coe.cnb.sample1.Application    : 
Started Application in 0.079 seconds (process running for 0.085)
ネイティブイメージはAOTのため、その高速化のメリットと、DIなど動的なリフレクションに強みを持つSpringのメリットはトレードオフになる可能性があります。ご利用の際はアプリケーションのユースケースなどに応じて慎重な判断が必要になります。

さいごに ~CNBを試してみて~

以上、Paketo Buildpacksを使用して、CNBの実用性を検証してまいりました。ソースコードを準備するだけでProduction Readyなコンテナイメージが作成できることは開発者にとって大変うれしいのではないでしょうか。Dockerfileの作成やメンテナンス作業から解放される分、アプリケーションの開発に専念できるようになります。

また、本記事の冒頭あたりで「Skafford」というツールにふれましたが、この「Skafford」はCNBとも統合(※)されており、合わせて使用すればk8sアプリケーション開発対してより強力なツールになることが期待できますので、是非こちらも積極的に使っていきたいと思っています。

※SkaffoldはCNBだけでなく、JibやBazelなど複数のビルドツールと統合されています。
Build

なお、過日AWSより、CNB + Code Build/Pipelineによるコンテナイメージ作成に関するブログが公開されてますので、こちらも是非ご参考ください。
Cloud Native Buildpacks による AWS CodeBuild と AWS CodePipeline を使ったコンテナイメージの作成 | Amazon Web Services