Getting Started with Gradle on Heroku をやってみた

# 動機
私は現在、TitterやLINEでbotを作り、特定の界隈向けに情報を発信しています。これらは既存のプラットフォームにのっかるだけなので比較的簡単に公開まで持って行くことができます。
しかし、これらの方法での発信では物足りなくなってきました。そこで、webサービスを作ろうと考えました。
完全に趣味としてやっているので、可能な限り無料でやろうということでHerokuに目を付けました。
というわけで今回はHeroku公式にあるチュートリアルをやっていこうと思います。
この記事はほぼチュートリアルの和訳みたいになってしまいましたがご了承ください。

# 準備
最初に見つけたのは**[Getting Started on Heroku with Java](https://devcenter.heroku.com/articles/getting-started-with-java)**というページだったのですが、こちらはMavenを使用するようです。
このページの中で**「MavenではなくGradleを使う人は[こっち](https://devcenter.heroku.com/articles/getting-started-with-gradle-on-heroku)をみてね。」**とあったので、今回はそちらをやっていきます。
※私個人がGradleの方が馴染みがあるため。

## 環境
今回使用するPCの環境をざっくりと。

- OS　Windows10 Home 64bit
- Java 11
- Git Bashは導入済み

# チュートリアル目次
- [Introduction](#Introduction)
- [Set up](#Set up)
- [Prepare the app](#Prepare the app)
- [Deploy the app](#Deploy the app)
- [View logs]()
- [Define a Procfile]()
- [Scale the app]()
- [Declare app dependencies]()
- [Run the app locally]()
- [Push local changes]()
- [Provision add-ons]()
- [Start a one-off dyno]()
- [Define config vars]()
- [Use a detabase]()
- [Next steps]()

## Introduction
こちらには今回のチュートリアルの前提条件が記載してあります。

- Herokuのアカウントを持っていること
- Java8が導入されていること

Javaのバージョン指定が8となっていましたが、私の環境では11でも問題ありませんでした。

## Set up
ここでは**Heroku Command Line Interface (CLI)**を導入します。
(以前は**Heroku Toolbelt**という名前だったようです。)
CLIはアプリケーションの管理とスケーリング、アドオンのプロビジョニング、Herokuで実行されるアプリケーションのログの表示、アプリケーションのローカル実行に使用するようです。
自分の環境にあったインストーラを選択してダウンロードします。

インストールが終わると```heroku```コマンドが使えるようになります。`heroku login`コマンドでHeroku CLIへログインしましょう。

``` sh:herokuへのログイン
$ heroku login
heroku: Press any key to open up the browser to login or q to exit
# ここでキーを押すとブラウザで画面が立ち上がる
# 画面にあるログインボタンを押すとログインできる
 ›   Warning: If browser does not open, visit
 ›   https://cli-auth.heroku.com/auth/browser/***
heroku: Waiting for login...
Logging in... done
Logged in as me@example.com
```
proxy設定ができるようですが、今回は必要ないので省略します。
参考：[CLI Usage / Using an HTTP proxy](https://devcenter.heroku.com/articles/using-the-cli#using-an-http-proxy)

##Prepare the app
用意されているサンプル用プロジェクトを任意のディレクトリにクローンします。

``` sh:プロジェクトをクローン
$ git clone https://github.com/heroku/gradle-getting-started.git
$ cd gradle-getting-started
```

## Deploy the app
先ほどクローンしたプロジェクトをデプロイしていきます。
まず、Herokuにアプリケーションを作成しHerokuでソースコードを受け取れるようにします。

``` sh:アプリケーションを作成
$ heroku create
Creating app... done, arcane-inlet-19935
https://arcane-inlet-19935.herokuapp.com/ | https://git.heroku.com/arcane-inlet-19935.git
```
`arcane-inlet-19935`というのはアプリケーション名でランダムに命名されたものです。アプリケーション名は指定することもできるようです。このアプリケーション名はURLの一部になります。今回はチュートリアルなので特に指定しませんでした。

`heroku`というリポジトリへpushします。

```
$ git push heroku master
```
アプリケーションが動いていることを確認します。

```
$ heroku ps:scale web=1
Scaling dynos... done, now running web at 1:Free
```
下記コマンドをを打つとブラウザが立ち上がり、ページを開いてくれます。

```
$ heroku open
```
URLを確認すると、`https://{アプリケーション名}.herokuapp.com`となっています。

## View logs
Herokuのログはすべてのアカウント、Herokuコンポーネント共通で単一の出力ストリームに時系列順でログを出力しています。

``` cmd:ログ出力
$ heroku logs --tail
2019-12-15T06:22:53.476464+00:00 app[web.1]: 2019-12-15 06:22:53.476  INFO 4 --- [           main] o.s.b.a.w.s.WelcomePageHandlerMapping    : Adding welcome page template: index
2019-12-15T06:22:54.441609+00:00 heroku[web.1]: State changed from starting to up
2019-12-15T06:22:54.408814+00:00 app[web.1]: 2019-12-15 06:22:54.405  INFO 4 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 37878 (http) with context path ''
2019-12-15T06:22:54.452349+00:00 app[web.1]: 2019-12-15 06:22:54.452  INFO 4 --- [           main] com.example.heroku.HerokuApplication     : Started HerokuApplication in 12.317 seconds (JVM running for 14.742)
2019-12-15T06:32:15.384939+00:00 app[web.1]: 2019-12-15 06:32:15.382  INFO 4 --- [io-37878-exec-8] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2019-12-15T06:32:15.384963+00:00 app[web.1]: 2019-12-15 06:32:15.382  INFO 4 --- [io-37878-exec-8] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2019-12-15T06:32:15.412236+00:00 app[web.1]: 2019-12-15 06:32:15.412  INFO 4 --- [io-37878-exec-8] o.s.web.servlet.DispatcherServlet        : Completed initialization in 29 ms
```
`--tail`はリアルタイムで表示するというオプションです。起動確認時に訪れたページに再度アクセスすると、そのログが流れていく様子を見ることができます。
`Ctrl + C`でリアルタイム表示を終了できます。
ログの出し方のオプションは他にもいろいろ用意されています。
ログの出し方：https://devcenter.heroku.com/articles/logging#view-logs

## Define a Procfile
アプリケーションのルートディレクトリにあるProcfileでアプリケーションを起動するコマンドを明示的に宣言します。
サンプルプロジェクトのProcfileは次のようになっています。

``` :Procfile
web: java -jar build/libs/gradle-getting-started-1.0.jar
```
単一のプロセスタイプ`web`とをれを実行するために必要なコマンドが宣言されています。ここでは`web`という名前が重要な意味を持ちます。
プロセスタイプやProcfileについては下記を読んでください。
Procfileについて：https://devcenter.heroku.com/articles/procfile

Procfileに書かれたコマンドがそのままUNIX環境で実行されるので、Syntaxが重要になります。

## Scale the app
現在、アプリケーションは単一のWeb dynoで実行されています。dynoというのはProcfileで指定されたコマンドを実行する小さなコンテナだと考えることができます。

psコマンドで実行されているdynoの数を確認できます。

```
$ heroku ps
Free dyno hours quota remaining this month: 949h 54m (94%)
Free dyno usage for this app: 0h 0m (0%)
For more information on dyno sleeping and how to upgrade, see:
https://devcenter.heroku.com/articles/dyno-sleeping

=== web (Free): java -jar build/libs/gradle-getting-started-1.0.jar (1)
web.1: up 2019/02/13 11:10:03 -0600 (~ 13s ago)
```
この例では`web.1`が動いていることを確認できます。

>
>この辺りは課金の話になりますが、dynoについてはこのように書かれています。
>
- デフォルトではFreeのdynoにデプロイされる。
- Freeのdynoは30分間操作しない（トラフィックを受信しない）とスリープ状態になる。
- スリープ時にリクエストを受けると起動のために数秒の遅延が起きる。
- アカウント単位で月ごとにFreeのdynoの起動時間の無料枠があり、それを消費しきるとFreeのdynoにデプロイされているアプリケーションは月末までスリープ状態のままになってしまう。
>
詳細は[こちら](https://devcenter.heroku.com/articles/free-dyno-hours)に書いてあります。

## Declare app dependencies
Herokuはルートディレクトリに`gradlew`または`build.gradle`があることでGradleアプリであることを認識しています。

サンプルプロジェクトには`build.gradle`が入っています。

``` gradle:build.gradle（抜粋）
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-jdbc'
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    runtimeOnly 'org.postgresql:postgresql'
    runtimeOnly 'org.webjars:jquery:3.3.1-1'
    runtimeOnly 'org.webjars:jquery-ui:1.12.1'
    runtimeOnly 'org.webjars:bootstrap:4.1.3'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
```
Javaのバージョンはルートディレクトリにある`system.properties`に記載します。

``` :system.properties
java.runtime.version=1.8
```
Introductionで書きましたが、このチュートリアルはJava8を前提としているため1.8となっています。

それでは`build`コマンドを実行しましょう。

```
$ ./gradlew build
```

## Run the app locally
続いてローカル環境での実行です。

```
$ heroku local web
```
Heroku上で起動するのと同様にProcfileで実行するものを決定します。ポートの指定は`src/main/resources/application.properties`に記載します。

``` properties:application.properties
server.port=${PORT:5000}
```

`heroku local`コマンドはアプリケーションの実行だけでなく`config vars`のセットもできます。（後述）

## Push local changes
このセクションではローカルの変更をHerokuへ反映する手順を説明します。
例として以下のようにサンプルに変更を加えていきます。

``` gradle:build.gradleの30行目
compile "org.jscience:jscience:4.3.1"
```

``` java:src/main/java/com/example/heroku/HerokuApplication.java
// 19行目
import static javax.measure.unit.SI.KILOGRAM;
import javax.measure.quantity.Mass;
import org.jscience.physics.model.RelativisticModel;
import org.jscience.physics.amount.Amount;

// 中略

// 59行目
  @RequestMapping("/hello")
  String hello(Map<String, Object> model) {
      RelativisticModel.select();
      Amount<Mass> m = Amount.valueOf("12 GeV").to(KILOGRAM);
      model.put("science", "E=mc^2: 12 GeV = " + m.toString());
      return "hello";
  }
```

``` html:src/main/resources/templates/hello.htmlを作成
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" th:replace="~{fragments/layout :: layout (~{::body},'hello')}">
<body>
  <div class="container">
    <p th:text="${science}"/>
  </div>
</body>
</html>
```
変更を終えたら再度ビルドし、アプリケーションを実行します。

```
$ ./gradlew build
...
BUILD SUCCESSFUL in 41s
5 actionable tasks: 5 executed

$ heroku local web
...
17:48:20 web.1   |  2019-12-15 17:48:20.865  INFO 14244 --- [           main] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 9409 ms
17:48:23 web.1   |  2019-12-15 17:48:23.462  INFO 14244 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
17:48:25 web.1   |  2019-12-15 17:48:25.429  INFO 14244 --- [           main] o.s.b.a.w.s.WelcomePageHandlerMapping    : Adding welcome page template: index
17:48:26 web.1   |  2019-12-15 17:48:26.397  INFO 14244 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 5000 (http) with context path ''
```

起動したら`http://localhost:5000/hello`にアクセスします。

```
E=mc^2: 12 GeV = (2.139194076302506E-26 ± 1.4E-42) kg
```
という何か物理に関する数式のようなものが出ればOKです。この計算をするために`dependencies`や`import`で追加したライブラリを使っているようです。
（**jscience**なんてライブラリがあるんですね…）

確認出来たらデプロイしていきます。

```
$ git add .
$ git commit -m "Demo"
$ git push heroku master
```
デプロイできたことを確認します。

```
$ heroku open
```
デモのトップ画面が開いたらURLの末尾に`/hello`を付けると、先ほどの数式のが表示されるはずです。

## Provision add-ons
Herokuには様々なサードパーティアドオンがあります。今回は**Papertrail**というログ用アドオンを追加します。
Pepertrailを入れるとブラウザ上でログを確認することができます。

```
$ heroku addons:create papertrail
Creating papertrail on mysterious-wildwood-86066... !
 !    Please verify your account to install this add-on plan (please enter a
 !    credit card) For more information, see
 !    https://devcenter.heroku.com/categories/billing Verify now at
 !    https://heroku.com/verify
```
どうやらアドオンを追加するにはクレジットカードの登録が必要なようです。クレジットカードを登録するとFree dyno時間（いわゆる無料枠）が400h/月から1000h/月に拡大されるので私は登録しました。

では改めて

```
$ heroku addons:create papertrail
Creating papertrail on mysterious-wildwood-86066... free
Welcome to Papertrail. Questions and ideas are welcome (support@papertrailapp.com). Happy logging!
Created papertrail-curly-70913 as PAPERTRAIL_API_TOKEN
Use heroku addons:docs papertrail to view documentation
```
無事に追加できました。追加されているアドオンは以下のようにして確認することができます。

```
$ heroku addons

Add-on                               Plan     Price  State
───────────────────────────────────  ───────  ─────  ───────
papertrail (papertrail-curly-70913)  choklad  free   created
 └─ as PAPERTRAIL

The table above shows add-ons and the attachments to the current app (mysterious-wildwood-86066) or other apps.
```
それでは下記のコマンドでログを見てみましょう。

```
$ heroku addons:open papertrail
```
ブラウザでログが表示されたページが立ち上がります。

## Start a one-off dyno
**one-off dyno**というのはHeroku上でコマンドを一度限りで実行するもののようです。one-off dynoでは`heroku run`というコマンドを用います。

``` :例）Javaのバージョンを確認する
$ heroku run java -version
Running java -version on arcane-inlet-19935... up, run.6561 (Free)
openjdk version "1.8.0_201-heroku"
OpenJDK Runtime Environment (build 1.8.0_201-heroku-b09)
OpenJDK 64-Bit Server VM (build 25.201-b09, mixed mode)
```
ほかにもローカル端末に接続されたREPLプロセスを起動してアプリの環境で実験したり、アプリケーションと共にデプロイしたコードを起動したりすることもできます。

## Define config vars
Herokuでは、暗号化キーや外部リソースアドレスなどのデータを**config vars**に保存して、構成を外部化できます。config varsは実行時に環境変数としてアプリケーションに公開されます。
例として先ほどサンプルに追加した`HerokuApplication.java`の`hello`メソッドが`ENERGY`環境変数からエネルギー値を取得するように変更してみます。

``` java:HerokuApplication.java
@RequestMapping("/hello")
String hello(Map<String, Object> model) {
    RelativisticModel.select();
// ここから追加
    String energy = System.getenv().get("ENERGY"); // ここで環境変数ENERGYを使用
    if (energy == null) {
       energy = "12 GeV";
    }
// ここまで
    Amount<Mass> m = Amount.valueOf(energy).to(KILOGRAM);　// 変数energyを使用するように変更
    model.put("science", "E=mc^2: " + energy + " = "  + m.toString()); // 変数energyを使用するように変更
    return "hello";
}
```
最上位ディレクトリの`.env`ファイルに環境変数を記述します。このファイルは`heroku local`コマンドの時に読み込まれるようです。

``` :.env
ENERGY=20 GeV
```

ビルドし直してlocalで起動後`http://localhost:5000/hello`へアクセスすると数値が変わっていることが確認できます。

``` :変更後の数式
E=mc^2: 20 GeV = (3.5653234605041761E-26 ± 2.9E-42) kg
```
確認できたところでHerokuにも同じ環境変数を設定しましょう。

```
$ heroku config:set ENERGY="20 GeV"
'C:\Program' は、内部コマンドまたは外部コマンド、
操作可能なプログラムまたはバッチ ファイルとして認識されていません。
```

？？？
とりあえずチュートリアルをさかのぼっていたらDefine a Procfileの項にこのように書いてありました。

>
When running locally under Windows, you may receive an error because the Unix way of passing in environment variables ($JAVA_OPTS) and of concatenating paths (:) is incompatible.

Unix環境では文字列結合の環境変数の渡し方とpathの連結の仕方に互換性がないことが原因のようです。
仕方がないのでHeroku上での確認はいったんここはスルーします。

## Use a database
add-on maketplaceにはRedisやMongoDBからPostgres、MySQLまで数多くのデータストアがあります。まずはPostgreSQLアドオンの新しいインスタンスをアプリにアタッチしてみましょう。

```
$ heroku addons:create heroku-postgresql
Creating heroku-postgresql on mysterious-wildwood-86066... free
Database has been created and is available
 ! This database is empty. If upgrading, you can transfer
 ! data from another database with pg:copy
Created postgresql-amorphous-60925 as DATABASE_URL
Use heroku addons:docs heroku-postgresql to view documentation
```
`heroku addons`コマンドで見てみましょう。

```
$ heroku addons

Add-on                                          Plan       Price  State
──────────────────────────────────────────────  ─────────  ─────  ───────
heroku-postgresql (postgresql-amorphous-60925)  hobby-dev  free   created
 └─ as DATABASE

papertrail (papertrail-curly-70913)             choklad    free   created
 └─ as PAPERTRAIL

The table above shows add-ons and the attachments to the current app (mysterious-wildwood-86066) or other apps.
```
`heroku-postgresql`が追加されています。

続いて、環境変数を見てみましょう。

```
$ heroku config
=== arcane-inlet-19935 Config Vars
DATABASE_URL:         postgres://pccigjsjmqxlvk:b3aedb1e099aa729d8037c0be6454e1028dc0c7c4dc16f8b366471486a8a7014@ec2-54-235-159-101.compute-1.amazonaws.com:5432/db2b5rodiinvv4
PAPERTRAIL_API_TOKEN: duMLEm2E5WHtf7mC163g
```
`DATABASE_URL`という変数が追加されています。
heroku には`pg`というコマンドも用意されています。

```
$ heroku pg
=== DATABASE_URL
Plan:                  Hobby-dev
Status:                Available
Connections:           10/20
PG Version:            11.6
Created:               2019-12-15 10:26 UTC
Data Size:             7.7 MB
Tables:                0
Rows:                  0/10000 (In compliance) - refreshing
Fork/Follow:           Unsupported
Rollback:              Unsupported
Continuous Protection: Off
Add-on:                postgresql-amorphous-60925
```
これをみると`Hobby-dev(＝無料プラン)`であり、PGのバージョンが11.6であることがわかりました。
これまでの手順でデプロイ済みのアプリケーションには既にデータベースの機能が含まれています。
`https://{アプリケーション名}.herokuapp.com/db`にアクセスするとアクセスした日時が表示されます。（北米リージョンなので日本時間の9時間前）

``` java:HerokuApplication.java(抜粋)
  @Value("${spring.datasource.url}")
  private String dbUrl;

  @Autowired
  private DataSource dataSource;

  @RequestMapping("/db")
  String db(Map<String, Object> model) {
    try (Connection connection = dataSource.getConnection()) {
      Statement stmt = connection.createStatement();
      stmt.executeUpdate("CREATE TABLE IF NOT EXISTS ticks (tick timestamp)");
      stmt.executeUpdate("INSERT INTO ticks VALUES (now())");
      ResultSet rs = stmt.executeQuery("SELECT tick FROM ticks");

      ArrayList<String> output = new ArrayList<String>();
      while (rs.next()) {
        output.add("Read from DB: " + rs.getTimestamp("tick"));
      }

      model.put("records", output);
      return "db";
    } catch (Exception e) {
      model.put("message", e.getMessage());
      return "error";
    }
  }

  @Bean
  public DataSource dataSource() throws SQLException {
    if (dbUrl == null || dbUrl.isEmpty()) {
      return new HikariDataSource();
    } else {
      HikariConfig config = new HikariConfig();
      config.setJdbcUrl(dbUrl);
      return new HikariDataSource(config);
    }
  }
```
これにより、`/db`にアクセスするたびに`tick`テーブルに現在時刻のレコードが追加され、これまでのアクセス日時が一覧表示されます。

## Next steps
ここまででデプロイの仕方、設定変更、ログチェック、アドオンの追加ができるようになりました。
次に学習するのにおすすめの記事がいくつか挙げられています。

- [How Heroku Works](https://devcenter.heroku.com/articles/how-heroku-works)
- [Deploying Gradle Apps on Heroku](https://devcenter.heroku.com/articles/deploying-gradle-apps-on-heroku)
- [Language Support - Java](https://devcenter.heroku.com/categories/java-support)

# 終わりに
Herokuの基本的なところはこれで押さえられたかなと思います。Herokuはアクセスしなければ30分でスリープ状態になり無料枠を消費しなくなるので、お試しでいくつもアプリケーションを作った後にわざわざ消したりしなくてもよいのが地味にうれしいところかなと思います。

# 参考
- [Getting Started with Gradle on Heroku](https://devcenter.heroku.com/articles/getting-started-with-gradle-on-heroku)