Terraformはstateが分かれば怖くない|AWSなしで試す入門

Terraformはstateが分かれば怖くない|AWSなしで試す入門

Terraformでつまずくのは、HCLの書き方ではありません。「state(状態ファイル)とは何なのか」と「plan の出力をどう読むか」です。ここが分かっていないと、書けるけれど怖くて本番に流せない、という状態が続きます。

この2つは、AWSアカウントなしで確かめられます。ローカルにファイルを作るだけのプロバイダーを使えば、課金も権限設定もリソースの消し忘れも起きません。この記事のコマンドと出力は、すべて実際に手元で実行したものです(Terraform v1.14.4)。

目次

まず動かす|書くのは15行

空のディレクトリに main.tf を1つ置きます。

terraform {
  required_providers {
    local = {
      source  = "hashicorp/local"
      version = "~> 2.5"
    }
  }
}

variable "environment" {
  type    = string
  default = "dev"
}

resource "local_file" "config" {
  filename = "${path.module}/out/${var.environment}.conf"
  content  = "environment = ${var.environment}\nreplicas    = 2\n"
}

terraform init でプロバイダーを取ってきます。

$ terraform init

Initializing provider plugins...
- Finding hashicorp/local versions matching "~> 2.5"...
- Installing hashicorp/local v2.9.0...
- Installed hashicorp/local v2.9.0 (signed by HashiCorp)

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository

最後の1行は覚えておいてください。.terraform.lock.hcl はコミットするファイルです。これがないと、チームの各自やCIで違うバージョンのプロバイダーが入り、同じコードなのに結果が変わります。

plan は「これから何をするか」の宣言

$ terraform plan

  # local_file.config will be created
  + resource "local_file" "config" {
      + content              = <<-EOT
            environment = dev
            replicas    = 2
        EOT
      + filename             = "./out/dev.conf"
      + id                   = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.

記号に意味があります。

記号意味
+作成する
~その場で変更する
-削除する
-/+削除してから作り直す

(known after apply) は「実行してみないと決まらない値」です。IDのように、作成後にプロバイダー側が採番するものがこれになります。

そしてplan を読むとき、最初に見るべきなのは最終行です。

Plan: 1 to add, 0 to change, 0 to destroy.

差分が長いときほど、上から順に読んでいると全体を見落とします。to destroy が 0 でないなら、そこで一度止まって理由を確認するのが習慣として一番効きます。

apply して state を見る

$ terraform apply -auto-approve

local_file.config: Creating...
local_file.config: Creation complete after 1s [id=893231be2a6406e0625bdb02bd46e0844045c3f8]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

$ cat out/dev.conf
environment = dev
replicas    = 2

$ terraform state list
local_file.config

ここで terraform.tfstate というファイルができています。これがstateです。「Terraformが自分で作ったものの台帳」だと思ってください。

stateの正体|手で変えると何が起きるか

コード・state・実物の3つをplanが突き合わせる関係と、手作業による変更が実物だけをズラすことを示した図
plan はこの3つを突き合わせている。差分の正体はいつもこのどれかのズレ

Terraformの動きを理解する一番早い方法は、Terraformが作ったものを手で変えてみることです。作られたファイルに1行足してみます。

$ echo "replicas = 99" >> out/dev.conf

$ terraform plan

  # local_file.config will be created
  + resource "local_file" "config" {
      ...
Plan: 1 to add, 0 to change, 0 to destroy.

「作成する」と言っています。ファイルは存在するのに、です。

Terraformは plan のたびに実物を確認しにいきます。そして「stateに記録した内容と実物が違う」ことを検出し、自分が管理している状態に戻そうとします。これがドリフト(drift)です。手で足した replicas = 99 は、次の apply で消えます。

ここがTerraform運用の核心です。

  • コードが正であり、手作業の変更は「間違い」として扱われる
    マネジメントコンソールでの変更は、次の apply で巻き戻される
  • stateに載っていないものは、Terraformにとって存在しない
    手で作ったリソースは、Terraformからは見えず、消えることもない

「急いでいたのでコンソールで直した」が事故になるのはこのためです。逆に言えば、コンソールで直したくなった時点で、コードに反映する手順を用意していないという設計の問題があります。

製図台の上に置かれた図面のロールと建物の白い模型を真上から撮った写真
実物の前に、図面と模型がある

「変更」と「作り直し」は別物

もっとも事故につながるのがここです。設定の replicas を 2 から 3 に変えて plan を取ります。

$ terraform plan

      ~ content = <<-EOT # forces replacement
      ~ id       = "893231be2a..." -> (known after apply)

Plan: 1 to add, 0 to change, 1 to destroy.

中身を少し変えただけなのに、1 to destroy、つまり一度削除されます。理由は content の行に書いてある # forces replacement です。

変数を変えた場合はもっとはっきり出ます。

$ terraform plan -var environment=prod

  # local_file.config must be replaced
      ~ filename = "./out/dev.conf" -> "./out/prod.conf" # forces replacement

Plan: 1 to add, 0 to change, 1 to destroy.

ここではただのテキストファイルですが、同じことがRDSやEBSでも起きます。あとから変更できない属性を書き換えると、Terraformは淡々と「削除して作り直す」計画を立てます。データは戻りません。

覚えておくべきことは1つです。forces replacement と must be replaced を見たら、apply の前に必ず止まる。この2つの文字列を plan の出力から探す癖をつけるだけで、防げる事故がかなりあります。

どの属性が作り直しを引き起こすかはリソースごとに違うので、プロバイダーのドキュメントで Forces new resource の記載を確認します。

実務に移すときに追加で要るもの

ここまではローカルで完結する話です。実際のクラウド環境に使うときは、次が必要になります。

stateをローカルに置かない

stateがローカルにあると、複数人で作業したときに互いの変更を上書きします。S3などのリモートバックエンドに置き、同時実行をロックできる構成にします。

tfstateをコミットしない

# .gitignore
*.tfstate
*.tfstate.*
.terraform/
*.tfvars       # 値によっては除外する

# .terraform.lock.hcl はコミットする(除外しない)

理由は運用上の都合ではなくセキュリティです。tfstateには、リソースの属性が平文で記録されます。データベースの初期パスワードやトークンを扱っていれば、それも入ります。公開リポジトリに上げると認証情報の流出になります。

複数リソースは count ではなく for_each

同じリソースを複数作るとき、count はインデックスで管理されるため、途中の要素を消すと後続がすべてずれて作り直しになります。キーで管理される for_each を使うのが基本です。この挙動の詳細はVPCのCIDR設計の記事で具体例つきで書いています。

CIでは plan と apply を分ける

terraform plan -out=tfplan で計画をファイルに保存し、レビュー後に terraform apply tfplan でそれを適用します。こうしないと、レビューした内容と実際に適用される内容がずれる可能性があります。plan と apply の間に誰かが別の変更を入れた場合が典型です。

最初に押さえる5つ

  1. stateはTerraformが作ったものの台帳
    載っていないものは存在しないのと同じ
  2. 手で変えたものは巻き戻される
    コンソールでの変更は次のapplyで消える
  3. planは最終行から読む
    to destroy が0でなければ止まる
  4. forces replacement は削除と再作成
    データを持つリソースでは致命的になりうる
  5. tfstateは秘密情報
    コミットしない。.terraform.lock.hcl はコミットする

この5つは、ローカルプロバイダーで全部確認できます。AWSアカウントを触る前に、消えても困らない場所で一通り壊してみるのが、結局いちばん早い理解の仕方だと思います。

次に読む記事

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

わどこんのアバター わどこん

実務12年のバックエンド・インフラエンジニア。バックエンド開発からクラウド・インフラの設計・構築・運用まで担当しています。主要言語は Java・Kotlin・PHP・Python。運用の現場で拾った知見を、再現できる手順に落として残すのがこのブログのテーマです。

目次