状態ファイルが作るものと壊すもの
一言でいうと
状態ファイルは、コードと実際のインフラをつなぐ唯一の橋であり、誤って扱うとチーム全体を止めてしまう単一障害点です。
なぜ必要なのか
状態ファイルが存在する理由は、4つあります。1つ目はマッピングです。コードに書いたリソース名と、実際に作られたリソースの識別子をつなぎます。2つ目は依存関係の追跡です。何を先に削除して何をあとに作るべきかを知るには、関係を記憶しておく必要があります。3つ目はパフォーマンスです。毎回すべてのリソースを照会するとAPI呼び出しが爆発的に増えるので、最後に見た値をキャッシュのように使います。4つ目はコラボレーションです。チームメンバーの間で同じ事実を共有する基準点になります。
問題は、このファイルが機密情報を平文で含みうることです。DBのパスワードや証明書がそのまま入ります。そのため、暗号化されたリモートバックエンドが事実上必須です。出力にsensitive = trueを付けるのは、画面上のマスキングにすぎず、状態ファイルの中では値がそのまま見えます。この2つを混同すると、「隠したから安全だ」と錯覚します。
どう動くのか
同時適用を防ぐ仕組みがロックです。applyを開始するときにロックの項目を条件付きで作成し、他の人がすでに取得していれば、競合エラーになります。ここでよく驚く点が、待機時間のデフォルトが0秒、つまり即座に失敗することです。静かにぶら下がるより、早く失敗して人が状況を見るほうがよいという設計です。
ロックは残ることもあります。ネットワークが切れたり、CIランナーがタイムアウトで死んだり、誰かがCtrl+Cで強制中断したりすると、ロックだけが残ります。強制解除のコマンドがありますが、他のユーザーが実際に作業中なら状態が壊れるおそれがあるので、最後の手段です。まず、そのロックを誰がいつ取得したかを確認する手順が必要です。
状態は、手で直すのではなく、専用のコマンドで扱います。state mvで移し、state rmで管理から外し、importですでに存在するリソースを取り込み、リファクタリングはmovedブロックで表現します。特にstate rmは、管理から外すだけで、実際のリソースは削除されないという点を、正確に知っておく必要があります。これを削除コマンドと誤解すると、正反対の事故が起きます。
規模が大きくなったら、状態を分割する必要があります。すべてを1つのファイルにまとめると、planが10分以上かかり、APIのレート制限にかかります。ネットワーキング・コンピュート・DB・モニタリングのように、コンポーネントごとに分けて、blast radius、つまり爆発半径を最小化します。環境の分離方式にも選択肢があります。ワークスペースは、コードの重複がありませんが、分離が弱く、爆発半径が広いです。ディレクトリによる分離は、一部重複が生じますが、環境ごとに別のバックエンドとIAMを置けるので、爆発半径が狭くなります。そのため、本番はディレクトリによる分離、短期のテスト環境はワークスペースが、基本の公式です。
現場での姿
ドリフトは、3つのタイプに分かれます。属性が変わった構成ドリフト、コードの外で作成または削除された存在ドリフト、参照関係が壊れた依存関係ドリフトです。原因も決まっています。コンソールでの手動変更、オートスケーリングのようなシステムの自動変更、プロバイダーのアップグレードによるデフォルト値の変更、並列のapplyです。
検知は、最低でも1日に1回は回し、深刻度のルールを付けます。セキュリティグループやIAMポリシーの変更はCRITICAL、削除や置き換えのアクションは、無条件でHIGH以上です。ただし、すべてのドリフトを検知しようとすると、false positiveがあふれます。オートスケーラーが管理するdesired_capacityやdesired_sizeのようなフィールドは、ignore_changesで外さなければ、アラートがシグナルとして残りません。
最後は文化です。緊急の障害対応で、コンソールへのアクセスを完全に禁止するのは非現実的です。その代わり、緊急の変更から24時間以内にコードを同期する文化を定着させ、自動検知がこれを監視するようにします。ルールで防ぐ代わりに、元に戻ってくるようにする設計です。
状態ファイルを失ったときと、ロックしたとき
状態は、コードでも実際のリソースでもない第3の真実です。この3つが食い違う方式が、そのままIaCで経験する事故の一覧です。
状態がなくなると、リソースは残り、管理だけが消えます。Terraformは、状態にないリソースを「まだ作っていないもの」と見なすので、次のapplyで同じものをまた作ろうとして名前の衝突で失敗したり、名前が自動生成されるリソースなら、静かに2つになったりします。復旧は、importで1つずつ元に戻す方法しかありません。そのため、リモートバックエンドには、バージョン管理と削除保護を必ず一緒に有効にします。
状態には秘密情報が平文で入ります。DBのパスワードを変数で渡したなら、その値が状態ファイルにそのまま書かれます。出力でsensitive = trueで隠しても、保存される値は同じです。状態ストレージをシークレットストアと同じレベルで扱う必要がある理由です。S3なら暗号化とアクセスポリシーを、ローカルならそもそもコミットしないように、.gitignoreを先に確認します。
ロックがないと、2人が同時に別の未来を書きます。2つのapplyが重なると、あとに終わったほうが、前の結果を消した状態をアップロードします。リソースは作られたのに状態にはない、最も直しにくい状態になります。S3バックエンドはDynamoDBテーブルで、GCSとAzureは独自の機能でロックします。
ロックが残っているときに、むやみに解除しません。CIが途中で死ぬと、ロックが残ります。force-unlockは、本当に誰も実行していないときだけ使います。実行中のapplyを解除してしまうと、前の段落の状況を手で作り出すようなものです。ロック情報に書かれた人と時刻を、先に確認します。
手で直したものは、次のプランで明らかになります。急いでコンソールで変更した設定は、terraform planに元に戻す予定として現れます。このときの選択は、2つです。コードを現実に合わせるか、現実をコードに合わせるかです。3つ目の選択である「とりあえず無視」を選ぶと、そのリソースは、次の人が何気なくapplyしたときに、元に戻ります。
terraform plan -refresh-only # 코드는 그대로 두고, 현실과의 차이만 본다
terraform state list # 상태가 아는 자원의 목록
terraform state show <주소> # 상태가 기억하는 속성
次のラボですること
本物の宣言的ツール(OpenTofu)で、この文章の事故を順番に起こします。状態ファイルを移して同じサーバーが2つになるのを見て、lineageの違う状態を上書きしようとして拒否され、importで失ったリソースを取り戻し、適用中の状態ロックに阻まれ、寿命の違う層の状態を分けます。続くラボでは、sensitiveで隠した秘密情報が、状態・プランのファイルのどこに平文で残るかを探し、状態の暗号化で防ぎます。2つのラボのあとのクイズで、状態ファイルの存在理由、機密情報の扱い、ロックと強制解除の危険、状態の分割と爆発半径を点検します。