HerokuにDuct + PedestalのWeb APIサーバを配置する

この記事は [Opt Technologies Advent Calendar 2019](https://qiita.com/advent-calendar/2019/opt-technologies) 18日目の記事です。

Clojureで書いたREST APIをHerokuに置く所まで記録しました。解説記事にしようとしましたが挫折しました。

ソースコードはこちらです。 https://github.com/v2okimochi/v2okimochi-apps-api-duct-pedestal

APIの書き方は、こちらを参考にしています。
https://github.com/lagenorhynque/clj-rest-api
https://github.com/lagenorhynque/aqoursql

ちなみにHerokuでのアプリケーション作成方法は既に書いてあるので省略します。
https://qiita.com/v2okimochi/items/f8eadfd668c36ad94277

あとHerokuの `clojure-getting-started`でJSONを返すようにするだけならこちらの記事のほうが早いかもです。
https://qiita.com/v2okimochi/items/5f3a29a0c9c04f2187d4

REST APIとは世界の果てまで雑に言うと `URLで接続したらJSONなどのレスポンスが返ってくるやつ`です。今回はたとえば `localhost:9001/api/`とかで接続するとJSONを返してくれます。
REST APIについては、こちらがわかりやすかったです。 https://qiita.com/masato44gm/items/dffb8281536ad321fb08
こういうのもあるんですね。 https://wiki.onap.org/display/DW/RESTful+API+Design+Specification

---

- Clojureです
    - 理由は訊かないでください
    - Duct + Pedestalです
    - テストなんてないです
    - バリデーションは迷子です
    - ログなんて飾りです
- Herokuです
    - なんと初期からClojureサポートがあったようです
    - しれっとGitHub連携から自動デプロイできます
- MariaDBです
    - HerokuはDB (データベース)まで用意してくれます
    - 何気にローカルではDockerで用意します
    - DB使うくせにマイグレーションとかはないです

環境のガバガバ具合をご覧いただけたところで、REST API `v2okimochi-apps`にまつわる全体の外観はこれです。

![Clojure_REST_API_Heroku.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201962/4a28da05-2893-6055-7c3b-dd62e5d215ed.png)





# できること
ローカル上では `localhost:9001/api/`、Heroku上では `xxx/api/` (作成したHeroku AppのURL)で接続できます。

接続するためには、[Postman](https://www.getpostman.com/)やHTTPクライアントを使います。

## /api/
ルートのエンドポイントからは固定のJSONを返します。

```json
{
    "okimochi": "文字",
    "日本語も使える": 10,
    "†記号も使える†": "aiue"
}
```

## /api/bad-request
400エラーを返します。エラー内容をJSONで伝えることができます……という例です。

```json
{
    "okimochi-error": "エラー文ですよね"
}
```

## /api/okimochi
固定値に加えて、クエリパラメータやDB情報を返します。
たとえば`/api/okimochi?v2_okimochi=v2`のようにすると、以下のようなJSONが返ってきます。

```json
{
    "data": {
        "okimochi": "おきもちv2",
        "v2": 2,
        "status": false
    },
    "requests": {
        "description": "okimochi-responseで受け取ったdbやtx-dataはこれ",
        "db": "duct.database.sql.Boundary@70e926c0",
        "tx-data": {
            "v2okimochi": "3"
        }
    }
}
```


## /api/staffs
DBの `staff`テーブルからデータを取得してJSONで返します。DBと接続しているのは実はこのエンドポイントだけです。

たとえばこんなデータが返ってきます。SQL文は単純に `SELECT * FROM staff;`で全件取得です。

```json
{
    "data": [
        {
            "sys-id": 1,
            "name": "v2okimochi",
            "message": "どうして"
        },
        {
            "sys-id": 2,
            "name": "おきもちv2",
            "message": "こうなった"
        },
        {
            "sys-id": 3,
            "name": "一体何者",
            "message": "なにも"
        },
        {
            "sys-id": 4,
            "name": "？？？？",
            "message": "わからない"
        }
    ]
}
```





# Herokuを動かすProcFile
HerokuでWebサーバやアプリを実行するためにはProcFileをルートディレクトリに置いておく必要があります。詳しい解説は公式のHeroku Dev Centerに書いてあります。
https://devcenter.heroku.com/articles/procfile

ProcFileの形式は `<process type>: <command>`です。

今回はClojure (JVMベース)なので、[Leiningen](https://leiningen.org/)にビルドしてもらって吐き出されたjarファイルを実行するように指定します。

```yaml:ProcFile
web: java $JVM_OPTS -cp target/v2okimochi-apps-api-standalone.jar clojure.main -m v2okimochi-apps-api.main
```

ちなみにHerokuではLeiningen 1.x を使おうとするようなので、 `それは違うよ`のおきもちを込めて 2.xを使うよう `project.clj`で指定します。jar名の指定も合わせて、全貌を書き残します。

```clojure:project.clj
(defproject v2okimochi-apps-api "1.0.0"
  :description "ductとpedestalを使ったルーティング処理"

  ;; 依存関係の管理（利用するライブラリはバージョンと共に指定する）
  :dependencies [[camel-snake-kebab "0.4.0"]
                 [duct/core "0.8.0"]
                 [duct.module.cambium "1.1.0"]
                 [duct.module.pedestal "2.0.2"]
                 [duct/module.sql "0.5.0"]
                 [honeysql "0.9.8"]
                 [metosin/ring-http-response "0.9.1"]
                 [org.clojure/clojure "1.10.1"]
                 [org.mariadb.jdbc/mariadb-java-client "2.5.1"]]
  :plugins [[duct/lein-duct "0.12.1"]]

  ;; leinバージョンの下限を指定 (herokuにlein 1.xを使わせないため)
  :min-lein-version "2.0.0"

  ;; ソースコードの場所を指定する
  :source-paths   ["src/clj"]
  :resource-paths ["resources"]

  ;; jarの名前を指定する
  :uberjar-name "v2okimochi-apps-api-standalone.jar"

  :main v2okimochi-apps-api.main

  :middleware     [lein-duct.plugin/middleware]
  :profiles {:repl {:prep-tasks   ^:replace ["javac" "compile"]
                    :repl-options {:init-ns user}}
             :dev [:project/dev]
             :project/dev {:source-paths ["dev/src"]
                           :resource-paths ["dev/resources"]
                           :dependencies [[fipp "0.6.21"]
                                          [hawk "0.2.11"]
                                          [integrant/repl "0.3.1"]]}
             :profiles/dev {}})
```





# Ductの一部はPedestalでやる
## (あまり例が)ないです
Duct + Pedestal という構成自体、あまり見られないらしいです。たとえばこういった解説があるくらいです。 https://qiita.com/lagenorhynque/items/fbd66ebaa0352ec4253d

Ductフレームワークが元々 `duct.module.xxx`の形で他の何かをがっちゃんこできますが、今回は [duct.module.pedestal](https://github.com/lagenorhynque/duct.module.pedestal)を使ってDuctのルーティング周りをPedestalにお任せします。


## Pedestalにするとどうなるのか
本質の特徴ではないかもしれませんが、インターセプター方式の書き方になります。玉ねぎの皮で包み込むように、関数の前後に別の処理を差し込む考え方です。

APIの `リクエストを送る => 処理する => レスポンスが返る` という状況でインターセプターを差し込むと、たとえばこんな感じのことができます。

- 処理を行う前に、インターセプターがリクエスト内のsnake_case :snake: をkebab-case :oden: に変換する
- 快適に処理する
- 処理を行った後に、インターセプターがレスポンス内のkebab-case :oden: をsnake_case :snake: に戻す

実際にインターセプターを差し込んでいる箇所は `src/clj/v2okimochi_apps_api/routes.clj`です。
`common-interceptors`としてまとめたインターセプター群を、 `#(route/expand-routes ...)`の各パスに適用しています。

```clojure:src/clj/v2okimochi_apps_api/routes.clj
(ns v2okimochi-apps-api.routes
  "ルーティング"
  (:require [integrant.core :as ig]
            [io.pedestal.http :as http]
            [io.pedestal.http.body-params :as body-params]
            [io.pedestal.http.route :as route]
            [v2okimochi-apps-api.handler.sample-responses :as sample]
            [v2okimochi-apps-api.handler.staffs :as staffs]
            [v2okimochi-apps-api.interceptors :as interceptors]))


;; reset時（のinit処理時）に更新する場所を指定？
(defmethod ig/init-key ::routes
  [_ {:keys [db]}]

  ;; ルーティング時、request mapをインターセプターに渡す
  ;; `common-interceptors`に入っている順にrequestを渡し、逆順にresponseが返っていく
  ;; `json-body`によってmapはJSONにして返す
  (let [common-interceptors [(body-params/body-params)
                             http/json-body
                             interceptors/attach-tx-data
                             (interceptors/attach-database db)]]
    #(route/expand-routes
      #{["/api/" :get
         ;; GET requestに対して `sample-responses/root-response`関数からレスポンスを返す
         (conj common-interceptors `sample/root-response)]

        ["/api/bad-request" :get
         (conj common-interceptors `sample/bad-request)]

        ["/api/okimochi" :get
         (conj common-interceptors `sample/okimochi-response)]

        ["/api/staffs" :get
         (conj common-interceptors `staffs/list-staffs)]})))
```

`http/json-body`の差し込みがわかりやすい気がします。実装は [io.pedestal.http/json-body](https://github.com/pedestal/pedestal/blob/89edba2b63776941a2580882ec1bb9849d37f30c/service/src/io/pedestal/http.clj#L115-L127)です。

処理はルーティング先にお任せして、 `on-response`つまりレスポンスを返す時だけContent-Typeを `"application/json;charset=UTF-8"`にします (これによってJSON形式が指定されます)。
ちゃっかり文字コードまで指定しています……。

また、先ほど例に挙げた :snake: => :oden: のインターセプターは `interceptors/attach-tx-data`にあります。

```clojure:src/clj/v2okimochi_apps_api/interceptors.clj（抜粋）
(def attach-tx-data
  {:name ::attach-tx-data
   :enter
   (fn [context]
     ;; クエリパラメータやJSONパラメータを一時的にkebab-caseに変換し、:tx-dataとして入れ直す
     (let [params (merge (get-in context [:request :query-params])
                         (get-in context [:request :json-params]))]
       (assoc-in context [:request :tx-data] (util/transform-keys-to-kebab params))))})
```

リクエストからクエリパラメータなどを抜き出して `_`部分を `-`に変換してから再びリクエストに突っ込んで返します。のちの処理では `突っ込まれたほう`を読めば :oden: です。おいしく食べましょう。

レスポンスを :oden: => :snake: する処理は、今回は書いていません。


# Herokuの host / port 問題
Herokuに置く際、つまづかされたのがサーバ設定です。

Herokuは、ホスト名とポート番号の **どちらかが誤っていると問答無用で `Web process failed to bind to $PORT within 60 seconds of launch`エラーを吐いて再起動を繰り返します**。
https://stackoverflow.com/questions/46445973/deploying-clojure-web-app-on-heroku-port-binding

- Herokuはポート番号が変化しているので、環境変数からポート番号を取得する必要がある。
- Herokuはホスト名が `localhost`だとダメ（IPv6で割り当ててなんやかんやで解決できない？）

要するに `ポート番号が合わなければ単純にバインド失敗`、 `ホスト名が合わなければポート以前の問題なので当然ポート番号もバインド失敗`という理屈で、どちらが間違っていても同じエラーが返るのだと思います。 ~~公式にも書いてあるとうれすぃ。。。~~

これを解決するために、 `config.edn`には以下のように書いてあります。

```clojure:resources/v2okimochi_apps_api/config.edn（抜粋）
  :duct.server/pedestal
  {:service #:io.pedestal.http{:routes #ig/ref :v2okimochi-apps-api.routes/routes
                               ;; herokuは `localhost`ではダメ
                               :host "0.0.0.0"
                               ;; herokuの環境変数は `PORT`で取得する
                               :port #duct/env ["PORT" Int :or 9001]}}
```

- まず、Herokuは `localhost`指定に屈してくれないので `0.0.0.0`を指定します。
- 次に、Herokuはポート番号が変わってしまうのでHeroku側の環境変数 `PORT`からポート番号を取得できるようにします。  
ちなみに `["PORT" Int :or 9001]`と書くと、環境変数を取得できなかった場合はポート番号9001が指定されます。これはローカル環境で実行した際にポート9001番となることを期待しています（ローカルには環境変数 `PORT`を作りません）。





# ローカル / Heroku上でのDB接続
duct.profileをいい感じに書くと、dev (開発)環境とprod (本番)環境で設定を分けられるようです。

なのでローカルのdev環境ではDocker上のMariaDBへ、Herokuのprod環境ではadd-onsであるJawsDB Mariaへ繋ぎに行きます。ややこしいことをしてすみません。

## ローカル上のdev環境ではDocker上のMariaDBに繋ぐ
下ごしらえとして、Docker上にMariaDBを立てておく必要があります。

Macの場合は、ググればたくさん情報が出るので本稿には書きません。
Windows10 Proの場合はDocker Desktop for Windowsが使えます。
Windows10 Homeの場合はとっても大変ですがこちらで頑張ればいけます。 https://qiita.com/v2okimochi/items/c9da7df8d4f03283121b

`docker-compose.yml`は用意してあるので、 `docker-compose up -d`するとコンテナを立ち上げるまでやってくれます。
`docker-compose.yml`の書き方は、たとえばこれらが参考になります。探すともっと出ます。
https://qiita.com/A-Kira/items/f401aea261693c395966
http://docs.docker.jp/compose/compose-file.html#driver

ちなみにマイグレーションがないので、テーブル作成から自力でやる必要があります :innocent: 

一応 `dev/resources/db_migrate.sql`と `dev/resources/db_data.sql`は用意してあります。

```sql:dev/resources/db_migrate.sql
CREATE TABLE `staff` (
  `sys_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
  `message` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  PRIMARY KEY (`sys_id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```

```sql:dev/resources/db_data.sql
INSERT INTO `staff` VALUES
(1,'@v2okimochi','どうして'),
(2,'おきもち','こうなった'),
(3,'一体何者','なにも'),
(4,'？？？？','わからない');
```


## Heroku上のprod環境ではadd-onsのJawsDB Mariaに繋ぐ
下ごしらえとして、Heroku側で `Resources`タブからadd-onsの1つである `JawsDB Maria`を追加しておく必要があります。
(確か)クレジットカードの登録が必要ですが、 `Free`プランがあります。add-onsから選択して `Provision`するだけでスッと準備されます。

![heroku_addons.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201962/37c713ae-5161-a639-af68-16db2783d249.jpeg)

有料プランもありますが、 `Free`を選ぶ場合はこうです。

![heroku_addons_provision.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201962/38e3c0ca-8446-720f-34ab-3cdb61df51f1.jpeg)


`v2okimochi-apps-api`をHerokuにデプロイするとあら不思議、今までDockerへ繋ぎに行っていたのが嘘のようにJawsDB Mariaへ繋いでくれます。

なお、ローカルと同じくテーブル作成やデータ挿入は自力です :innocent: 
接続情報は `Resources`タブでJawsDB Mariaを押せば見られます。


## HerokuではどうやってJawsDB Mariaに繋げているのか
`dev.edn`をご覧になった方の中には「URLもないし環境変数も見当たらないのにどうやって繋いでいるのか？」と思われた方がいるかもしれません。

dev環境で読み込まれる `dev/resources/dev.edn`では、Docker上のMariaDBにアクセスするためのURLが指定されています。なので、ローカル上のdev環境でDBに繋がるのは納得できます。

```clojure:dev/resources/dev.edn
{:duct.database.sql/hikaricp
 {:jdbc-url "jdbc:mysql://localhost:3399/v2okimochi?user=v2okimochi_dev&password=password1"}}
```

一方で、JawsDB Mariaに関する情報は `resources/v2okimochi_apps_api/config.edn`に書かれたコレです。

```clojure:resources/v2okimochi_apps_api/config.edn（抜粋）
 ;; Herokuに置かれた時は、JawsDB Mariaに接続するURLを環境変数から取得できる
 :duct.module/sql
 {:database-url #duct/env "JDBC_DATABASE_URL"}
```

Herokuでは `JDBC_DATABASE_URL`という環境変数にJawsDB MariaへのURLがあって、これを利用しています (Herokuが勝手に用意してくれる環境変数です)。

ではHerokuに環境変数 `JDBC_DATABASE_URL`があるのかと思って `Settings`タブから `Config Vars`を見に行くと、 **ありません**。あるのは `JAWSDB_MARIA_URL`くらいです（JawsDB Maria追加時に自動で加わったもの）。

実は `JDBC_DATABASE_URL`はこっそり隠れていて、[Heroku CLI](https://devcenter.heroku.com/articles/heroku-cli) 経由の `heroku run env`などのコマンドでしか確認できません。詳しい事情は公式のHeroku Dev Centerで説明されています。
 https://devcenter.heroku.com/articles/connecting-to-relational-databases-on-heroku-with-java#using-the-jdbc_database_url

ともあれ、Heroku上では環境変数 `JDBC_DATABASE_URL`を使ってJawsDB Mariaへ接続しています。 `JAWSDB_MARIA_URL`と合わせて、自動追加された環境変数はすべて必要です。





# GitHub連携による自動デプロイ
## Buildpacksの指定
`Settings`タブから `Buildpacks`を見て、Clojureビルドパックが追加されていることを確かめます。無ければ追加します。


## GitHubとの連携
`Deploy`タブで行います。まず `Deployment method`でGitHubを選択します。

次に `App connected to GitHub`で接続したいリポジトリを選択します。

そして `Automatic deploys`で自動デプロイしたいブランチを選択します。ちょっとわかりにくいですが、 `Enable Automatic Deploys`ボタンを押すと連携できます。

ここで選択したブランチにpushされると、自動でHerokuにデプロイされます。何気にCIを待つ機能もありますが、今回は使いません。 ( `Wait for CI to pass before deploy`オプション)


## 手動デプロイもできる
できます。 `Manual deploy`でブランチを選択し、 `Deploy Branch`ボタンを押します。

ファイル変更はないけどもう1回試したいな～という時に使っています。





# Herokuが寝落ちするアレ
既に有名な話かもしれませんが、無料Dyno時間を使っているHerokuアプリは30分間サーバアクセスがない場合スリープします。
https://devcenter.heroku.com/articles/free-dyno-hours

Dyno時間についての詳しい解説は省略しますが、雑に言うとAWSなどの従量課金制で用いられるような `時間あたりいくら`の概念がHerokuにもあるという認識です。

個人アカウントには毎月550時間の無料Dyno時間が与えられ、クレジットカード登録まで完了したアカウントには加えて月450時間の無料Dyno時間が与えられます。24時間 × 31日で744時間なので、クレジットカードを登録したアカウントでは1つのHerokuアプリを無料で延々と動かし続けられる、ということです。
無料Dyno時間を使い切ったとしても、無料アプリがスリープ状態となるだけで課金が始まることはないようです。
https://devcenter.heroku.com/articles/free-dyno-hours#free-dyno-hour-pool

一方で、上述したように無料アプリでは30分間アクセスがないと **寝落ち**します。寝ている `v2okimochi-apps-api`を叩き起こすと、 `レスポンスが返ってくるまで30秒かかる`なんてこともザラにあります。

この寝落ち現象については、 `heroku スリープ`などでググると **寝かさない方法**とともに多くの先駆者たちによる有益な情報と出会えます。

私自身も [似たような記事](https://qiita.com/v2okimochi/items/f8eadfd668c36ad94277)を書いたことがありますが、こちらはHeroku無料アプリのスリープ回避を狙ったものではなく、偶然スリープしない条件だったというだけです。




# Duct 0.8.0 の話 (おまけ)
## 依存関係
当初は 0.7.0 で書いていたのですが、なんとなく 0.8.0 にしました。許してください。

0.8.0 ではdev環境の依存関係に `fipp`と `hawk`を追加する必要があるようです。これらがないと、dev名前空間のロード時にエラーを吐かれました。

こんな感じで追加します。バージョンは 0.8.0 の `project.clj`をこっそり覗きました。 https://github.com/duct-framework/core/blob/master/project.clj

```clojure:project.clj（抜粋）
:project/dev {:source-paths ["dev/src"]
              :resource-paths ["dev/resources"]
              :dependencies [[fipp "0.6.21"]
                             [hawk "0.2.11"]
                             [integrant/repl "0.3.1"]]}
```


## auto-reset機能
依存関係のうち `hawk`は、 0.8.0 で追加された `(auto-reset)`関数で使われているようでした。 https://github.com/duct-framework/core/blob/6faa2ea29b81e8cb242c549de923aca2d0242081/src/duct/core/repl.clj#L31-L37

従来はファイル変更を取り込んでサーバの再起動を行うためにREPL上で `(reset)`関数を手動実行することが（おそらく）主流だったのですが、この `(auto-reset)`関数を呼んでおくとファイル保存時に自動で `(reset)`してくれるようです。
`npm run serve`した時のアレみたいでチョット便利そうです。

なんと `(auto-reset)`を2回呼ぶこともできます。書いてみただけです。そんなことをしても `(reset)`が実行された瞬間に壊れてREPL再起動を余儀なくされます。





# まとめ
まとめもヘチマもないです。ClojureでDuctでPedestalでHerokuでした。完