LoginSignup
5
2

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-12-17

この記事は 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にまつわる全体の外観はこれです。

Clojure_REST_API_Heroku.png

できること

ローカル上では 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ファイルを実行するように指定します。

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名の指定も合わせて、全貌を書き残します。

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を使って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 ...)の各パスに適用しています。

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です。

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

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

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には以下のように書いてあります。

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.sqldev/resources/db_data.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;
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

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

heroku_addons_provision.jpg

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に繋がるのは納得できます。

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に書かれたコレです。

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環境の依存関係に fipphawkを追加する必要があるようです。これらがないと、dev名前空間のロード時にエラーを吐かれました。

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

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でした。完

5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2