この記事は Opt Technologies Advent Calendar 2019 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
にまつわる全体の外観はこれです。
できること
ローカル上では localhost:9001/api/
、Heroku上では xxx/api/
(作成したHeroku AppのURL)で接続できます。
接続するためには、PostmanやHTTPクライアントを使います。
/api/
ルートのエンドポイントからは固定のJSONを返します。
{
"okimochi": "文字",
"日本語も使える": 10,
"†記号も使える†": "aiue"
}
/api/bad-request
400エラーを返します。エラー内容をJSONで伝えることができます……という例です。
{
"okimochi-error": "エラー文ですよね"
}
/api/okimochi
固定値に加えて、クエリパラメータやDB情報を返します。
たとえば/api/okimochi?v2_okimochi=v2
のようにすると、以下のような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;
で全件取得です。
{
"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にビルドしてもらって吐き出されたjarファイルを実行するように指定します。
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名の指定も合わせて、全貌を書き残します。
(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を使ってDuctのルーティング周りをPedestalにお任せします。
Pedestalにするとどうなるのか
本質の特徴ではないかもしれませんが、インターセプター方式の書き方になります。玉ねぎの皮で包み込むように、関数の前後に別の処理を差し込む考え方です。
APIの リクエストを送る => 処理する => レスポンスが返る
という状況でインターセプターを差し込むと、たとえばこんな感じのことができます。
- 処理を行う前に、インターセプターがリクエスト内のsnake_case をkebab-case に変換する
- 快適に処理する
- 処理を行った後に、インターセプターがレスポンス内のkebab-case をsnake_case に戻す
実際にインターセプターを差し込んでいる箇所は src/clj/v2okimochi_apps_api/routes.clj
です。
common-interceptors
としてまとめたインターセプター群を、 #(route/expand-routes ...)
の各パスに適用しています。
(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です。
処理はルーティング先にお任せして、 on-response
つまりレスポンスを返す時だけContent-Typeを "application/json;charset=UTF-8"
にします (これによってJSON形式が指定されます)。
ちゃっかり文字コードまで指定しています……。
また、先ほど例に挙げた => のインターセプターは interceptors/attach-tx-data
にあります。
(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))))})
リクエストからクエリパラメータなどを抜き出して _
部分を -
に変換してから再びリクエストに突っ込んで返します。のちの処理では 突っ込まれたほう
を読めば です。おいしく食べましょう。
レスポンスを => する処理は、今回は書いていません。
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
には以下のように書いてあります。
: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
ちなみにマイグレーションがないので、テーブル作成から自力でやる必要があります
一応 dev/resources/db_migrate.sql
と dev/resources/db_data.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;
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
するだけでスッと準備されます。
有料プランもありますが、 Free
を選ぶ場合はこうです。
v2okimochi-apps-api
をHerokuにデプロイするとあら不思議、今までDockerへ繋ぎに行っていたのが嘘のようにJawsDB Mariaへ繋いでくれます。
なお、ローカルと同じくテーブル作成やデータ挿入は自力です
接続情報は Resources
タブでJawsDB Mariaを押せば見られます。
HerokuではどうやってJawsDB Mariaに繋げているのか
dev.edn
をご覧になった方の中には「URLもないし環境変数も見当たらないのにどうやって繋いでいるのか?」と思われた方がいるかもしれません。
dev環境で読み込まれる dev/resources/dev.edn
では、Docker上のMariaDBにアクセスするためのURLが指定されています。なので、ローカル上のdev環境でDBに繋がるのは納得できます。
{:duct.database.sql/hikaricp
{:jdbc-url "jdbc:mysql://localhost:3399/v2okimochi?user=v2okimochi_dev&password=password1"}}
一方で、JawsDB Mariaに関する情報は 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 経由の 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 スリープ
などでググると 寝かさない方法とともに多くの先駆者たちによる有益な情報と出会えます。
私自身も 似たような記事を書いたことがありますが、こちらは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
: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でした。完