開発・ステージング・本番のための環境設定パターン
開発・ステージング・本番で URL、キー、機能フラグをコード外に保ち、Web・バックエンド・モバイルで安全に運用するための環境設定パターン。

なぜハードコーディングされた設定が問題を引き起こし続けるのか
ハードコーディングされた設定は最初は手早くて便利に感じます。しかし、ステージング環境が必要になったり、別の API を使いたくなったり、機能の切り替えが必要になったとき、「簡単な」変更がリリースリスクに変わります。対策は単純で、環境ごとの値をソースファイルから取り出し、予測可能な仕組みに入れることです。
よくあるトラブルの原因はすぐに見つかります。
- アプリに埋め込まれた API のベース URL(テスト中に本番を呼んだり、リリース後に開発環境を呼んだりする)
- リポジトリにコミットされた API キー(漏洩、想定外の請求、緊急ローテーション)
- 定数として書かれた機能トグル(オフにするためにコードをデプロイしなければならない)
- ハードコーディングされた分析・エラーレポートの ID(データが間違った先に流れる)
「本番のために変えればいい」は、直前の編集を常態化させます。そうした編集はレビューやテスト、再現性をすり抜けがちです。ある人が URL を変え、別の人がキーを変えると、そのビルドに「どの設定が含まれていたか」を答えられなくなります。
よくある状況:モバイルの新しいバージョンをステージング向けにビルドしたら、リリース直前に誰かが URL を本番に切り替えます。翌日バックエンドが再び変わり、ロールバックが必要になる。URL がハードコーディングされていると、ロールバックは別のアプリ更新を意味します。ユーザーは待たされ、サポートチケットが増えます。
ここでの目標は、Web、Go バックエンド、Flutter モバイルで共通して使える単純な仕組みです。
- コードに置くべきものと設定に置くべきものの明確なルール
- dev、staging、prod に対する安全なデフォルト
- フルリビルドなしで切り替えられる機能スイッチ
- リポジトリ外で扱うシークレットとローテーション余地
dev、staging、prod で本当に変わるもの
Dev、staging、prod は同じアプリが三つの場所で動いているように感じられるべきです。目的は「値を変えること」であって「動作を変えること」ではありません。
変えるべきものは、アプリがどこで動くか、誰が使うかに結びつくもの:ベース URL やホスト名、認証情報、サンドボックスか本番の統合、ログレベルや本番での厳しいセキュリティ設定などの安全制御です。
変わってはいけないのはロジックとパーツ間の契約です。API ルート、リクエスト/レスポンスの形、機能名、コアのビジネスルールは環境で変わってはいけません。ステージングが本番と異なる挙動を示すと、信頼できるリハーサルになりません。
「新しい環境」か「新しい設定値」かの実用的なルール:分離されたシステム(別データ、別アクセス、別リスク)が必要なときだけ新しい環境を作る。エンドポイントや数値を変えたいだけなら、設定値を追加するだけで十分です。
例:新しい検索プロバイダーをテストしたい場合。少人数に安全に有効化できるなら、ステージングは一つのまま機能フラグを追加する。別 DB と厳しいアクセス制御が必要なら、新しい環境を検討します。
どこでも使える実践的な設定モデル
良いセットアップは一つのことをうまくやります:誤って dev URL、テストキー、未完成の機能を出荷してしまうことを難しくすること。
すべてのアプリ(Web、バックエンド、モバイル)に同じ三層を使いましょう。
- デフォルト:ほとんどの場所で動く安全な値。
- 環境ごとのオーバーライド:dev、staging、prod で変わるもの。
- シークレット:リポジトリに決して置かない機微な値。
混乱を避けるには、各アプリで一つの真実のソースを選び、それに従ってください。例えば、バックエンドは起動時に環境変数を読む、Web はビルド時変数か小さなランタイム設定ファイルを読む、モバイルはビルド時に選ぶ小さな環境ファイルを読む、といった具合です。アプリ内部で一貫性を保つことが、全体で同じ仕組みにこだわるより重要です。
単純で再利用可能なスキームは次のようになります。
- デフォルト:コード内の非機微な定数(タイムアウト、ページサイズ、リトライ回数)。
- オーバーライド:環境固有のファイルや環境変数(API ベース URL、分析の ON/OFF)。
- シークレット:シークレットストアに置き、デプロイ/ビルド時に注入(JWT シークレット、DB パスワード、外部 API キー)。
誰でもわかる命名
各設定項目には「何か」「どこに適用されるか」「型は何か」が分かる明確な名前を付けましょう。
実用的な慣例:
- アプリでプレフィックス:WEB_, API_, MOBILE_
- ALL_CAPS とアンダースコアで区切る
- 目的別にグループ化:API_BASE_URL、AUTH_JWT_SECRET、FEATURES_NEW_CHECKOUT
- 真偽値は明示的に:FEATURES_SEARCH_ENABLED=true
こうすれば「BASE_URL」が React 用か Go サービス用か Flutter 用かを誰も推測する必要がなくなります。
手順:ハードコーディングしない Web アプリ(React)の設定
React のコードはユーザーのブラウザで動くため、配布したものは誰でも読めます。目標は簡単:シークレットはサーバに置き、ブラウザには API ベース URL、アプリ名、非機微な機能トグルなどの「安全な」設定だけを渡すこと。
1) ビルド時とランタイムの区別を決める
ビルド時設定はバンドルを作るときに注入されます。めったに変わらない、安全に公開できる値に向きます。
ランタイム設定はアプリ起動時に読み込まれます(例えばアプリと一緒に配信する小さな JSON ファイルや注入されたグローバル)。デプロイ後に切り替えたい値(環境間で API ベース URL を切り替えるなど)はランタイムにする方が良いです。
簡単なルール:変更に UI の再ビルドが不要ならランタイムにする。
2) API ベース URL をコミットせずに保存する
開発者用のローカルファイルは(コミットせず)用意し、実際の値はデプロイパイプラインで設定します。
- ローカル開発:
.env.local(gitignore)にVITE_API_BASE_URL=http://localhost:8080などを置く - CI/CD:ビルドジョブで
VITE_API_BASE_URLを環境変数として設定するか、デプロイ時に生成するランタイム設定ファイルに入れる
ランタイムの例(アプリと同じ場所で配信):
{ "apiBaseUrl": "https://api.staging.example.com", "features": { "newCheckout": false } }
起動時に一度だけ読み込み、アプリの一箇所で管理します。
export async function loadConfig() {
const res = await fetch('/config.json', { cache: 'no-store' });
return res.json();
}
3) ブラウザに公開する値は安全なものだけにする
React の環境変数は公開されると考えてください。パスワードやプライベート API キー、データベース URL を Web アプリに入れないでください。
安全な例:API ベース URL、Sentry DSN(公開可能なもの)、ビルドバージョン、単純な機能フラグ。
手順:検証可能なバックエンド設定(Go)
バックエンドの設定は型付けして、環境変数から読み込み、サーバがトラフィックを受け入れる前に検証すると安全です。
まずバックエンドが起動するために必要な値を明示します。典型的な「必須」値:
APP_ENV(dev、staging、prod)HTTP_ADDR(例:8080)DATABASE_URL(Postgres DSN)PUBLIC_BASE_URL(コールバックやリンク用)API_KEY(サードパーティサービス用)
それらを構造体に読み込み、欠落や不正があれば即座に失敗するようにします。そうすれば部分的なデプロイ後ではなく、数秒で問題を見つけられます。
package config
import (
"errors"
"net/url"
"os"
"strings"
)
type Config struct {
Env string
HTTPAddr string
DatabaseURL string
PublicBaseURL string
APIKey string
}
func Load() (Config, error) {
c := Config{
Env: mustGet("APP_ENV"),
HTTPAddr: getDefault("HTTP_ADDR", ":8080"),
DatabaseURL: mustGet("DATABASE_URL"),
PublicBaseURL: mustGet("PUBLIC_BASE_URL"),
APIKey: mustGet("API_KEY"),
}
return c, c.Validate()
}
func (c Config) Validate() error {
if c.Env != "dev" && c.Env != "staging" && c.Env != "prod" {
return errors.New("APP_ENV must be dev, staging, or prod")
}
if _, err := url.ParseRequestURI(c.PublicBaseURL); err != nil {
return errors.New("PUBLIC_BASE_URL must be a valid URL")
}
if !strings.HasPrefix(c.DatabaseURL, "postgres://") {
return errors.New("DATABASE_URL must start with postgres://")
}
return nil
}
func mustGet(k string) string {
v, ok := os.LookupEnv(k)
if !ok || strings.TrimSpace(v) == "" {
panic("missing env var: " + k)
}
return v
}
func getDefault(k, def string) string {
if v, ok := os.LookupEnv(k); ok && strings.TrimSpace(v) != "" {
return v
}
return def
}
これにより DB の DSN、API キー、コールバック URL をコードや Git から排除できます。ホスティングされる環境では、環境ごとにこれらの env var を注入し、dev、staging、prod が一行も変えずに差別化できるようにします。
手順:柔軟性を保つモバイル設定(Flutter)
Flutter アプリは通常、ビルド時フレーバー(何を出荷するか)とランタイム設定(新しいリリースなしに変えられるもの)の二層が必要です。これを分けることで「ちょっと URL を変えるだけ」が緊急再ビルドになるのを防ぎます。
1) エンドポイントではなくアイデンティティのためにフレーバーを使う
dev、staging、prod の三つのフレーバーを作り、フレーバーはビルド時に固定する必要のあるもの(アプリ名、バンドル ID、署名、分析プロジェクト、デバッグツールの有無)を制御します。
そのうえで --dart-define(または CI)で非機微なデフォルトだけを渡し、コードに直書きしないようにします。
ENV=stagingDEFAULT_API_BASE=https://api-staging.example.comCONFIG_URL=https://config.example.com/mobile.json
Dart 側では String.fromEnvironment で読み、起動時にシンプルな AppConfig オブジェクトを作ります。
2) URL とスイッチは取得する設定に入れる
小さなエンドポイント変更のために再ビルドしたくなければ、API ベース URL を定数扱いにしないでください。アプリ起動時に小さな設定ファイルをフェッチしてキャッシュするようにすると良いです。フレーバーは「どこから設定を取得するか」だけを決めます。
実用的な分割:
- フレーバー(ビルド時):アプリのアイデンティティ、デフォルト設定 URL、クラッシュレポートのプロジェクト
- リモート設定(ランタイム):API ベース URL、機能フラグ、ロールアウト割合、メンテナンスモード
- シークレット:アプリに同梱しない(バイナリは解析可能)
バックエンドを移す必要が生じたら、リモート設定を更新して新しいベース URL を指すようにします。既存ユーザーは次回起動時に取得し、最後にキャッシュした値を安全にフォールバックとして使えます。
混乱しない機能フラグとスイッチ
機能フラグは段階的ロールアウト、A/B テスト、緊急キルスイッチ、ステージングでの検証に便利です。だだし、セキュリティ制御の代替にはなりません。もしフラグが保護すべきものを守っているなら、それはフラグではなく認可ルールです。
すべてのフラグを API と同様に扱いましょう:明確な名前、担当者、終了日を持たせます。
意図が分かる命名
フラグが ON のときに何が起きるか、どの領域に影響するかが分かる名前を使います。
例:
feature.checkout_new_ui_enabled(顧客向け機能)ops.payments_kill_switch(緊急オフスイッチ)exp.search_rerank_v2(実験)release.api_v3_rollout_pct(段階的ロールアウト)debug.show_network_logs(診断)
肯定のブール(..._enabled)を好み、プレフィックスを安定させて検索や監査がしやすいようにします。
デフォルト、ガードレール、クリーンアップ
安全なデフォルトを設定しましょう:フラグサービスが落ちてもアプリは安定版の動作をするべきです。
現実的なパターン:バックエンドに新しいエンドポイントを追加し、古いものを残しておき、release.api_v3_rollout_pct を使って徐々にトラフィックを移す。もしエラーが増えれば、ホットフィックスなしで元に戻せます。
フラグを溜め込まないためのルール:
- すべてのフラグにオーナーと「削除予定日」を設定
- 完全ロールアウト後 1–2 リリース以内にフラグを削除
- キーフローでフラグ値をログに残す(デバッグ用)
- フラグを月次で見直す(依存関係レビューと同じように)
シークレット:保管、アクセス、ローテーションの基本
「シークレット」とは漏洩すると被害が起きるものすべてです。API トークン、DB パスワード、OAuth クライアントシークレット、署名鍵(JWT)、Webhook シークレット、プライベート証明書などが該当します。シークレットではないもの:API ベース URL、ビルド番号、機能フラグ、公開分析 ID など。
設定からシークレットを分離しましょう。開発者は安全な設定を自由に変えられるべきで、シークレットはランタイムに注入され、必要な場所だけで使われるべきです。
環境別にシークレットを置く場所
開発環境ではシークレットはローカルで使いやすく破棄可能にします。.env ファイルや OS のキーチェーンを使い、リセットしやすくしてください。コミットは厳禁です。
ステージングと本番では、専用のシークレットストアに置き、コードリポジトリにもチャットログにも、モバイルアプリ内にも置かないでください。
- Web(React):ブラウザにシークレットを置かない。クライアントがトークンを必要とする場合は、バックエンドが短命トークンを発行する。
- バックエンド(Go):起動時に環境変数やシークレットマネージャから読み込み、メモリ内にのみ保持する。
- モバイル(Flutter):アプリは公開物と見なす。バイナリ内のシークレットは抽出可能なので、シークレットはバックエンド発行のトークンにし、ユーザーセッションは端末のセキュアストレージにのみ保存する。
ローテーションの基本(本番を壊さない)
ローテーションが失敗するのは、ある鍵を入れ替えたあと古いクライアントがまだそれを使っていることを忘れるからです。オーバーラップの窓を計画しましょう。
- 短時間は 2 つの有効なシークレット(アクティブ + 前回)をサポートする
- まず新しいシークレットを展開し、その後「アクティブ」のポインタを切り替える
- 認証失敗を監視し、ウィンドウ終了後に古いシークレットを削除する
- デバッグ用にシークレットのバージョン(値ではなく)をログに残す
このオーバーラップ方式は API キー、Webhook シークレット、署名鍵に有効で、サプライズな停止を避けます。
例:ユーザーを壊さずに API URL を切り替えるロールアウト
ステージング API と新しい本番 API があるとします。目標は段階的にトラフィックを移し、問題があれば素早く戻せること。アプリが API ベース URL をコードに埋め込まず設定から読むと簡単です。
API URL はどこでもデプロイ時の値と扱ってください。Web(React)はビルド時値かランタイム設定ファイル、モバイル(Flutter)はフレーバー+リモート設定、バックエンド(Go)はランタイムの env var が一般的です。重要なのは一貫性:コードは一つの変数名(例 API_BASE_URL)だけを参照し、コンポーネントやサービス、画面に URL を埋め込まないこと。
安全な段階的ロールアウトの例:
- 本番 API をデプロイしつつ内部トラフィックのみで「ダーク」状態にする。ステージングはデフォルトのまま。
- バックエンドの依存関係を先に切り替え(他サービスへの呼び出しがある場合)、env var を使って短時間で再起動。
- Web トラフィックを一部(または内部アカウントのみ)切り替える。
- 新しいセットアップでモバイルアプリをリリースするが、切り替えはサーバ制御のフラグで遅らせる。
- 徐々にトラフィックを増やし、ロールバック手順を用意しておく。
検証はミスマッチを早く捕まえることが中心です。実ユーザーが当たる前にヘルスエンドポイント、認証フローが正しく動作するか、同じテストアカウントで主要な操作が一通り完了するかを確認します。
出荷前の簡単チェックリスト
多くの本番設定バグは単純です:ステージング値が残っている、フラグのデフォルトが逆になっている、あるリージョンで API キーが欠けている、など。簡単な確認で多くを防げます。
デプロイ前に対象環境に合わせて三つを確認してください:エンドポイント、シークレット、デフォルト。
- ベース URL が正しい先を指しているか(API、認証、CDN、決済)。Web、バックエンド、モバイルそれぞれでチェック。\n- 本番にテストキーが残っていないか、dev/staging に本番キーがないか。キー名がアプリの期待と一致しているかも確認。\n- 機能フラグのデフォルトは安全か。リスクの高いものはデフォルトでオフにして、意図的に有効化する。\n- ビルド/リリース設定が一致しているか(バンドル ID/パッケージ名、カスタムドメイン、CORS 起点、OAuth リダイレクト URL)。\n- オブザーバビリティが設定されているか(ログ、エラー報告、トレーシング)と、環境ラベルが正しいか。
その後、素早いスモークテストを行います。主要なユーザーフローを一つ選び、クリーンなブラウザプロファイルや新しいインストールでエンドツーエンドを実行し、キャッシュされたトークンに依存しないようにします。
- アプリを開いてコンソールエラーがないか確認。
- サインインして認証が必要な API 呼び出し(プロフィールや設定、単純なデータ一覧など)を一つ叩く。
- 制御された失敗(不正な入力やオフライン)を一つ起こし、白い画面ではなくユーザ向けの優しいメッセージが出ることを確認する。
- ログとエラー報告:テストエラーが数分以内に正しい環境で表示されるか確認。
実用的な習慣:ステージングを本番のまま扱うこと。つまり、同じ設定スキーマ、同じ検証ルール、同じデプロイ形にし、値だけを変える。
アウトage に繋がる一般的なミス
多くの設定による障害は珍しいものではありません。ファイル、ビルド手順、ダッシュボードに設定が散らばり、誰も「今このアプリはどの値を使うのか」を即答できないことが原因です。良い仕組みはその質問に簡単に答えられるようにします。
ビルド時とランタイムの設定を混ぜること
よくある落とし穴は、ランタイム値をビルド時の場所に置くことです。React ビルドに API ベース URL を焼き込むと、環境ごとに再ビルドが必要になります。すると間違ったアーティファクトがデプロイされ、本番がステージングを指す事態が起こります。
安全なルール:リリース後に変わる可能性があるもの(API URL や機能スイッチ、分析エンドポイント)はランタイムに置き、本当に変わらないものだけをビルド時に焼き込む。
開発向けエンドポイントやテストキーを出荷すること
デフォルトが「便利すぎて危険」だとこれが起きます。モバイルが設定を読めなかったときに dev API をデフォルトにしたり、バックエンドが env var がないとローカル DB にフォールバックしたりすると、小さなミスが大きな障害になります。
二つの習慣が有効です:
- フェイルクローズ:必須値が欠けている場合は早期にクラッシュして明確なエラーを出す。\n- 本番を誤設定しにくくする:dev デフォルトは置かず、テストキーを本番で受け入れない、デバッグ用エンドポイントを無効にする。\n 現実例:金曜夜に出したリリースがステージングの決済キーを含んでいて、最初は「動いている」ように見えますが請求が失敗する。解決は新しい決済ライブラリではなく、本番で非本番キーをはじく検証です。
ステージングを本番から乖離させること
本番と乖離したステージングは誤検知を招きます。別の DB 設定、欠けたバッックグラウンドジョブ、追加の機能フラグなどが本番後にバグを露呈させます。
ステージングを本番に近づけるには、同じ設定スキーマ、同じ検証ルール、同じデプロイ形を保ち、値だけを変えることです。
次のステップ:設定を退屈で再現可能かつ安全にする
目標は派手なツールではなく、退屈な一貫性です:同じ名前、同じ型、同じルールを dev、staging、prod 全てで使うこと。設定が予測可能なら、リリースの不安は減ります。
まずは短くても具体的な設定契約を一箇所に書きましょう:すべてのキー名、その型(string、number、boolean)、どこから来てもよいか(env var、リモート設定、ビルド時)、デフォルト値。クライアントアプリに絶対に置いてはいけない値(プライベート API キーなど)についての注意も書きます。設定契約は API のように扱い、変更はレビューを要するようにします。
次に、ミスが早く見つかるようにしましょう。欠けた API ベース URL を見つけるベストなタイミングは CI であって、デプロイ後ではありません。アプリが設定を読むのと同じ方法で設定を読み込み、自動検証を追加してチェックする項目の例:
- 必須値が存在する(空文字でない)
- 型が正しい(
"true"と true のバグを防ぐ) - 本番固有ルールを満たす(例:HTTPS 必須)
- 機能フラグの名前が既知である(タイプミス防止)
- シークレットがリポジトリにコミットされていない
最後に、設定変更が間違っていたときに復旧しやすくします。稼働中のスナップショットを取り、変更は一度に一つずつ行い、素早く検証し、ロールバック手順を用意しておくこと。
もし Koder.ai (koder.ai) のようなプラットフォームでビルドとデプロイを行っているなら、同じルールが当てはまります:環境値をビルドやホスティングの入力として扱い、シークレットをエクスポートされたソースに入れない、出荷前に設定を検証する。こうした一貫性があれば、再デプロイやロールバックが日常的なものになります。
設定が文書化され、検証され、可逆的であれば、設定は障害の原因ではなく、通常の出荷作業の一部になります。
よくある質問
コードではなく設定に入れるべきものは何ですか?
環境によって変わる値はソースファイルに含めないでください。機密性のないデフォルト値はコードに置き、URLや切り替え設定には環境別の設定を使い、シークレットはビルド時またはデプロイ時に注入します。
新しい環境が必要になるのはいつですか?
データ、アクセス、リスクを分離する必要がある場合は、dev、staging、prod を別々の環境にします。URL、上限、切り替え設定だけが変わる場合は、代わりに設定値を追加してください。
React アプリにシークレットを保存できますか?
いいえ。ブラウザに配信されるものはすべてユーザーに見えます。秘密鍵やパスワードはバックエンドに置き、公開 API URL や単純な機能フラグなど、安全な値だけを公開してください。
Web アプリではビルド時設定とランタイム設定のどちらを使うべきですか?
ほとんど変わらず、安全に公開できる設定にはビルド時の値を使います。API ベース URL やメンテナンス切り替えのように、デプロイ後に変更する必要がある値にはランタイム設定を読み込みます。
Go バックエンドでは設定をどのように扱うべきですか?
サービスの起動時に、必須の環境変数を型付きの設定構造体に読み込みます。許可する環境、URL、データベース接続文字列を検証し、必須値が欠けている場合は明確なエラーで起動を停止してください。
Flutter で dev、staging、prod をどのように扱うべきですか?
アプリ ID、署名、分析プロジェクトなど、ビルド固有の詳細には Flutter flavors を使います。API URL や機能フラグなど、機密性のないランタイム設定はリモート設定ソースから取得してください。
機能フラグが増えすぎないようにするにはどうすればよいですか?
feature.checkout_new_ui_enabled のように、有効にする動作に基づいてフラグに名前を付けます。各フラグに所有者と削除日を設定し、リスクのある変更はデフォルトでオフにして、全面展開後すぐにフラグを削除してください。
API キーとデータベースパスワードはどこに保存すべきですか?
staging と production のシークレットは専用のシークレットストアに保存し、必要なサービスにだけ注入します。ローカル開発用のシークレットは、無視対象のローカルファイルまたは OS のキーチェーンに保管してください。
ダウンタイムを発生させずにシークレットをローテーションするにはどうすればよいですか?
短い移行期間は、古い認証情報と新しい認証情報を併用します。新しい値を使うクライアントまたはサービスをデプロイし、アクティブなトラフィックを切り替え、認証失敗を監視してから、古い認証情報を削除してください。
本番リリース前に何を確認すべきですか?
すべてのエンドポイントが対象環境を指していること、必須のシークレットが存在すること、リスクのあるフラグがデフォルトでオフになっていることを確認してください。次に、クリーンなブラウザプロファイルまたは新規アプリインストールで完全なユーザーフローを 1 回実行し、ログが正しい環境ラベルを使っていることを確認します。