【Day 15】Web アプリにする......その前にデプロイパイプラインを組む【じゃんけんアドカレ】

[じゃんけんアドベントカレンダー](https://qiita.com/advent-calendar/2020/janken) の 15 日目です。

- 初回 ... [【Day 1】とりあえず 1 クラスに全部書く【じゃんけんアドカレ】](https://qiita.com/os1ma/items/630e2d7fb5d744bbdf8a)
- 前回 ... [【Day 14】ID の採番問題【じゃんけんアドカレ】](https://qiita.com/os1ma/items/1f5d3dc4794fa4f510fc)

---

前回までで、アプリケーションの構造がかなり整理されてきました。
そろそろじゃんけんアプリケーションを **CLI から Web アプリケーションに置き換えたい**と思います。

Web アプリケーションを実装するといっても、**最近はサーバサイドは API を提供するだけという場合が多い**と思います。
そこで、このアドベントカレンダーでも API として実装しようと思います。

## 実装の前に

さて、早速実装に入りたいところですが、Web アプリケーションなどを開発する際、実装の前にするべきことがあります。
それは、**デプロイパイプラインを組む**ことです。

デプロイパイプラインを初期に構築しておくことで、**実環境では動作しないようなコードを書いてしまっても早期に検知できるようになり、修正コストが低くなります**。
また、実際にどの程度開発が進んでいるかも可視化されるため、**開発の進捗を客観的に共有することもできる**ようになります。

そこで今回は、**Web アプリケーションをほぼ空のモックのような状態で用意して、デプロイパイプラインを構築**しようと思います。

## ほぼ空の Web アプリケーション実装

### 何を使うか

最近の Web 開発ではフレームワークを使うことが一般的です。
このアドベントカレンダーでも、Java でよく使われるフレームワークの 1 つである Spring Framework を導入したいと思っています。

ですが、Spring を導入すると DI などにも目が行ってしまい、説明する箇所が多くなってしまいます。
話を簡単にするために、ひとまずは最低限の Servlet と Gson だけを使って API を実装することにします。

### 何を実装するか

さて、それでは Servlet と Gson を使って、最低限の疎通を確認できる API を作りたいと思います。

こういうときに実装するのは Hello World でもいいのですが、今回は**ヘルスチェックの API** を実装しようと思います。
といっても、/api/health を叩いたら `{"status": 200}` という JSON を返すだけの API です。

この API の中でデータベースとの疎通まで確認し、問題なければ上記のレスポンスを返すようにしようと思います。
このように、**データベースなどの依存先との疎通を含めたヘルスチェックを「ディープヘルスチェック」と言います**。

ヘルスチェックの API は監視にも使えるので、どんなアプリケーションでも実装しておくと良い場合が多いです。

### 依存関係の追加

まずは必要な依存関係を追加します。

```build.gradle
plugins {
    :
    id 'war'
    :
}
:
dependencies {
    :
    implementation 'javax.servlet:javax.servlet-api:4.0.1'
    implementation 'com.google.code.gson:gson:2.8.6'
    :
```

build.gradle に、WAR ファイルを作成するためのプラグインと、Servlet・Gson を追加しました。

### 実装

では、API を実装します。

実装結果は以下のようになりました。

```java
@WebServlet("/api/health")
public class HealthController extends HttpServlet {

    private TransactionManager tm = new JDBCTransactionManager();
    private SimpleJDBCWrapper simpleJDBCWrapper = new SimpleJDBCWrapper();
    private Gson gson = new Gson();

    @Override
    protected void doGet(HttpServletRequest request,
                         HttpServletResponse response) throws ServletException, IOException {

        // DB との疎通チェック
        tm.transactional(tx -> {
            simpleJDBCWrapper.findFirst(tx, new SingleRowMapper<Long>(), "SELECT 1");
        });

        // レスポンスヘッダを設定
        response.setContentType("application/json");

        // レスポンスボディを設定
        val status = 200;
        val responseBody = new HealthResponseBody(status);
        val responseBodyStr = gson.toJson(responseBody);
        val writer = response.getWriter();
        writer.print(responseBodyStr);
        writer.flush();
    }

}

@AllArgsConstructor
@Getter
@EqualsAndHashCode
@ToString
class HealthResponseBody {
    private int status;
}
```

本来であれば

- 依存は ServiceLocator で解決する
- DB とのやりとりは ApplicationService や Repository 等に隠蔽する
- 例外発生時にデフォルトのエラー画面を表示しない

といったことをするべきですが、デプロイパイプラインを作ってから修正するよう、ひとまずは Controller に書いてしまいました。

これでひとまず実装完了です。

なお、Servlet による API の実装については以下の情報を参考にしました。

- [サーブレットからJSONレスポンスを返す](https://www.codeflow.site/ja/article/servlet-json-response)
- [Basic RESTful API from a Java servlet](https://gist.github.com/leefsmp/b4c089734852c793cf85)

### ローカルでの動作確認

手元で動作確認するためには、まず以下のコマンドで WAR ファイルを作成します。

```shell
./gradlew war
```

この WAR ファイルをもとに Web アプリケーションを起動するには、アプリケーションサーバが必要になります。
**ローカルにこのようなミドルウェアが必要になったら、Docker で構築することを第一に考える**と便利です。

Docker Compose で Tomcat をローカルに起動するよう、docker-compose-tomcat.yaml を作成します。

```yaml
version: '3'
services:

  app:
    image: tomcat:9.0.41-jdk11-corretto
    volumes:
      - ./app/build/libs/app.war:/usr/local/tomcat/webapps/ROOT.war
    depends_on:
      - mysql
    ports:
      - "8080:8080"
    environment:
      MYSQL_HOST: mysql
      MYSQL_DATABASE: janken
      MYSQL_USER: user
      MYSQL_PASSWORD: password
```

「Docker を使うときは Dockerfile を書くことになるから面倒だ」という方もいらっしゃるかもしれません。
実は、上記の YAML の volumes の箇所のように、**マウントを上手に使えばローカルで Docker を使うのに Dockerfile が必要な場面はほとんどありません**。
むしろ、**マウントを使う方がメリットが大きい**ことが多いです。
詳しくは以前書いた記事「[その Dockerfile、本当に必要ですか ? マウントで解決できませんか ?](https://qiita.com/os1ma/items/d72ff84094a9eeb81c6f)」を参照ください。

上記のファイルを作成したら、以下のコマンドで MySQL と Web アプリケーションの両方が起動します。

```shell
docker-compose -f docker-compose.yaml -f docker-compose-tomcat.yaml up -d
```

あとはブラウザや curl などでアクセスすれば疎通確認できます。

```shell
$ curl localhost:8080/api/health
{"status":200}
```

この動作確認も自動テストにすることはできますが、今回はそこまでは実施しません。

これでヘルスチェックの API が実装できたので、デプロイパイプラインの構築に進もうと思います。

## デプロイパイプラインの構築

### どのプラットフォームを使うか

まず、デプロイ先としてどのプラットフォームを使うのか決める必要があります。

MySQL を含めて無料ということなら、GCE などの IaaS や Heroku あたりの無料枠が候補になります。
IaaS を使うとなると VM の脆弱性なども気になってしまうので、管理が楽な **PaaS である Heroku** を使おうと思います。

Heroku を使うのであれば CI も Heroku CI に引っ越してもいいのですが、Heroku CI は有料のため、**CI は GitHub Actions を使い、デプロイは Heroku の機能で自動化**することにしようと思います。

### Heroku の環境構築

Heroku 側の環境構築は簡単です。

Heroku と GitHub の main ブランチを連携して、自動デプロイされるようにしましょう。
具体的には以下の記事が参考になります。

- [heroku 初級編 - GitHub から deploy してみよう -](https://qiita.com/sho7650/items/ebd87c5dc2c4c7abb8f0)

なお、この手順の場合、GitHub Actions でビルドした成果物とは別に Heroku 上でビルドした成果物をデプロイすることになります。
**本来は CI でビルドしてテストも実施した成果物をデプロイしたほうがより動作を保証しやすい**ですが、今回そこは許容しました。

MySQL については、Heroku のアドオンの ClearDB で、ある程度まで無料で使うことができます。[^1]
ただし、ClearDB はインターネットから直接接続可能なので、データベースがインターネットから直接接続可能なことを許容できない場合は使用できません。

[^1]: 無料でもクレジットカード情報の登録が必要です

### Heroku へのデプロイ関係の設定

Heroku へのデプロイのため、各種設定を書きます。

まずは Heroku 上でのビルドの際に実行されるコマンドの設定を build.gradle に書きます。

```build.gradle
dependencies {
    :
    compile 'com.github.jsimone:webapp-runner:9.0.27.1'
}
:
// 以下、Heroku の設定
// 参考: https://devcenter.heroku.com/articles/deploying-gradle-apps-on-heroku#using-webapp-runner-to-deploy-war-files

task stage() {
    dependsOn clean, war
}
war.mustRunAfter clean

task copyToLib(type: Copy) {
    into "$buildDir/server"
    from(configurations.compile) {
        include "webapp-runner*"
    }
}

war.dependsOn(copyToLib)
```

続いて、デプロイ時の起動コマンドを Procfile に書きます。

```Procfile
web: java -jar app/build/server/webapp-runner-*.jar app/build/libs/*.war --port $PORT
```

Heroku はデフォルトでは Java 8 を使うので、Java 11 を使うよう設定します。

```system.properties
java.runtime.version=11
```

webapp-runner を使う際は web.xml が必須のようなので、作成します。

```web.xml
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
                      http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
  version="4.0">

  <display-name>JankenEnterpriseEdition</display-name>
  <absolute-ordering />
</web-app>
```

以上で、Heroku へのデプロイ関係の設定は完了です。
これで main ブランチに push すれば自動デプロイされます。

なお、2020/12/15 時点で webapp-runner の最新版である com.github.jsimone:webapp-runner:9.0.27.1 には Tomcat 関係等の大量の脆弱性が含まれており、それが原因でビルドに組み込んでいる OWASP Dependency Check の脆弱性診断を通過できませんでした。
今回は、一時的に OWASP Dependency Check を無効化し、Spring Framework 導入時に再度有効化することにさせていただきます。
本来的には 1 つ 1 つ吟味するか、webapp-runner を使わずに自前でセキュアなコンテナ等を作成してデプロイするといった対応が望ましいと思います。

### ブランチ戦略

さて、これで main ブランチへの push とともにデプロイされるようになりました。
ですが、この状態では、GitHub Actions の CI を通過するかしないかに限らず Heroku へのデプロイが進んでしまいます。

そこで、GitHub の運用を main ブランチ一本から変更し、

- デフォルトブランチを新規作成した develop ブランチに変更
- develop ブランチへの push 時に GitHub Actions で CI を実行
- main ブランチにマージしたら Heroku 側で自動デプロイを実施

という流れで開発するように変更しました。

このように **Git のどのブランチをどのように使うかという設計を「ブランチ戦略」と呼んだりします**。

チーム開発であれば

- main ブランチへのプルリクエストのレビューを必須にし、main ブランチに直接 push できないようにする

といった設定を入れることも考えられます。

具体的な設定方法は GitHub のドキュメントや以下の記事等を参照ください。

- [[GitHub] ブランチの保護設定を活用しよう 【レビューが通るまでマージさせんぞ】](https://dev.classmethod.jp/articles/protect-branch/)

## 次回のテーマ

以上で、Web アプリケーションのデプロイパイプラインを整えることができました。
これでついに、Web アプリケーションのコードを実装できます。
次回は早速、**プレゼンテーション層を書き換えて、じゃんけんプログラムを Web API にしてみようと思います**。

それでは、今回の記事はここまでにします。最後まで読んでくださりありがとうございました。
[じゃんけんアドベントカレンダー](https://qiita.com/advent-calendar/2020/janken) に興味を持ってくださった方は、是非購読お願いします。

## 現時点のコード

コードは GitHub の [この時点のコミット](https://github.com/os1ma/JankenEnterpriseEdition/tree/df342dd81c90430b70b69bd24427ed7aa9f03aa3) を参照ください。