宣言的インフラと状態ファイルという台帳
一言でいうと
Terraformは、「このコマンドを実行せよ」ではなく、「終わったときにこのようなインフラがあるべきだ」を書くツールです。その約束を守るために、ツールは状態ファイルという帳簿を持ちます。
なぜ必要なのか
コンソールでクリックしてサーバーを作ると、3つのものが同時に失われます。同じ環境を作り直す方法、誰がいつ何を変更したかの記録、そして開発と本番が同じだという確信です。サーバー10台までは、人が覚えて持ちこたえますが、100台は不可能です。dev・stage・prodが微妙に食い違う構成ドリフトは、まさにこの地点から始まります。
そこで、シェルスクリプトに移してみると、新しい問題が出てきます。最初は「作れ」の1行だったのが、2回目の実行で「すでにある」というエラーが出るので条件文を付け、属性が変わっているかもしれないので比較文を付けます。結局、スクリプトの半分が現在の状態を確認するコードになります。宣言的なツールは、その半分を丸ごと引き受けます。人は望ましい姿だけを書き、現在と望ましいものの差を計算する仕事は、ツールが行います。
ここで本当の疑問が出てきます。ツールは「現在の状態」をどうやって知るのか。毎回クラウド全体をなぞるのは遅く、何より、その膨大なリソースの中から、何が自分が作ったものかを区別する方法がありません。この問題の答えが、状態ファイルです。
どう動くのか
planは、3つの値を比較します。
| 値 | どこにあるか | 意味 |
|---|---|---|
| 望ましい状態 | .tfコード |
人が宣言したもの |
| 最後に知っている状態 | terraform.tfstate |
前回の適用の結果 |
| 実際の状態 | プロバイダーのAPI | 今本当に存在するもの |
まず実際を照会して(refresh)状態を更新し、更新された状態とコードを比較して、プランを作ります。そのため、状態ファイルがなければ、ツールは「初めて見るインフラ」と判断して、すべてを新しく作ろうとします。状態ファイルが、インフラ自体よりも危険な資産である理由が、ここにあります。
状態ファイルはJSONで、実務で実際に読むフィールドは、いくつもありません。
{
"version": 4,
"serial": 42,
"lineage": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"resources": [
{ "mode": "managed", "type": "local_file", "name": "hello",
"instances": [ { "attributes": { "filename": "/root/out/hello.txt" },
"dependencies": ["random_pet.suffix"] } ] }
]
}
ここで、attributesは最後に適用された結果の記録であるという点が重要です。状態に書かれた値と、実際のリソースの値が食い違っていれば、それ自体がドリフトの定義です。そのため、状態を読むときは、習慣的に「この値は実際と同じか」も一緒に確認する癖が必要です。
versionは、状態ファイルのフォーマットのバージョンで、現在は4です。serialは、状態が変わるたびに上がる番号で、リモートバックエンドで、同時の更新の競合を捕まえるのに使われます。lineageは、この状態ファイルの系譜を表す固有の識別子で、別のプロジェクトの状態を誤って上書きする事故を防ぎます。resourcesの中のtypeとnameをつなげたlocal_file.helloが、私たちが扱うアドレスです。
一方、initは、コードのrequired_providersを読んで、プロバイダーをダウンロードし、.terraform.lock.hclに、確定したバージョンとハッシュを書いておきます。このロックファイルは、リポジトリにコミットする必要があります。そうすれば、自分のノートパソコンとCIが、同じプロバイダーを使います。
現場での姿
1つ目は、状態ファイルをGitにコミットする事故です。状態には、データベースのパスワードやトークンが平文で入ります。しかも、2人がそれぞれapplyすると、マージの競合が起き、誤ってマージされた状態は、リソースを丸ごと失わせます。そのため、実務では、S3のようなリモートバックエンドにバージョニングを有効にして置き、ロックで同時のapplyを防ぎます。AWSなら、長い間、S3+DynamoDBの組み合わせが標準でした。
2つ目は、系譜が違う状態を上書きした日です。チームがバックエンドのキーを間違えて、別のスタックの状態の上に、自分の状態をアップロードしてしまうと、次のplanは「数百個を削除する」と言います。このとき、復活するのは、S3バージョニングの以前のバージョンです。lineageは、そのような事故を事前に察知するために存在するフィールドです。
3つ目は、状態の中の秘密情報です。OpenTofuは、状態とプランファイルの暗号化を、ツール自体でサポートします。Terraformで同じレベルを得るには、有料のサービスが必要です。コマンド名とサブコマンドの構造はほとんど同じなので、このラボでtofuで学んだことは、terraformにそのまま持ち越せます。
次のラボですること
/root/tf/firstで、プロバイダーを取得して初期化し、ファイルを1つ宣言して適用します。続けて、ランダムな名前を作るリソースを加え、宣言を変えて再び適用しながら、serialが上がるのを目で確認します。一時的なリソースを1つ作ってから、それだけを選んで削除し、状態に残ったアドレスの一覧を取り出します。最後に、値をコピーする代わりに参照して、依存関係が状態に記録されるようにします。