👻

Biff の設定ファイルについて

2024/08/01に公開

Clojure web framework である Biff で最近ゴニョゴニョやっています。
設定ファイルに関してまとめて置きたかったのでメモ程度に書いていきます。

設定ファイルについてのみ説明しますので create project せずに書いていきます。

1. テストディレクトリ構成

以下のディレクトリ構成から始めます。

> tree . 
.
├── deps.edn
├── resources
└── src
    └── core.clj # 今はまだ空ファイル

:deps には Biff の依存だけ入れてください。

deps.edn
{:paths ["src" "resources"]
 :deps  {com.biffweb/biff #:git{:url "https://github.com/jacobobryant/biff"
                                :sha "146f2b1"
                                :tag "v1.8.10"}}}

REPL を起動します。

2. 設定ファイルと読み込み関数

設定ファイルは以下2つです。

  • config.edn : 通常の設定ファイル。設置場所は :paths で指定したディレクトリのいずれかです。
  • config.env : 機密情報などを保持するファイル。設置場所はプロジェクトディレクトリ直下です。

設定ファイルは biff/use-aero-config 関数を使って読み込みます。

src/core.clj
(ns core
  (:require [com.biffweb :as biff]))

(biff/use-aero-config {})  ;; 引数は後で説明

3. 設定ファイル読み込み

実際の設定ファイルを作って読み込みましょう。

> tree .
.
├── config.env # 新規作成
├── deps.edn
├── resources
│   └── config.edn # 新規作成
└── src
    └── core.clj

これで一度上記の src/core.clj を評価しましょう。下記の例外が発生してREPLがCloseされます。

Evaluating file: core.clj

;; (一部ログ略)

Secrets are missing. Make sure you have a config.env file in the current  directory, or set config via environment variables.
nREPL Connection was closed

原因を探るために use-aero-config 関数のソースを確認します。

https://github.com/jacobobryant/biff/blob/502a4f5414a82ab96c7fd6809aef142bf807c0e7/libs/config/src/com/biffweb/config.clj

86行目(2024/07/30現在)に、

  (when-not (or skip-validation
                  (and (secret :biff.middleware/cookie-secret)
                       (secret :biff/jwt-secret)))

という条件式があり、これに引っかかれば先ほどのエラーが発生することがわかりました。

よって一番かんたんな方法として ctx{:biff.config/skip-validation true} を追加すればとりあえずエラーは発生しないはずです。試してみましょう。

src/core.clj
(ns core
  (:require [com.biffweb :as biff]))

- (biff/use-aero-config {})  
+ (biff/use-aero-config {:biff.config/skip-validation true})  

REPLを立ち上げ直して評価してみます。

Evaluating file: core.clj
;; (一部ログ略)

{:biff.config/skip-validation true, :biff/secret #function[com.biffweb.config/use-aero-config/secret--9225]}

読み込めました👍

4. 読み込み関数のソースを読んで理解する

設定に関する公式ドキュメントはこちらです:Biff の設定ファイルに関するドキュメント

ただし、読んでもあまりピンと来ないのと、一部説明が抜けているので、configulation に関するソースを読みながら解説してみたいと思います。
https://github.com/jacobobryant/biff/blob/502a4f5414a82ab96c7fd6809aef142bf807c0e7/libs/config/src/com/biffweb/config.clj

63行目のget-env 関数が 機密情報を保持するconfig.env の読み込み、
75行目のuse-aero-config 関数がconfig.env情報もマージしながら config.edn を読み込む関数として定義されています。

4.1. get-env 関数

get-env 関数が、なかなか渋くて初見殺しと言いたくなる関数です🤪。

この関数内で、機密情報を書くことができる場所が実は3箇所用意されていて、同じキーワードで値が設定されている場合、優先順位が

  1. (System/getProperties) で取得できる値
  2. (System/getenv) で取得できる値
  3. config.env に記述されている値

の順であると定義されています。

1と2の違いをまとめると

特徴 (System/getenv) (System/getProperties)
データのソース オペレーティングシステムの環境変数 JVMシステムプロパティ
用途 環境変数(システム全体およびユーザーセッション) アプリケーション設定やシステム情報

さらに、JVMシステムプロパティに値をセットする際は必ずキーに biff.env.をつける必要があります。後ほど実際の値を入れてテストしてみましょう。

また、66行目の (slurp "config.env")config.env をプロジェクトディレクトリの直下に設定しなくては行けない理由です。ただし、先程の優先順位を利用すれば柔軟に対応はできそうです。

4.2. use-aero-config 関数

config.edn を読み込むuse-aero-config 関数を見ていきます。この関数は aero を利用してファイルを読み込んでいます。

77行目で profile の値を得て、aero の機能を使って ctx に設定をマージします。
82行目、(io/resource "config.edn") とあります。よってconfig.edn:path で指定されたディレクトリからサーチされることになります。

面白いのは83行目にある無名関数 secretと、その関数を ctxassoc しているところです。

        secret (fn [k]
                 (some-> (get ctx k) (.invoke)))
        ctx (assoc ctx :biff/secret secret)

無名関数 secretctx から k キーワードで値を取得し値があれば(.invoke)に渡します。つまり値は Javaオブジェクトということになります。どうしてこんなことをしているかというと、公式ドキュメント Configuration で説明されている通り、

This is done so that your secrets won't be exposed if you serialize your system map (e.g. by printing it to your logs).

ctx を出力しても値(つまり機密情報)がプリントされないようにするためです。出力したい場合は、

(defn hello [{:keys [biff/secret] :as ctx}]
  (println (secret :yoursecret/key))
  ...)

といった形にする必要があります。機密情報がもれないよういろんな工夫がされています。(おかげで頭がクラクラします😵‍💫)

5. 設定値を入れる

5.1. 通常の設定値を入れる

設定値は、biff/starter/resources/config.ednbiff/starter/resources/config.template.env を参考にするとよいです。この2つのファイルは create project するときに使われる設定ファイルテンプレートです。

https://github.com/jacobobryant/biff/blob/master/starter/resources/config.edn
https://github.com/jacobobryant/biff/blob/master/starter/resources/config.template.env

説明のために一部だけコピペして使ってみます。

resources/config.edn
{:biff/base-url #profile {:prod    #join ["https://" #biff/env DOMAIN]
                          :default #join ["http://localhost:" #ref [:biff/port]]}
 :biff/host     #or [#biff/env "HOST"
                     #profile {:dev     "0.0.0.0"
                               :default "localhost"}]
 :biff/port     #long #or [#biff/env "PORT" 8080]}
config.env
DOMAIN=example.com

config.edn#biff/env タグがついているのは config.env から読み込まれる仕組みです。

ではこれで src/core.clj を評価してみましょう。

src/core.clj
(ns core
  (:require [com.biffweb :as biff]))

(biff/use-aero-config {:biff.config/skip-validation true})  

:profileを渡していないので :default 扱いになります。
よってデフォルト設定が読み込まれているのがわかります。

; Evaluating file: core.clj
{:biff.config/skip-validation true,
 :biff/host "localhost",
 :biff/port 8080,
 :biff/base-url "http://localhost:8080",
 :biff/secret #function[com.biffweb.config/use-aero-config/secret--9225]}

では、:profile:prod で実行してみましょう

src/core.clj
(ns core
  (:require [com.biffweb :as biff]))

(biff/use-aero-config {:biff.config/skip-validation true
+                      :biff.config/profile         :prod})  

そうすると、:prod 設定が読み込まれます。
:prod として指定してあるとおり、DOMAINが config.env から読み込まれています。

; Evaluating file: core.clj
{:biff.config/skip-validation true,
 :biff.config/profile :prod,
 :biff/base-url "https://example.com",
 :biff/host "localhost",
 :biff/port 8080,
 :biff/secret #function[com.biffweb.config/use-aero-config/secret--9225]}

resources/config.edn で、#biff/env "HOST"#biff/env "PORT" がありますが、config.env ではこの2つの値を設定していませんので追加してみましょう。

config.env
DOMAIN=example.com
+ HOST="127.0.0.1"
+ PORT=8888

評価するとHOSTとPORTが読み込まれたことがわかります。

; Evaluating file: core.clj
{:biff.config/skip-validation true,
 :biff.config/profile :prod,
 :biff/base-url "https://example.com",
 :biff/host "127.0.0.1",
 :biff/port 8888,
 :biff/secret #function[com.biffweb.config/use-aero-config/secret--9225]}

5.2. 機密情報を挿入する

機密情報を挿入する時は以下の手順で行います

  1. config.edn#biff/secret タグを付けた変数を値に持つマップを作成
  2. config.env に 同変数名で値を追加

このような例になります。

resources/config.edn
{ :biff/base-url      #profile {:prod    #join ["https://" #biff/env DOMAIN]
                                :default #join ["http://localhost:" #ref [:biff/port]]}
  :biff/host          #or [#biff/env "HOST"
                          #profile {:dev     "0.0.0.0"
                                    :default "localhost"}]
  :biff/port          #long #or [#biff/env "PORT" 8080]
+ :mailersend/api-key #biff/secret MAILERSEND_API_KEY
}
config.env
DOMAIN=example.com
HOST="127.0.0.1"
PORT=8888
+ MAILERSEND_API_KEY=AAAAA-BBBBB-CCCCC-DDDDD

core.clj を評価してみましょう

; Evaluating file: core.clj
{:biff.config/skip-validation true,
 :biff.config/profile :prod,
 :biff/base-url "https://example.com",
 :biff/host "127.0.0.1",
 :biff/port 8888,
 :mailersend/api-key #function[com.biffweb.config/eval9203/fn--9205/fn--9207],
 :biff/secret #function[com.biffweb.config/use-aero-config/secret--9225]}

:mailersend/api-key が入りました!また、use-aero-config 関数で説明した通り、機密情報は出力されません。

出力したい時はたとえばこのように書くとよいでしょう。

(let [ctx (biff/use-aero-config {:biff.config/skip-validation true
                                 :biff.config/profile         :prod})
      {:keys [biff/secret]} ctx]
  (secret :mailersend/api-key))
;; "AAAAA-BBBBB-CCCCC-DDDDD"

さて、:biff.config/skip-validation true はデフォルト設定ではないのでそろそろ外したいと思います。そのためには 3. 設定ファイル読み込み で述べた通り :biff.middleware/cookie-secret:biff/jwt-secret を機密情報として追記する必要があります。ファイルを以下の様に修正してください。

resources/config.edn
{:biff/base-url                 #profile {:prod    #join ["https://" #biff/env DOMAIN]
                                          :default #join ["http://localhost:" #ref [:biff/port]]}
 :biff/host                     #or [#biff/env "HOST"
                                     #profile {:dev     "0.0.0.0"
                                               :default "localhost"}]
 :biff/port                     #long #or [#biff/env "PORT" 8080]
 :mailersend/api-key            #biff/secret MAILERSEND_API_KEY
+ :biff.middleware/cookie-secret #biff/secret COOKIE_SECRET
+ :biff/jwt-secret               #biff/secret JWT_SECRET
}
config.env
DOMAIN=example.com
HOST="127.0.0.1"
PORT=8888
MAILERSEND_API_KEY=AAAAA-BBBBB-CCCCC-DDDDD
+ COOKIE_SECRET=xxxxxxxx
+ JWT_SECRET=zzzzzzzz

では:biff.config/skip-validation trueを削除して評価してみましょう。

src/core.clj
(ns core
  (:require [com.biffweb :as biff]))

- (biff/use-aero-config {:biff.config/skip-validation true
-                        :biff.config/profile         :prod})  

+ (biff/use-aero-config {:biff.config/profile :prod})  
; Evaluating file: core.clj
{:biff.config/profile :prod,
 :biff/base-url "https://example.com",
 :biff/host "127.0.0.1",
 :biff/port 8888,
 :mailersend/api-key #function[com.biffweb.config/eval9203/fn--9205/fn--9207],
 :biff.middleware/cookie-secret #function[com.biffweb.config/eval9203/fn--9205/fn--9207],
 :biff/jwt-secret #function[com.biffweb.config/eval9203/fn--9205/fn--9207],
 :biff/secret #function[com.biffweb.config/use-aero-config/secret--9225]}

できました👍

5.3. 機密情報を config.env 以外に記述

4.1. get-env 関数 で述べた通り機密情報は3ヶ所に書くことができます。優位性の順番は、1位 JVMシステムプロパティにセット、2位 OSの環境変数にセットする、3位 config.env です。

JVMシステムプロパティにセット

優先順位1位のJVMシステムプロパティに値をセットするには以下の手順になります。

  1. config.edn に キーと秘密情報用変数をセット
  2. System/setProperty 関数で同変数名に値をセット。
    • ただし、setPropertyする時にbiff.env.という接頭辞を変数名に付けなくては行けない

例:

resources/config.edn
{:biff/base-url                 #profile {:prod    #join ["https://" #biff/env DOMAIN]
                                          :default #join ["http://localhost:" #ref [:biff/port]]}
 :biff/host                     #or [#biff/env "HOST"
                                     #profile {:dev     "0.0.0.0"
                                               :default "localhost"}]
 :biff/port                     #long #or [#biff/env "PORT" 8080]
 :mailersend/api-key            #biff/secret MAILERSEND_API_KEY
 :biff.middleware/cookie-secret #biff/secret COOKIE_SECRET
 :biff/jwt-secret               #biff/secret JWT_SECRET
+ :info/myemail                  #biff/env EMAIL
+ :mysecret/secret-spell         #biff/secret SECRET-KEY}
src/core.clj
(ns core
  (:require [com.biffweb :as biff]))

+ (System/setProperty "biff.env.EMAIL" "me@example.com")
+ (System/setProperty "biff.env.SECRET-KEY" "OPEN-SESAME")
+ (System/setProperty "biff.env.DOMAIN" "GOOGLE.COM") ; 優先順位が高いことを確認

(biff/use-aero-config {:biff.config/profile :prod})  

評価すると期待通り情報が追加されたことがわかります。また、優先順位が高いことも確認できました。

; Evaluating file: core.clj
{:biff.config/profile :prod,
 :mailersend/api-key #function[com.biffweb.config/eval9203/fn--9205/fn--9207],
 :biff/port 8888,
 :info/myemail "me@example.com",
 :mysecret/secret-spell #function[com.biffweb.config/eval9203/fn--9205/fn--9207],
 :biff/secret #function[com.biffweb.config/use-aero-config/secret--9225],
 :biff/base-url "https://GOOGLE.COM", ;; ← DOMEIN が優先度高が得られたぜ
 :biff/jwt-secret #function[com.biffweb.config/eval9203/fn--9205/fn--9207],
 :biff.middleware/cookie-secret #function[com.biffweb.config/eval9203/fn--9205/fn--9207],
 :biff/host "127.0.0.1"}

OSの環境変数にセット

環境変数の値を使う

まずは環境変数の値をそのまま使う方法を紹介します。たとえばマシンの言語を取得したい場合、LANG で得られますので

> echo $LANG
ja_JP.UTF-8

これをそのまま config.edn に使います。

resources/config.edn
{:biff/base-url                 #profile {:prod    #join ["https://" #biff/env DOMAIN]
                                          :default #join ["http://localhost:" #ref [:biff/port]]}
 :biff/host                     #or [#biff/env "HOST"
                                     #profile {:dev     "0.0.0.0"
                                               :default "localhost"}]
 :biff/port                     #long #or [#biff/env "PORT" 8080]
 :mailersend/api-key            #biff/secret MAILERSEND_API_KEY
 :biff.middleware/cookie-secret #biff/secret COOKIE_SECRET
 :biff/jwt-secret               #biff/secret JWT_SECRET
 :info/myemail                  #biff/env EMAIL
 :mysecret/secret-spell         #biff/secret SECRET-KEY
+ :system/lang                   #biff/env "LANG"}

core.clj を評価するとLANGを取得できます。

; Evaluating file: core.clj
{:biff.config/profile :prod,
 :mailersend/api-key #function[com.biffweb.config/eval9203/fn--9205/fn--9207],
 :biff/port 8888,
 :info/myemail "me@example.com",
 :mysecret/secret-spell #function[com.biffweb.config/eval9203/fn--9205/fn--9207],
 :biff/secret #function[com.biffweb.config/use-aero-config/secret--9225],
 :biff/base-url "https://GOOGLE.COM",
 :biff/jwt-secret #function[com.biffweb.config/eval9203/fn--9205/fn--9207],
 :biff.middleware/cookie-secret #function[com.biffweb.config/eval9203/fn--9205/fn--9207],
 :biff/host "127.0.0.1",
 :system/lang "ja_JP.UTF-8"} ;; ←ゲットだぜ
環境変数にセットした値を使う

export などして環境変数にセットした値を使うこともできます。
簡単に例を示すためにエディタ(私の場合は VSCode です)を立ち上げるときに export して値をセットしてみます。

export SHINSEI="TARO"; code . 

次にテストとして VSCode でターミナルで echo してみましょう。

config.edn にキーと変数名を追加します

resources/config.edn
{:biff/base-url                 #profile {:prod    #join ["https://" #biff/env DOMAIN]
                                          :default #join ["http://localhost:" #ref [:biff/port]]}
 :biff/host                     #or [#biff/env "HOST"
                                     #profile {:dev     "0.0.0.0"
                                               :default "localhost"}]
 :biff/port                     #long #or [#biff/env "PORT" 8080]
 :mailersend/api-key            #biff/secret MAILERSEND_API_KEY
 :biff.middleware/cookie-secret #biff/secret COOKIE_SECRET
 :biff/jwt-secret               #biff/secret JWT_SECRET
 :info/myemail                  #biff/env EMAIL
 :mysecret/secret-spell         #biff/secret SECRET-KEY
 :system/lang                   #biff/env "LANG"
+:system/shinsei                #biff/env "SHINSEI"}

REPL を立ち上げて core.clj を評価すると値を得ることができます。

; Evaluating file: core.clj
{:biff.config/profile :prod,
 :mailersend/api-key #function[com.biffweb.config/eval9203/fn--9205/fn--9207],
 :biff/port 8888,
 :info/myemail "me@example.com",
 :mysecret/secret-spell #function[com.biffweb.config/eval9203/fn--9205/fn--9207],
 :biff/secret #function[com.biffweb.config/use-aero-config/secret--9225],
 :biff/base-url "https://GOOGLE.COM",
 :biff/jwt-secret #function[com.biffweb.config/eval9203/fn--9205/fn--9207],
 :biff.middleware/cookie-secret #function[com.biffweb.config/eval9203/fn--9205/fn--9207],
 :biff/host "127.0.0.1",
 :system/lang "ja_JP.UTF-8"
 :system/shinsei "TARO"} ;; ←ゲットだぜ 🔥

以上です。いやー疲れた!でもスッキリしました!

6. 参照

Discussion