OpenShift用のシンプルなQuarkusネイティブアプリケーションの作成

# はじめに
この記事では、OpenShiftへのデプロイを目的として、Quarkusによるシンプルなネイティブアプリケーションを作成します。デプロイ先のOpenShift環境は下記の記事で紹介しています。

https://qiita.com/y-akio/items/ad0093daa504be746573

■ 参考

https://qiita.com/y-akio/items/155ae44188a6df02138c

# 1. 環境準備
アプリケーションはWindows 10のVisual Studio Code(VScode)で作成します。
VScodeにはJavaとGradleのプラグインを、Windows 10には下記を導入しています。
OpenShiftにデプロイするネイティブアプリケーションのLinux用バイナリはDockerで作成します。

- Docker Desktop 4.0.0
- Maven 3.8.2
- Gradle 7.1.1
- Java 11

## 1.1. ソースコード作成
gradle用のプロジェクトをmavenで作成し、拡張「quarkus-resteasy-qute」を追加します。
自動生成されたbuild.gradleのversion行のみ修正しました。

```powershell:PowerShell
mvn io.quarkus:quarkus-maven-plugin:2.2.1.Final:create `
  -DcodestartsEnabled -DprojectGroupId=dummy `
  -DprojectArtifactId=quarkus-native -DbuildTool=GRADLE
cd quarkus-native
gradle listExtensions
### 標準出力↓
…
RESTEasy Qute                                      quarkus-resteasy-qute
…

gradle addExtension --extensions="quarkus-resteasy-qute"
```

下表のソースコードを「src/main」以下のディレクトリに追加します。

| ディレクトリ | ファイル | 用途 |
| ---- | ---- | ---- | 
| java/dummy | IndexResource.java | 要求「/index」にindex.htmlを応答。 |
| java/dummy | HealthzResource.java | 要求「/healthz」にhezlthz.htmlを応答。 |
| resources/templates | index.html | ユーザーエージェントとホスト名を表示。 |
| resources/templates | healthz.html | OKを表示。 |

```java:IndexResource.java
package dummy;

import java.net.InetAddress;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import org.jboss.resteasy.annotations.jaxrs.HeaderParam;

import io.quarkus.qute.TemplateInstance;
import io.quarkus.qute.Template;

@Path("/index")
public class IndexResource {

    @Inject
    Template index; 

    @GET
    @Produces(MediaType.TEXT_HTML)
    public TemplateInstance get(@HeaderParam("User-Agent") String userAgent) {
        String hostName = "";
        try {
            InetAddress ia = InetAddress.getLocalHost();
            hostName = ia.getHostName();
        } catch (Exception e) {
            e.printStackTrace();
        }

        return index.data("userAgent", userAgent, "hostName", hostName);  
    }
}
```

```java:HealthzResource.java
package dummy;

import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import io.quarkus.qute.TemplateInstance;
import io.quarkus.qute.Template;

@Path("/healthz")
public class HealthzResource {

    @Inject
    Template healthz; 

    @GET
    @Produces(MediaType.TEXT_HTML)
    public TemplateInstance get() {
        return healthz.instance();  
    }
}
```

```html:index.html
<html><head><title>Quarkus Native</title></head><body>
    <table border="1" style="font-size: 20pt" cellpadding="10">
        <tr><th>Version</th><th>20210904</th></tr>
        <tr><th>User Agent</th><th>{userAgent}</th></tr>
        <tr><th>Pod</th><th>{hostName}</th></tr>
    </table>
</body></html>
```

```html:healthz.html
OK
```

```gradle:build.gradle
plugins {
    id 'java'
    id 'io.quarkus'
}

repositories {
    mavenCentral()
    mavenLocal()
}

dependencies {
    implementation 'io.quarkus:quarkus-resteasy-qute'
    implementation enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}")
    implementation 'io.quarkus:quarkus-arc'
    implementation 'io.quarkus:quarkus-resteasy'
    testImplementation 'io.quarkus:quarkus-junit5'
    testImplementation 'io.rest-assured:rest-assured'
}

group 'dummy'
version '1.0'

java {
    sourceCompatibility = JavaVersion.VERSION_11
    targetCompatibility = JavaVersion.VERSION_11
}

compileJava {
    options.encoding = 'UTF-8'
    options.compilerArgs << '-parameters'
}

compileTestJava {
    options.encoding = 'UTF-8'
}
```

■ 参考資料

https://quarkus.io/guides/gradle-tooling

https://quarkus.io/guides/qute

# 2. Quarkusアプリケーションのビルドと実行

## 2.1. Quarkusアプリケーションのビルド

『gradle build』によりコンパイルされ「build/quarkus-app/quarkus-run.jar」、「build/quarkus-app/lib/～」が作成されます。これらのファイルはQuarkusアプリケーションをJava仮想マシンで実行する場合に使用します。

```powershell:PowerShell
gradle build
### 標準出力↓
> Task :quarkusBuild
building quarkus jar

BUILD SUCCESSFUL in 7s
8 actionable tasks: 3 executed, 5 up-to-date
```

『gradle build』にネイティブイメージをコンテナで作成するオプションを追加することで、Docker Desktop上にコンテナが起動され、Linux用のネイティブコード「build/quarkus-native-1.0-runner」が作成されます。

```powershell:PowerShell
gradle build -D quarkus.package.type=native -D quarkus.native.container-build=true
### 標準出力↓
building quarkus jar

21.2-java11: Pulling from quarkus/ubi-quarkus-native-image
Digest: sha256:2b32e2494199c6096e68c66ffd7dc8b4924735e1e237c8d821deb5d2209b1da9
Status: Image is up to date for quay.io/quarkus/ubi-quarkus-native-image:21.2-java11
quay.io/quarkus/ubi-quarkus-native-image:21.2-java11
[quarkus-native-1.0-runner:25]    classlist:   3,238.74 ms,  0.96 GB
[quarkus-native-1.0-runner:25]        (cap):     555.31 ms,  0.96 GB
[quarkus-native-1.0-runner:25]        setup:   2,063.57 ms,  0.96 GB
The bundle named: messages, has not been found. If the bundle is part of a module, verify the bundle name is a fully qualified class name. Otherwise verify the bundle path is accessible in the classpath.
13:10:11,346 INFO  [org.jbo.threads] JBoss Threads version 3.4.2.Final
[quarkus-native-1.0-runner:25]     (clinit):     489.94 ms,  4.25 GB
[quarkus-native-1.0-runner:25]   (typeflow):   9,909.48 ms,  4.25 GB
[quarkus-native-1.0-runner:25]    (objects):  12,342.45 ms,  4.25 GB
[quarkus-native-1.0-runner:25]   (features):     877.11 ms,  4.25 GB
[quarkus-native-1.0-runner:25]     analysis:  24,646.53 ms,  4.25 GB
[quarkus-native-1.0-runner:25]     universe:   1,195.41 ms,  4.25 GB
[quarkus-native-1.0-runner:25]      (parse):   2,535.66 ms,  4.25 GB
[quarkus-native-1.0-runner:25]     (inline):   4,183.78 ms,  4.63 GB
[quarkus-native-1.0-runner:25]    (compile):  18,716.19 ms,  4.36 GB
[quarkus-native-1.0-runner:25]      compile:  27,282.70 ms,  4.36 GB
[quarkus-native-1.0-runner:25]        image:   3,782.32 ms,  4.36 GB
[quarkus-native-1.0-runner:25]        write:   1,512.34 ms,  4.40 GB
[quarkus-native-1.0-runner:25]      [total]:  64,244.58 ms,  4.40 GB
# Printing build artifacts to: /project/quarkus-native-1.0-runner.build_artifacts.txt

BUILD SUCCESSFUL in 1m 22s
6 actionable tasks: 1 executed, 5 up-to-date
```

## 2.2. Quarkusアプリケーションの実行
『gradle quarkusDev』でQuarkusアプリケーションを起動することができます。
JBoss上で動作しているようです。Windowsネイティブではありません。

```powershell:PowerShell
gradle quarkusDev
### 標準出力↓
> Task :quarkusDev
Listening for transport dt_socket at address: 5005
SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/C:/.gradle/caches/modules-2/files-2.1/org.jboss.slf4j/slf4j-jboss-logmanager/1.1.0.Final/5f1c0e3f5082c21f6b4964b97fe5b1d5f8c42f53/slf4j-jboss-logmanager-1.1.0.Final.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/C:/.m2/repository/org/jboss/slf4j/slf4j-jboss-logmanager/1.1.0.Final/slf4j-jboss-logmanager-1.1.0.Final.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [org.slf4j.impl.Slf4jLoggerFactory]
Press [h] for more options>NG [8s]
__  ____  __  _____   ___  __ ____  ______ 
 --/ __ \/ / / / _ | / _ \/ //_/ / / / __/ 
 -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \   
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/
2021-09-04 17:11:57,608 INFO  [io.quarkus] (Quarkus Main Thread) quarkus-native 1.0 on JVM (powered by Quarkus 2.2.1.Final) started in 2.003s. Listening on: http://localhost:8080
2021-09-04 17:11:57,621 INFO  [io.quarkus] (Quarkus Main Thread) Profile dev activated. Live Coding activated.
2021-09-04 17:11:57,622 INFO  [io.quarkus] (Quarkus Main Thread) Installed features: [cdi, qute, resteasy, resteasy-qute, smallrye-context-propagation]
```

`http://localhost:8080/index`を開くと以下のような画面が表示されます。
OpenShiftにデプロイしていないので、Pod名ではなくホスト名が表示されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1413476/55b1bd25-0135-5413-e583-bc5c80ca33bb.png)

## 2.3. GitHubの利用
ソースコードをGitHubに公開しました。gitコマンドとJava 11とDocker(あるいはPodman)が導入されていれば、下記の手順でネイティブのQuarkusアプリケーションをビルド・実行することができます。Linux用のバイナリであるため、CentOSの場合は「./build/quarkus-native-1.0-runner」を直接実行可能です。Gradleも自動的にダウンロードされます。
`https://github.com/y-akio/source.git`

```bash:CentOSでの実行例
git clone https://github.com/y-akio/source.git
cd source/quarkus-native
chmod +x gradlew
./gradlew build -D quarkus.package.type=native -D quarkus.native.container-build=true
### 標準出力↓
> Task :quarkusGenerateCode
preparing quarkus application

> Task :quarkusGenerateCodeTests
preparing quarkus application

> Task :quarkusBuild
building quarkus jar

Trying to pull quay.io/quarkus/ubi-quarkus-native-image:21.2-java11...
Getting image source signatures
Copying blob sha256:6f9adeed15adce73e9f73519fb1f314eb58ed175594daabaa944f26b787daa67
Copying blob sha256:c2c17d84f25a8380da8196e4173249e5324e4f653231b00d2e95a6efaac5687a
Copying blob sha256:46cdcde062b27f6c24775848f60eeb6a7216a98d4d6d7617927f5024fc8edfc3
Copying config sha256:2dffe88ed345adf89d68f8a1312bf1a405a4da1b857ba7dc052f1d82640f6bd0
Writing manifest to image destination
Storing signatures
2dffe88ed345adf89d68f8a1312bf1a405a4da1b857ba7dc052f1d82640f6bd0
[quarkus-native-1.0-runner:25]    classlist:   3,844.03 ms,  0.96 GB
[quarkus-native-1.0-runner:25]        (cap):     580.38 ms,  0.96 GB
[quarkus-native-1.0-runner:25]        setup:   2,631.16 ms,  0.96 GB
The bundle named: messages, has not been found. If the bundle is part of a module, verify the bundle name is a fully qualified class name. Otherwise verify the bundle path is acces
sible in the classpath.
09:00:43,724 INFO  [org.jbo.threads] JBoss Threads version 3.4.2.Final
[quarkus-native-1.0-runner:25]     (clinit):     571.91 ms,  2.95 GB
[quarkus-native-1.0-runner:25]   (typeflow):  18,347.68 ms,  2.95 GB
[quarkus-native-1.0-runner:25]    (objects):  18,567.88 ms,  2.95 GB
[quarkus-native-1.0-runner:25]   (features):     860.43 ms,  2.95 GB
[quarkus-native-1.0-runner:25]     analysis:  39,571.10 ms,  2.95 GB
[quarkus-native-1.0-runner:25]     universe:   1,631.93 ms,  2.96 GB
[quarkus-native-1.0-runner:25]      (parse):   5,390.79 ms,  2.96 GB
[quarkus-native-1.0-runner:25]     (inline):   6,300.51 ms,  3.17 GB
[quarkus-native-1.0-runner:25]    (compile):  30,355.53 ms,  3.38 GB
[quarkus-native-1.0-runner:25]      compile:  44,119.20 ms,  3.38 GB
[quarkus-native-1.0-runner:25]        image:   4,570.07 ms,  3.36 GB
[quarkus-native-1.0-runner:25]        write:     616.21 ms,  3.36 GB
[quarkus-native-1.0-runner:25]      [total]:  97,073.20 ms,  3.36 GB
# Printing build artifacts to: /project/quarkus-native-1.0-runner.build_artifacts.txt

BUILD SUCCESSFUL in 1m 45s
6 actionable tasks: 6 executed

./build/quarkus-native-1.0-runner
### 標準出力↓
__  ____  __  _____   ___  __ ____  ______
 --/ __ \/ / / / _ | / _ \/ //_/ / / / __/
 -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/
2021-09-04 04:44:08,871 INFO  [io.quarkus] (main) quarkus-native 1.0 native (powered by Quarkus 2.2.1.Final) started in 0.014s. Listening on: http://0.0.0.0:8080
2021-09-04 04:44:08,871 INFO  [io.quarkus] (main) Profile prod activated.
2021-09-04 04:44:08,871 INFO  [io.quarkus] (main) Installed features: [cdi, qute, resteasy, resteasy-qute, smallrye-context-propagation]
```

# 3. OpenShiftへのデプロイ
## 3.1. コンテナイメージのビルド
OpenShiftでコンテナイメージをビルドします。結果としてイメージストリームが作成されます。

```bash:classicノード
ls -l
### 標準出力
-rw-r--r--. 1 root root      273  9月  4 17:19 Dockerfile
-rw-r--r--. 1 root root 42787352  9月  4 17:31 quarkus-native-1.0-runner
-rw-r--r--. 1 root root     1097  9月  4 17:20 quarkus-native.yaml

oc new-project quarkus-native
oc new-build --name=quarkus-native --strategy=docker --binary
oc start-build quarkus-native --from-dir=. --follow
### 標準出力↓
Uploading directory "." as binary input for the build ...
.
Uploading finished
build.build.openshift.io/quarkus-native-1 started
Receiving source from STDIN as archive ...
Caching blobs under "/var/cache/blobs".

Pulling image registry.access.redhat.com/ubi8/ubi-minimal:8.4 ...
･･･
STEP 1: FROM registry.access.redhat.com/ubi8/ubi-minimal:8.4
STEP 2: RUN mkdir /work
--> bd1f6bd04b5
STEP 3: COPY quarkus-native-1.0-runner /work/
--> 1153e514fc7
STEP 4: RUN chmod +x /work/quarkus-native-1.0-runner
--> 5e90ecd40a9
STEP 5: EXPOSE 8080
--> 4373d6396f7
STEP 6: CMD ["/work/quarkus-native-1.0-runner","-Dquarkus.http.host=0.0.0.0"]
--> 98a1c910c10
STEP 7: ENV "OPENSHIFT_BUILD_NAME"="quarkus-native-1" "OPENSHIFT_BUILD_NAMESPACE"="quarkus-native"
--> 1770b08a74a
STEP 8: LABEL "io.openshift.build.name"="quarkus-native-1" "io.openshift.build.namespace"="quarkus-native"
STEP 9: COMMIT temp.builder.openshift.io/quarkus-native/quarkus-native-1:775e898a
･･･
Successfully pushed image-registry.openshift-image-registry.svc:5000/quarkus-native/quarkus-native@sha256:fcb197e10922265a8e84fe9d184b80064cc4192327860d9a9e60114f1bc7eaf4
Push successful

oc get is
### 標準出力↓
NAME             IMAGE REPOSITORY                                                                 TAGS     UPDATED
quarkus-native   image-registry.openshift-image-registry.svc:5000/quarkus-native/quarkus-native   latest   22 seconds ago
```

```Dockerfile:Dockerfile
FROM registry.access.redhat.com/ubi8/ubi-minimal:8.4

RUN mkdir /work
COPY quarkus-native-1.0-runner /work/
RUN chmod +x /work/quarkus-native-1.0-runner

EXPOSE 8080

CMD ["/work/quarkus-native-1.0-runner", "-Dquarkus.http.host=0.0.0.0"]
```

## 3.2. コンテナのデプロイ
デプロイメント「quarkus-native」としてコンテナをデプロイします。

```bash:classicノード
oc apply -f quarkus-native.yaml
oc get pod,svc,ing
### 標準出力↓
NAME                                  READY   STATUS      RESTARTS   AGE
pod/quarkus-native-1-build           0/1     Completed   0          2m54s
pod/quarkus-native-65d6c4fbd-2zfxz   1/1     Running     0          95s
pod/quarkus-native-65d6c4fbd-b45ns   1/1     Running     0          95s
pod/quarkus-native-65d6c4fbd-z6zbw   1/1     Running     0          95s

NAME                     TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)   AGE
service/quarkus-native   ClusterIP   172.30.233.121   <none>        80/TCP    95s

NAME                                       CLASS    HOSTS                               ADDRESS                             PORTS   AGE
ingress.networking.k8s.io/quarkus-native   <none>   quarkus-native.apps.ocp.cloud.vpc   router-default.apps.ocp.cloud.vpc   80      95s
```

```yaml:quarkus-native.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: quarkus-native
  labels:
    app: quarkus-native
spec:
  replicas: 3
  selector:
    matchLabels:
      app: quarkus-native
  template:
    metadata:
      labels:
        app: quarkus-native
    spec:
      containers:
        - name: quarkus-native
          image: image-registry.openshift-image-registry.svc:5000/quarkus-native/quarkus-native
          ports:
            - containerPort: 8080
          readinessProbe:
            httpGet:
              path: /healthz
              port: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: quarkus-native
spec:
  selector:
    app: quarkus-native
  type: ClusterIP
  ports:
   - protocol: TCP
     port: 80
     targetPort: 8080
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: quarkus-native
spec:
  rules:
    - host: quarkus-native.apps.ocp.cloud.vpc
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: quarkus-native
                port:
                  number: 80
```

ブラウザで`http://quarkus-native.apps.ocp.cloud.vpc/index`を開くと以下のような画面が表示されます。端末で実行した時と違いPod名が表示されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1413476/411d14a7-c87c-29c2-db4d-0a8516676fa7.png)