AWS CDK で Infrastructure as Code する: SpringBoot編1

こんにちは。ただいまAWS CDK絶賛勉強中です。

https://qiita.com/masatomix/items/90c16313040394251100

ちょっと前に書いた上記の記事では、AWS CDKをつかってECSを構築しました。その際は 公式のnginxのイメージをつかって、静的なhtmlを表示させるところまでやりましたが、こんどは**SpringBootのイメージを自分で作成**して、それを動かすようにしてみます。


SpringBootのサンプルコード自体は、遠い昔に

https://qiita.com/masatomix/items/816a5b831579654f75ef

でHelloWorld的なモノを作成したので、そのソースを流用します[^1]。

[^1]: 参考 https://github.com/masatomix/spring-boot-sample-tomcat.git

## 前提条件

今回は

- SpringBootアプリをMavenから起動
- SpringBootアプリをまるごとjar化して起動
- そのjarを内包したDocker イメージを作成して、ローカルのDocker上で起動

って流れで進めるので、Java、Maven、Docker などが動く環境が必要です。
構築方法は割愛しますが、たとえば下記のサイトあたりが良さそうです。

- Java

https://kazuhira-r.hatenablog.com/entry/2021/11/13/161119

うまくいかないときは

```console
$ sudo apt update
$ sudo apt upgrade -y
```

とかするとイイかも。

- Maven

https://qiita.com/fsdg-adachi_h/items/f4cdc4039158153365fb

- Docker

https://qiita.com/nujust/items/d7cd395baa0c5dc94fc5



今回も記事の流れ上WSL上で動かしてみようと思いますが、結果的に、


```console
$ java --version
openjdk 17.0.8.1 2023-08-24
OpenJDK Runtime Environment (build 17.0.8.1+1-Ubuntu-0ubuntu122.04)
OpenJDK 64-Bit Server VM (build 17.0.8.1+1-Ubuntu-0ubuntu122.04, mixed mode, sharing)
  
$ mvn --version
Apache Maven 3.6.3
Maven home: /usr/share/maven
Java version: 17.0.8.1, vendor: Private Build, runtime: /usr/lib/jvm/java-17-openjdk-amd64
Default locale: en, platform encoding: UTF-8
OS name: "linux", version: "5.15.90.1-microsoft-standard-wsl2", arch: "amd64", family: "unix"
  
$ docker --version
Docker version 24.0.6, build ed223bc
$
```

とかなっていればOKです[^2]。

[^2]: Javanのバージョンは17以上じゃないとSpringBoot起動時にエラーになっちゃいました



## やってみる

### ソースの取得から、Maven経由の起動まで


```console
$ git clone --branch 0.0.3  https://github.com/masatomix/spring-boot-sample-tomcat.git
$ cd spring-boot-sample-tomcat
spring-boot-sample-tomcat$ mvn spring-boot:run
。..

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.1.2)

2023/10/02 13:29:08 INFO  [background-preinit] - HV000001: Hibernate Validator 8.0.1.Final
2023/10/02 13:29:08 INFO  [main] - Starting SampleTomcatApplication using Java 17.0.8.1 with PID 2572 (/tmp/spring-boot-sample-tomcat/target/classes started by sysmgr in /tmp/spring-boot-sample-tomcat)

...

2023/10/02 13:29:13 INFO  [main] - Tomcat started on port(s): 8080 (http) with context path ''
2023/10/02 13:29:13 INFO  [main] - Started SampleTomcatApplication in 5.827 seconds (process running for 6.655)

```

起動できたらアクセスしてみましょう。

```console
$ curl http://localhost:8080/actuator/health   -i
HTTP/1.1 200 
Content-Type: application/vnd.spring-boot.actuator.v3+json
Transfer-Encoding: chunked
Date: Mon, 25 Sep 2023 15:17:50 GMT
  
{"status":"UP"}
$
```

よさそうですね。


### SpringBootアプリをjar化して起動

つづいてSpringBootアプリを jar化して起動してみます。jar化は mvnのコマンドをつかいます。

```console
spring-boot-sample-tomcat$ mvn package
...
[INFO] Scanning for projects...
[INFO]
[INFO] ---------------< nu.mine.kino:spring-boot-sample-tomcat >---------------
[INFO] Building spring-boot-sample-tomcat 0.1.1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------


[INFO] Scanning for projects...
[INFO] 
[INFO] ---------------< nu.mine.kino:spring-boot-sample-tomcat >---------------
[INFO] Building spring-boot-sample-tomcat 0.1.1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
Downloading from spring-snapshots: https://repo.spring.io/snapshot/org/apache/maven/plugins/maven-surefire-plugin/3.0.0/maven-surefire-plugin-3.0.0.pom

....

[INFO] --- maven-jar-plugin:3.3.0:jar (default-jar) @ spring-boot-sample-tomcat ---
[INFO] Building jar: /tmp/spring-boot-sample-tomcat/target/app.jar
[INFO]
[INFO] --- spring-boot-maven-plugin:3.1.2:repackage (repackage) @ spring-boot-sample-tomcat ---
[INFO] Replacing main artifact /tmp/spring-boot-sample-tomcat/target/app.jar with repackaged archive, adding nested dependencies in BOOT-INF/.
[INFO] The original artifact has been renamed to /tmp/spring-boot-sample-tomcat/target/app.jar.original
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  6.138 s
[INFO] Finished at: 2023-10-02T13:31:18+09:00
[INFO] ------------------------------------------------------------------------

spring-boot-sample-tomcat$ ls -lrt ./target/
total 36896
...
-rw-r--r-- 1 sysmgr sysmgr 37739139 Oct  2 13:31 app.jar
spring-boot-sample-tomcat$
```

jar化できました。ではこのjarを使ってアプリを起動してみます。

```console
spring-boot-sample-tomcat$ java -jar ./target/app.jar
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.1.2)

2023/10/02 13:33:23 INFO  [background-preinit] - HV000001: Hibernate Validator 8.0.1.Final
2023/10/02 13:33:23 INFO  [main] - Starting SampleTomcatApplication v0.1.1-SNAPSHOT using Java 17.0.8.1 with PID 2694 (/tmp/spring-boot-sample-tomcat/target/app.jar started by sysmgr in /tmp/spring-boot-sample-tomcat)

...

2023/10/02 13:33:30 INFO  [main] - Tomcat started on port(s): 8080 (http) with context path ''
2023/10/02 13:33:30 INFO  [main] - Started SampleTomcatApplication in 8.088 seconds (process running for 8.647)
```

よさそうですね。さっきみたいにcurlでアクセスしてみてください。



### jarを内包したDockerイメージを作成してローカルのDocker上で起動

最後にDockerです。先ほど取ってきたソース群に以下のDockerfile がありますので、

```dockerfile
FROM openjdk:17-alpine
VOLUME /tmp

COPY target/app.jar app.jar
EXPOSE 8080
 
ENTRYPOINT ["java","-jar","/app.jar"]
```

これを使って Dockerのイメージを作成します。ちなみに、ココで作成したDockerのイメージは、最終的にECSにデプロイされるモノとなります。

```console
spring-boot-sample-tomcat$ ECR_REPOSITORY_NAME=spring-boot-sample-tomcat
spring-boot-sample-tomcat$ version=0.1.1-SNAPSHOT   ← 便宜上 pom.xmlのバージョンとそろえておく
spring-boot-sample-tomcat$ docker image build -t myorg/${ECR_REPOSITORY_NAME}:${version} .

...
Sending build context to Docker daemon     38MB
Step 1/5 : FROM openjdk:17-alpine
 ---> 264c9bdce361
Step 2/5 : VOLUME /tmp
 ---> Using cache
 ---> 881669e9c06a
Step 3/5 : COPY target/app.jar app.jar
 ---> Using cache
 ---> 1d8cd0cd0381
Step 4/5 : EXPOSE 8080
 ---> Using cache
 ---> 8f18719b124c
Step 5/5 : ENTRYPOINT ["java","-jar","/app.jar"]
 ---> Using cache
 ---> 84d2e22f35bb
Successfully built 84d2e22f35bb
Successfully tagged myorg/spring-boot-sample-tomcat:0.1.1-SNAPSHOT

spring-boot-sample-tomcat$ docker images 
myorg/spring-boot-sample-tomcat                                               0.1.1-SNAPSHOT         84d2e22f35bb   2 minutes ago   363MB

spring-boot-sample-tomcat$
```

イメージができたようなので、起動します。


```console
spring-boot-sample-tomcat$ docker container run --rm -p 8080:8080 \
 --name app \
 myorg/${ECR_REPOSITORY_NAME}:${version}

.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.1.2)

2023/10/02 04:42:14 INFO  [background-preinit] - HV000001: Hibernate Validator 8.0.1.Final
2023/10/02 04:42:15 INFO  [main] - Starting SampleTomcatApplication v0.1.1-SNAPSHOT using Java 17-ea with PID 1 (/app.jar started by root in /)
2023/10/02 04:42:15 DEBUG [main] - Running with Spring Boot v3.1.2, Spring v6.0.11
2023/10/02 04:42:15 INFO  [main] - No active profile set, falling back to 1 default profile: "default"

...

2023/10/02 04:42:28 INFO  [main] - Tomcat started on port(s): 8080 (http) with context path ''
2023/10/02 04:42:28 INFO  [main] - Started SampleTomcatApplication in 16.476 seconds (process running for 18.073)
```

起動できたようですね！curlで確認もやっておきましょう。

## 蛇足: TIPSというか、サンプルの使い道

環境構築をやっていて「構築うまくいったかな？テストアプリをうごかして確認してみよ」なんてときありますよね。今回動かしてみたアプリは、そんなときのテストアプリとして使えたり、ほかにもこの機能どうつかうんだっけ？みたいなのをサンプルとして実装したりしています。

蛇足ですがその辺の備忘メモです。

### ヘルスチェックにつかう

先の確認で

```console
$ curl http://localhost:8080/actuator/health   -i
HTTP/1.1 200 
Content-Type: application/vnd.spring-boot.actuator.v3+json
Transfer-Encoding: chunked
Date: Mon, 25 Sep 2023 15:17:50 GMT
  
{"status":"UP"}
$
```

などとしたとおり、ヘルスチェックのサンプルを組み込んでます。というかこのヘルスチェックは [spring-boot-actuator](https://github.com/spring-projects/spring-boot/tree/v3.1.4/spring-boot-project/spring-boot-actuator) という便利ライブラリの1機能です。spring-boot-actuator はそのほかにも、環境変数を表示したり(``/actuator/env``)、Loggerの定義情報を表示・変更したり(``/actuator/loggers``)など、さまざまな機能を持っています。

参考: https://spring.pleiades.io/spring-boot/docs/current/reference/html/actuator.html

###  プロファイル機能で、環境ごとに設定を切り替える

SpringBoot標準の機能ですが、アプリ起動時に``--spring.profiles.active`` オプションを指定して、プロファイルを切り替えることができます。具体的には、

- 通常の起動の場合

```console
$ java -jar ./target/app.jar --spring.profiles.active=dev
```

- Docker上で起動する場合

```console
$ docker container run --rm -p 8080:8080 \
 -e --spring.profiles.active=dev \
 --name app \
 myorg/${ECR_REPOSITORY_NAME}:${version}
```

などプロファイル名「``dev``」などを渡してあげることで、環境設定ファイルを切り替えることができます。

上記「``dev``」の場合、通常読み込まれる設定ファイル``/src/main/resources/application.properties`` にくわえ、プロファイル名に従って``/src/main/resources/application-dev.properties`` も読み込まれます。各設定ファイルに同じプロパティ名の情報があった場合はdev側が優先される、そんな機構です。

jarやDockerイメージにあらかじめ各プロファイル用の設定ファイルを同梱しておいて、先の起動オプションでプロファイルを切り替えることで、環境によって異なる設定でアプリを起動できるってことですね。なるほど[^3]。。

[^3]: プロファイル名が未指定の場合はプロファイル名は「``default``」となり ``/src/main/resources/application.properties`` が読み込まれます。



### ログのフォーマットを環境によって変更したい

先のプロファイル機構の延長で、Loggerライブラリの挙動も切り替える方法です。
取得したソースコード内のLogger の設定ファイル ``/src/main/resources/logback-spring.xml``をみてみると、

```xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <property name="logFileName" value="app" />

    <!--ローカル環境用設定 -->
    <springProfile name="default">
        <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
            <target>System.out</target>
            <encoder>
                <charset>UTF-8</charset>
                <pattern>%d{yyyy/MM/dd HH:mm:ss} %-5level [%thread] - %msg%n</pattern>
            </encoder>
        </appender>
    </springProfile>

    <!--develop環境用設定 -->
    <springProfile name="dev">
        <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
            <target>System.out</target>
            <encoder>
                <charset>UTF-8</charset>
                <pattern>%d{yyyy/MM/dd HH:mm:ss} %-5level [%thread] - %msg%n</pattern>
            </encoder>
        </appender>
    </springProfile>

    <!--production環境用設定 -->
    <springProfile name="production">
        <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
            <encoder class="net.logstash.logback.encoder.LogstashEncoder">
              <pattern>[%-5level]	%c{0}	%date{yyyy/MM/dd HH:mm:ss.SSS}	%msg%n</pattern>
            </encoder>
        </appender>
    </springProfile>
    <root>
      <appender-ref ref="STDOUT" />
    </root>
</configuration>
```

となっています。`` <springProfile name="dev">``、``  <springProfile name="production">`` などの要素がありますが、先のプロファイル名にしたがって、読まれるLoggerの設定が切り替わるようになっています。

プロファイル名が「``dev``」の場合は 通常のLogger

```xml
    <springProfile name="dev">
        <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
            <target>System.out</target>
            <encoder>
                <charset>UTF-8</charset>
                <pattern>%d{yyyy/MM/dd HH:mm:ss} %-5level [%thread] - %msg%n</pattern>
            </encoder>
        </appender>
    </springProfile>
```

が設定され、プロファイル名が「``production``」の場合は

```xml
    <springProfile name="production">
        <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
            <encoder class="net.logstash.logback.encoder.LogstashEncoder">
              <pattern>[%-5level]	%c{0}	%date{yyyy/MM/dd HH:mm:ss.SSS}	%msg%n</pattern>
            </encoder>
        </appender>
    </springProfile>
```

``net.logstash.logback.encoder.LogstashEncoder``という**JSON形式でログ出力する設定**が適用されます。

実際のログはこんな感じです。

プロファイル名が「``dev``」の場合

```console
2023/10/04 16:49:47 INFO  [http-nio-8080-exec-4] - hostname: 4d1ad5e6f9f7
2023/10/04 16:49:47 INFO  [http-nio-8080-exec-4] - Default Message.
```

プロファイル名が「``production``」の場合

```console
{"@timestamp":"2023-10-04T16:51:38.530015247Z","@version":"1","message":"hostname: 78ef74fd7fde","logger_name":"nu.mine.kino.web.EchoController","thread_name":"http-nio-8080-exec-4","level":"INFO","level_value":20000}
{"@timestamp":"2023-10-04T16:51:38.533395968Z","@version":"1","message":"Default Message.","logger_name":"nu.mine.kino.web.EchoController","thread_name":"http-nio-8080-exec-4","level":"INFO","level_value":20000}
```

こんなふうに、**開発などの通常時はログを普通に出力し、production環境(AWS本番とかです) ではJSON形式で構造的にログを出力する**ことができたりします。開発時にJSON形式のログだと普通に読みづらいわけですが、本番のAWS上ではログをJSONで出せたりすると何かと便利[^4]だったりするので、このような切替機能はとっても便利ですね。


[^4]: AWS上のログを操作する機構( [fluentbit](https://fluentbit.io/) とか) をもちいて、JSONの特定のプロパティを用いた処理ができたりします。たとえばJSONのプロパティ``loglevel``が「ERROR」のヤツだけを取り出して、そのログはCloudWatchに送ってアラームを発報するとか、そんな感じです。時間をみていつか記事にしようと思います。



次回は先ほど作成したDockerイメージを ECRにアップロードして、ECSから利用してみます。

以上お疲れさまでした！

## 関連リンク

- [AWS CDKのTIPS集](https://qiita.com/masatomix/items/7087e7813637ff5e9c36)
- [AWS CDK で Infrastructure as Code する: VPC編](https://qiita.com/masatomix/items/fc414e98d6d0091db90d)
- [AWS CDK で Infrastructure as Code する: EC2編](https://qiita.com/masatomix/items/03dd59929ef1869ea027)
- [AWS CDK で Infrastructure as Code する: ECS編](https://qiita.com/masatomix/items/90c16313040394251100)