1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Clojure x ClojureScript で深める Web 開発 (2) 環境の構築

Last updated at Posted at 2021-03-29

Git Repo

ソースコードと原文が入ったレポジトリ
https://github.com/MokkeMeguru/clj-web-dev-ja/tree/main/chap2

シリーズ

本編

本稿では、Web API サーバを書いていくにあたり必要な、1. 開発環境の Dockerize、2. 基礎的なライブラリの列挙、3. integrant のセットアップを行います。

開発環境の Dockerize

開発を進めていく上で、デプロイやスケーリングの観点から、Docker という選択肢はかなり受け入れられたものになっています。

今回は Docker を利用して実行環境を構築し、更に RDB もまとめて管理できるよう、docker-compose の利用を行います。 ディレクトリとファイルを次のように追加します。

picture_gallery
├── README.md
├── containers           (各 Docker コンテナの設定)
│   ├── api-server
│   │   └── Dockerfile
│   └── postgres
│       └── Dockerfile
├── dev
├── doc
├── docker-compose.yml  (docker-compose の設定)
├── project.clj
├── resources
├── src
├── target
└── test

Clojure の API Server 用の Dockerfile (api-server/Dockerfile) は次の通り

FROM clojure:openjdk-11-lein
MAINTAINER MokkeMeguru <meguru.mokke@gmail.com>
ENV LANG C.UTF-8
ENV APP_HOME /app
RUN apt-get update
RUN apt-get -y install tmux
RUN mkdir $APP_HOME
WORKDIR $APP_HOME

PostgreSQL の Dockerfile (postgres/Dockerfile) は次の通り

FROM postgres:10.5
MAINTAINER MokkeMeguru <meguru.mokke@gmail.com>

docker-compose.yml は次の通り

version: "3"
services:
  dev_db:
    build: containers/postgres
    ports:
      - 5566:5432
    volumes:
      - "dev_db_volume:/var/lib/postgresql/data"
    environment:
      POSTGRES_USER: meguru
      POSTGERS_PASSWORD: emacs
      POSTGRES_INITDB_ARGS: "--encoding=UTF-8"
      POSTGRES_DB: pic_gallery
    restart: always
  repl:
    build: containers/api-server
    command: /bin/bash
    ports:
      - 3000:3000
      - 39998:39998
    volumes:
      - ".:/app"
      - "lib_data:/root/.m2"
    depends_on:
      - dev_db
volumes:
  dev_db_volume:
  lib_data:

dev_db_volume、lib_data は docker-compose のデータ永続化の機能 (named volume) を用いるために記述されています。

Port の開放

docker-compose で走る Docker コンテナの内部と交信するために、 port の開放をすることができます。

  • localhost:5566 で内部の DB へ接続するため、dev_db/ports に 5566:5432 を追加しています。

  • localhost:3000 を通して API サーバとやり取りするために、 repl/ports に 3000:3000 を追加しています。

  • localhost:39998 を通して repl コンテナ内の Clojure インタプリタへ接続するために、 repl/ports に 39998:39998 を追加しています。

Directory のマウント

今回作るサーバ picture-gallery のソースコードをそのまま repl コンテナで読み込むために、repl/volumes に .:/app としてコンテナ内部の /app に picture-gallery フォルダをそのままマウントさせています。

動作確認

試しに動かしてみましょう。

# ビルド
$ docker-compose build
# 立ち上げ
$ docker-compose run --service-port repl bash
# REPL 環境の立ち上げ
root:@xxx:/app# lein repl
user=> (+ 1 2)
3
user=> exit
Bye for now!
# 環境から抜け出す (Ctrl-p Ctrl-q)
root:@xxx:/app#
$

ちなみに、今回は Ctrl-p Ctrl-q で Docker コンテナから抜け出しましたが、これに復帰するには、 docker ps コマンドで実行していた CONTAINER ID (e.g. 5b6d5b45e8aa) を確認し、

$ docker exec -it 5b6d5b45e8aa bash

とします。

管理のために、 Docker コンテナ内で tmux や byobu といったツールを利用すると良いでしょう。 5.2

ライブラリの追加

いよいよ具体的な API サーバ開発を進めていくわけですが、それに伴っていくつかのライブラリを追加する必要性があります。 Rails や Spring といったより便利なフレームワークを用いたサーバ開発ではこの工程は不要ですが、ライブラリ選定を自分で行うことで、 よくわからないけど動く を減らすことができます。 (本ガイドでは以下に紹介するライブラリを用いましたが、勿論別のライブラリで代替することが可能です。)

追加するライブラリ一覧
簡単のため、追加するライブラリの詳細については省き、一覧と捕捉のみ紹介します。 これらのライブラリの追加は、Clojure x ClojureScript で深める Web 開発 (1) で紹介される `project.clj` に追加されています。
;; integrant
[integrant "0.8.0"]
[integrant/repl "0.3.2"]

;; firebase auth のためのライブラリ
[com.google.firebase/firebase-admin "7.1.0"]

;; ルーティング、HTTP ハンドラ のためのライブラリ
[ring/ring-jetty-adapter "1.9.1"]
[metosin/reitit "0.5.12"]
[metosin/reitit-swagger "0.5.12"]
[metosin/reitit-swagger-ui "0.5.12"]

[ring-cors "0.1.13"]
[ring-logger "1.0.1"]
[com.fasterxml.jackson.core/jackson-core "2.12.2"]

;; 暗号化通信のためのライブラリ
[buddy/buddy-hashers "1.7.0" ]

;; 環境変数の読み込みのためのライブラリ
[environ "1.2.0"]

;; ロギング処理のためのライブラリ
[com.taoensso/timbre "5.1.2"]
[com.fzakaria/slf4j-timbre "0.3.20"]

;; データベースとの通信を行うためのライブラリ
[honeysql "1.0.461"]
[seancorfield/next.jdbc "1.1.643"]
[hikari-cp "2.13.0"]
[org.postgresql/postgresql "42.2.19"]
[net.ttddyy/datasource-proxy "1.7"]

;; マイグレーションを行うためのライブラリ
[ragtime "0.8.1"]

;; テスト、 Spec のためのライブラリ
[orchestra "2021.01.01-1"]
[org.clojure/test.check "1.1.0"]

;; CLI コマンドの実行のためのライブラリ
[org.clojure/tools.cli "1.0.206"]

;; JSON 処理、時刻処理、文字列処理のためのライブラリ
[clj-time "0.15.2"]
[cheshire "5.10.0"]
[camel-snake-kebab "0.4.2"]

なお、注意する点として、ライブラリを追加したら、 REPL は再起動が必要ですexit から lein repl で再接続して下さい。

エディタとの接続

ここまでで、Docker コンテナ内で REPL が立ち上がりました。

REPL は各エディタと連携することでより開発を快適にすることができます。 具体的には、コードを書いたところから環境に反映して動かすことができるようになります。

Clojure の REPL と連携できるエディタは Emacs、Vim、VSCode、InteliJ などありますが、今回は多くの人が使っているという理由で VSCode での使い方を紹介します。

まず project.clj に以下の設定を追加します。

:repl-options
{:host "0.0.0.0"
 :port 39998}

これで REPL が開いているポートが、 39998 に固定されます。 先程 docker-compose で port 39998 を開放しているので、 Docker コンテナの外部から REPL のポートへ接続できるようになります。

  1. lein repl を Docker コンテナ内で実行します。

  2. VSCode に拡張機能 Calva をインストールします。

  3. 左下のボタン nREPL → connect to a running server in your project → Leiningen → localhost:39998

  4. output.calva-repl という画面が出て来ます。

    clj::user=>
    (+ 1 1) ;; (ここで ctrl+enter で評価)
    2
    clj::user=>
    

    VSCode 上で、 Docker コンテナ内の REPL へ接続することができました。

なお、Calva そのものの詳細な使い方は、 https://calva.io/ を参考にして下さい。

integrant のセットアップ

integrant (https://github.com/weavejester/integrant) は Data-Driven Architecture で アプリケーションを構築するための Clojure および ClojureScript のマイクロフレームワークです。

integrant で重要となるファイルに、 システムの内部構成を記述したものである、config があります。

例えば、次のようなサーバの例を考えます。 登場人物は、環境変数、データベースのコネクションプール、そしてサーバです。 それぞれには依存関係があり、例えば、

  • データベースのコネクションプールには環境変数から得られるアドレスが必要となり、
  • サーバには環境変数と DB のコネクションプールの両方が必要になります。

これを、integrant の config 、 config.edn を用いて記述すると次のようになります。

;; config.edn
{:env {}
 :db-connector {:ref-env #ig/ref :env}
 :server {:ref-port 3000
          :ref-env #ig/ref :env
          :ref-db-connector #ig/ref :db-connector}}

環境変数 :env に対しては、特に必要要素がないので空辞書 {} が与えられています。 コネクションプール :db-connector に対しては、環境変数が必要となるので :ref-env として先に宣言した :env{:ref-env #ig/ref :env} として追加します。

この静的なシステム構成ファイルはプログラムコードとは独立であり、 設計と実装を分離 することができます。

さらに、例えばサーバの起動が不要な CLI コマンドを書く際に、 :server を省いた config を別に作ることで、 :db-connector をはじめとする他の実装をそのまま再利用することもできます。 この仕組みは Clean Architecture の他要素を変えずに UI や DB を置き換えられる、という考え方と合致しています。

開発時には、コード編集後に config を再読込みすることで、全体のシステムをアップデートすることができます。

以降では、integrant に慣れる、ということで 環境変数を読み込むというコンポーネントを作っていきます。

integrant と REPL

integrant を使うためには、 config を書き、読み込む機構を書く必要があります。 さらに、 REPL 開発と組み合わせるための機構も書いておくと便利です。 幸い、この部分は非常にシンプルに書くことができるので、ここですべて紹介します。

最初に integrant の config を作ります。 まだ何も作っていないので何も要素がありません。

;; resources/config.edn
{}

次に config を読み込むためのコードを作ります。

まずはコマンドで実行する用。 コマンド lein run によって 関数 -main が実行され、サーバが立ち上がります。

(ns picture-gallery.core
  (:gen-class)
  (:require [environ.core :refer [env]]
            [taoensso.timbre :as timbre]
            [clojure.java.io :as io]
            [integrant.core :as ig]))

(def config-file
  (if-let [config-file (env :config-file)]
    config-file
    "config.edn"))

(defn load-config [config]
  (-> config
      io/resource
      slurp
      ig/read-string
      (doto
       ig/load-namespaces)))

(defn -main
  [& args]
  (-> config-file
      load-config
      ig/init))

次に REPL で実行する用。 REPL を起動して、 (start) で実行、 (restart) で再読込して実行、 (stop) で停止します。

(ns user)

(defn dev
  "Load and switch to the 'dev' namespace"
  []
  (require 'dev)
  (in-ns 'dev)
  (println ":switch to the develop namespace")
  :loaded)
(ns dev
  (:require
   [picture-gallery.core :as pg-core]
   [integrant.repl :as igr]))

(defn start
  ([]
   (start pg-core/config-file))
  ([config-file]
   (igr/set-prep! (constantly (pg-core/load-config config-file)))
   (igr/prep)
   (igr/init)))

(defn stop []
  (igr/halt))

(defn restart []
  (igr/reset-all))

試しに REPL で実行してみましょう。

user> (dev)
:switch to the develop namespace
;; => :loaded
dev> (start)
;; => :initiated
dev> (restart)
:reloading ()
;; => :resumed
dev> (stop)
;; => :halted
dev> (in-ns 'user)
;; => #namespace[user]
user>

環境変数を読み込む

環境変数を読み込むための機構を作ります。

まずはコード。 具体的には、環境変数を読み込むライブラリ environ を用いて環境変数を読み込み、それを辞書として返す、ということを行っています。

この部分は入力になるので、 infrastructure に含められます。

(ns picture-gallery.infrastructure.env
  (:require [environ.core :refer [env]]
            [integrant.core :as ig]
            [orchestra.spec.test :as st]))

(defn decode-log-level [str-log-level]
  (condp = str-log-level
    "trace" :trace
    "debug" :debug
    "info" :info
    "warn" :warn
    "error" :error
    "fatal" :fatal
    "report" :report
    :info))

;; (start) で実行される部分
(defmethod ig/init-key ::env [_ _]
  (println "loading environment via environ")
  (let [running (env :env)
    log-level (decode-log-level (env :log-level))]
    (println "running in " running)
    (println "log-level " log-level)
    (when (.contains ["test" "dev"] running)
      (println "orchestra instrument is active")
      (st/instrument))
    {:running running
     :log-level log-level}))

;; (stop) で実行される部分
(defmethod ig/halt-key! ::env [_ _]
  nil)

次に config の更新。

;; resources/config.edn
{:picture-gallery.infrastructure.env/env {}}

実際に動かしてみましょう。

user> (dev)
:switch to the develop namespace
;; => :loaded
dev> (start)
loading environment via environ
running in  nil
log-level  :info
;; => :initiated
dev>

なんの環境変数も設定していないので、nil ばかり返ってきますね。

環境変数の設定を書いてみましょう。

環境変数は、1. export コマンドを使って宣言する 2. profiles.clj に記述する の手段を用いることができますが、今回は 2. を用います。

まず、 project.clj の profiles を次のように編集し、plugin を追加します。

;; project.clj
{;;...
 :plugins
 [;; 開発のためのプラグイン
  [lein-ancient "0.6.15"]
  ;; test coverage
  [lein-cloverage "1.2.2"]
  ;; environ in leiningen (leiningen と environ を組み合わせるために必要な plugin)
  [lein-environ "1.1.0"]]

 :profiles
  {:dev [:project/dev :profiles/dev]
   :repl {:prep-tasks ^:replace ["javac" "compile"]
          :repl-options {:init-ns user}}
   :project/dev {:source-paths ["dev/src"]
                 :resource-paths ["dev/resources"]}
   :profiles/dev {}
   :uberjar {:aot :all
             :jvm-opts ["-Dclojure.compiler.direct-linking=true"]}
   }

次に、 profiles.clj を用いて、 profiles/dev を上書きします。

;; profiles.clj
{:profiles/dev
 {:env
  {:env "dev"
   :log-level "info"}}}

これで準備は完了です。 REPL で実行してみましょう。 環境変数を更新したので、REPL を立ち上げ直して下さい。

user=> (dev)
:switch to dev
:loaded
dev=> (start)
loading environment via environ
running in  dev
log-level  :info
orchestra instrument is active
:initiated
dev=> exit
Bye for now!

次に、 lein run を用いて実行してみましょう。 with-profile で dev profile を指定します。

# せっかくなので、 log-level を変えてみます。
$ export LOG_LEVEL=error
$ lein with-profile dev run
Warning: environ value info for key :log-level has been overwritten with error
loading environment via environ
running in  dev
log-level  :error
orchestra instrument is active

環境変数を読み込む CLI の作成

今までは REPL ないしサーバ本体の実行コードで環境変数の読み込みができるようになっていました。 しかし、実用上、サーバ本体の実行コードではなく別の CLI コマンドで機能を実行したいケースが出てくると思います。

別の CLI コマンドで実行できるようにするためのコードを書くには、次の手順が必要です。

  1. 該当の config を記述する

       ;; resources/cmd/print_env/config.edn
       {:picture-gallery.infrastructure.env/env {}}
    
    1. 該当の config を読み込んで動かすロジックを書く

      ;; src/picture_gallery/cmd/print_env/core.clj
      (ns picture-gallery.cmd.print-env.core
        (:gen-class)
        (:require
         [picture-gallery.core :as pg-core]
         [integrant.core :as ig]))
      
      (defn -main
        [& args]
        (let [config-file  "cmd/print_env/config.edn"]
          (println "print environment variables")
          (-> config-file
              pg-core/load-config
              ig/init)))
      (-main)
      
  2. 実行スクリプトを書く

    # scripts/print_env.sh
    #!/usr/bin/env bash
    
    lein with-profile dev run -m picture-gallery.cmd.print-env.core/-main
    

以上です。 実際に動かしてみましょう。

$ chmod +x ./scripts/print_env.sh
$ ./scripts/print_env.sh
print environment variables
loading environment via environ
running in  dev
log-level  :info
orchestra instrument is active

動いていることが確認できますね。

付録

ここまでのディレクトリの確認

ここまででできたディレクトリ構造を再確認します。 src/picture_gallery 以下が Clean Architecture を踏襲したソースコード部分です。

.
├── CHANGELOG.md
├── LICENSE
├── README.md
├── containers           (Dockerize に利用しました)
│   ├── api-server
│   │   └── Dockerfile
│   └── postgres
│       └── Dockerfile
├── dev
│   ├── resources
│   └── src              (integrant と REPL を組み合わせるために利用しました)
│       ├── dev.clj
│       └── user.clj
├── doc
├── docker-compose.yml   (Dockerize に利用しました)
├── profiles.clj         (環境変数の設定に利用しました)
├── project.clj          (ライブラリの追加/環境変数の設定に利用しました)
├── resources
│   ├── cmd              (CLIのために利用しました)
│   │   └── print_env
│   │       └── config.edn
│   └── config.edn       (integrant の config に利用しました)
├── scripts              (CLI のために利用しました)
│   └── print_env.sh
├── src
│   └── picture_gallery
│       ├── cmd          (CLI のために利用しました)
│       │   └── print_env
│       │       └── core.clj
│       ├── core.clj     (integrant の実行のために利用しました)
│       ├── domain
│       ├── infrastructure
│       ├── interface
│       ├── usecase
│       └── utils
├── target
└── test
    └── picture_gallery

Docker コンテナ内で tmux を走らせる フロー

$ docker exec -it 5b6d5b45e8aa bash
root@5b6d5b45e8aa:/app# apt update
root@5b6d5b45e8aa:/app# apt install tmux
root@5b6d5b45e8aa:/app# tmux
# (以下 tmux コンソール)
# (Ctrl-b $ より session 名を repl に変更)
# PATH の設定
root@5b6d5b45e8aa:/app# export PATH=/usr/local/openjdk-11/bin:$PATH
root@5b6d5b45e8aa:/app# lein repl
user=> (dev)
dev=> (go)
:initialized
dev=>
# (Ctrl-b d より デタッチ)
[detached (from session repl)]
# (以降 コンテナ内のシェル)
# 環境から抜け出す (Ctrl-p Ctrl-q)
root:@xxx:/app#
$ docker exec -it 5b6d5b45e8aa bash
root:@xxx:/app# tmux a -t repl
# (repl session へ復帰)

Emacs で Clojure 開発を行う Tips

Emacs で Clojure 開発を行う際には Cider https://github.com/clojure-emacs/cider が有名であり、例えば Doom Emacs https://github.com/hlissner/doom-emacs と組み合わせて用いることができます。

Vim や Emacs を使ったことのある人であれば、 Doom Emacs を利用するほうが良いでしょう。

Emacs で Docker コンテナ内の REPL と接続するには、 M-x cider-connect より localhost:39998 で接続することができます。

1
1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?