ステージングと本番でアプリの動作が異なる理由

ステージングに同じコードをデプロイし、テストを実行してすべてパスする。その後、本番にデプロイするとアプリがクラッシュする。コードは同一だ。違いは、更新し忘れたたった一つの設定値にある。

このシナリオは、毎週のようにチームで発生している。コードは問題ない。設定が問題なのだ。そして、問題がデータベースパスワードやAPIトークンのようなシークレットである場合、リスクはさらに高まる。漏洩したシークレットは、ほとんどのコードバグよりも大きな損害を引き起こす可能性がある。

設定とシークレットの正体

設定とは、コードを変更せずにアプリケーションの動作を変えるすべての値である。サーバーアドレス、最大接続数、ログレベル、フィーチャーフラグ——これらはすべて設定だ。同じコードベースが開発、ステージング、本番で異なる動作をするのは、設定が変わるからである。

シークレットは設定の特別なカテゴリである。パスワード、トークン、暗号化キー、証明書など、機密性を保たなければならない値だ。シークレットは、その露出が設定ミスではなくセキュリティインシデントとなるため、異なる取り扱いが必要となる。

重要な洞察は、設定とシークレットの両方をアプリケーションコードとは別に管理する必要があるということだ。それらはデリバリーシステムの異なるレイヤーに存在する。

設定テンプレートから始める

設定を適切に管理する前に、アプリケーションが実際に必要とする設定を把握する必要がある。設定テンプレートとは、アプリケーションが期待するすべての設定値を環境ごとに整理した完全なリストである。

各設定項目について、以下を記録する:

以下は、そのようなテンプレートをYAMLで記述した具体例である:

# config-template.yaml
# アプリケーション設定テンプレート
# 環境固有のファイルで値を上書きする

app:
  name: my-app
  version: 1.0.0
  log_level: info  # 上書き: dev=debug, staging=info, prod=warn
  max_retry: 3

database:
  host: localhost  # 上書き: staging=db-staging.example.com, prod=db-prod.example.com
  port: 5432
  name: myapp_db
  pool_size: 10  # 上書き: prod=50

cache:
  host: localhost  # 上書き: staging=redis-staging.example.com, prod=redis-prod.example.com
  port: 6379
  ttl_seconds: 3600

feature_flags:
  new_checkout: false  # 上書き: staging=true, prod=false
  dark_mode: true
  • 変数名(DB_HOSTMAX_RETRY など)
  • 値の型(文字列、数値、ブール値)
  • どの環境で使用するか
  • 環境間で値が異なるかどうか

一部の値はどこでも同じである。MAX_RETRY は開発、ステージング、本番で3かもしれない。他の値は異ならなければならない。DB_HOST は開発ではローカルデータベースを指し、本番では本番データベースクラスタを指す。

テンプレートには、各設定値を誰が変更できるか、およびその変更がどのように追跡されるかも記載する必要がある。チームの全員が本番データベースの接続文字列を変更できるべきではない。

シークレットには独自のテンプレートが必要

シークレットも同じ考え方だが、より厳格なルールが適用される。シークレットテンプレートには以下を記録する:

  • シークレット名
  • その出所(Vault、パラメータストア、暗号化ファイル)
  • アクセスが必要な環境
  • 最後にローテーションした日時
  • 有効期間
  • ローテーションの正確な手順
  • ローテーション後もアプリケーションが正常に動作することを確認する方法

ローテーションとは、古いシークレットを新しいものに定期的に置き換えるプロセスである。多くのチームは、侵害が発生して強制されるまでこれをスキップする。テンプレートがあれば、ローテーションを緊急対応ではなく日常業務にできる。

確認手順は極めて重要である。データベースパスワードをローテーションした後、アプリケーションが引き続き接続してデータを照会できることを確認する必要がある。そうしないと、シークレットをローテーションして本番を壊しても、ユーザーがエラーを報告し始めるまで気づかない可能性がある。

すべてを監査する

設定とシークレットには監査証跡が必要である。監査ログは、誰がいつ、なぜ設定値やシークレットにアクセスまたは変更したかを記録する。

監査ログのテンプレートには以下を記録する:

  • アクションのタイムスタンプ
  • 実行した人物
  • 実行したアクション(表示、変更、削除)
  • 影響を受けた設定またはシークレット

このログは、何か問題が発生したときに不可欠である。シークレットが漏洩した場合、誰がいつアクセスしたかを知る必要がある。設定変更が本番を壊した場合、誰が変更を行い、以前の値は何だったかを追跡する必要がある。

監査ログがなければ、手探りでデバッグすることになる。何かが変更されたことはわかっているが、何が、あるいは誰が原因かを突き止める方法がない。

設定とシークレットをテストする

ほとんどのチームが見落としているステップがある。設定とシークレットはテストする必要がある。コードはテストする。インフラもテストする。しかし、アプリケーションが各環境で設定とシークレットを正しく読み取れるかどうかをどれだけの頻度でテストしているだろうか?

設定テストでは以下を検証する:

  • 必要な設定値がすべて存在すること
  • 正しい型であること
  • 期待される範囲内であること
  • アプリケーションがこれらの値で正常に起動すること

シークレットテストでは以下を検証する:

  • シークレットが存在しアクセス可能であること
  • アプリケーションがシークレットを使用して認証できること
  • 認証後にアプリケーションが基本操作を実行できること

これらのテストは、デプロイ前に問題をキャッチする。シークレットの欠落や誤った形式の設定値は、起動時にアプリケーションをクラッシュさせる。本番ではなくテストでそれを発見する方がはるかに良い。

実用的なチェックリスト

新しいアプリケーションやサービスの設定とシークレット管理を設定する際に使用できる短いチェックリストを以下に示す:

  • アプリケーションが必要とするすべての設定値を環境ごとにリストアップする
  • 環境間で異なる値と、同じままの値を特定する
  • どの値がシークレットであり、特別な取り扱いが必要かを特定する
  • 各シークレットの出所(Vault、パラメータストアなど)を文書化する
  • 各シークレットのローテーションスケジュールを作成する
  • 設定とシークレットへのアクセスに対する監査ログを設定する
  • 各環境での設定とシークレットの読み込みを検証するテストを作成する
  • 各設定値とシークレットを誰が変更できるかを文書化する

まとめ

設定とシークレットは後付けではない。それらは、独自のテンプレート、独自のテスト、独自の監査証跡を必要とする独立した関心事である。ステージングで完璧に動作する同じコードでも、たった一つの設定値が間違っていたり、たった一つのシークレットが欠落していたりすると、本番で失敗する。設定とシークレットをアプリケーションコードと同じ厳格さで扱えば、デプロイメント障害のカテゴリ全体を排除できる。