環境を混在させるのはもうやめよう:開発と本番の状態は決して交わってはいけない
あなたの手元には3つのディレクトリがある:dev、staging、prod。それぞれに設定ファイル、状態レコード、リソース定義が格納されている。ファイアウォールルールを更新する必要が生じると、3つのディレクトリをすべて開き、同じ変更を3回行う。ある日、うっかりstagingの更新を忘れる。2週間後、ステージングへのデプロイが失敗する。ファイアウォールが、他の環境では問題なく通る接続をブロックしてしまうからだ。誰も気づかない。リリースが止まるまで。
これはよくある状況だ。チームはまず1つの環境から始め、次にもう1つ、さらにもう1つと追加していく。フォルダ名が異なるため、分離は明確に見える。しかし、その分離は見かけだけだ。本当の問題は、同じ人物、同じツール、同じアクセスパターンで、何のガードレールもなくすべての環境に触れることができる点にある。
環境分離の本当の問題
環境分離とはフォルダ名の問題ではない。開発での変更が誤って本番に影響を与えることがなく、ステージングでのテストが実際に本番の状態を反映していることを保証することだ。
すべての環境で同じステートファイルを使うと、単一障害点が生まれる。開発でのミスが本番リソースを削除してしまうかもしれない。設定を誤ったCIパイプラインが、ステージングの値を本番インフラに適用してしまうかもしれない。これらは理論上のリスクではない。チームが環境間でステートファイル、バックエンド、アクセス認証情報を共有しているときに実際に発生する。
目標は、開発作業中に誤って本番を変更することを不可能にすることだ。それには規律だけでは足りない。構造的な分離が必要だ。
環境分離の3つのアプローチ
1. ディレクトリの分離
次の図は、各アプローチの構造と主要な特性を示している。
これは最もシンプルなアプローチだ。環境ごとにdev/、staging/、prod/というフォルダを作成する。各フォルダには、独自の設定ファイルと独自のステートファイルが含まれる。コマンドを実行するときは、どのフォルダを使うかを指定する。
これは、環境が少ない小規模チームに適している。ファイルを並べて比較すれば、環境間の違いを一目で確認できる。構造は理解しやすい。新しいチームメンバーも、聞かずに必要なものを見つけられる。
問題は、環境が設定の大部分を共有している場合に現れる。開発とステージングは、サーバーサイズとデータベース名だけが異なるかもしれない。セキュリティ設定を変更する必要がある場合、3つのファイルを更新しなければならない。1つを忘れると、目に見えないドリフトが生じる。時間が経つにつれて環境は乖離し、ステージングが本番を再現しなくなる。
2. 共有構造と個別の設定ファイル
設定全体を複製する代わりに、リソース定義のセットを1つ保持し、環境固有の値は別のファイルに分ける。メインファイルはロジックと構造を定義する。設定ファイルはサーバー名、インスタンスサイズ、接続文字列などの値を保持する。
このアプローチは重複を減らす。リソース定義は一度だけ記述する。ファイアウォールルールを変更するときは、1箇所を変更するだけで済む。環境固有のファイルには、実際に異なる値だけを含める。
トレードオフは複雑さだ。設定ファイルがメインの定義とどのようにマージされるかを理解する必要がある。すべての環境に正しい設定ファイルがあることを確認する必要がある。値が欠けていると、デプロイが失敗したり、さらに悪いことに、その環境にとって間違ったデフォルト値が使われたりする可能性がある。
3. ステートバックエンドの分離
これが最も成熟したアプローチだ。各環境は異なるバックエンドを使ってステートファイルを保存する。ステートファイルは、作成・管理されたすべてのリソースを記録する。バックエンドが分離されていれば、開発のステートが誤って本番のステートと混ざることはない。
以下は、TerraformでS3を使って個別のステートバックエンドを設定する具体例だ。
# 本番環境のバックエンド設定
terraform {
backend "s3" {
bucket = "my-company-tfstate"
key = "prod/terraform.tfstate"
region = "us-east-1"
}
}
ステージング環境では、同じバケットだが異なるキーを使う。
terraform {
backend "s3" {
bucket = "my-company-tfstate"
key = "staging/terraform.tfstate"
region = "us-east-1"
}
}
開発環境では次のようにする。
terraform {
backend "s3" {
bucket = "my-company-tfstate"
key = "dev/terraform.tfstate"
region = "us-east-1"
}
}
各環境のステートファイルは、同じS3バケット内の別々のプレフィックスに保存される。そして、バケットまたはプレフィックスレベルでアクセスポリシーを適用し、開発者はdev/プレフィックスのみ読み書きでき、本番へのアクセスはCI/CDパイプラインに制限されるようにできる。
バックエンドレベルでアクセスポリシーを適用できる。開発チームのメンバーは開発バックエンドにのみアクセスできる。運用チームやSREチームは本番バックエンドへのアクセスを保持する。開発者が自分のラップトップからコマンドを実行しても、本番リソースには触れられない。その認証情報が本番バックエンドへのアクセス権を持たないからだ。
この分離は、ステートファイルに機密情報が含まれているため、極めて重要だ。本番のステートには通常、サーバーのIPアドレス、データベース接続、セキュリティ設定が記録されている。本番と開発のステートが同じ場所に保存されていると、情報漏洩や誤った変更が両方の環境に影響を及ぼす。バックエンドを分離すれば、環境ごとに異なるセキュリティポリシーを適用できる。
各アプローチを使うべきタイミング
プロジェクトが小さく、環境が少なく、環境間でリソースが大きく異なる場合は、ディレクトリの分離を選ぶ。これは良い出発点だが、チームが成長するにつれて移行する計画を立てよう。
環境が同じ構造だが異なる値を持つ場合は、共有構造と個別の設定ファイルを選ぶ。これは、インフラを標準化し、サイズ、リージョン、命名などのパラメータのみを変える必要があるチームに適している。
チームが大規模で、本番を厳格に保護する必要がある場合、またはすべての変更に監査証跡が必要な場合は、ステートバックエンドの分離を選ぶ。これは組織の複雑さに応じてスケールするアプローチだ。
環境分離のポリシー面
技術的な分離は解決策の半分に過ぎない。完璧なバックエンド分離があっても、すべてのチームメンバーが自分のラップトップから本番バックエンドにアクセスできてしまえば、分離は無意味だ。
環境分離にはポリシーが必要だ。誰が本番のステートを読めるのか?誰がそれを変更できるのか?誰が本番インフラへの変更を承認できるのか?これらの問いに答え、口頭での合意ではなく、アクセス制御を通じて強制しなければならない。
本番デプロイ用に別のCI/CDパイプラインを使うチームもある。本番パイプラインは特定のブランチからのみ実行され、承認が必要で、開発者が利用できない認証情報を使う。これにより、バックエンド自体に加えて、さらに別の分離レイヤーが追加される。
実践的なチェックリスト
- 各環境が異なるステートバックエンドを使っている
- 本番バックエンドはCI/CDパイプラインからのみアクセス可能で、ローカルマシンからはアクセスできない
- 開発用とステージング用のバックエンドは開発者がアクセス可能
- ステートファイルは保存時に暗号化されている
- 本番ステートへのアクセスには承認が必要で、ログが記録される
- 設定値は本番に到達する前に検証される
具体的な教訓
環境分離はフォルダ構造の問題ではない。ステートの分離の問題だ。各環境を、独自のステートバックエンド、独自のアクセス制御、独自のデプロイパイプラインを持つ完全に独立したシステムとして扱うことで、環境をまたがった偶発的な変更の可能性を排除できる。必要ならディレクトリの分離から始めてもよいが、チームが5人を超える前に、ステートバックエンドの分離に移行しよう。共有ステートが原因で本番障害を修正するコストは、最初から適切な分離を設定するコストよりも常に高い。