設定が一ファイルでない理由とマージの規則
一言でいうと
Backstageの設定は、app-config.yaml1枚ではなく、複数枚を順番に積み重ねた結果です。マッピングは深くマージされ、リストはまるごと置き換えられ、あとに来るファイルが勝ちます。このルール1つを正確に知っていれば、「なぜ自分が直した値が効かないのか」の大半が解決します。
なぜ必要なのか
同じコードが、4か所で動きます。開発者のノートPC、CI、ステージング、本番です。ところが、この4つは、ほぼすべての値が違っている必要があります。
- アドレスが違います。ノートPCは
localhost:3000、本番は会社のドメインです。 - データベースが違います。ノートPCはファイル1つのSQLite、本番はPostgreSQLです。
- 認証が違います。ノートPCはゲストログインで十分ですが、本番は会社のIDプロバイダーを接続する必要があります。
- ドキュメントのビルド方式が違います。ノートPCはポータルが直接ビルドしてもよいですが、本番ではCIがビルドしてアップロードしたものを配信します。
値を1つのファイルにまとめておくと、誰かが自分の環境に合わせて直してコミットするたびに、ほかの人の環境が壊れます。しかも、トークンやパスワードは、そもそもファイルに置けません。そのため、Backstageは、設定を複数のファイルに分け、順番に重ねて読む方式を選びました。
どう動くのか
ファイル複数枚が順番に積み重なる
慣例として使うファイルは、3つです。
| ファイル | どこに置くか | 何を入れるか |
|---|---|---|
app-config.yaml |
リポジトリにコミットします | すべての環境に共通のデフォルト値 |
app-config.local.yaml |
個人のノートPC。gitからは除外します | その人だけの上書き |
app-config.production.yaml |
コンテナイメージに一緒に載せます | 本番でだけ違う値 |
起動するときに--configで並べた順序で読み、あとに来たものが前のものを上書きします。ファイル名が特別なのではなく、順序がすべてです。
マージのルールは、たった2行
매핑(map) : 키 단위로 깊게 합친다. 뒤에 없는 키는 앞의 값이 그대로 남는다
리스트(list): 합치지 않는다. 뒤에 있으면 통째로 교체된다
リストのルールが、事故の原因です。基本のファイルにcatalog.locationsを3件書いておき、ローカルのファイルに1件だけ書くと、結果は4件ではなく1件です。残りの2件は静かに消え、警告は何も出ません。
そして、マッピング側にも落とし穴が1つあります。値の種類が変わると、深くマージする対象がないので、そのまま置き換えられます。connectionが基本のファイルでは文字列1つだったのに、オーバーライドでマッピングになると、文字列は消えて、マッピングだけが残ります。
秘密情報はファイルではなく、環境から来る
値の位置に${GITHUB_TOKEN}のように書くと、起動するときに環境変数で置換されます。この形式を使う理由は、便宜ではなく、設定ファイルがリポジトリにコミットされ、コンテナイメージに焼き込まれるからです。値を直接書いた瞬間、その秘密情報はgitの履歴とイメージのレイヤーに、永遠に残ります。
逆方向もあります。APP_CONFIG_で始まる環境変数は、設定の特定のパス1か所を上書きします。パスのドットをアンダースコアに変えた名前を使います。
APP_CONFIG_app_baseUrl=https://portal.example.com → app.baseUrl 을 덮는다
APP_CONFIG_backend_listen_port=7007 → backend.listen.port 를 덮는다
イメージを焼き直さずに、デプロイのマニフェストだけを直して値を変えられるので、Kubernetesに載せるときに特に重宝します。
何がブラウザーまで届くのか
設定値には、可視性(visibility)があります。デフォルトはバックエンド専用で、スキーマでfrontendと表示した値だけが、フロントエンドのバンドルに入ります。この区別がなければ、integrations.github[0].tokenのような値が、ブラウザーのソースにそのまま載ります。試験で「設定に書いた値は、すべてフロントエンドから読めるか」と問われたら、答えはいいえです。
よく使うセクション
| キー | 役割 |
|---|---|
app · organization |
ブラウザーが使うアドレスと名前 |
backend |
リッスンのアドレスとポート、CORS、データベース |
integrations |
GitHub・GitLabのようなSCMの認証情報 |
proxy |
ブラウザーの代わりに、バックエンドが外部APIを呼び出す通り道 |
catalog |
許可するkind(rules)、手で書く入口(locations)、ディスカバリー(providers) |
auth |
ログインプロバイダーと身元の決定 |
techdocs |
ドキュメントを誰がビルドし、どこに置くか |
proxyがなぜ別にあるのかが、試験のポイントです。フロントエンドが外部APIを直接呼び出すと、CORSに止められ、止められなくても、認証情報をブラウザーに置く必要があります。そのため、バックエンドが代わりに呼び出し、フロントエンドは、自分のバックエンドのパスだけを呼び出します。
catalog.providersのscheduleも、注目すべき箇所です。周期を短くすると鮮度は上がりますが、SCMのAPIの上限に引っかかり、長くすると、人々がポータルの情報を信じなくなります。
現場での姿
著者のホームラボのクラスターで、まったく同じ性格の事故が2回起きました。
1つ目は、containerdでした。/etc/containerd/conf.d/の下のドロップインファイルが複数、同じプラグインに触れると、フィールドが合わさるのではなく、名前順で最後のファイルが、そのプラグインの設定をまるごと持っていきます。前のファイルが書いておいた値は残らず、残らなかった場所は、前のファイルの値ではなく、デフォルトに落ちます。「1行だけ追加」するつもりでファイルを1つ置いたら、ノードのランタイム設定がすべてデフォルトになりました。Backstageのリスト置換のルールは、これと同じ性格です。
2つ目は、ゲートウェイでした。HTTPSリダイレクトを有効にすると、証明書の更新が止まり、/.well-known/acme-challenge/をより具体的に書いても、効果がありませんでした。Ciliumは、パスの具体性を優先しないからです。ここで得た感覚が、設定の階層にもそのまま通用します。より詳しく書いたから勝つ、という理屈は通用しません。勝つのは順序です。
そして、このクラスターで繰り返し得た教訓、つまり「状態がReadyであることと、実際に動作することは別の命題」という点も、設定にそのまま当てはまります。ポータルが起動しているからといって、自分が意図した値で起動しているわけではありません。マージの結果を目で確認する習慣が必要です。
次のラボですること
/root/cba-config/に、基本設定、ローカルのオーバーライド、本番設定の3枚を、自分で書きます。そのあと、2枚を重ねたときのマージ結果を手で予測して書き、採点ツールが同じルールで再計算した値と照らし合わせます。最後に、本番設定をConfigMapとしてクラスターに作成し、APP_CONFIG_環境変数で1行を上書きしてみます。