OpenShift用のシンプルなアプリケーションの作成

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

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

# 1. Visual Studio Code準備
アプリケーションはWindows 10のVisual Studio Code(VScode)で作成します。
事前にJavaやGradleと以下のVSCode用プラグインを導入しておきます。

- Java Extension Pack
- Spring Boot Extension Pack

## 1.1. Spring Bootプロジェクトの作成

(1) コマンドパレットから「Spring Initializer: Create a Gradle Project..」を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1413476/ac3e10a9-10c2-9acc-568e-c01aaeb90518.png)
(2) Javaを選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1413476/340c9b68-88fb-6ca7-6d05-1cb8786190a4.png)
(3) Group Idを「dummy」とします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1413476/5e09c6b8-724c-4732-03b0-cb98fdd9cfdd.png)
(4) Artifact Idを「spring-liberty」とします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1413476/646d274b-81d2-07e9-0cce-a6373d1fd4f5.png)
(5) packaging typeを「War」にします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1413476/cd554d32-567e-792c-7f5b-1c16f74ec573.png)
(6) Java versionを「11」にします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1413476/a2afd177-467d-da78-7817-96575605ef5d.png)
(7) dependenciesとして「Spring Web」と「Thymeleaf」を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1413476/c4566a2b-f5c1-e127-887b-7be2a4d658f4.png)

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

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


```java:SpringLibertyController.java
package dummy.springliberty;

import java.net.InetAddress;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.ui.Model;

@Controller
public class SpringLibertyController {
    @RequestMapping("/")
    public String index(Model model, @RequestHeader("User-Agent") String userAgent) {
        model.addAttribute("userAgent", userAgent);
 
        try {
            InetAddress ia = InetAddress.getLocalHost();
            model.addAttribute("hostName", ia.getHostName());
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "index";
    }
 
    @RequestMapping("/healthz")
    public String healthz() {
        return "healthz";
    }
}
```

```html:index.html
<html><head><title>Spring Boot + Liberty</title></head><body>
    <table border="1" style="font-size: 20pt" cellpadding="10">
        <tr><th>Version</th><th>20210716_01</th></tr>
        <tr><th>User Agent</th><th><p th:text="${userAgent}"></p></th></tr>
        <tr><th>Pod</th><th><p th:text="${hostName}"></p></th></tr>
    </table>
</body></html>
```

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

## 1.3. Open Liberty用Gradleプラグイン追加
Open Liberty開発支援プラグインを使用するためにbuild.gradleを編集します。

https://github.com/OpenLiberty/ci.gradle

```bash:build.gradle
plugins {
	id 'org.springframework.boot' version '2.5.2'
	id 'io.spring.dependency-management' version '1.0.11.RELEASE'
	id 'io.openliberty.tools.gradle.Liberty' version '3.2'
	id 'java'
	id 'war'
}

group = 'dummy'
version = ''
sourceCompatibility = '11'

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'

	libertyRuntime group: 'io.openliberty', name: 'openliberty-runtime', version: '21.0.0.7'
}

test {
	useJUnitPlatform()
}

liberty {
    server {
        name = 'defaultServer'
        deploy {
            apps = [file('build/libs/spring-liberty-plain.war')]
        }
    }
}

clean.dependsOn 'libertyStop'
```

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

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

『**gradle build**』によりコンパイルされ「build/libs/spring-liberty-plain.war」が作成されます。

```bash:VSCodeターミナル
gradle build
### 標準出力↓
Starting a Gradle Daemon, 2 busy and 3 incompatible and 1 stopped Daemons could not be reused, use --status for details

BUILD SUCCESSFUL in 8s
7 actionable tasks: 7 up-to-date
```

## 2.2. Open Liberty上でアプリケーションを実行
『**gradle libertyRun**』により以下の処理が実行されます。

- build.gradleで指定されたバージョンのOpen Libertyが存在しないときはダウンロードする。
- Open Libertyのディレクトリ「build/wlp」を作成する。
- src/main/liberty/server.xmlを「build/wlp/user/servers/defaultServer」にコピーする。
- build/libs/spring-liberty-plain.warを「build/wlp/usr/servers/defaultServer/apps」にコピーする。
- Open Liberty をフォアグラウンドで実行する。

```bash:VSCodeターミナル
gradle libertyRun
### 標準出力↓
> Task :deploy
Application spring-liberty-plain.war was installed as a file as specified. To install as a loose application, specify the plugin or task generating the archive.

> Task :installFeature


> Task :libertyRun
OpenJDK 64-Bit Server VM バージョン 11.0.11+9-LTS (ja_JP) で、defaultServer (Open Liberty 21.0.0.7/wlp-1.0.54.cl210720210629-1900) を起動しています
[監査      ] CWWKE0001I: サーバー defaultServer が起動されました。
[監査      ] CWWKZ0058I: アプリケーションの dropins をモニター中です。
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by com.ibm.ws.util.ThreadContextAccessor (file:/C:/workspace/source/spring-liberty/build/wlp/lib/com.ibm.ws.container.service_1.0.54.jar) to field java.lang.Thread.contextClassLoader
WARNING: Please consider reporting this to the maintainers of com.ibm.ws.util.ThreadContextAccessor
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
[監査      ] CWWKT0016I: Web アプリケーションが使用可能です (default_host): http://xxx.xxx.xxx.xxx:9080/
  .   ____          _            __ _ _  
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \ 
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.5.2)
2021-08-07 10:03:09.803  INFO 11352 --- [cutor-thread-22] dummy.springliberty.ServletInitializer   : Starting ServletInitializer using Java 11.0.11 on mark2 with PID 11352 (C:\workspace\source\spring-liberty\build\wlp\usr\servers\defaultServer\apps\expanded\spring-liberty-plain.war\WEB-INF\classes 
started by root in C:\workspace\source\spring-liberty\build\wlp\usr\servers\defaultServer)
2021-08-07 10:03:09.803  INFO 11352 --- [cutor-thread-22] dummy.springliberty.ServletInitializer   : No active profile set, falling back to default profiles: default
2021-08-07 10:03:10.649  INFO 11352 --- [cutor-thread-22] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 796 m 
2021-08-07 10:03:11.092  INFO 11352 --- [cutor-thread-22] o.s.b.a.w.s.WelcomePageHandlerMapping    : Adding welcome page template: index
2021-08-07 10:03:11.244  INFO 11352 --- [cutor-thread-22] dummy.springliberty.ServletInitializer   : Started ServletInitializer in 1.996 seconds (JVM running for 10.896)
[監査      ] CWWKZ0001I: アプリケーション spring-liberty-plain が 4.134 秒で開始しました。
[監査      ] CWWKF0012I: サーバーは次のフィーチャーをインストールしました。[el-3.0, jsp-2.3, servlet-3.1]。
[監査      ] CWWKF0011I: defaultServer サーバーは、Smarter Planet に対応する準備ができました。defaultServer サーバーは 10.786 秒で始動しました。
```

```xml:src/main/liberty/server.xml
<?xml version="1.0" encoding="UTF-8"?>
<server description="new server">
    <!-- Enable features -->
    <featureManager>
        <feature>jsp-2.3</feature>
    </featureManager>

    <!-- To access this server from a remote client add a host attribute to the following element, e.g. host="*" -->
    <httpEndpoint id="defaultHttpEndpoint" host="*" httpPort="9080" />

    <!-- Automatically expand WAR files and EAR files -->
    <applicationManager autoExpand="true"/>

    <webApplication contextRoot="/" location="spring-liberty-plain.war" />
</server>
```

また、『**gradle libertyDev**』した場合は、Open Libertyがバックグラウンドで実行され、Javaのソースコードが更新されると自動的にOpen Libertyが再起動するようになります。

Open Libertyを起動した状態でブラウザで`http://localhost:9080/`を開くと以下のような画面が表示されます。OpenShiftにデプロイしていないので、Pod名ではなくホスト名が表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1413476/38151fb8-cf24-afae-a174-f58b7a6abd33.png)
※上記ではOpen Libertyで「build/libs/spring-liberty-plain.war」を実行しています。「build/libs/spring-liberty.war」に含まれるTomcatでアプリケーションを実行することも可能です。

```
■ 起動方法
java -jar build/libs/spring-liberty.war

■ URL
http://localhost:8080/
```

## 2.3. GitHubの利用
ソースコードをGitHubに公開しました。gitコマンドとJava 11が導入されていれば、下記の手順でSpring BootアプリケーションをOpen Liberty上で実行することがでできます。Gradleも自動的にダウンロードされます。
`https://github.com/y-akio/source.git`

```bash:CentOSでの実行例
git clone https://github.com/y-akio/source.git
cd source/spring-liberty
chmod +x gradlew
./gradlew build
### 標準出力↓
Downloading https://services.gradle.org/distributions/gradle-7.1.1-bin.zip
..........10%...........20%...........30%..........40%...........50%...........60%..........70%...........80%...........
90%...........100%
Starting a Gradle Daemon (subsequent builds will be faster)
> Task :compileJava
> Task :processResources
> Task :classes
> Task :bootWarMainClassName
> Task :bootWar
> Task :war
> Task :assemble
> Task :compileTestJava
> Task :processTestResources NO-SOURCE
> Task :testClasses
> Task :test
> Task :check
> Task :build

BUILD SUCCESSFUL in 1m 18s
7 actionable tasks: 7 executed

./gradlew libertyRun
### 標準出力↓
> Task :compileJava UP-TO-DATE
> Task :processResources UP-TO-DATE
> Task :classes UP-TO-DATE
> Task :bootWarMainClassName UP-TO-DATE
> Task :bootWar UP-TO-DATE
> Task :installLiberty
> Task :libertyCreate
> Task :war UP-TO-DATE

> Task :deploy
Application spring-liberty.war was installed as a file as specified. To install as a loose application, specify the plugin or task generating the archive.

> Task :installFeature


> Task :libertyRun
OpenJDK Server VM バージョン 11.0.11+9-LTS (ja_JP) で、defaultServer (Open Liberty 21.0.0.7/wlp-1.0.54.cl210720210629-1900) を起動しています
[監査      ] CWWKE0001I: サーバー defaultServer が起動されました。
[監査      ] CWWKZ0058I: アプリケーションの dropins をモニター中です。
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by com.ibm.ws.util.ThreadContextAccessor (file:/tmp/source/spring-liberty/build/wlp/lib/com.ibm.ws.container.service_1.0.54.jar) to field java.lang.Thread.contextClassLoader
WARNING: Please consider reporting this to the maintainers of com.ibm.ws.util.ThreadContextAccessor
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
[監査      ] CWWKT0016I: Web アプリケーションが使用可能です (default_host): http://haproxy:9080/
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.5.2)
2021-08-07 10:23:03.049  INFO 14369 --- [ecutor-thread-4] dummy.springliberty.ServletInitializer   : Starting ServletInitializer using Java 11.0.11 on haproxy with PID 14369 (/tmp/
source/spring-liberty/build/wlp/usr/servers/defaultServer/apps/expanded/spring-liberty-plain.war/WEB-INF/classes started by root in /tmp/source/spring-liberty/build/wlp/usr/servers
/defaultServer)
2021-08-07 10:23:03.067  INFO 14369 --- [ecutor-thread-4] dummy.springliberty.ServletInitializer   : No active profile set, falling back to default profiles: default
2021-08-07 10:23:08.991  INFO 14369 --- [ecutor-thread-4] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 5318 ms
2021-08-07 10:23:11.566  INFO 14369 --- [ecutor-thread-4] o.s.b.a.w.s.WelcomePageHandlerMapping    : Adding welcome page template: index
2021-08-07 10:23:12.485  INFO 14369 --- [ecutor-thread-4] dummy.springliberty.ServletInitializer   : Started ServletInitializer in 12.681 seconds (JVM running for 35.805)
[監査      ] CWWKZ0001I: アプリケーション spring-liberty-plain が 24.971 秒で開始しました。
[監査      ] CWWKF0012I: サーバーは次のフィーチャーをインストールしました。[el-3.0, jsp-2.3, servlet-3.1]。
[監査      ] CWWKF0011I: defaultServer サーバーは、Smarter Planet に対応する準備ができました。defaultServer サーバーは 35.766 秒で始動しました。
```

# 3. OpenShiftへのデプロイ
DockerfileによりOpen Liberty上でSpring Bootアプリケーションを実行するコンテナのイメージを作成し、コンテナイメージからデプロイメント「spring-liberty」を作成します。

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

```bash:classicノード
ls -l
### 標準出力
-rw-r--r--. 1 root root      139  8月  7 10:22 Dockerfile
-rw-r--r--. 1 root root      669  8月  7 10:22 server.xml
-rw-r--r--. 1 root root 13138834  8月  7 10:06 spring-liberty-plain.war
-rw-r--r--. 1 root root     1097  8月  7 10:22 spring-liberty.yaml

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

Pulling image open-liberty:21.0.0.7-full-java11-openj9 ...
･･･
STEP 1: FROM open-liberty:21.0.0.7-full-java11-openj9
STEP 2: COPY server.xml /config/
--> bb126b57c0d
STEP 3: COPY spring-liberty-plain.war /config/apps/
--> 5b9a3b886cd
STEP 4: USER 1001
--> fde4d2230c0
STEP 5: EXPOSE 9080
･･･
Successfully pushed image-registry.openshift-image-registry.svc:5000/spring-liberty/spring-liberty@sha256:1ab62ce3019906
cf896185b701053189d01528603f55c7004f103b469354a672
Push successful

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

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

```bash:classicノード
oc apply -f spring-liberty.yaml
oc get pod,svc,ing
### 標準出力↓
NAME                                  READY   STATUS      RESTARTS   AGE
pod/spring-liberty-1-build            0/1     Completed   0          2m28s
pod/spring-liberty-84c44ffc5b-24w8n   0/1     Running     0          25s
pod/spring-liberty-84c44ffc5b-js6cg   0/1     Running     0          25s

NAME                     TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
service/spring-liberty   ClusterIP   172.30.162.86   <none>        80/TCP    25s

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

```Dockerfile:Dockerfile
FROM open-liberty:21.0.0.7-full-java11-openj9

COPY server.xml /config/
COPY spring-liberty-plain.war /config/apps/

EXPOSE 9080
```

```xml:server.xml
<?xml version="1.0" encoding="UTF-8"?>
<server description="new server">
    <!-- Enable features -->
    <featureManager>
        <feature>jsp-2.3</feature>
    </featureManager>

    <!-- To access this server from a remote client add a host attribute to the following element, e.g. host="*" -->
    <httpEndpoint id="defaultHttpEndpoint" host="*" httpPort="9080" accessLoggingRef="accessLogging" />
    <httpAccessLogging id="accessLogging" filePath="/logs/http_access.log"/>

    <!-- Automatically expand WAR files and EAR files -->
    <applicationManager autoExpand="true"/>

    <webApplication contextRoot="/" location="spring-liberty-plain.war" />
</server>
```

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

ブラウザで`http://spring-liberty.apps.ocp.cloud.vpc/`を開くと以下のような画面が表示されます。端末で実行した時と違いPod名が表示されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1413476/7b2f92e6-307d-74ec-b189-b14f4a02877e.png)