Biff の設定ファイルについて
Clojure web framework である Biff で最近ゴニョゴニョやっています。
設定ファイルに関してまとめて置きたかったのでメモ程度に書いていきます。
設定ファイルについてのみ説明しますので create project せずに書いていきます。
1. テストディレクトリ構成
以下のディレクトリ構成から始めます。
> tree .
.
├── deps.edn
├── resources
└── src
└── core.clj # 今はまだ空ファイル
:deps には Biff の依存だけ入れてください。
{: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 関数を使って読み込みます。
(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 関数のソースを確認します。
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} を追加すればとりあえずエラーは発生しないはずです。試してみましょう。
(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 に関するソースを読みながら解説してみたいと思います。
63行目のget-env 関数が 機密情報を保持するconfig.env の読み込み、
75行目のuse-aero-config 関数がconfig.env情報もマージしながら config.edn を読み込む関数として定義されています。
4.1. get-env 関数
get-env 関数が、なかなか渋くて初見殺しと言いたくなる関数です🤪。
この関数内で、機密情報を書くことができる場所が実は3箇所用意されていて、同じキーワードで値が設定されている場合、優先順位が
-
(System/getProperties)で取得できる値 -
(System/getenv)で取得できる値 -
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と、その関数を ctx に assoc しているところです。
secret (fn [k]
(some-> (get ctx k) (.invoke)))
ctx (assoc ctx :biff/secret secret)
無名関数 secret は ctx から 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.edn や biff/starter/resources/config.template.env を参考にするとよいです。この2つのファイルは create project するときに使われる設定ファイルテンプレートです。
説明のために一部だけコピペして使ってみます。
{: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]}
DOMAIN=example.com
config.edn で #biff/env タグがついているのは config.env から読み込まれる仕組みです。
ではこれで 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 で実行してみましょう
(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つの値を設定していませんので追加してみましょう。
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. 機密情報を挿入する
機密情報を挿入する時は以下の手順で行います
-
config.ednに#biff/secretタグを付けた変数を値に持つマップを作成 -
config.envに 同変数名で値を追加
このような例になります。
{ :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
}
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 を機密情報として追記する必要があります。ファイルを以下の様に修正してください。
{: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
}
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を削除して評価してみましょう。
(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システムプロパティに値をセットするには以下の手順になります。
-
config.ednに キーと秘密情報用変数をセット -
System/setProperty関数で同変数名に値をセット。- ただし、setPropertyする時に
biff.env.という接頭辞を変数名に付けなくては行けない
- ただし、setPropertyする時に
例:
{: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}
(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 に使います。
{: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 にキーと変数名を追加します
{: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. 参照
- Configuration | Biff - https://biffweb.com/docs/reference/config/
- biff/libs/config/src/com/biffweb/config.clj at master · jacobobryant/biff - https://github.com/jacobobryant/biff/blob/master/libs/config/src/com/biffweb/config.clj
- juxt/aero: A small library for explicit, intentful configuration. - https://github.com/juxt/aero?tab=readme-ov-file#tag-literals
Discussion