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
を読み込む関数として定義されています。
get-env
関数
4.1. 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
をプロジェクトディレクトリの直下に設定しなくては行けない理由です。ただし、先程の優先順位を利用すれば柔軟に対応はできそうです。
use-aero-config
関数
4.2. 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]}
できました👍
config.env
以外に記述
5.3. 機密情報を 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