コンテンツにスキップ

アプリケーション設定

設定の取得元は複数ありますが、解決後の見え方はひとつの型付き構造体です。Popcorn Web は最初のリクエストより前に TOML ファイル、環境変数、コマンドラインオプションを構造体へ バインドするため、不正な値は実行途中ではなく起動時に処理を止めます。

ここには実行時のリフレクションがありません。pw generate が登録の呼び出しを読み、 バインディングコードを事前に書き出します。仕組み全体が TinyGo でも使えるのはそのため であり、以下の規則がリフレクション方式より少し厳しいのも同じ理由です。

扱うのは、動いているアプリケーションが読む設定です。pw 自身が読む popcornweb.toml はここには出てきません。そちらはビルドツール設定一覧です。

フレームワーク自身のキーと既定値は アプリケーション設定一覧にあります。このページが扱うのは、 その下で動いている仕組みのほうです。

APP_ENV が実行環境を選択します。devstgprod、またはその他の小文字・数字・ -_ からなるトークンを受け付けます。不正なトークンは ParseConfig を失敗させます。 未設定または空の場合は dev が既定です。

Terminal window
APP_ENV=prod ./myapp

pw.Env() が解決済みのトークンを返し、pw.EnvDevelopmentpw.EnvStagingpw.EnvProduction がよく使う値を表します。

環境を選ぶと、プロジェクトローカルのファイル名が決まります。Popcorn Web は作業 ディレクトリ、次にその config/ ディレクトリの順で探索します。

  1. ./config.{APP_ENV}.toml
  2. ./config/config.{APP_ENV}.toml

ユーザおよびシステムの設定ディレクトリでは、環境非依存の config.toml を使います。 プロジェクトツリーでは事情が異なり、素の config.toml は読まれません。この非対称は 意図的です。すべての環境に効くファイルは、運用者の手元のマシンでは妥当でも、 「これはどの環境向けなのか」に答えられないリポジトリの中では誤解のもとになります。

--config-path は探索そのものを置き換えます。

Terminal window
./myapp --config-path ./deploy/staging.toml

手元のマシンから各ホストまでシークレットがたどる道筋の全体は シークレットの扱いにあります。この節はその 手元側の半分です。

本番用のファイルは秘密を持たず、名前だけを書きます。スキャフォールドされた config.prod.toml には dsn = "${DATABASE_URL}" とあり、変数を供給するのは デプロイ側の仕事です。では手元のマシンではその変数はどこから来るのか。シェルの プロファイルに書くと、開発者がプロジェクトを移っても付いてきてしまいます。そこで アプリケーションは起動時に、作業ディレクトリの dotenv ファイルを次の順で読みます。

  1. ./.env
  2. ./.env.local
  3. ./.env.{APP_ENV}
  4. ./.env.{APP_ENV}.local
  5. /run/secrets の下の各ファイル。コンテナランタイムがシークレットをマウントする 場所で、1 ファイルが 1 変数
  6. プロセスの環境変数

同じ名前は後のものが勝ちます。Vite や Next がこの一族を読むのと同じ順序です。 .env.stg.env.local を上書きします。環境固有のファイルはマシン全体のものより 具体的だからです。.env.stg.local はすべてを上書きします。シェルで export した 変数やプラットフォームが注入した変数はすべてのファイルを上書きします。 .local ファイルとシークレットのマウントから来た値は、キーの名前が何であれ起動時の サマリでマスクされます。共有ファイルの値はそのまま表示されます。 線を引く場所は「共有するもの」と「このマシンのもの」の間です。.env.env.{APP_ENV} はコミットされ、どのチェックアウトでも同じ値を持ちます。ふたつの .local ファイルは .gitignore が除外するもので、だから秘密はそこに置きます。 ファイルは独立したレイヤーではなく、環境変数レイヤーを埋めるもうひとつの手段です。 .env.dev.local に書いた名前とシェルで export した名前はアプリケーションには区別が つかず、違いが見えるのは起動時のサマリだけです。そこでは各値がどのファイルから来たかが 示されます。

pw init.env.example を書きます。選んだ機能が開発以外の環境で読む変数を、 値を空にして並べたものです。あわせて .gitignore.env.local.env.*.local を追加します。テンプレートを .env.local に、ひとつの環境だけの値なら .env.dev.local にコピーして値を埋めてください。テンプレートに値が書かれていれば、 pw doctor はどの環境でもエラーとして報告します。コミットされるファイルだからです。 .env.env.{APP_ENV} にある秘密は、追跡されている TOML の秘密と同じ扱いで 報告されます。

APP_ENV 自体はまずプロセスから、次に .env.env.local から読まれるので、 チェックアウトごとに環境を固定できます。ただし .env.{APP_ENV} とその .local には書けません。それらはトークンで選ばれたファイルなので、そこにある値は警告付きで 無視されます。

読み込みは、プロジェクトのディレクトリから設定を解決するすべてのプロセスで無条件に 行われます。pw doctor も含みます。切り忘れるスイッチはありません。ファイルシステムが ない場所、たとえば Cloudflare Workers のビルドではローダーにバインディングが環境として 渡されるので、ファイルは探されません。すべての変数をプラットフォームが注入する構成なら dotenv ファイルは不要で、なくても何も変わりません。

各キーには4つの経路があり、後のものが前を上書きします。

既定値 < TOML ファイル < 環境変数 < コマンドラインオプション

dotenv ファイルは環境変数の段に入り、プロセス自身の環境変数の下に置かれます。

3 つの名前はひとつの構造体フィールドから導かれます。フィールド名は snake_case に なり、prefix の下にネストします。その prefix はどこからも導出されません。登録が 渡すリテラルそのものです。

type AppConfig struct {
Mailer MailerConfig
}
type MailerConfig struct {
FromAddress string `default:"noreply@example.com"`
}
// 以下のキーの先頭セグメントは、この "app" です。型名を変えても何も動きませんが、
// この文字列を変えると3つの名前がまとめて動きます。
func RegisterConfig() { pw.RegisterConfig[AppConfig]("app") }

この呼び出しをどこに置くか、なぜ位置が問題になるかは後述の 独自の設定を追加するにあります。これがある状態で、 キーは app.mailer.from_address、TOML は [app.mailer]from_address = …、オプションは --app-mailer-from_address、環境変数は APP_MAILER_FROM_ADDRESS になります。階層を区切るドットはオプションでは ダッシュ、環境変数ではアンダースコアになりますが、キーの中のアンダースコアは そのまま残ります。

5 つのタグで結果を調整できます。

タグ 効果
default:"value" 他のどこからも値が来なかったときの値
key:"name" TOML や設定のキーを上書きする
opt:"long" / opt:"long,s" CLI オプションを上書きする。短縮形も指定可
env:"NAME" / env:"-" 環境変数名を明示する、または環境変数入力を無効にする
help:"text" usage やスキャフォールドに表示される説明

opt を上書きすると環境変数名も動きます。環境変数名はキーではなくロングオプション から導かれるためです。server.port--portPORT に応答するのはこの規則に よります。

さらに 3 つのタグは、名前ではなくキーの性質を記述します。

タグ 効果
secret:"mask" / "hide" / "show" 起動サマリでの値の見せ方
falsy:"value" このキーに依存する側から見て「未設定」とみなされる値
dependon:".sibling" 従う親キー。先頭のドットは、書かれている構造体からの相対を意味する

dependon はバインドを変えません。従属するキーも読まれ、適用されます。親が空の間、 起動サマリから省かれるだけです。無効な機能が7行ではなく1行で済むのはこのためです。 つまり、設定したのにサマリに見つからないキーは、綴りではなく親についての問いです。

タグの全一覧、duration の記法、テーブル配列の規則、ローダーが読む TOML のサブセット—— 宣言の全面はアプリケーション設定定義にあります。

アプリケーションの設定もフレームワークと同じ経路を通ります。構造体を宣言し、prefix を付けて登録し、リクエストの context から読みます。pw generate が登録呼び出しを バインディングコードに変換するため、設定を増やしても別のパーサーは増えません。

package handlers
import "github.com/shibukawa/popcornweb/pw"
type AppConfig struct {
EnvLabel string `default:"local" help:"environment name shown in the page badge"`
EnvLabelColor string `default:"#64748b" help:"CSS color of the environment badge"`
}
func RegisterConfig() { pw.RegisterConfig[AppConfig]("app") }
func main() {
handlers.RegisterConfig()
if err := pw.Run(context.Background(), handlers.Handlers()); err != nil {
log.Fatal(err)
}
}

この呼び出しをどこに置くかは重要です。生成された定義はパッケージの init で登録 されるため、バインディングの作成はすべての initあと、解析のでなければ なりません。ParseConfig のあとに登録すると panic します。また prefix は、 ジェネレータが読める文字列リテラルである必要があります。

規模の大きなアプリケーションでは領域ごとに自分の構造体を登録できます (プロジェクト構成を参照)。ただし prefix はひとつの名前空間を共有するので、appbillingsearch のように別々の 名前を付けてください。

app := pw.Config[AppConfig](r)

pw.Config はリクエスト context があるところならどこでも使え、リクエスト外では nil を渡せます。エラーは返しません。未解析の prefix は宣言された既定値を、未登録の 型はゼロ値を返します。設定を読むハンドラはすでにレスポンスの経路上にいて、そこで nil チェックを書いても、同じ「値がない」を数行あとへ先送りするだけだからです。

[app]
env_label = "development"
env_label_color = "#059669"
Terminal window
APP_ENV_LABEL=development ./myapp
./myapp --app-env_label=development

登録済みの prefix は —— フレームワークのものもアプリケーションのものも —— 自分自身を 出力できます。default の値が埋まり、help はコメントになります。

Terminal window
./myapp --generate-config toml > config.dev.toml
./myapp --generate-config env > .env.example

バイナリは実際の import から登録内容を報告するため、スキャフォールドはそのビルドに リンクされたパッケージと一致します。構造体を足してコマンドを再実行すれば、新しい キーが現れます。どちらの形式も書き出したあと終了し、サーバは起動しません。 カスタムコマンドを参照してください。

env 形式は変数の完全な一覧で、pw init が書くテンプレートより多くを含みます。 テンプレートが載せるのは、選んだ機能が環境から読む変数だけです。出力先は .env ではなく .env.example にしてください。.env は次の起動で読まれ、ひな形に並ぶ 既定値がすべて「設定された値」として扱われてしまいます。

解決済みの設定は起動時に一度だけ報告されます。端末ではツリー、それ以外では構造化 レコード1件です。おかげで「あの値は本当に効いたのか」に、ログを1行足さずに答えが 出ます。各エントリはその値がどこから来たのかを示し、secret タグが表示・マスク・ 非表示のどれになるかを決めます。形式は observability.boot_log が選びます。 設定サマリを参照してください。