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

ハードコーディングされた設定は最初は手早くて便利に感じます。しかし、ステージング環境が必要になったり、別の API を使いたくなったり、機能の切り替えが必要になったとき、「簡単な」変更がリリースリスクに変わります。対策は単純で、環境ごとの値をソースファイルから取り出し、予測可能な仕組みに入れることです。
よくあるトラブルの原因はすぐに見つかります。
「本番のために変えればいい」は、直前の編集を常態化させます。そうした編集はレビューやテスト、再現性をすり抜けがちです。ある人が URL を変え、別の人がキーを変えると、そのビルドに「どの設定が含まれていたか」を答えられなくなります。
よくある状況:モバイルの新しいバージョンをステージング向けにビルドしたら、リリース直前に誰かが URL を本番に切り替えます。翌日バックエンドが再び変わり、ロールバックが必要になる。URL がハードコーディングされていると、ロールバックは別のアプリ更新を意味します。ユーザーは待たされ、サポートチケットが増えます。
ここでの目標は、Web、Go バックエンド、Flutter モバイルで共通して使える単純な仕組みです。
Dev、staging、prod は同じアプリが三つの場所で動いているように感じられるべきです。目的は「値を変えること」であって「動作を変えること」ではありません。
変えるべきものは、アプリがどこで動くか、誰が使うかに結びつくもの:ベース URL やホスト名、認証情報、サンドボックスか本番の統合、ログレベルや本番での厳しいセキュリティ設定などの安全制御です。
変わってはいけないのはロジックとパーツ間の契約です。API ルート、リクエスト/レスポンスの形、機能名、コアのビジネスルールは環境で変わってはいけません。ステージングが本番と異なる挙動を示すと、信頼できるリハーサルになりません。
「新しい環境」か「新しい設定値」かの実用的なルール:分離されたシステム(別データ、別アクセス、別リスク)が必要なときだけ新しい環境を作る。エンドポイントや数値を変えたいだけなら、設定値を追加するだけで十分です。
例:新しい検索プロバイダーをテストしたい場合。少人数に安全に有効化できるなら、ステージングは一つのまま機能フラグを追加する。別 DB と厳しいアクセス制御が必要なら、新しい環境を検討します。
良いセットアップは一つのことをうまくやります:誤って dev URL、テストキー、未完成の機能を出荷してしまうことを難しくすること。
すべてのアプリ(Web、バックエンド、モバイル)に同じ三層を使いましょう。
混乱を避けるには、各アプリで一つの真実のソースを選び、それに従ってください。例えば、バックエンドは起動時に環境変数を読む、Web はビルド時変数か小さなランタイム設定ファイルを読む、モバイルはビルド時に選ぶ小さな環境ファイルを読む、といった具合です。アプリ内部で一貫性を保つことが、全体で同じ仕組みにこだわるより重要です。
単純で再利用可能なスキームは次のようになります。
各設定項目には「何か」「どこに適用されるか」「型は何か」が分かる明確な名前を付けましょう。
実用的な慣例:
こうすれば「BASE_URL」が React 用か Go サービス用か Flutter 用かを誰も推測する必要がなくなります。
React のコードはユーザーのブラウザで動くため、配布したものは誰でも読めます。目標は簡単:シークレットはサーバに置き、ブラウザには API ベース URL、アプリ名、非機微な機能トグルなどの「安全な」設定だけを渡すこと。
ビルド時設定はバンドルを作るときに注入されます。めったに変わらない、安全に公開できる値に向きます。
ランタイム設定はアプリ起動時に読み込まれます(例えばアプリと一緒に配信する小さな JSON ファイルや注入されたグローバル)。デプロイ後に切り替えたい値(環境間で API ベース URL を切り替えるなど)はランタイムにする方が良いです。
簡単なルール:変更に UI の再ビルドが不要ならランタイムにする。
開発者用のローカルファイルは(コミットせず)用意し、実際の値はデプロイパイプラインで設定します。
.env.local(gitignore)に VITE_API_BASE_URL=http://localhost:8080 などを置く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();
}
React の環境変数は公開されると考えてください。パスワードやプライベート API キー、データベース URL を Web アプリに入れないでください。
安全な例:API ベース URL、Sentry DSN(公開可能なもの)、ビルドバージョン、単純な機能フラグ。
バックエンドの設定は型付けして、環境変数から読み込み、サーバがトラフィックを受け入れる前に検証すると安全です。
まずバックエンドが起動するために必要な値を明示します。典型的な「必須」値:
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 アプリは通常、ビルド時フレーバー(何を出荷するか)とランタイム設定(新しいリリースなしに変えられるもの)の二層が必要です。これを分けることで「ちょっと URL を変えるだけ」が緊急再ビルドになるのを防ぎます。
dev、staging、prod の三つのフレーバーを作り、フレーバーはビルド時に固定する必要のあるもの(アプリ名、バンドル ID、署名、分析プロジェクト、デバッグツールの有無)を制御します。
そのうえで --dart-define(または CI)で非機微なデフォルトだけを渡し、コードに直書きしないようにします。
ENV=stagingDEFAULT_API_BASE=https://api-staging.example.comCONFIG_URL=https://config.example.com/mobile.jsonDart 側では String.fromEnvironment で読み、起動時にシンプルな AppConfig オブジェクトを作ります。
小さなエンドポイント変更のために再ビルドしたくなければ、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 を使って徐々にトラフィックを移す。もしエラーが増えれば、ホットフィックスなしで元に戻せます。
フラグを溜め込まないためのルール:
「シークレット」とは漏洩すると被害が起きるものすべてです。API トークン、DB パスワード、OAuth クライアントシークレット、署名鍵(JWT)、Webhook シークレット、プライベート証明書などが該当します。シークレットではないもの:API ベース URL、ビルド番号、機能フラグ、公開分析 ID など。
設定からシークレットを分離しましょう。開発者は安全な設定を自由に変えられるべきで、シークレットはランタイムに注入され、必要な場所だけで使われるべきです。
開発環境ではシークレットはローカルで使いやすく破棄可能にします。.env ファイルや OS のキーチェーンを使い、リセットしやすくしてください。コミットは厳禁です。
ステージングと本番では、専用のシークレットストアに置き、コードリポジトリにもチャットログにも、モバイルアプリ内にも置かないでください。
ローテーションが失敗するのは、ある鍵を入れ替えたあと古いクライアントがまだそれを使っていることを忘れるからです。オーバーラップの窓を計画しましょう。
このオーバーラップ方式は API キー、Webhook シークレット、署名鍵に有効で、サプライズな停止を避けます。
ステージング API と新しい本番 API があるとします。目標は段階的にトラフィックを移し、問題があれば素早く戻せること。アプリが API ベース URL をコードに埋め込まず設定から読むと簡単です。
API URL はどこでもデプロイ時の値と扱ってください。Web(React)はビルド時値かランタイム設定ファイル、モバイル(Flutter)はフレーバー+リモート設定、バックエンド(Go)はランタイムの env var が一般的です。重要なのは一貫性:コードは一つの変数名(例 API_BASE_URL)だけを参照し、コンポーネントやサービス、画面に URL を埋め込まないこと。
安全な段階的ロールアウトの例:
検証はミスマッチを早く捕まえることが中心です。実ユーザーが当たる前にヘルスエンドポイント、認証フローが正しく動作するか、同じテストアカウントで主要な操作が一通り完了するかを確認します。
多くの本番設定バグは単純です:ステージング値が残っている、フラグのデフォルトが逆になっている、あるリージョンで API キーが欠けている、など。簡単な確認で多くを防げます。
デプロイ前に対象環境に合わせて三つを確認してください:エンドポイント、シークレット、デフォルト。
その後、素早いスモークテストを行います。主要なユーザーフローを一つ選び、クリーンなブラウザプロファイルや新しいインストールでエンドツーエンドを実行し、キャッシュされたトークンに依存しないようにします。
実用的な習慣:ステージングを本番のまま扱うこと。つまり、同じ設定スキーマ、同じ検証ルール、同じデプロイ形にし、値だけを変える。
多くの設定による障害は珍しいものではありません。ファイル、ビルド手順、ダッシュボードに設定が散らばり、誰も「今このアプリはどの値を使うのか」を即答できないことが原因です。良い仕組みはその質問に簡単に答えられるようにします。
よくある落とし穴は、ランタイム値をビルド時の場所に置くことです。React ビルドに API ベース URL を焼き込むと、環境ごとに再ビルドが必要になります。すると間違ったアーティファクトがデプロイされ、本番がステージングを指す事態が起こります。
安全なルール:リリース後に変わる可能性があるもの(API URL や機能スイッチ、分析エンドポイント)はランタイムに置き、本当に変わらないものだけをビルド時に焼き込む。
デフォルトが「便利すぎて危険」だとこれが起きます。モバイルが設定を読めなかったときに dev API をデフォルトにしたり、バックエンドが env var がないとローカル DB にフォールバックしたりすると、小さなミスが大きな障害になります。
二つの習慣が有効です:
本番と乖離したステージングは誤検知を招きます。別の DB 設定、欠けたバッックグラウンドジョブ、追加の機能フラグなどが本番後にバグを露呈させます。
ステージングを本番に近づけるには、同じ設定スキーマ、同じ検証ルール、同じデプロイ形を保ち、値だけを変えることです。
目標は派手なツールではなく、退屈な一貫性です:同じ名前、同じ型、同じルールを dev、staging、prod 全てで使うこと。設定が予測可能なら、リリースの不安は減ります。
まずは短くても具体的な設定契約を一箇所に書きましょう:すべてのキー名、その型(string、number、boolean)、どこから来てもよいか(env var、リモート設定、ビルド時)、デフォルト値。クライアントアプリに絶対に置いてはいけない値(プライベート API キーなど)についての注意も書きます。設定契約は API のように扱い、変更はレビューを要するようにします。
次に、ミスが早く見つかるようにしましょう。欠けた API ベース URL を見つけるベストなタイミングは CI であって、デプロイ後ではありません。アプリが設定を読むのと同じ方法で設定を読み込み、自動検証を追加してチェックする項目の例:
"true" と true のバグを防ぐ)最後に、設定変更が間違っていたときに復旧しやすくします。稼働中のスナップショットを取り、変更は一度に一つずつ行い、素早く検証し、ロールバック手順を用意しておくこと。
もし Koder.ai (koder.ai) のようなプラットフォームでビルドとデプロイを行っているなら、同じルールが当てはまります:環境値をビルドやホスティングの入力として扱い、シークレットをエクスポートされたソースに入れない、出荷前に設定を検証する。こうした一貫性があれば、再デプロイやロールバックが日常的なものになります。
設定が文書化され、検証され、可逆的であれば、設定は障害の原因ではなく、通常の出荷作業の一部になります。