CLOVER🍀

That was when it all began.

Serverless FrameworkをLocalStackで使ってみる(Amazon API Gateway+AWS Lambda)

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

LocalStackでAWS Lambda、それからAmazon API Gatewayの組み合わせでいろいろ遊んでいるのですが。

1度、Serverless Framewokにも触れた方がいいかなと思い、試してみることにしました。

LocalStackにも、Serverless Framework向けの話があるようですし。

https://localstack.cloud/docs/how-to/serverless/

Serverless Framework

まずはServerless Frameworkとは?というとこからですが。

Serverless: Develop & Monitor Apps On AWS Lambda

Serverless Frameworkは、サーバーレスアプリケーションの開発、デプロイ、トラブルシュートなどのオーバーヘッド、
コストを削減するフレームワークのようです。

Serverless Framework Documentation

OSSで公開されているCLIと、ホストされているダッシュボードで構成されています。

AWS SAMと似たような感じですが、AWS以外にもAzure Functions、Google Cloud Functionsなど
複数のクラウドプロバイダーに対応しているところがポイントです。

Serverless - Infrastructure & Compute Providers

プロジェクトを作成する際に、テンプレートを指定して複数の言語からプロジェクトの構成を選ぶことができるみたいです。

Serverless Framework Services

LocalStackとServerless Framework

次に、LocalStackとServerless Frameworkについて。

LocalStackのドキュメントに、Serverless Frameworkに向けた対応が書かれています。

https://localstack.cloud/docs/how-to/serverless/

LocalStack / Serverless Framework

具体的には、LocalStackがServerless Framework向けのプラグインを提供しているので、こちらを使えばよいみたいです。

GitHub - localstack/serverless-localstack: ⚡ Serverless plugin for running against LocalStack

一方で、Serverless Frameworkもオフラインで動作させるためのプラグインを提供しているようです。

Serverless Framework: Plugins

視点がLocalStackのようなAWSの多くのサービスをエミュレーションするものの一部として使いたいのか、
Serverless Frameworkの範囲で動作すればいいのかで選ぶんでしょうね。

では、今回はAmazon API GatewayAWS Lambdaの構成のアプリケーションを、Serverless Frameworkと
LocalStackで使ってみましょう。

環境

今回の環境は、こちら。

LocalStack。

$ localstack --version
0.12.18.3

起動。

$ LAMBDA_EXECUTOR=docker-reuse localstack start

AWS CLI

$ aws --version
aws-cli/2.2.44 Python/3.8.8 Linux/5.4.0-88-generic exe/x86_64.ubuntu.20 prompt/off

Serverless Frameworkを使うための、Node.js。

$ node -v
v14.18.0


$ npm -v
6.14.15

Javaに関するもの。

$ java --version
openjdk 11.0.11 2021-04-20
OpenJDK Runtime Environment (build 11.0.11+9-Ubuntu-0ubuntu2.20.04)
OpenJDK 64-Bit Server VM (build 11.0.11+9-Ubuntu-0ubuntu2.20.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.8.3 (ff8e977a158738155dc465c6a97ffaf31982d739)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 11.0.11, 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-88-generic", arch: "amd64", family: "unix"

Serverless Frameworkをインストールする

では、Serverless Frameworkのインストールから

Setting Up Serverless Framework With AWS

インストール方法はいろいろあるのですが、LocalStackのServerless Frameworkのプラグインnpmでインストールする
形態なので、Serverless Frameworkも合わせてnpmでインストールすることにしました。

というわけで、Serverless FramewokとLocalStackのプラグインをインストール。

$ npm i -g serverless
$ npm i -D serverless-localstack

バージョン。

$ serverless --version
Framework Core: 2.62.0 (local)
Plugin: 5.4.6
SDK: 4.3.0
Components: 3.17.1

LocalStackのServerless Frameworkプラグインのバージョンは、こちら。

$ cat node_modules/serverless-localstack/package.json | jq '._id'
"serverless-localstack@0.4.35"

では、こちらのドキュメントに書かれているテンプレートを見つつ

Serverless Framework Services

プロジェクトの作成。テンプレートは、aws-java-mavenを選択。

$ serverless create --template aws-java-maven --path serverless-localstack-java

プロジェクトが作成されました。

Serverless: Generating boilerplate...
Serverless: Generating boilerplate in "/path/to/serverless-localstack-java"
Serverless: Successfully generated boilerplate for template: "aws-java-maven"

ディレクトリ内に移動して

$ cd serverless-localstack-java

できあがったファイルを確認。

$ tree
.
├── pom.xml
├── serverless.yml
└── src
    └── main
        ├── java
        │   └── com
        │       └── serverless
        │           ├── ApiGatewayResponse.java
        │           ├── Handler.java
        │           └── Response.java
        └── resources
            └── log4j2.xml

6 directories, 6 files

pom.xml

<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>com.serverless</groupId>
  <artifactId>hello</artifactId>
  <packaging>jar</packaging>
  <version>dev</version>
  <name>hello</name>

  <properties>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>
    <dependency>
      <groupId>com.amazonaws</groupId>
      <artifactId>aws-lambda-java-log4j2</artifactId>
      <version>1.1.0</version>
    </dependency>
    <dependency>
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-core</artifactId>
      <version>2.8.2</version>
    </dependency>
    <dependency>
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-api</artifactId>
      <version>2.8.2</version>
    </dependency>
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-core</artifactId>
      <version>2.9.10</version>
    </dependency>
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.9.10</version>
    </dependency>
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-annotations</artifactId>
      <version>2.9.10</version>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <!--
        Using the Apache Maven Shade plugin to package the jar

        "This plugin provides the capability to package the artifact
        in an uber-jar, including its dependencies and to shade - i.e. rename -
        the packages of some of the dependencies."

        Link: https://maven.apache.org/plugins/maven-shade-plugin/
      -->
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-shade-plugin</artifactId>
        <version>2.3</version>
        <configuration>
          <createDependencyReducedPom>false</createDependencyReducedPom>
        </configuration>
        <executions>
          <execution>
            <phase>package</phase>
            <goals>
              <goal>shade</goal>
            </goals>
            <configuration>
              <transformers>
                <transformer
                  implementation="com.github.edwgiz.mavenShadePlugin.log4j2CacheTransformer.PluginsCacheFileTransformer">
                </transformer>
              </transformers>
            </configuration>
          </execution>
        </executions>
        <dependencies>
          <dependency>
            <groupId>com.github.edwgiz</groupId>
            <artifactId>maven-shade-plugin.log4j2-cachefile-transformer</artifactId>
            <version>2.8.1</version>
          </dependency>
        </dependencies>
      </plugin>
    </plugins>
  </build>

</project>

よく見ると、プロジェクト名とは全然関係ないアーティファクト名などになっているんですね。まあ、テンプレート
そのままなんでしょう。

  <groupId>com.serverless</groupId>
  <artifactId>hello</artifactId>
  <packaging>jar</packaging>
  <version>dev</version>
  <name>hello</name>

serverless.ymlの内容。

$ grep -v '#' serverless.yml

service: serverless-localstack-java

frameworkVersion: '2'

provider:
  name: aws
  runtime: java8
  lambdaHashingVersion: 20201221




package:
  artifact: target/hello-dev.jar

functions:
  hello:
    handler: com.serverless.Handler

自動生成された、Javaソースコード

src/main/java/com/serverless/Handler.java

package com.serverless;

import java.util.Collections;
import java.util.Map;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;

public class Handler implements RequestHandler<Map<String, Object>, ApiGatewayResponse> {

    private static final Logger LOG = LogManager.getLogger(Handler.class);

    @Override
    public ApiGatewayResponse handleRequest(Map<String, Object> input, Context context) {
        LOG.info("received: {}", input);
        Response responseBody = new Response("Go Serverless v1.x! Your function executed successfully!", input);
        return ApiGatewayResponse.builder()
                .setStatusCode(200)
                .setObjectBody(responseBody)
                .setHeaders(Collections.singletonMap("X-Powered-By", "AWS Lambda & serverless"))
                .build();
    }
}

ApiGatewayResponseResponseといったクラスは、Serverless Frameworkなどが提供しているものではなく、
生成されたソースコードの中に含まれているものになります。

src/main/java/com/serverless/ApiGatewayResponse.java

package com.serverless;

import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Collections;
import java.util.Map;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

public class ApiGatewayResponse {

    private final int statusCode;
    private final String body;
    private final Map<String, String> headers;
    private final boolean isBase64Encoded;

    public ApiGatewayResponse(int statusCode, String body, Map<String, String> headers, boolean isBase64Encoded) {
        this.statusCode = statusCode;
        this.body = body;
        this.headers = headers;
        this.isBase64Encoded = isBase64Encoded;
    }

    public int getStatusCode() {
        return statusCode;
    }

    public String getBody() {
        return body;
    }

    public Map<String, String> getHeaders() {
        return headers;
    }

    // API Gateway expects the property to be called "isBase64Encoded" => isIs
    public boolean isIsBase64Encoded() {
        return isBase64Encoded;
    }

    public static Builder builder() {
        return new Builder();
    }

    public static class Builder {

        private static final Logger LOG = LogManager.getLogger(ApiGatewayResponse.Builder.class);

        private static final ObjectMapper objectMapper = new ObjectMapper();

        private int statusCode = 200;
        private Map<String, String> headers = Collections.emptyMap();
        private String rawBody;
        private Object objectBody;
        private byte[] binaryBody;
        private boolean base64Encoded;

        public Builder setStatusCode(int statusCode) {
            this.statusCode = statusCode;
            return this;
        }

        public Builder setHeaders(Map<String, String> headers) {
            this.headers = headers;
            return this;
        }

        /**
        * Builds the {@link ApiGatewayResponse} using the passed raw body string.
        */
        public Builder setRawBody(String rawBody) {
            this.rawBody = rawBody;
            return this;
        }

        /**
        * Builds the {@link ApiGatewayResponse} using the passed object body
        * converted to JSON.
        */
        public Builder setObjectBody(Object objectBody) {
            this.objectBody = objectBody;
            return this;
        }

        /**
        * Builds the {@link ApiGatewayResponse} using the passed binary body
        * encoded as base64. {@link #setBase64Encoded(boolean)
        * setBase64Encoded(true)} will be in invoked automatically.
        */
        public Builder setBinaryBody(byte[] binaryBody) {
            this.binaryBody = binaryBody;
            setBase64Encoded(true);
            return this;
        }

        /**
        * A binary or rather a base64encoded responses requires
        * <ol>
        * <li>"Binary Media Types" to be configured in API Gateway
        * <li>a request with an "Accept" header set to one of the "Binary Media
        * Types"
        * </ol>
        */
        public Builder setBase64Encoded(boolean base64Encoded) {
            this.base64Encoded = base64Encoded;
            return this;
        }

        public ApiGatewayResponse build() {
            String body = null;
            if (rawBody != null) {
                body = rawBody;
            } else if (objectBody != null) {
                try {
                    body = objectMapper.writeValueAsString(objectBody);
                } catch (JsonProcessingException e) {
                    LOG.error("failed to serialize object", e);
                    throw new RuntimeException(e);
                }
            } else if (binaryBody != null) {
                body = new String(Base64.getEncoder().encode(binaryBody), StandardCharsets.UTF_8);
            }
            return new ApiGatewayResponse(statusCode, body, headers, base64Encoded);
        }
    }
}

src/main/java/com/serverless/Response.java

package com.serverless;

import java.util.Map;

public class Response {

    private final String message;
    private final Map<String, Object> input;

    public Response(String message, Map<String, Object> input) {
        this.message = message;
        this.input = input;
    }

    public String getMessage() {
        return this.message;
    }

    public Map<String, Object> getInput() {
        return this.input;
    }
}

では、こちらをLocalStackにデプロイしていきましょう。

とりあえず、プロジェクトで使うJavaが1.8になっているので、こちらを11に変更しておきましょう。

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

serverless.ymlruntimejava11に変更しておきます。

provider:
  name: aws
  runtime: java11
  lambdaHashingVersion: 20201221

次に、LocalStackのServerless Frameworkプラグインの設定を行います。

LocalStack Serverless Plugin / Configuring / Configuration via serverless.yml

最小構成はpluginsserverless-localstackを足せばOKなようですが、なんとなくcustomの方も定義だけは
書いておきました。

frameworkVersion: '2'

plugins:
  - serverless-localstack

custom:
  localstack:

provider:
  name: aws
  runtime: java11
  lambdaHashingVersion: 20201221

あとはなにも考えずにmvn packageして

$ mvn package

デプロイ。

$ serverless deploy

LocalStackへのデプロイが始まり

Serverless: Using serverless-localstack
Serverless: Packaging service...
Serverless: Creating Stack...
Serverless: Checking Stack create progress...
........
Serverless: Stack create finished...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service hello-dev.jar file to S3 (3.38 MB)...
Serverless: Validating template...
Serverless: Skipping template validation: Unsupported in Localstack
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
........

完了。

Serverless: Stack update finished...
Service Information
service: serverless-localstack-java
stage: dev
region: us-east-1
stack: serverless-localstack-java-dev
resources: 6
api keys:
  None
endpoints:
functions:
  hello: serverless-localstack-java-dev-hello
layers:
  None

ただ、これだとAWS Lambda関数はデプロイされているのですが、Amazon API Gatewayなどは作成されません。

$ awslocal apigateway get-rest-apis
{
    "items": []
}

serverless.ymlの変更が必要そうです。

こちらのドキュメントを参照して

Serverless Framework - AWS Lambda Events - REST API (API Gateway v1)

リソース定義を変更。

functions:
  hello:
    handler: com.serverless.Handler
#    The following are a few example events you can configure
#    NOTE: Please make sure to change your handler code to work with those events
#    Check the event documentation for details
    events:
      - http:
          path: /hello
          method: get

httpをイベントとして設定。

ちなみに、最初の定義はこうでした。

functions:
  hello:
    handler: com.serverless.Handler

では、再度デプロイ。

$ serverless deploy
Serverless: Using serverless-localstack
Serverless: Packaging service...
 
 Serverless Error ----------------------------------------
 
  The serverless deployment bucket "serverless-localstack-java-de-serverlessdeploymentbuck-d23aaeee" does not exist. Create it manually if you want to reuse the CloudFormation stack "serverless-localstack-java-dev", or delete the stack if it is no longer required.
 
  Get Support --------------------------------------------
     Docs:          docs.serverless.com
     Bugs:          github.com/serverless/serverless/issues
     Issues:        forum.serverless.com
 
  Your Environment Information ---------------------------
     Operating System:          linux
     Node Version:              14.18.0
     Framework Version:         2.62.0
     Plugin Version:            5.4.6
     SDK Version:               4.3.0
     Components Version:        3.17.1
 

S3バケットがないと言われたので…表示されている名前のバケットを作成して

$ awslocal s3 mb s3://serverless-localstack-java-de-serverlessdeploymentbuck-d23aaeee
make_bucket: serverless-localstack-java-de-serverlessdeploymentbuck-d23aaeee

再度デプロイ。

$ serverless deploy

今度はうまくいきました。

Serverless: Using serverless-localstack
Serverless: Packaging service...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service hello-dev.jar file to S3 (3.38 MB)...
Serverless: Validating template...
Serverless: Skipping template validation: Unsupported in Localstack
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
.............
Serverless: Stack update finished...
Service Information
service: serverless-localstack-java
stage: dev
region: us-east-1
stack: serverless-localstack-java-dev
resources: 11
api keys:
  None
endpoints:
  http://localhost:4566/restapis/mo8mdxctbu/dev/_user_request_
functions:
  hello: serverless-localstack-java-dev-hello
layers:
  None

REST APIのIDも表示されているので、こちらを使って

$ REST_API_ID=mo8mdxctbu

LocalStackで動作しているAmazon API Gateway越しにcurlでアクセスしてみます。

$ curl http://localhost:4566/restapis/$REST_API_ID/dev/_user_request_/hello
{"message":"Go Serverless v1.x! Your function executed successfully!","input":{"path":"/hello","headers":{"remote-addr":"172.17.0.1","host":"localhost:4566","user-agent":"curl/7.68.0","accept":"*/*","x-forwarded-for":"172.17.0.1, localhost:4566, 127.0.0.1, localhost:4566","x-localstack-edge":"http://localhost:4566","authorization":"AWS4-HMAC-SHA256 Credential=__internal_call__/20160623/us-east-1/apigateway/aws4_request, SignedHeaders=content-type;host;x-amz-date;x-amz-target, Signature=1234","x-localstack-tgt-api":"apigateway","__apigw_request_region__":"us-east-1"},"multiValueHeaders":{"remote-addr":["172.17.0.1"],"host":["localhost:4566"],"user-agent":["curl/7.68.0"],"accept":["*/*"],"x-forwarded-for":["172.17.0.1, localhost:4566, 127.0.0.1, localhost:4566"],"x-localstack-edge":["http://localhost:4566"],"authorization":["AWS4-HMAC-SHA256 Credential=__internal_call__/20160623/us-east-1/apigateway/aws4_request, SignedHeaders=content-type;host;x-amz-date;x-amz-target, Signature=1234"],"x-localstack-tgt-api":["apigateway"],"__apigw_request_region__":["us-east-1"]},"body":"","isBase64Encoded":false,"httpMethod":"GET","queryStringParameters":{},"multiValueQueryStringParameters":{},"pathParameters":{},"resource":"/hello","requestContext":{"path":"/dev/hello","resourcePath":"/hello","apiId":"mo8mdxctbu","domainPrefix":"mo8mdxctbu","domainName":"mo8mdxctbu.execute-api.localhost.localstack.cloud","accountId":"000000000000","resourceId":"35mudhf87g","stage":"dev","identity":{"accountId":"000000000000","sourceIp":"127.0.0.1","userAgent":"curl/7.68.0"},"httpMethod":"GET","protocol":"HTTP/1.1","requestTime":"2021-10-12T06:40:09.696Z","requestTimeEpoch":1634020809696},"stageVariables":{}}}

OKですね。

あと、removeでリソースを削除できるはずなのですが…

$ serverless remove

こちらはうまくいかなかったので、今回は置いておくことにしました。

Serverless: Using serverless-localstack
Serverless: Getting all objects in S3 bucket...
 
 Serverless Error ----------------------------------------
 
  ServerlessError: UnknownError
      at /path/to/serverless/node_modules/serverless/lib/aws/request.js:225:11
      at processTicksAndRejections (internal/process/task_queues.js:95:5)
      at async persistentRequest (/path/to/serverless/node_modules/serverless/lib/aws/request.js:149:14)
 
     For debugging logs, run again after setting the "SLS_DEBUG=*" environment variable.
 
  Get Support --------------------------------------------
     Docs:          docs.serverless.com
     Bugs:          github.com/serverless/serverless/issues
     Issues:        forum.serverless.com
 
  Your Environment Information ---------------------------
     Operating System:          linux
     Node Version:              14.18.0
     Framework Version:         2.62.0
     Plugin Version:            5.4.6
     SDK Version:               4.3.0
     Components Version:        3.17.1
 
Serverless: Removing objects in S3 bucket...
Serverless: Removing Stack...

まとめ

Serverless FrameworkをLocalStackで使ってみました。

とはいえ、単純にデプロイしてみただけなので、Serverless Frameworkのほとんどの機能は使えていないわけですが。

ドキュメントを見ているとダッシュボード、CI/CD、モニタリング、テストなどいろいろできそうなのですが、
AWSに閉じるのならまずはAWS SAMあたりを使っておけばよいのかもな、とも。

ちょっといろいろ試しながら、考えていきましょう。

Quarkusでコンテナイメージを作成する(Docker Extension/Jib Extension)

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

Quarkusのドキュメントを見ていて、コンテナイメージを作成するExtensionができていることに気づいたので。

Quarkus - Container Images

ちょっと試してみようかなと。

Quarkus Container Images extension

Quarkus 1.3.0.Finalで、Jib、Docker、S2Iを使ってコンテナイメージを作成するExtensionが追加されたようです。

Quarkus - Quarkus 1.3.0.Final released - New class loader infrastructure, GraalVM 20 support and much much more

ドキュメントはこちら。

Quarkus - Container Images

最初に書きましたが、Jib、Docker、S2Iをサポートしていて、今回はJibとDockerを扱おうと思います。

Quarkusはプロジェクト作成時にDockerfileも作成していたはずなので、Docker Extensionは最初は位置づけが
よくわからなかったのですが、これらのExtensionは「コンテナイメージのビルドやPushを抽象化するもの」みたいですね。

環境

今回の環境は、こちら。

$ java --version
openjdk 11.0.11 2021-04-20
OpenJDK Runtime Environment (build 11.0.11+9-Ubuntu-0ubuntu2.20.04)
OpenJDK 64-Bit Server VM (build 11.0.11+9-Ubuntu-0ubuntu2.20.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.8.3 (ff8e977a158738155dc465c6a97ffaf31982d739)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 11.0.11, 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-88-generic", arch: "amd64", family: "unix"

Docker Extensionのあり/なしの差を確認する

まずは、Docker Extensionを見てみましょう。

Build Container Images / Docker

比較のために、Docker extensionを使わないプロジェクトの作成を合わせて行います。

RESTEasy Reactive Extensionを使った、シンプルなプロジェクトを作成。プロジェクト名はcontainer-image-simply
しておきましょう。

$ mvn io.quarkus.platform:quarkus-maven-plugin:2.3.0.Final:create \
    -DprojectGroupId=org.littlewings \
    -DprojectArtifactId=container-image-simply \
    -DprojectVersion=0.0.1-SNAPSHOT \
    -Dextensions="resteasy-reactive"

Extensionと生成されるコードの種類が表示されます。

[INFO] selected extensions: 
- io.quarkus:quarkus-resteasy-reactive

[INFO] 
applying codestarts...
[INFO] 📚  java
🔨  maven
📦  quarkus
📝  config-properties
🔧  dockerfiles
🔧  maven-wrapper
🚀  resteasy-reactive-codestart
[INFO] 
-----------

そのまま、Docker Extension(container-image-docker)を追加したプロジェクトも作成します。こちらのプロジェクト名は、
container-image-dockerとしておきます。

$ mvn io.quarkus.platform:quarkus-maven-plugin:2.3.0.Final:create \
    -DprojectGroupId=org.littlewings \
    -DprojectArtifactId=container-image-docker \
    -DprojectVersion=0.0.1-SNAPSHOT \
    -Dextensions="resteasy-reactive,container-image-docker"

先ほどとの差は、Extensionが追加されたくらいです。

[INFO] selected extensions: 
- io.quarkus:quarkus-container-image-docker
- io.quarkus:quarkus-resteasy-reactive

[INFO] 
applying codestarts...
[INFO] 📚  java
🔨  maven
📦  quarkus
📝  config-properties
🔧  dockerfiles
🔧  maven-wrapper
🚀  resteasy-reactive-codestart
[INFO] 

それぞれ、treeで表示してみましょう。

$ tree container-image-simply
container-image-simply
├── README.md
├── mvnw
├── mvnw.cmd
├── pom.xml
└── src
    ├── main
    │   ├── docker
    │   │   ├── Dockerfile.jvm
    │   │   ├── Dockerfile.legacy-jar
    │   │   ├── Dockerfile.native
    │   │   └── Dockerfile.native-distroless
    │   ├── java
    │   │   └── org
    │   │       └── littlewings
    │   │           └── ReactiveGreetingResource.java
    │   └── resources
    │       ├── META-INF
    │       │   └── resources
    │       │       └── index.html
    │       └── application.properties
    └── test
        └── java
            └── org
                └── littlewings
                    ├── NativeReactiveGreetingResourceIT.java
                    └── ReactiveGreetingResourceTest.java

13 directories, 13 files


$ tree container-image-docker
container-image-docker
├── README.md
├── mvnw
├── mvnw.cmd
├── pom.xml
└── src
    ├── main
    │   ├── docker
    │   │   ├── Dockerfile.jvm
    │   │   ├── Dockerfile.legacy-jar
    │   │   ├── Dockerfile.native
    │   │   └── Dockerfile.native-distroless
    │   ├── java
    │   │   └── org
    │   │       └── littlewings
    │   │           └── ReactiveGreetingResource.java
    │   └── resources
    │       ├── META-INF
    │       │   └── resources
    │       │       └── index.html
    │       └── application.properties
    └── test
        └── java
            └── org
                └── littlewings
                    ├── NativeReactiveGreetingResourceIT.java
                    └── ReactiveGreetingResourceTest.java

13 directories, 13 files

差がわかりません…。

diffを見てみます。

$ diff -r container-image-simply container-image-docker
diff -r container-image-simply/README.md container-image-docker/README.md
1c1
< # container-image-simply Project
---
> # container-image-docker Project
46c46
< You can then execute your native executable with: `./target/container-image-simply-0.0.1-SNAPSHOT-runner`
---
> You can then execute your native executable with: `./target/container-image-docker-0.0.1-SNAPSHOT-runner`
diff -r container-image-simply/pom.xml container-image-docker/pom.xml
6c6
<   <artifactId>container-image-simply</artifactId>
---
>   <artifactId>container-image-docker</artifactId>
31a32,35
>     <dependency>
>       <groupId>io.quarkus</groupId>
>       <artifactId>quarkus-container-image-docker</artifactId>
>     </dependency>
diff -r container-image-simply/src/main/docker/Dockerfile.jvm container-image-docker/src/main/docker/Dockerfile.jvm
10c10
< # docker build -f src/main/docker/Dockerfile.jvm -t quarkus/container-image-simply-jvm .
---
> # docker build -f src/main/docker/Dockerfile.jvm -t quarkus/container-image-docker-jvm .
14c14
< # docker run -i --rm -p 8080:8080 quarkus/container-image-simply-jvm
---
> # docker run -i --rm -p 8080:8080 quarkus/container-image-docker-jvm
21c21
< # docker run -i --rm -p 8080:8080 -p 5005:5005 -e JAVA_ENABLE_DEBUG="true" quarkus/container-image-simply-jvm
---
> # docker run -i --rm -p 8080:8080 -p 5005:5005 -e JAVA_ENABLE_DEBUG="true" quarkus/container-image-docker-jvm
diff -r container-image-simply/src/main/docker/Dockerfile.legacy-jar container-image-docker/src/main/docker/Dockerfile.legacy-jar
10c10
< # docker build -f src/main/docker/Dockerfile.legacy-jar -t quarkus/container-image-simply-legacy-jar .
---
> # docker build -f src/main/docker/Dockerfile.legacy-jar -t quarkus/container-image-docker-legacy-jar .
14c14
< # docker run -i --rm -p 8080:8080 quarkus/container-image-simply-legacy-jar
---
> # docker run -i --rm -p 8080:8080 quarkus/container-image-docker-legacy-jar
21c21
< # docker run -i --rm -p 8080:8080 -p 5005:5005 -e JAVA_ENABLE_DEBUG="true" quarkus/container-image-simply-legacy-jar
---
> # docker run -i --rm -p 8080:8080 -p 5005:5005 -e JAVA_ENABLE_DEBUG="true" quarkus/container-image-docker-legacy-jar
diff -r container-image-simply/src/main/docker/Dockerfile.native container-image-docker/src/main/docker/Dockerfile.native
10c10
< # docker build -f src/main/docker/Dockerfile.native -t quarkus/container-image-simply .
---
> # docker build -f src/main/docker/Dockerfile.native -t quarkus/container-image-docker .
14c14
< # docker run -i --rm -p 8080:8080 quarkus/container-image-simply
---
> # docker run -i --rm -p 8080:8080 quarkus/container-image-docker
diff -r container-image-simply/src/main/docker/Dockerfile.native-distroless container-image-docker/src/main/docker/Dockerfile.native-distroless
10c10
< # docker build -f src/main/docker/Dockerfile.native-distroless -t quarkus/container-image-simply .
---
> # docker build -f src/main/docker/Dockerfile.native-distroless -t quarkus/container-image-docker .
14c14
< # docker run -i --rm -p 8080:8080 quarkus/container-image-simply
---
> # docker run -i --rm -p 8080:8080 quarkus/container-image-docker
diff -r container-image-simply/src/main/resources/META-INF/resources/index.html container-image-docker/src/main/resources/META-INF/resources/index.html
5c5
<     <title>container-image-simply - 0.0.1-SNAPSHOT</title>
---
>     <title>container-image-docker - 0.0.1-SNAPSHOT</title>
147c147
<                 <li>ArtifactId: <code>container-image-simply</code></li>
---
>                 <li>ArtifactId: <code>container-image-docker</code></li>

作成した時のプロジェクト名の差だけが出ていると思いきや、よく見るとpom.xmlには依存関係の差が出ています。

$ diff container-image-simply/pom.xml container-image-docker/pom.xml
6c6
<   <artifactId>container-image-simply</artifactId>
---
>   <artifactId>container-image-docker</artifactId>
31a32,35
>     <dependency>
>       <groupId>io.quarkus</groupId>
>       <artifactId>quarkus-container-image-docker</artifactId>
>     </dependency>

主な差はこれくらいみたいですね。

一気にまとめて書きますが、ふつうにmvn packageする分にはどちらのプロジェクトも同じ振る舞いになります。

$ cd container-image-simply
# または
$ cd container-image-docker


$ mvn package

# どちらのプロジェクトも起動は同じ
$ java -jar target/quarkus-app/quarkus-run.jar

動作確認。

$ curl localhost:8080/hello
Hello RESTEasy Reactive

差が出るのは、以下のようにビルドの設定をして実行した場合です。
mvn package時にコンテナイメージのビルドやPushが行われるようになります。

Build Container Images / Building

Build Container Images / Pushing

Docker Extensionを加えた方のプロジェクトで、quarkus.container-image.buildtrueにしてmvn package
実行してみましょう。

$ cd container-image-docker
$ mvn package -Dquarkus.container-image.build=true

すると、Quarkus Maven Pluginの挙動が代わり、Dockerイメージのビルドが始まります。

[INFO] --- quarkus-maven-plugin:2.3.0.Final:build (default) @ container-image-docker ---
[INFO] [org.jboss.threads] JBoss Threads version 3.4.2.Final
[INFO] [io.quarkus.container.image.docker.deployment.DockerProcessor] Building docker image for jar.
[INFO] [io.quarkus.container.image.docker.deployment.DockerProcessor] Executing the following command to build docker image: 'docker build -f /path/to/container-image-docker/src/main/docker/Dockerfile.jvm -t username/container-image-docker:0.0.1-SNAPSHOT /path/to/container-image-docker'

ログからもわかりますが、docker buildがOSコマンド呼び出しで実行されます。Dockerイメージのビルド中に、
プロセスを見るとdocker buildが実行されているのが確認できますし。

今回は確認しませんが、quarkus.container-image.pushtrueにするとDockerレジストリへのPushが行われるようです。

動作確認。

$ docker container run -it --rm -p 8080:8080 username/container-image-docker:0.0.1-SNAPSHOT


$ curl localhost:8080/hello
Hello RESTEasy Reactive

OKですね。

設定は、こちらを参照。Docker Extension固有のオプションと、コンテナイメージに関するExtension共通のものが
あります。

Build Container Images / Customizing / Docker Options / Configuration property

Build Container Images / Customizing / Container Image Options / Configuration property

コンテナイメージの名前とタグの名前は、デフォルトでは
OSユーザー名/Quarkusアプリメーション名:Quarkusアプリケーションバージョンとなるようです。
ログでも、こんな感じでしたね。

-t username/container-image-docker:0.0.1-SNAPSHOT

これを変更するには、quarkus.container-image.groupquarkus.container-image.nameなどを指定します。
たとえば、以下の例では

$ mvn package -Dquarkus.container-image.build=true -Dquarkus.container-image.group=my-user -Dquarkus.container-image.name=my-image -Dquarkus.container-image.tag=v1

このような指定でビルドされます。

-t my-user/my-image:v1

コンテナイメージ名やタグの他にも、Push先のDockerレジストリの設定なども行えるようです。

また、Dockerイメージ作成の際は以下のDockerfileが使われました。

src/main/docker/Dockerfile.jvm

####
# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode
#
# Before building the container image run:
#
# ./mvnw package
#
# Then, build the image with:
#
# docker build -f src/main/docker/Dockerfile.jvm -t quarkus/container-image-docker-jvm .
#
# Then run the container using:
#
# docker run -i --rm -p 8080:8080 quarkus/container-image-docker-jvm
#
# If you want to include the debug port into your docker image
# you will have to expose the debug port (default 5005) like this :  EXPOSE 8080 5005
#
# Then run the container using :
#
# docker run -i --rm -p 8080:8080 -p 5005:5005 -e JAVA_ENABLE_DEBUG="true" quarkus/container-image-docker-jvm
#
###
FROM registry.access.redhat.com/ubi8/ubi-minimal:8.4 

ARG JAVA_PACKAGE=java-11-openjdk-headless
ARG RUN_JAVA_VERSION=1.3.8
ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en'
# Install java and the run-java script
# Also set up permissions for user `1001`
RUN microdnf install curl ca-certificates ${JAVA_PACKAGE} \
    && microdnf update \
    && microdnf clean all \
    && mkdir /deployments \
    && chown 1001 /deployments \
    && chmod "g+rwX" /deployments \
    && chown 1001:root /deployments \
    && curl https://repo1.maven.org/maven2/io/fabric8/run-java-sh/${RUN_JAVA_VERSION}/run-java-sh-${RUN_JAVA_VERSION}-sh.sh -o /deployments/run-java.sh \
    && chown 1001 /deployments/run-java.sh \
    && chmod 540 /deployments/run-java.sh \
    && echo "securerandom.source=file:/dev/urandom" >> /etc/alternatives/jre/conf/security/java.security

# Configure the JAVA_OPTIONS, you can add -XshowSettings:vm to also display the heap size.
ENV JAVA_OPTIONS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
# We make four distinct layers so if there are application changes the library layers can be re-used
COPY --chown=1001 target/quarkus-app/lib/ /deployments/lib/
COPY --chown=1001 target/quarkus-app/*.jar /deployments/
COPY --chown=1001 target/quarkus-app/app/ /deployments/app/
COPY --chown=1001 target/quarkus-app/quarkus/ /deployments/quarkus/

EXPOSE 8080
USER 1001

ENTRYPOINT [ "/deployments/run-java.sh" ]

Docker Extensionのソースコードは、こちら。

https://github.com/quarkusio/quarkus/tree/2.3.0.Final/extensions/container-image/container-image-docker

実行時のExtensionはほぼなく、

https://github.com/quarkusio/quarkus/tree/2.3.0.Final/extensions/container-image/container-image-docker/runtime

あくまでビルド時が主体のExtensionのようです。

https://github.com/quarkusio/quarkus/tree/2.3.0.Final/extensions/container-image/container-image-docker/deployment

主となるDockerProcessorクラスでdockerコマンドを使っている感じですね。

https://github.com/quarkusio/quarkus/blob/2.3.0.Final/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerProcessor.java

で、どうしてDockerイメージのビルド時にDockerfile.jvmが選択されたのかは、こちらを参照。

https://github.com/quarkusio/quarkus/blob/2.3.0.Final/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerProcessor.java#L227-L246

ビルド方法に応じて、選択するDockerfileが切り替わるようです。

たとえば、以下のビルドコマンドだとDockerfile.legacy-jarファイルが使われます。

$ mvn package -Dquarkus.container-image.build=true -Dquarkus.package.type=legacy-jar

ネイティブイメージについても同様です。

もっとも、Dockerfile.jvmファイルとDockerfile.nativeファイルの2つについては、設定で指定できそうですけどね。

Build Container Images / Customizing / Docker Options / Configuration property

Docker Extensionについては、こんな感じでしょうか。

あと、そもそもDocker Extensionがない場合はどうやってDockerイメージをビルドするかというと、Dockerfile
書いてありますね。以下のように、ふつうにdockerコマンドで対象のDockerfileを指定してビルドすることになります。

# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode
#
# Before building the container image run:
#
# ./mvnw package
#
# Then, build the image with:
#
# docker build -f src/main/docker/Dockerfile.jvm -t quarkus/container-image-docker-jvm .
#
# Then run the container using:
#
# docker run -i --rm -p 8080:8080 quarkus/container-image-docker-jvm

なので、Docker Extensionを使うとdockerコマンドを直接使わなくても(裏で使われますが)Dockerイメージを
ビルド、Pushできるように抽象化できるというわけですね。

ただ、使われるDockerfile自体はDocker Extensionを使わない場合と同じなのですが。

カスタマイズは、Dockerfile自体を修正することになりますね。

Jib Extensionを使う

続いて、Jib Extensionを使ってみましょう。

Build Container Images / Jib

Extensionにcontainer-image-jibを入れて、プロジェクトを作成。プロジェクト名は、container-image-jibとします。

$ mvn io.quarkus.platform:quarkus-maven-plugin:2.3.0.Final:create \
    -DprojectGroupId=org.littlewings \
    -DprojectArtifactId=container-image-jib \
    -DprojectVersion=0.0.1-SNAPSHOT \
    -Dextensions="resteasy-reactive,container-image-jib"

作成されるファイルは、ここまでのパターンと変わらないようです。

$ tree container-image-jib
container-image-jib
├── README.md
├── mvnw
├── mvnw.cmd
├── pom.xml
└── src
    ├── main
    │   ├── docker
    │   │   ├── Dockerfile.jvm
    │   │   ├── Dockerfile.legacy-jar
    │   │   ├── Dockerfile.native
    │   │   └── Dockerfile.native-distroless
    │   ├── java
    │   │   └── org
    │   │       └── littlewings
    │   │           └── ReactiveGreetingResource.java
    │   └── resources
    │       ├── META-INF
    │       │   └── resources
    │       │       └── index.html
    │       └── application.properties
    └── test
        └── java
            └── org
                └── littlewings
                    ├── NativeReactiveGreetingResourceIT.java
                    └── ReactiveGreetingResourceTest.java

13 directories, 13 files

プロジェクトへ移動。

$ cd container-image-jib

Maven依存関係にはquarkus-container-image-jibが追加されているところがポイントです。

    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-container-image-jib</artifactId>
    </dependency>

こちらの場合も、単純にmvn packageしただけではJARファイルができるだけです。

$ mvn package

なので、Docker Extensionの時と同じようにquarkus.container-image.buildtrueとして実行すると、
Dockerイメージのビルドが始まります。

$ mvn package -Dquarkus.container-image.build=true

こんな感じですね。

[INFO] --- quarkus-maven-plugin:2.3.0.Final:build (default) @ container-image-jib ---
[INFO] [org.jboss.threads] JBoss Threads version 3.4.2.Final
[INFO] [io.quarkus.container.image.jib.deployment.JibProcessor] Starting container image build
[WARNING] [io.quarkus.container.image.jib.deployment.JibProcessor] Base image 'fabric8/java-alpine-openjdk11-jre' does not use a specific image digest - build may not be reproducible
[INFO] [io.quarkus.container.image.jib.deployment.JibProcessor] The base image requires auth. Trying again for fabric8/java-alpine-openjdk11-jre...
[INFO] [io.quarkus.container.image.jib.deployment.JibProcessor] LogEvent [level=INFO, message=Docker config auths section defines credentials for index.docker.io]
[INFO] [io.quarkus.container.image.jib.deployment.JibProcessor] LogEvent [level=LIFECYCLE, message=Using credentials from Docker config ($HOME/.docker/config.json) for fabric8/java-alpine-openjdk11-jre]
[INFO] [io.quarkus.container.image.jib.deployment.JibProcessor] Using base image with digest: sha256:b459cc59d6c7ddc9fd52f981fc4c187f44a401f2433a1b4110810d2dd9e98a07
[INFO] [io.quarkus.container.image.jib.deployment.JibProcessor] Container entrypoint set to [java, -Djava.util.logging.manager=org.jboss.logmanager.LogManager, -jar, quarkus-run.jar]
[INFO] [io.quarkus.container.image.jib.deployment.JibProcessor] Created container image username/container-image-jib:0.0.1-SNAPSHOT (sha256:c33a38daaffbbd93fab7a8bcee9d26625f6d8842b6f3998b04fb95722f63debc)

このあたりから、これらのExtensionは有効にするとコンテナイメージのビルドやPushを行うことを抽象化したもので
あることがわかります。quarkus.container-image.buildquarkus.container-image.pushを有効にした時に、
背後で動作するExtensionを追加するという感じですね。

動作確認は、省略します(Dockerイメージ名が異なるだけで、Docker Extensionの時と同じなので)。

作成されるDockerイメージの名前は、Docker Extensionの時と同じルールで決まります。

つまり、こちらの設定はコンテナイメージを作成するExtensionで共通のものということですね。

Build Container Images / Customizing / Container Image Options / Configuration property

こんな感じで、Dockerコンテナイメージ名やタグを指定できます。

$ mvn package -Dquarkus.container-image.build=true -Dquarkus.container-image.group=my-user -Dquarkus.container-image.name=my-image -Dquarkus.container-image.tag=v1

Jib Extensionに関する設定は、こちら。

Build Container Images / Customizing / Jib Options / Configuration property

たとえば、デフォルトのベースイメージはfabric8/java-alpine-openjdk11-jreですが、これを変更するには
quarkus.jib.base-jvm-imageを指定します。
※ネイティブイメージの場合は、quarkus.jib.base-native-imageです

今回は、ベースイメージをeclipse-temurin:11-jreに変更してビルドしてみます。

$ mvn package -Dquarkus.container-image.build=true -Dquarkus.jib.base-jvm-image=eclipse-temurin:11-jre

確認。

$ docker container run -it --rm -p 8080:8080 --name app --entrypoint java username/container-image-jib:0.0.1-SNAPSHOT --version
openjdk 11.0.12 2021-07-20
OpenJDK Runtime Environment Temurin-11.0.12+7 (build 11.0.12+7)
OpenJDK 64-Bit Server VM Temurin-11.0.12+7 (build 11.0.12+7, mixed mode)

ベースイメージがEclipse Temurinになっていることが確認できました。

その他、Dockerイメージにファイルを追加する方法や、ENTRYPOINTを変更する方法については以下に記載があります。

Build Container Images / Container Image extensions / Jib / Including extra files

Build Container Images / Container Image extensions / Jib / JVM Debugging

ソースコードについてはこちらですが、Docker Extensionと同様にdeploymentが主です。

https://github.com/quarkusio/quarkus/tree/2.3.0.Final/extensions/container-image/container-image-jib

完全に蛇足ですが。Jib Extensionの場合は、Dockerfileを使わないので極端な話Dockerfileを削除してもふつうに動きます。

$ rm -rf src/main/docker
$ mvn package -Dquarkus.container-image.build=true

Jib Extensionについては、だいたいこんなところでしょうか。

オマケ

Jib ExtensioonやDocker Extensionのような、コンテナイメージに関するExtensionを複数追加している場合は、
quarkus.container-image.builderでどのExtensionを使うか指定するようです。

Build Container Images / Selecting among multiple extensions

dockerjibs2iといった指定になります。

あとは、コンテナイメージに関するExtensionの共通部分のソースコードはこちらです。

https://github.com/quarkusio/quarkus/tree/2.3.0.Final/extensions/container-image/deployment

https://github.com/quarkusio/quarkus/tree/2.3.0.Final/extensions/container-image/runtime

主は、やっぱりdeploymentですけどね。

https://github.com/quarkusio/quarkus/tree/2.3.0.Final/extensions/container-image/deployment/src/main/java/io/quarkus/container/image/deployment

まとめ

Quarkusのコンテナイメージに関するExtensionのうち、Docker ExtensionとJib Extensionを試してみました。

簡単にコンテナイメージを作りたかったらJib Extensionが良いような気はしますが、どうでしょうね。

とりあえず、覚えておきましょう。