複数環境での設定管理を頭痛なしで実現する方法

アプリケーションは開発、ステージング、本番の各環境で動作します。開発環境ではテストデータを使ったローカルデータベースが必要です。ステージング環境では本番のミラーに接続しますが、APIキーは異なります。本番環境では、ごく一部の人間だけが知るべき実際のインフラと認証情報を使います。

単純な方法は、環境ごとに個別の設定ファイルを作成することです。開発用に config.dev.yaml、ステージング用に config.staging.yaml、本番用に config.prod.yaml。各ファイルにはその環境の完全な設定が含まれます。

この方法は、新しいパラメータを追加するたびに問題が発生します。開発ファイルを更新し、次にステージング、そして本番。1つでも更新を忘れると、その環境は静かに壊れます。環境が5つあれば、5つのファイルを毎回更新しなければなりません。重複がバグの原因となり、解決策にはなりません。

本当の問題:ほとんどの設定は同じ

誰も声に出して言いませんが、設定の大部分は環境間で同一です。テーブル名、データ構造、内部エンドポイントURL、タイムアウト値、リトライロジック、技術パラメータは、開発と本番でほとんど変わりません。実際に異なるのは、ホスト名、ポート、ユーザー名、パスワード、APIキーといったごく一部の値だけです。

環境ごとに完全な設定ファイルを保存すると、変化する10%を上書きするために、同じままの90%を重複させることになります。構造変更のたびにすべてのファイルを修正する必要があり、1つ更新を忘れると、環境が異なる動作をしたり、完全に失敗したりします。

よりクリーンなアプローチ:テンプレートとオーバーレイ

すべてを重複させる代わりに、設定を2つの層に分割します。テンプレートと環境固有のオーバーレイです。

テンプレートには、デフォルト値またはプレースホルダーを含む完全な設定構造が含まれます。これはアプリケーションが期待する設定の信頼できる唯一の情報源(シングルソースオブトゥルース)です。新しいパラメータを追加するときに1回変更するだけで、すべての環境がその変更を継承します。

オーバーレイには、特定の環境で異なる値のみが含まれます。小さなファイルで、レビューが容易で、ミスが起こりにくいです。

実際の例を示します。

次の図は、テンプレートとオーバーレイがどのように結合されて各環境の最終設定を生成するかを示しています。

flowchart TD T[Template Config<br/>database.host = {{DB_HOST}}<br/>database.port = 5432<br/>database.name = myapp<br/>database.timeout = 30<br/>database.pool.size = 10] --> M1[Merge Engine] O1[Dev Overlay<br/>DB_HOST = localhost] --> M1 M1 --> F1[Final Dev Config<br/>host: localhost<br/>port: 5432<br/>name: myapp<br/>timeout: 30<br/>pool.size: 10] T --> M2[Merge Engine] O2[Staging Overlay<br/>DB_HOST = staging.db.internal<br/>DB_POOL_SIZE = 20] --> M2 M2 --> F2[Final Staging Config<br/>host: staging.db.internal<br/>port: 5432<br/>name: myapp<br/>timeout: 30<br/>pool.size: 20] T --> M3[Merge Engine] O3[Production Overlay<br/>DB_HOST = prod.db.internal<br/>DB_USER = prod_admin<br/>DB_PASSWORD = {{VAULT_REF}}] --> M3 M3 --> F3[Final Prod Config<br/>host: prod.db.internal<br/>port: 5432<br/>name: myapp<br/>timeout: 30<br/>pool.size: 10<br/>user: prod_admin<br/>password: from vault]

テンプレートファイルは次のようになります。

database.host = {{DB_HOST}}
database.port = 5432
database.name = myapp
database.timeout = 30
database.pool.size = 10

開発用オーバーレイ:

DB_HOST = localhost

ステージング用オーバーレイ:

DB_HOST = staging.db.internal
DB_POOL_SIZE = 20

本番用オーバーレイ:

DB_HOST = prod.db.internal
DB_USER = prod_admin
DB_PASSWORD = {{VAULT_REF}}

デプロイ時に、システムはテンプレートを読み込み、対象環境のオーバーレイを適用して最終設定を生成します。オーバーレイに存在しない値はテンプレートのデフォルト値が使われます。パスワードなどの機密値は、オーバーレイファイル自体ではなく、ボールトやシークレットマネージャーから取得します。

なぜこの方法が優れているのか

構造変更は1箇所で済む。 新しいパラメータを追加するときは、テンプレートを1回更新するだけです。すべての環境が自動的に変更を反映します。5つのファイルを探し回って同じ編集をする必要はもうありません。

ミスの発生範囲が狭い。 オーバーレイファイルには変更される値だけが含まれます。ホスト名のタイポは、200行の設定ファイルに埋もれるより、3行のファイルの方が見つけやすいです。

自然なアクセス制御。 本番用オーバーレイには環境固有の値だけが含まれますが、その中には認証情報や内部ホスト名が含まれます。オーバーレイをアクセス制限付きのリポジトリに保存できます。開発者はテンプレートと開発用オーバーレイを使って作業でき、本番の認証情報を見る必要はありません。新しいチームメンバーは開発用オーバーレイだけでコーディングを始められ、本番の秘密情報にアクセスする必要はありません。

バージョン管理での差分が明確。 テンプレートを変更するプルリクエストをレビューするとき、その変更が全環境に影響することがわかります。オーバーレイの変更をレビューするときは、1つの環境だけに影響することがわかります。変更の意図がファイルを見るだけで明確です。

さらに進む:環境階層

一部のチームはこのパターンをさらに発展させ、階層化オーバーレイを採用しています。環境ごとに1つのオーバーレイではなく、複数の層を積み重ねます。

  • グローバルオーバーレイ:すべての環境に適用される値。
  • リージョンオーバーレイ:データセンター固有の設定。
  • ローカルオーバーレイ:単一インスタンス用。

システムはこれらの層を順にマージします。より具体的な層の値が、より一般的な層の値を上書きします。これは、アプリケーションが複数のリージョンで動作し、ほとんどの設定は同一だが、DNSサーバー、タイムゾーン設定、規制コンプライアンスフラグなどに小さな地域差がある場合に便利です。

例えば、グローバルオーバーレイで timeout = 30 を設定します。アジアリージョンのオーバーレイは、レイテンシが高いため timeout = 45 に上書きします。東京インスタンスのオーバーレイは timezone = Asia/Tokyo を設定します。東京インスタンスの最終設定は3つの層をすべて組み合わせ、東京固有の値が優先されます。

オーバーレイに関する注意点

オーバーレイはバリデーションの代わりにはなりません。最終的にマージされた設定は、使用前にスキーマチェックが必要です。オーバーレイの値のタイポは、マージ後に初めて明らかになります。誰かが DB_HOST = db.prod,internaldb.prod.internal の代わりに書いた場合、テンプレートは壊れた設定を喜んで生成します。個々のファイルだけでなく、マージ結果を検証してください。

実践的なチェックリスト

このパターンを採用する前に、以下の点を確認してください。

  • デプロイツールはテンプレートとオーバーレイファイルをマージできますか?ほとんどの設定管理ツールはこれをネイティブにサポートしています。そうでない場合、プレースホルダーを置換するシンプルなスクリプトで十分です。
  • オーバーレイは適切なアクセス制御で保存されていますか?本番用オーバーレイはアクセス制限が必要です。開発用オーバーレイは公開可能です。
  • マージされた設定のバリデーションはありますか?パイプラインにスキーマチェックまたはドライランステップを追加して、エラーが環境に到達する前にキャッチしてください。
  • テンプレートは信頼できる唯一の情報源ですか?誰かがテンプレートを迂回してオーバーレイの値を直接変更できる場合、パターンは機能しません。レビュープロセスまたは自動化でこれを強制してください。

まとめ

環境ごとに設定ファイルを重複させるのはやめましょう。変わらないものと変わるものを分離してください。共通構造は1つのテンプレートに、環境固有の値は小さなオーバーレイファイルに保持します。デプロイはより予測可能になり、レビューはより速くなり、5つのファイルのうち1つを更新し忘れてステージングを壊すこともなくなります。