CLOVER🍀

That was when it all began.

Cloud Native Buildpacksで遊んでみる(Java)

これは、なにをしたくて書いたもの?

前に、Spring BootのCloud Native Buildpacksサポートで遊んでみました。

Spring BootのCloud Native Buildpacksサポートを試す - CLOVER🍀

今回は、Cloud Native Buildpacksそのものに焦点を当てて見てみようかな、と。

Cloud Native Buildpacks · Cloud Native Buildpacks

ビルドパック(Buildpacks)がCloud Native Computing Foundationのプロジェクトに。HerokuやCloud Foundryなどが開発 - Publickey

Cloud Native Buildpackで めんどうなコンテナイメージ作成を 自動化しよう - Speaker Deck

JavaアプリケーションのコンテナイメージをCloud Native Buildpacksで作ってみようかなと思います。

Cloud Native Buildpacks

Cloud Native Buildpacksは、アプリケーションをどのクラウドでも実行できるイメージに変換するツールだそうです。

Cloud Native Buildpacks transform your application source code into images that can run on any cloud.

Cloud Native Buildpacks · Cloud Native Buildpacks

イメージとは、OCIイメージを指します。

2011年にHerokuが始め、その後Pivotalでも採用し、2018年にCloud Native Buildpacksになったようです(Pivotal+Heroku)。

f:id:Kazuhira:20201031154859p:plain

こちらに、特徴が書いてあります。

Features · Cloud Native Buildpacks

  • Advanced Caching
    • パフォーマンスを向上させるためのキャッシュ
  • Bill-of-Materials
    • アプリケーションのイメージのための知見
  • Modular / Pluggable
    • 複数のbuildpacksの利用
  • Multi-language
  • Multi-process
    • イメージは、オペレーションモードによって複数のエントリーポイントを持つことができる
  • Minimal app image
    • イメージには、必要なものだけが含まれる
  • Rebasing
    • 再構築なしで、ベースイメージを即座に更新できる
  • Reproducibility
    • ビルドを再実行すると、同じアプリケーションイメージのダイジェストを再現する
  • Reusability
    • コミュニティによってメンテナンスされている、production-readyなbuildpacksを活用できる

ドキュメントは、こちら。

Getting Started · Cloud Native Buildpacks

Cloud Native Buildpacksのコンセプト

こちらに、Cloud Native Buildpacksのコンセプトが書かれているので、少し見てみましょう。

Concepts · Cloud Native Buildpacks

5つのコンポーネントと、2つの操作がコンセプトのようです。

  • Components
    • Builder
    • Buildpack
    • Lifecycle
    • Platform
    • Stack
  • Operations
    • Build
    • Rebase
Components

コンポーネントについては、以下に説明があります。

Components · Cloud Native Buildpacks

  • Builder · Cloud Native Buildpacks
    • Buildpack、Lifecycleの実装、PlatformがLifecycleの実行時に使用する可能性があるビルド時の環境、アプリケーションのビルドに関する情報をバンドルしたイメージのこと
    • Buildpacks、Lifecycle、Stackで構成される
  • Buildpack · Cloud Native Buildpacks
    • アプリケーションのソースコードを検査し、アプリケーションのビルドと実行の計画を作る作業単位のこと
    • 主に、buildpack.toml、bin/detect、bin/buildの3ファイルで構成される
    • 以下の2つのフェーズがある
      • Detect
        • Buildpacksのグループをアプリケーションソースコードに対して順次テストし、アプリケーションに適していると見なされた最初のグループが、アプリケーション用に選択されたBuildpackのセットとなる
        • 検出の基準はアプリケーション固有で、たとえばnpm用のBuildpackの場合はpackage.jsonを検索する
      • Build
        • 最終的なアプリケーションイメージをビルドする際に、contributeするフェーズ
        • 環境変数を設定する、依存関係を追加する(npm install、pip instlalなど)といったことなどが行える
  • Lifecycle · Cloud Native Buildpacks
    • Buildpackの実行を調整し、アーティファクトを最終的なアプリケーションイメージに組み上げる
    • 以下の4つのフェーズのこと
      • Detection
      • Analysis
      • Build
      • Export
  • Platform · Cloud Native Buildpacks
    • Lifecycle、(Builderにパッケージされた)Buildpacks、そしてアプリケーションソースコードを使用して、OCIイメージを作成する
    • 以下のようなものが、Platformの例
      • Buildpackを使ってOCIイメージを作るような、ローカルCLIツール
      • Buildpackを使ってOCIイメージを作るCIサービスのプラグイン
      • デプロイ前にBuildpackを使用してソースコードをビルドするクラウドアプリケーションプラットフォーム
  • Stack · Cloud Native Buildpacks
    • BuildpackのLifecycleに、ビルド時および実行時の環境をイメージとして提供する
    • Builderの設定ファイル内で、[stack]として設定し、ビルドや実行時のイメージを指定する
Operations

操作については、以下に説明があります。

Operations · Cloud Native Buildpacks

  • Build · Cloud Native Buildpacks
    • アプリケーションのソースコードに対して、1つまたは複数のbuildpacksを実行し、実行可能なOCIイメージを作成するプロセスのこと
  • Rebase · Cloud Native Buildpacks
    • アプリケーション開発者または運用者は、Stackの実行イメージが変更された時に、アプリケーションイメージを素早く更新できる
つまり?

すごいざっくりと言うと、イメージ生成までのプロセスは

  • Buildpackというビルド、実行に関する2つのイメージを持ったコンポーネントがあり、かつ言語検出機能を持っている
  • 複数のBuildpackの中から、適切なもの(最初に選ばれたもの)でアプリケーションイメージのビルドを行う
  • ビルドに使うイメージと、実行に使うイメージは分かれている

という感じですかね。

アプリケーションのビルドからイメージ作成まで、プロセスが決められているところが特徴的ですね。

とまあ、ここまで見てきたところで、実際に試してみましょう。

環境

今回の環境は、こちら。

$ uname -srvmpio
Linux 5.4.0-52-generic #57-Ubuntu SMP Thu Oct 15 10:57:00 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux


$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.1 LTS
Release:    20.04
Codename:   focal

Dockerも必要になります。

$ docker version
Client: Docker Engine - Community
 Version:           19.03.13
 API version:       1.40
 Go version:        go1.13.15
 Git commit:        4484c46d9d
 Built:             Wed Sep 16 17:02:52 2020
 OS/Arch:           linux/amd64
 Experimental:      false

Server: Docker Engine - Community
 Engine:
  Version:          19.03.13
  API version:      1.40 (minimum version 1.12)
  Go version:       go1.13.15
  Git commit:       4484c46d9d
  Built:            Wed Sep 16 17:01:20 2020
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.3.7
  GitCommit:        8fba4e9a7d01810a393d5d25a3621dc101981175
 runc:
  Version:          1.0.0-rc10
  GitCommit:        dc9208a3303feef5b3839f4323d9beb36df0a9dd
 docker-init:
  Version:          0.18.0
  GitCommit:        fec3683

Getting Startedで始めてみる

ドキュメントのGetting Startedをまずは眺めてみます。

An App's Brief Journey from Source to Image · Cloud Native Buildpacks

どうやら、packというツールを最初にインストールする必要があるようです。

packをインストールする

ドキュメントに従い、packをインストールします。

Pack · Cloud Native Buildpacks

Linux向けのマニュアルインストール手順を実行。

$ (curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.14.2/pack-v0.14.2-linux.tgz" | sudo tar -C /usr/local/bin/ --no-same-owner -xzv pack)

インストールされました。

$ which pack
/usr/local/bin/pack


$ pack --version
0.14.2+git-0fd189d.build-1450

ヘルプを確認。

$ pack -h
CLI for building apps using Cloud Native Buildpacks

Usage:
  pack [command]

Available Commands:
  build                 Generate app image from source code
  rebase                Rebase app image with latest run image
  inspect-image         Show information about a built image
  inspect-buildpack     Show information about a buildpack
  set-run-image-mirrors Set mirrors to other repositories for a given run image
  set-default-builder   Set default builder used by other commands
  inspect-builder       Show information about a builder
  suggest-builders      Display list of recommended builders
  trust-builder         Trust builder
  untrust-builder       Stop trusting builder
  list-trusted-builders List Trusted Builders
  create-builder        Create builder image
  package-buildpack     Package buildpack in OCI format.
  suggest-stacks        Display list of recommended stacks
  version               Show current 'pack' version
  report                Display useful information for reporting an issue
  completion            Outputs completion script location
  help                  Help about any command

Flags:
  -h, --help         Help for 'pack'
      --no-color     Disable color output
  -q, --quiet        Show less output
      --timestamps   Enable timestamps in output
  -v, --verbose      Show more output
      --version      Show current 'pack' version

Use "pack [command] --help" for more information about a command.

bashのauto-completionがあるようなので、有効にしておきましょう。

. $(pack completion)

その他の情報については、packのドキュメントへ。

pack · Cloud Native Buildpacks

サンプルアプリケーションを作る

Getting Startedを見ているとサンプルアプリケーションが用意されているようなのですが、ここは自分で作成しておきましょう。

Javaで作るので、環境を書いておきましょう。ローカルの情報ですが。

$ java --version
openjdk 11.0.9 2020-10-20
OpenJDK Runtime Environment (build 11.0.9+11-Ubuntu-0ubuntu1.20.04)
OpenJDK 64-Bit Server VM (build 11.0.9+11-Ubuntu-0ubuntu1.20.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.6.3 (cecedd343002696d0abb50b32b541b8a6ba2883f)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 11.0.9, vendor: Ubuntu, runtime: /usr/lib/jvm/java-11-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.4.0-52-generic", arch: "amd64", family: "unix"

あとでわかりますが、packを使ったビルドはコンテナ内で行われます。

今回は、Vert.x Webを使い、Maven Shade Pluginも合わせて実行可能JARファイルを作成するMavenプロジェクトを作ります。
pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.littlewings</groupId>
    <artifactId>sample-app</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>io.vertx</groupId>
            <artifactId>vertx-web</artifactId>
            <version>3.9.4</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.2.4</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <transformers>
                        <transformer
                                implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                            <mainClass>org.littlewings.buildpacks.App</mainClass>
                        </transformer>
                    </transformers>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

ソースコード。至ってシンプルです。
src/main/java/org/littlewings/buildpacks/App.java

package org.littlewings.buildpacks;

import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;

import io.vertx.core.Vertx;
import io.vertx.core.http.HttpServer;
import io.vertx.core.http.HttpServerResponse;

public class App {
    public static void main(String... args) throws InterruptedException {
        Vertx vertx = Vertx.vertx();
        HttpServer server = vertx.createHttpServer();

        try {
            server.requestHandler(request -> {
                HttpServerResponse response = request.response();

                response.putHeader("Content-Type", "text/plain");
                response.end(String.format("[%s] Hello World", LocalDateTime.now()));
            });

            server.listen(8080);

            System.out.printf("[%s] Server startup.", LocalDateTime.now());

            while (true) {
                TimeUnit.SECONDS.sleep(5L);
            }

        } finally {
            server.close();
            vertx.close();
        }
    }
}

ビルドして

$ mvn package

動作確認。

$ java -jar target/sample-app-0.0.1-SNAPSHOT.jar 
[2020-10-31T21:53:02.876200] Server startup.

OKですね。

$ curl localhost:8080
[2020-10-31T21:53:15.596597] Hello World

cleanしておきます。

$ mvn clean

以降、カレントディレクトリはMavenプロジェクトのルートディレクトリとします。

こういう状態ですね。

$ find . -type f
./pom.xml
./src/main/java/org/littlewings/buildpacks/App.java

samplesの手順をマネてみる

Getting Startedに戻って、その手順をそのまま実行してみましょう。

Next stop, the end

本来は、こちらのサンプルコードを使うようです。

samples/apps/java-maven at main · buildpacks/samples · GitHub

とりあえず、なにも考えずにコマンドをそのまま実行してみます。

$ pack build myapp --builder cnbs/sample-builder:bionic

Javaのバージョンが合わず、エラーになりました。

[builder] [INFO] Changes detected - recompiling the module!
[builder] [INFO] Compiling 1 source file to /workspace/target/classes
[builder] [INFO] ------------------------------------------------------------------------
[builder] [INFO] BUILD FAILURE
[builder] [INFO] ------------------------------------------------------------------------
[builder] [INFO] Total time: 48.397 s
[builder] [INFO] Finished at: 2020-10-31T08:03:37Z
[builder] [INFO] ------------------------------------------------------------------------
[builder] [ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.1:compile (default-compile) on project sample-app: Fatal error compiling: invalid target release: 11 -> [Help 1]
[builder] [ERROR] 
[builder] [ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[builder] [ERROR] Re-run Maven using the -X switch to enable full debug logging.
[builder] [ERROR] 
[builder] [ERROR] For more information about the errors and possible solutions, please read the following articles:
[builder] [ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoExecutionException
[builder] ERROR: failed to build: exit status 1
ERROR: failed to build: executing lifecycle. This may be the result of using an untrusted builder: failed with status code: 145

このディレクトリの中のスクリプトを見ると、Java 8を想定していそうな感じです。

https://github.com/buildpacks/samples/tree/main/buildpacks/java-maven

さて、どうしましょう。

Paketo Buildpacks

ここで、Paketo Buildpacksを見てみることにします。

Paketo Buildpacks - Paketo Buildpacks

Paketo Buildpacksは、Cloud FoundryコミュニティによるCloud Native Buildpacksの実装です。

Java、Java Native Image、.NET Core、Node.js、Go、PHP、Ruby、nginxのBuildpacksが提供されています。

ドキュメントは、こちら。

Getting Started - Paketo Buildpacks

Dockerを要求するのは、Paketo Buildpacksでした…。

Builderはfull、base、tinyの3種類があるようです。

Builders - Paketo Buildpacks

pack suggest-buildersで、Paketo BuildpacksのBuilderも確認することができます。

$ pack suggest-builders
Suggested builders:
    Google:                gcr.io/buildpacks/builder:v1      Ubuntu 18 base image with buildpacks for .NET, Go, Java, Node.js, and Python                      
    Heroku:                heroku/buildpacks:18              heroku-18 base image with buildpacks for Ruby, Java, Node.js, Python, Golang, & PHP               
    Paketo Buildpacks:     paketobuildpacks/builder:base     Ubuntu bionic base image with buildpacks for Java, NodeJS and Golang                              
    Paketo Buildpacks:     paketobuildpacks/builder:full     Ubuntu bionic base image with buildpacks for Java, .NET, NodeJS, Golang, PHP, HTTPD and NGINX     
    Paketo Buildpacks:     paketobuildpacks/builder:tiny     Tiny base image (bionic build image, distroless run image) with buildpacks for Golang             

Tip: Learn more about a specific builder with:
    pack inspect-builder <builder-image>

inspect-builderで、どのようなものが含まれているかを確認することもできます。

$ pack inspect-builder paketobuildpacks/builder:base

では、デフォルトのBuilderをJava向けのイメージも含まれているpaketobuildpacks/builder:baseとしましょう。

$ pack set-default-builder paketobuildpacks/builder:base
Builder paketobuildpacks/builder:base is now the default builder

これで、pack build時にBuilderを指定しない場合は、paketobuildpacks/builder:baseがBuilderとして使われます。

$ pack build kazuhira/sample-app

buildに続くのは作成するイメージ名なので、タグ付けも好きに行うことができます。

$ pack build kazuhira/sample-app:latest
$ pack build kazuhira/sample-app:0.0.1

また、デフォルトのBuilder以外を使いたくなった場合は、--builderで他のBuilderを指定することもできます。

$ pack build kazuhira/sample-app --builder paketobuildpacks/builder:base

Getting Startedの例が、--builderを使っていましたね。

これが、「Operation」の「Build」なわけですねぇ…。

で、ビルドしてみると、初回はBuilderのDockerイメージのダウンロードが行われ

$ pack build kazuhira/sample-app
base: Pulling from paketobuildpacks/builder

〜省略〜

Detect、Analyzeが行われ

===> DETECTING
6 of 17 buildpacks participating
paketo-buildpacks/bellsoft-liberica 4.0.0
paketo-buildpacks/maven             3.1.1
paketo-buildpacks/executable-jar    3.1.1
paketo-buildpacks/apache-tomcat     2.3.0
paketo-buildpacks/dist-zip          2.2.0
paketo-buildpacks/spring-boot       3.2.1
===> ANALYZING
Previous image with name "kazuhira/sample-app" not found
Restoring metadata for "paketo-buildpacks/bellsoft-liberica:jdk" from cache
Restoring metadata for "paketo-buildpacks/maven:application" from cache
Restoring metadata for "paketo-buildpacks/maven:cache" from cache
Restoring metadata for "paketo-buildpacks/maven:maven" from cache
===> RESTORING
Restoring data for "paketo-buildpacks/bellsoft-liberica:jdk" from cache
Restoring data for "paketo-buildpacks/maven:application" from cache
Restoring data for "paketo-buildpacks/maven:cache" from cache
Restoring data for "paketo-buildpacks/maven:maven" from cache
===> BUILDING

選択されたBuildpackが次々に現れます。

Paketo BellSoft Liberica Buildpack 4.0.0
  https://github.com/paketo-buildpacks/bellsoft-liberica
  Build Configuration:
    $BP_JVM_VERSION              11.*            the Java version
  Launch Configuration:
    $BPL_JVM_HEAD_ROOM           0               the headroom in memory calculation
    $BPL_JVM_LOADED_CLASS_COUNT  35% of classes  the number of loaded classes in memory calculation
    $BPL_JVM_THREAD_COUNT        250             the number of threads in memory calculation
    $JAVA_TOOL_OPTIONS                           the JVM launch flags
  BellSoft Liberica JDK 11.0.8: Reusing cached layer
  BellSoft Liberica JRE 11.0.8: Contributing to layer
    Downloading from https://github.com/bell-sw/Liberica/releases/download/11.0.8+10/bellsoft-jre11.0.8+10-linux-amd64.tar.gz
    Verifying checksum
    Expanding to /layers/paketo-buildpacks_bellsoft-liberica/jre
    Adding 127 container CA certificates to JVM truststore
    Writing env.launch/BPI_APPLICATION_PATH.default
    Writing env.launch/BPI_JVM_CACERTS.default
    Writing env.launch/BPI_JVM_CLASS_COUNT.default
    Writing env.launch/BPI_JVM_SECURITY_PROVIDERS.default
    Writing env.launch/JAVA_HOME.default
    Writing env.launch/MALLOC_ARENA_MAX.default
  Launch Helper: Contributing to layer
    Creating /layers/paketo-buildpacks_bellsoft-liberica/helper/exec.d/active-processor-count
    Creating /layers/paketo-buildpacks_bellsoft-liberica/helper/exec.d/java-opts
    Creating /layers/paketo-buildpacks_bellsoft-liberica/helper/exec.d/link-local-dns
    Creating /layers/paketo-buildpacks_bellsoft-liberica/helper/exec.d/memory-calculator
    Creating /layers/paketo-buildpacks_bellsoft-liberica/helper/exec.d/openssl-certificate-loader
    Creating /layers/paketo-buildpacks_bellsoft-liberica/helper/exec.d/security-providers-configurer
    Creating /layers/paketo-buildpacks_bellsoft-liberica/helper/exec.d/security-providers-classpath-9
    Writing profile.d/helper
  JVMKill Agent 1.16.0: Contributing to layer
    Downloading from https://github.com/cloudfoundry/jvmkill/releases/download/v1.16.0.RELEASE/jvmkill-1.16.0-RELEASE.so
    Verifying checksum
    Copying to /layers/paketo-buildpacks_bellsoft-liberica/jvmkill
    Writing env.launch/JAVA_TOOL_OPTIONS.append
    Writing env.launch/JAVA_TOOL_OPTIONS.delim
  Java Security Properties: Contributing to layer
    Writing env.launch/JAVA_SECURITY_PROPERTIES.default
    Writing env.launch/JAVA_TOOL_OPTIONS.append
    Writing env.launch/JAVA_TOOL_OPTIONS.delim

Paketo Maven Buildpack 3.1.1
  https://github.com/paketo-buildpacks/maven
  Build Configuration:
    $BP_MAVEN_BUILD_ARGUMENTS  -Dmaven.test.skip=true package  the arguments to pass to Maven
    $BP_MAVEN_BUILT_ARTIFACT   target/*.[jw]ar                 the built application artifact explicitly.  Supersedes $BP_MAVEN_BUILT_MODULE
    $BP_MAVEN_BUILT_MODULE                                     the module to find application artifact in
  Apache Maven 3.6.3: Reusing cached layer
    Creating cache directory /home/cnb/.m2
  Compiled Application: Contributing to layer
    Executing mvn -Dmaven.test.skip=true package

Mavenビルドが行われ

[INFO] Scanning for projects...
[INFO] 
[INFO] ---------------------< org.littlewings:sample-app >---------------------
[INFO] Building sample-app 0.0.1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------

ビルド完了。

[INFO] Replacing original artifact with shaded artifact.
[INFO] Replacing /workspace/target/sample-app-0.0.1-SNAPSHOT.jar with /workspace/target/sample-app-0.0.1-SNAPSHOT-shaded.jar
[INFO] Dependency-reduced POM written at: /workspace/dependency-reduced-pom.xml
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  3.057 s
[INFO] Finished at: 2020-10-31T13:00:25Z
[INFO] ------------------------------------------------------------------------
  Removing source code

さらに、もうひとつBuildpackが実行されて完了。

Paketo Executable JAR Buildpack 3.1.1
  https://github.com/paketo-buildpacks/executable-jar
    Writing env.launch/CLASSPATH.delim
    Writing env.launch/CLASSPATH.prepend
  Process types:
    executable-jar: java org.littlewings.buildpacks.App
    task:           java org.littlewings.buildpacks.App
    web:            java org.littlewings.buildpacks.App
===> EXPORTING
Adding layer 'paketo-buildpacks/bellsoft-liberica:helper'
Adding layer 'paketo-buildpacks/bellsoft-liberica:java-security-properties'
Adding layer 'paketo-buildpacks/bellsoft-liberica:jre'
Adding layer 'paketo-buildpacks/bellsoft-liberica:jvmkill'
Adding layer 'paketo-buildpacks/executable-jar:class-path'
Adding 1/1 app layer(s)
Adding layer 'launcher'
Adding layer 'config'
Adding layer 'process-types'
Adding label 'io.buildpacks.lifecycle.metadata'
Adding label 'io.buildpacks.build.metadata'
Adding label 'io.buildpacks.project.metadata'
Setting default process type 'web'
*** Images (2038d8c3b916):
      kazuhira/sample-app
Reusing cache layer 'paketo-buildpacks/bellsoft-liberica:jdk'
Adding cache layer 'paketo-buildpacks/maven:application'
Reusing cache layer 'paketo-buildpacks/maven:cache'
Reusing cache layer 'paketo-buildpacks/maven:maven'
Successfully built image kazuhira/sample-app

Dockerイメージができあがりました。

$ docker image ls | grep sample
kazuhira/sample-app                latest              2038d8c3b916        40 years ago        254MB

実行してみます。

$ docker container run -it --rm --name sample -p 8080:8080 kazuhira/sample-app:latest 
Setting Active Processor Count to 8
WARNING: Container memory limit unset. Configuring JVM for 1G container.
Calculated JVM Memory Configuration: -XX:MaxDirectMemorySize=10M -Xmx466790K -XX:MaxMetaspaceSize=69785K -XX:ReservedCodeCacheSize=240M -Xss1M (Total Memory: 1G, Thread Count: 250, Loaded Class Count: 9907, Headroom: 0%)
Adding 127 container CA certificates to JVM truststore
Picked up JAVA_TOOL_OPTIONS: -Djava.security.properties=/layers/paketo-buildpacks_bellsoft-liberica/java-security-properties/java-security.properties -agentpath:/layers/paketo-buildpacks_bellsoft-liberica/jvmkill/jvmkill-1.16.0-RELEASE.so=printHeapHistogram=1 -XX:ActiveProcessorCount=8 -XX:MaxDirectMemorySize=10M -Xmx466790K -XX:MaxMetaspaceSize=69785K -XX:ReservedCodeCacheSize=240M -Xss1M
[2020-10-31T13:07:41.661609] Server startup.

OKですね。

$ curl localhost:8080
[2020-10-31T13:07:52.735419] Hello World

ビルドは、Dockerコンテナ内で行われているようで、初回のMavenビルド時は大量のJARファイルがダウンロードされます。

Downloading from central: https://repo.maven.apache.org/maven2/org/apache/maven/plugins/maven-resources-plugin/2.6/maven-resources-plugin-2.6.pom
Downloaded from central: https://repo.maven.apache.org/maven2/org/apache/maven/plugins/maven-resources-plugin/2.6/maven-resources-plugin-2.6.pom (8.1 kB at 6.0 kB/s)
Downloading from central: https://repo.maven.apache.org/maven2/org/apache/maven/plugins/maven-plugins/23/maven-plugins-23.pom
Downloaded from central: https://repo.maven.apache.org/maven2/org/apache/maven/plugins/maven-plugins/23/maven-plugins-23.pom (9.2 kB at 35 kB/s)
Downloading from central: https://repo.maven.apache.org/maven2/org/apache/maven/maven-parent/22/maven-parent-22.pom
Downloaded from central: https://repo.maven.apache.org/maven2/org/apache/maven/maven-parent/22/maven-parent-22.pom (30 kB at 83 kB/s)
Downloading from central: https://repo.maven.apache.org/maven2/org/apache/apache/11/apache-11.pom
Downloaded from central: https://repo.maven.apache.org/maven2/org/apache/apache/11/apache-11.pom (15 kB at 61 kB/s)
Downloading from central: https://repo.maven.apache.org/maven2/org/apache/maven/plugins/maven-resources-plugin/2.6/maven-resources-plugin-2.6.jar

2回目以降は起こらなくなりますが、これはキャッシュとしてvolumeに保存されているようです。

$ docker volume ls
DRIVER              VOLUME NAME
local               pack-cache-387b2463780f.build
local               pack-cache-387b2463780f.launch

イメージ名を変えると、キャッシュは再利用されなくなるようで

$ pack build kazuhira/sample-app:0.0.1

volumeも別になります。

$ docker volume ls
DRIVER              VOLUME NAME
local               pack-cache-24b92486e761.build
local               pack-cache-24b92486e761.launch
local               pack-cache-387b2463780f.build
local               pack-cache-387b2463780f.launch

ここまで出てきたBuildpackを見てみる

今回、Builderとしてはpaketobuildpacks/builder:baseを選択しましたが、この中からJava向けのBuildpackが使われたことになります。

Java Buildpack - Paketo Buildpacks

さらにJava向けのBuildpackはいくつかあり、今回はこの中から選択されたことになります。

Components

Spring Boot用のものもあったりしますね。

これらは、ビルド対象のアプリケーションのファイルの種類から決定(Detect)されます。

ビルド時のログは、こう出力されていました。

Paketo BellSoft Liberica Buildpack 4.0.0

Paketo Maven Buildpack 3.1.1

Paketo Executable JAR Buildpack 3.1.1

というわけで、今回使われたのは以下の3つのBuildpackです。

GitHub - paketo-buildpacks/bellsoft-liberica: A Cloud Native Buildpack that provides the Bellsoft Liberica implementations of JREs and JDKs

GitHub - paketo-buildpacks/maven: A Cloud Native Buildpack that builds Maven-based applications from source

GitHub - paketo-buildpacks/executable-jar: A Cloud Native Buildpack that contributes a Process Type for executable JARs.

このうち、Paketo BellSoft Liberica BuildpackはJavaでは必須になっているみたいですね。

ソースコードを見てみると、DetectやBuildはGoで表現されていて面白いです。

https://github.com/paketo-buildpacks/maven/blob/v3.1.1/maven/detect.go

https://github.com/paketo-buildpacks/maven/blob/v3.1.1/maven/build.go

実行コマンド。

https://github.com/paketo-buildpacks/maven/blob/v3.1.1/cmd/main/main.go

こうやって、どのBuildpackを使うかをアプリケーションのソースコードから検出して、ビルドもこの中で行ってしまうのが
Buildpackです、と。

ホスト側に言語環境がなくても、ビルドできるってことになりますね。

PaaSとかでのビルドには、確かに便利そうですね。この仕組み内でビルドできることが強制されますし。

設定を行う

ところで、ビルドには成功しましたが設定方法なども気になるところです。

Buildpackには、環境変数(ビルド時、実行時)、buildpack.yml、Binding、Procfileの4つの設定があるようです。

Types of Configuration

今回は、環境変数を見てみましょう。

JavaのBuildpackで、ビルド時の設定。

Configuring the Build Tool

たとえば、Mavenビルドの設定を変えてみましょう。

Configuration

GitHub - paketo-buildpacks/maven: A Cloud Native Buildpack that builds Maven-based applications from source

BP_MAVEN_BUILD_ARGUMENTSを使うと、Mavenに渡すオプションを設定することができます。

pack buildでは、--envで指定します。

$ pack build kazuhira/sample-app --env BP_MAVEN_BUILD_ARGUMENTS='-DskipTests=true package'

結果。

Paketo Maven Buildpack 3.1.1
  https://github.com/paketo-buildpacks/maven
  Build Configuration:
    $BP_MAVEN_BUILD_ARGUMENTS  -DskipTests=true package  the arguments to pass to Maven
    $BP_MAVEN_BUILT_ARTIFACT   target/*.[jw]ar           the built application artifact explicitly.  Supersedes $BP_MAVEN_BUILT_MODULE
    $BP_MAVEN_BUILT_MODULE                               the module to find application artifact in
  Apache Maven 3.6.3: Reusing cached layer
    Creating cache directory /home/cnb/.m2
  Compiled Application: Contributing to layer
    Executing mvn -DskipTests=true package

この部分に反映されましたね。

    Executing mvn -DskipTests=true package

この環境変数の値は、デフォルトでは-Dmaven.test.skip=true packageになっています。

Paketo Maven Buildpack 3.1.1
  https://github.com/paketo-buildpacks/maven
  Build Configuration:
    $BP_MAVEN_BUILD_ARGUMENTS  -Dmaven.test.skip=true package  the arguments to pass to Maven
    $BP_MAVEN_BUILT_ARTIFACT   target/*.[jw]ar                 the built application artifact explicitly.  Supersedes $BP_MAVEN_BUILT_MODULE
    $BP_MAVEN_BUILT_MODULE                                     the module to find application artifact in
  Apache Maven 3.6.3: Reusing cached layer
    Creating cache directory /home/cnb/.m2
  Compiled Application: Contributing to layer
    Executing mvn -Dmaven.test.skip=true package

実行時に関する設定は、こちら。

Runtime JVM Configuration

実行時の引数を指定する

実行するコンテナイメージの引数は、そのままコンテナ起動時に渡せばOKです。

Providing Additional Arguments

たとえば、サンプルアプリケーションを引数を受け取るように修正してみましょう。
src/main/java/org/littlewings/buildpacks/App.java

package org.littlewings.buildpacks;

import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;

import io.vertx.core.Vertx;
import io.vertx.core.http.HttpServer;
import io.vertx.core.http.HttpServerResponse;

public class App {
    public static void main(String... args) throws InterruptedException {
        int port;

        if (args.length > 0) {
            port = Integer.parseInt(args[0]);
        } else {
            port = 8080;
        }

        Vertx vertx = Vertx.vertx();
        HttpServer server = vertx.createHttpServer();

        try {
            server.requestHandler(request -> {
                HttpServerResponse response = request.response();

                response.putHeader("Content-Type", "text/plain");
                response.end(String.format("[%s] Hello World", LocalDateTime.now()));
            });

            server.listen(port);

            System.out.printf("[%s] Server startup, port = %d%n", LocalDateTime.now(), port);

            while (true) {
                TimeUnit.SECONDS.sleep(5L);
            }

        } finally {
            server.close();
            vertx.close();
        }
    }
}

リッスンポートを指定できるようにしました。

    public static void main(String... args) throws InterruptedException {
        int port;

        if (args.length > 0) {
            port = Integer.parseInt(args[0]);
        } else {
            port = 8080;
        }

そして、リッスンポートをログ出力します。

            server.listen(port);

            System.out.printf("[%s] Server startup, port = %d%n", LocalDateTime.now(), port);

再度ビルド。

$ pack build kazuhira/sample-app

引数を指定しない場合。

$ docker container run -it --rm --name sample kazuhira/sample-app:latest 
Setting Active Processor Count to 8
WARNING: Container memory limit unset. Configuring JVM for 1G container.
Calculated JVM Memory Configuration: -XX:MaxDirectMemorySize=10M -Xmx466790K -XX:MaxMetaspaceSize=69785K -XX:ReservedCodeCacheSize=240M -Xss1M (Total Memory: 1G, Thread Count: 250, Loaded Class Count: 9907, Headroom: 0%)
Adding 127 container CA certificates to JVM truststore
Picked up JAVA_TOOL_OPTIONS: -Djava.security.properties=/layers/paketo-buildpacks_bellsoft-liberica/java-security-properties/java-security.properties -agentpath:/layers/paketo-buildpacks_bellsoft-liberica/jvmkill/jvmkill-1.16.0-RELEASE.so=printHeapHistogram=1 -XX:ActiveProcessorCount=8 -XX:MaxDirectMemorySize=10M -Xmx466790K -XX:MaxMetaspaceSize=69785K -XX:ReservedCodeCacheSize=240M -Xss1M
[2020-10-31T13:25:08.819790] Server startup, port = 8080

引数を指定した場合。

$ docker container run -it --rm --name sample kazuhira/sample-app:latest 9000
Setting Active Processor Count to 8
WARNING: Container memory limit unset. Configuring JVM for 1G container.
Calculated JVM Memory Configuration: -XX:MaxDirectMemorySize=10M -Xmx466790K -XX:MaxMetaspaceSize=69785K -XX:ReservedCodeCacheSize=240M -Xss1M (Total Memory: 1G, Thread Count: 250, Loaded Class Count: 9907, Headroom: 0%)
Adding 127 container CA certificates to JVM truststore
Picked up JAVA_TOOL_OPTIONS: -Djava.security.properties=/layers/paketo-buildpacks_bellsoft-liberica/java-security-properties/java-security.properties -agentpath:/layers/paketo-buildpacks_bellsoft-liberica/jvmkill/jvmkill-1.16.0-RELEASE.so=printHeapHistogram=1 -XX:ActiveProcessorCount=8 -XX:MaxDirectMemorySize=10M -Xmx466790K -XX:MaxMetaspaceSize=69785K -XX:ReservedCodeCacheSize=240M -Xss1M
[2020-10-31T13:26:07.587287] Server startup, port = 9000

反映されましたね。

ところで、ベースイメージは?

ビルドに使われているBuildpackはわかりましたが、そういえば実行時に使われているイメージがわかりません。

Stackの説明を見ると、Builderの設定ファイルに含まれていそうな気がします。

Packeto Executable JAR Buildpackの[[stack]]を見てみます。

[[stacks]]
id = "io.buildpacks.stacks.bionic"

[[stacks]]
id = "io.paketo.stacks.tiny"

[[stacks]]
id = "org.cloudfoundry.stacks.cflinuxfs3"

https://github.com/paketo-buildpacks/executable-jar/blob/v3.1.1/buildpack.toml

Buildpackをinspect-buildpackしてみます。

$ pack inspect-buildpack gcr.io/paketo-buildpacks/executable-jar
Inspecting buildpack: gcr.io/paketo-buildpacks/executable-jar

REMOTE IMAGE:

Stacks:
  ID: io.buildpacks.stacks.bionic
    Mixins:
      (omitted)
  ID: io.paketo.stacks.tiny
    Mixins:
      (omitted)
  ID: org.cloudfoundry.stacks.cflinuxfs3
    Mixins:
      (omitted)

Buildpacks:
  ID                                      VERSION        HOMEPAGE
  paketo-buildpacks/executable-jar        3.1.1          https://github.com/paketo-buildpacks/executable-jar

Detection Order:
 └ Group #1:
    └ paketo-buildpacks/executable-jar@3.1.1

よくわかりませんね…。

suggest-stacksを見てみます。

$ pack suggest-stacks 
Stacks maintained by the community:

    Stack ID: heroku-18
    Description: The official Heroku stack based on Ubuntu 18.04
    Maintainer: Heroku
    Build Image: heroku/pack:18-build
    Run Image: heroku/pack:18

    Stack ID: io.buildpacks.stacks.bionic
    Description: A minimal Paketo stack based on Ubuntu 18.04
    Maintainer: Paketo Project
    Build Image: paketobuildpacks/build:base-cnb
    Run Image: paketobuildpacks/run:base-cnb

    Stack ID: io.buildpacks.stacks.bionic
    Description: A large Paketo stack based on Ubuntu 18.04
    Maintainer: Paketo Project
    Build Image: paketobuildpacks/build:full-cnb
    Run Image: paketobuildpacks/run:full-cnb

    Stack ID: io.paketo.stacks.tiny
    Description: A tiny Paketo stack based on Ubuntu 18.04, similar to distroless
    Maintainer: Paketo Project
    Build Image: paketobuildpacks/build:tiny-cnb
    Run Image: paketobuildpacks/run:tiny-cnb

今回、Builderはbaseを選んだので、イメージはこちらですね。

    Stack ID: io.buildpacks.stacks.bionic
    Description: A minimal Paketo stack based on Ubuntu 18.04
    Maintainer: Paketo Project
    Build Image: paketobuildpacks/build:base-cnb
    Run Image: paketobuildpacks/run:base-cnb

まとめ

Cloud Native Buildpacksを試してみました。

Dockerfileを使ったビルドとだいぶ違いますが、これはこれで便利ですねぇ。代わりに、イメージ作成の際の自由度は低い…というか、
Buildpackによって決められたことしかできない感じもします。

いろいろ学ぶことが多そうですけど、Spring Bootでも使われていますし、多少は知っておいた方がいいのかな、と。