ワークスペースは環境分離のための道具ではない
一言でいうと
ワークスペースは、1つのバックエンド・1つの認証情報の中で、状態だけを複数持つ仕組みです。環境が分かれる地点が権限なら、ワークスペースでは分けられません。
なぜ必要なのか、そしてなぜ誤解されるのか
環境を分ける作業は、ほとんどいつも、こうして始まります。devで作ったスタックをprodにも載せる必要があるのに、コードを丸ごとコピーしたくありません。このとき、workspace new prodは、完璧な答えのように見えます。コードは1つで、コマンド1行で乗り換えられ、プロバイダーのキャッシュも再取得しません。
問題は、このツールが解決するものが何かにあります。ワークスペースが分けてくれるのは、状態(state)だけです。バックエンドはそのまま1つで、そのバックエンドを開く認証情報も1つで、コードも1つです。そのため、devを触る権限だけを渡すべき人にワークスペースを使わせると、その人は、prodの状態が入っている同じストレージを開く鍵を、すでに握っていることになります。
公式ドキュメントも、同じことを言っています。OpenTofuのWorkspacesのドキュメントは、ワークスペースが「システムの分解や、別の認証情報とアクセス制御が必要なデプロイには適切ではない」と書いており、より大きなシステムでは、アーキテクチャの境界に合わせて、設定そのものを分けなさいと勧めています。つまり、ワークスペースは環境を分離するツールではなく、同じ環境の一時的なコピーを作るツールです。ドキュメントが挙げる代表的な例も、機能ブランチ用の一時的な複製です。
どう動くのか
localバックエンドで実際に何が起きるかは、ファイルの配置がすべて語っています。
tfa-ws/
├── terraform.tfstate ← default workspace
├── terraform.tfstate.d/
│ ├── dev/terraform.tfstate
│ └── prod/terraform.tfstate
└── .terraform/
├── providers/ ← 캐시는 한 벌뿐이다
└── environment ← 지금 선택된 이름이 여기에만 있다
読み取るべきことが3つあります。
- デフォルトのワークスペースだけ、パスが違います。
defaultの状態は、以前と同じく作業ディレクトリにあり、残りはterraform.tfstate.d/<이름>/の下に置かれます(プレースホルダーは名前です)。リモートバックエンドは、それぞれ異なるルールでプレフィックスを付けます。 - 選択はローカルの状態です。いまどのワークスペースなのかは、
.terraform/environmentというファイルの1行にだけ書かれています。このファイルはコミットされず、他の人の作業ディレクトリと共有されることもありません。「どの環境にコマンドを出しているのか」が、コードやレビューに残らないという意味です。 - 設定では名前だけが見えます。
terraform.workspaceで名前を読み取って、値を差し替えられます。サイズをマップに置いてlookupする方式が、よく使われます。便利ですが、マップにない名前を選択すると、デフォルト値に黙って落ちます。
ディレクトリ分離は、まったく反対の性質を持ちます。環境ごとにディレクトリが別なので、状態も別、initも別、プロバイダーのキャッシュも別です。ドキュメントが認めているとおり、ディスクと帯域をより多く使い、設定の更新も、それぞれ行う必要があります。その代わり、バックエンド設定を環境ごとに違えられ、リポジトリの権限で、「prodディレクトリは、誰が変更できるのか」を、コードレビューのルールにできます。
現場での姿
最もよくある事故は、文法ではなく選択です。ターミナルには、prodとどこにも書かれておらず、プロンプトにも出ず、コマンドにも入りません。昨日prodを見ていて、そのまま退勤したディレクトリで、今朝destroyを打てば、それで終わりです。ラボのステップ5で、この事故をわざと起こします。
2つ目は、権限がすでに分かれているのに、ワークスペースで分けたチームです。監査で「dev担当者がprodの状態を読めるか」を聞かれたら、答えはいつも「はい」です。バックエンドが1つだからです。この指摘は、コードを直して防げるものではなく、ディレクトリ(または別の設定)に移す移行作業になります。
3つ目は、ワークスペース名がコードの条件式に漏れ出すことです。terraform.workspace == "prod" ? ... : ...が、3、4か所を超えると、そのコードはすでに、環境ごとに異なる2つのコードです。そのときが、ディレクトリで分ける時点です。
防ぐ方法は単純です。書き込みコマンドの前に、ガードの1行を立てて、期待したワークスペースでなければ止まるようにします。CIでは、環境名をパイプラインの変数として受け取り、同じ検査を行います。人の記憶の代わりに、終了コードが判断するようにするのです。終了コードを2つではなく3つに分けることも、実務では価値があります。「期待と違う」と「スクリプトの呼び出し方が間違っている」をパイプラインが区別できてこそ、リトライするか人を呼ぶかを決められるからです。
まとめると、選択の基準は次のようになります。状態だけを分ければよいのか、それとも、認証情報・バックエンド・レビュー権限まで分ける必要があるのか。前者ならワークスペースが最も安い答えで、後者ならワークスペースは答えではありません。両者を混ぜて使うこともよくあります。prodと非本番をディレクトリで大きく分け、非本番の中で、開発者ごとの一時的なコピーをワークスペースで持つ、という形です。
次のラボですること
/root/tfa-wsでワークスペースを3つ作り、状態ファイルがどこにできるかを自分で開いて確認し、環境ごとに違う値をマップで渡し、prodを選択したままdestroyを回して事故を起こしたあと、復旧します。続いて、同じ結果をディレクトリ分離で作り直し、状態ファイルの数とプロバイダーのキャッシュの数を数えて、2つの方式を表で比べます。