Cloud Run で Java のコンテナとうまいことやっていく

# はじめに
こちらは[NewsPicks Advent Calendar 2019](https://qiita.com/advent-calendar/2019/newspicks)の12日目の記事となります。
昨日は@nckenさんの「新チームでasanaを少し工夫して利用した件」でした。

今回は最近 GA になった GCP の Cloud Run と、以前までコンテナと相性が悪いと言われていた Java のアプリケーションを使って色々と試していこうと思います。

# [Cloud Run](https://cloud.google.com/run/) とは？
フルマネージド環境または GKE クラスタでステートレスな HTTP コンテナを実行できるサービスです。
AWS で言う所の Fargate に近いところもありますが、HTTP コンテナに関してはこちらの方が簡単に設定でき、扱いやすいかと思います。

フルマネージド環境と GKE での違いとして一番大きいのは、固定費用がかからないということだと思います。
GKE 上で動かす場合、 Cluster を構成する GCE に関しては常に費用が発生します。一方でフルマネージド環境の場合、コンテナが稼働した時間分だけ費用が発生します。アプリケーションによってどちらの方がより費用を抑えることができるのかは異なるため、各自で計算する必要があります。

その他違いとしては、 GKE 上で動かす場合は マシンタイプ を変えることができる、VPC へのアクセスの有無など、多少違いがあるので、費用以外の制約に関しても確認し、最適な環境を選択しましょう。

今回は他のサービスとの連携はせず、費用も抑えたいので、フルマネージド環境での Cloud Run に関して記載していこうと思います。

# Java とコンテナの相性について
昨今、マイクロサービスやサーバレスといったものでアプリケーションを動かす機会が増えてきているかと思います。
これらの環境では、コンテナを用いてスケールさせたり、一時的に起動したりなど、これまでの常に動いているウェブアプリとは異なる運用をすることが多々あります。ここでは Java とコンテナの相性について簡単に書いていきたいと思います。

## JVM とコンテナ
JVM を使用する場合、下記の理由からコンテナとの相性がそこまで良いとは言えないかと思います。

1. JVM のアプリケーションは起動が遅い
2. JVM のアプリケーションはリソースを大量に使う

しかしながら、現在マイクロサービスやサーバレスなど、負荷に合わせてサーバーをオートスケールさせるようなケースが増えてきています。
そうした時に、これら２つの特徴をもつ Java は他の言語に比べて大きくデメリットがあります。

今回題材としている Cloud Run に関しても、 GKE 上、もしくはフルマネージド環境でのサーバレスなアプリケーションです。
そのため、 Cloud Run 上で JVM のアプリケーションを扱うにはこれらのデメリットを解消するように工夫する必要があります。そこで登場するのが GraalVM です。

## GraalVM とコンテナ
[GraalVM](https://www.graalvm.org/) とは、 Oracle が2018年に発表した多言語に対応しているランタイムです。
![graalvm_architecture.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258859/13e208c9-666e-cf84-84d5-6ea7649b137f.png)

Java を build する際に GraalVM を使うと、 AOT コンパイルをすることによって、即時実行可能な native イメージを作成することができます。

注意すべき点としては、 GraalVM を使用すると JVM の起動は早くなり、使用するリソースも比較的少なくなるのですが、決して処理が早くなるわけではないことです。場合によっては、 JVM を使用した方がパフォーマンスが良いこともあるので、使用する場面によって選択する必要があります。今回はコンテナ、サーバレスに最適化させるため、GraalVM を使用します。

# サービスの作成
それでは実際にサービスを作成していきます。
GraalVM を使用するにあたり、今回は [Quarkus](https://quarkus.io/) というフレームワークを使用します。 Quarkus とは、 GraalVM および HotSpot 用にカスタマイズされた Kubernetesネイティブ なフレームワークで、現在 Java と Kotlin に対応しています。
この Quarkus も 2019/11/25 に正式版がリリースされました。そのため、まだまだ機能として Spring 等の他のフレームワークと比較して足りていないところもあるのですが、今後少しずつ機能が追加されていくかと思います。

## プロジェクトの作成
まずは動かす Java のアプリケーションを作成していきます。
以下のコマンドを実行すると Quarkus のプロジェクトが作成されます。

```shell
mvn io.quarkus:quarkus-maven-plugin:1.0.1.Final:create \
    -DprojectGroupId=org.acme \
    -DprojectArtifactId=getting-started \
    -DclassName="org.acme.quickstart.GreetingResource" \
    -Dpath="/hello"
```

## build 用の Dockerfile 作成
続いて Dockerfile の作成です。プロジェクトを作成時に、 jvm を動かす用の Dokcerfile と、 native-image を動かす用の Dockerfile が自動生成されます。
しかしながらこの Dockerfile を使用する場合、 local 環境で build しなければならないので、 コンテナのイメージ作成時に build するような Dockerfile を作ります。（今回作成したものは Quarkus 公式ドキュメントの物を参考に作成しました。）

```Docker:Dockerfile.multistage
## Stage 1 : build with maven builder image with native capabilities
FROM quay.io/quarkus/centos-quarkus-maven:19.2.1 AS build
COPY ./ /usr/src/app
USER root
RUN chown -R quarkus /usr/src/app
USER quarkus
RUN mvn -f /usr/src/app/pom.xml -Pnative clean package

## Stage 2 : create the docker final image
FROM registry.access.redhat.com/ubi8/ubi-minimal
WORKDIR /work/
COPY --from=build /usr/src/app/target/*-runner /work/application
RUN chmod 775 /work
EXPOSE 8080
ENV DISABLE_SIGNAL_HANDLERS true
CMD ["./application", "-Dquarkus.http.host=0.0.0.0", "-Dquarkus.http.port=8080"]
```

このイメージを build し、実行するとアプリケーションが立ち上がります。

## CI/CD
続いて CI/CD 環境を整えていきます。今回使うのは [Cloud Build](https://cloud.google.com/cloud-build/docs/) です。
### cloudbuild.ymlの作成

Cloud Build では、各 build ステップを yml　もしくは json で記述していきます。
今回は次のようなステップのものを記述します。

1. Docker Image の build
2. Docker Image の push
3. Cloud Run への deploy

実際の yml ファイルは下記のようになります。

```yml:cloudbuild.yml
steps:
  - name: 'gcr.io/cloud-builders/docker'
    id: 'Build Image'
    args: ['build', '-t', 'asia.gcr.io/{MyProject}/quarkus-example', '.', '-f', './src/main/docker/Dockerfile.multistage']
    dir: './'

  - name: 'gcr.io/cloud-builders/docker'
    id: 'Push to Container Registry'
    args: ['push', 'asia.gcr.io/{MyProject}/quarkus-example']
    dir: './'

  - name: 'gcr.io/cloud-builders/docker'
    id: 'Deploy to Cloud Run'
    args: ['beta', 'run', 'deploy', 'quarkus-example', '--image', 'asia.gcr.io/{MyProject}/quarkus-example', '--platform', 'managed', '--region', 'asia-northeast1', '--allow-unauthenticated']
    dir: './'

```
ここで出てくる `gcr.io/cloud-builders/docker` と `gcr.io/cloud-builders/docker` はクラウドビルダーと呼ばれるもので、タスク実行のためにビルドステップで参照できる事前ビルド済みのイメージです。これらは Cloud Build や、その他コミュニティから提供されています。
今回は docker と gcloud のイメージを使用していますが、他にも kubectl や gradle など、様々なイメージがあります。

### トリガーの作成
master に commit が発生したタイミングで Cloud Build のジョブが動くように設定します。
トリガーをキックすると、以下のように画面から各ステップの状態を可視化することができます。

![スクリーンショット 2019-12-09 23.57.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258859/fde74096-4f9b-2e30-7ce4-289704b1c597.png)

試しに適当な機能を開発し、プルリクをマージしてみると build が開始されるかと思います。


# 動作確認
最後に作成したサービスのレスポンスタイムとオートスケールの動作確認をしていこうと思います。

今回は Scala 製のテストツールである [Gatling](https://gatling.io/) を使用します。
Gatling を使うと、テストの結果が html として出力されるので、簡単に結果を把握できます。

GCP 上でも下記のようなメトリクスを取得することはできるのですが、クライアント側からみたデータも取得したいと考えたため、今回は Gatling を採用しています。
![スクリーンショット 2019-12-11 2.47.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258859/3d60eb52-8d6c-cdc4-24a0-32d03cdd8778.png)


## Gatling のテストシナリオ作成
まずは空の sbt プロジェクトを作成し、plugins.sbt, build.sbt にライブラリを追加します。
続いて以下のようなコードを作成し、実行します。

```scala:plugins.sbt
addSbtPlugin("io.gatling" % "gatling-sbt" % "3.1.0")
```

```scala:build.sbt
libraryDependencies += "io.gatling.highcharts" % "gatling-charts-highcharts" % "3.3.1" % "test,it"
libraryDependencies += "io.gatling"            % "gatling-test-framework"    % "3.3.1" % "test,it"
```

```scala:BasicSimulation.scala
package computerdatabase

import io.gatling.core.Predef._
import io.gatling.core.structure.ScenarioBuilder
import io.gatling.http.Predef._
import io.gatling.http.protocol.HttpProtocolBuilder
import scala.concurrent.duration._

class BasicSimulation extends Simulation {

  val url = "your app url"

  val httpProtocol: HttpProtocolBuilder = http
    .baseUrl(url)
    .acceptHeader("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
    .acceptEncodingHeader("gzip, deflate")
    .acceptLanguageHeader("en-US,en;q=0.5")
    .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:16.0) Gecko/20100101 Firefox/16.0")

  val scn: ScenarioBuilder = scenario("cloud run example scinario")
    .exec(http("cloud run example scinario")
      .get("/"))

  setUp(scn.inject(rampUsersPerSec(1) to (150) during (600 seconds)).protocols(httpProtocol))
}
```

簡単にコードの説明をすると、デプロイされている Cloud Run のアプリケーションに対して、10分間かけて徐々に負荷をかけていき、最大で 150rps の負荷をかけるといった物です。
今回は一つのエンドポイントしかアクセスしていませんが、複数のエンドポイントに負荷をかけることも可能です。

## テスト結果
それでは実際に上記で作成したシナリオを動かしていこうと思います。

### レスポンスタイム
まずはレスポンスタイムに関して確認していきます。
![スクリーンショット 2019-12-11 2.39.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258859/fc5a041b-4f29-15d4-9ba7-3374ad2ce2ac.png)
こちらの棒グラフを確認する限り、ほとんどのリクエストが 800ms 以内にレスポンスを受け取ることができていました。

### オートスケール
続いてオートスケールに関して確認していきます。
![スクリーンショット 2019-12-11 2.34.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258859/4d16b065-7b85-2db3-c84a-d81775549a12.png)
オレンジ色の線が rps を表しており、積み上げ面グラフがレスポンスタイムを表しています。
これを見ると、いくつかレスポンスタイムが遅くなっている部分があります。おそらくこれはコールドスタートによる物ではないかと考えられます。しかしながらコールドスタートするとしてもほとんどのリクエストを2秒以内に返しているので、サービス運用に耐えていけるのではないでしょうか？

# 終わりに
Cloud Run は触っていくと、コンテナ間通信ができないため１コンテナ内に複数プロセス立ち上げなければいけない場面が出たり、 VPC に接続できないので、できることが制限されたりと壁にぶち当たることはあるかと思います。
しかしながらとても簡単にウェブアプリを立ち上げることができるので、機会があれば選択肢の一つにしてみてはいかがでしょうか？

最後に注意点です。今回作成した物に関しては、料金が発生する恐れがあるので、最後に必ず消しておきましょう。

明日はNewsPicksの手越こと、@kohei1218さんの「Sign with Appleをぶちこむ話」です。お楽しみに！

# 参考記事
- https://cloud.google.com/run/
- https://cloud.google.com/cloud-build/docs/
- https://www.graalvm.org/
- https://quarkus.io/