Terraform 基礎 #9 リモート state とチーム協業 — S3 バックエンド、ネイティブロック、機密情報の保護

読了 5分

#5 の結論は「ローカルディスクは state の保管場所として不適切」でした。基礎シリーズの最終回で、その宿題を片付けます。state を S3 へ移してロックをかけ、1 人で使っていた Terraform をチームで共有できる状態にします。この 1 本を終えれば、実践シリーズでインフラ全体を立ち上げる準備が整います。

ローカル state の 3 つの問題 #

terraform.tfstate がノート PC にしかないとき、チームには 3 つの問題が生まれます。

  1. 共有できません: 同僚が同じコードを受け取って apply すると、同僚の空の state を基準に Terraform がすべてのリソースをもう一度作ろうとします。state のないコードは半分だけの存在です。
  2. 同時実行を防げません: 2 人が同じインフラに同時に apply すると、それぞれの計算が互いを知らないまま進み、state がずれたりリソースが重複したりします。
  3. 失いやすいです: ノート PC の紛失、ディスクの故障、誤削除。#5 で見たとおり、state を失えば import という手作業が待っています。

この 3 つを一度に解決するのがリモートバックエンド(backend)です。state をチームで共有するストレージに置き、Terraform が実行のたびにそこから読み書きするようにします。AWS 環境での標準的な選択は S3 です。

state バケットの準備 #

まず state を入れるバケットが必要です。このバケット 1 つは、鶏と卵の問題(state を入れるバケットを Terraform で作ったら、その state はどこに置くのか)を避けるため、コンソールか CLI で作るのが一般的です。

バケット作成
aws s3api create-bucket --bucket my-team-tfstate \
  --region ap-northeast-2 \
  --create-bucket-configuration LocationConstraint=ap-northeast-2
aws s3api put-bucket-versioning --bucket my-team-tfstate \
  --versioning-configuration Status=Enabled

バージョニングは任意ではなく必須です。 state が壊れたり誤った操作で上書きされたりしたとき、バージョニングが有効なら以前のバージョンに戻せます。state 事故に対する最後の保険です。S3 のデフォルト暗号化(SSE-S3)は最近のバケットには自動で適用されますが、組織のポリシーによっては KMS キー暗号化へ引き上げることもできます。

backend ブロックとマイグレーション #

バックエンドは terraform ブロックの中に宣言します。

main.tf
terraform {
  required_version = ">= 1.15.0"

  backend "s3" {
    bucket       = "my-team-tfstate"
    key          = "myapp/prod/terraform.tfstate"
    region       = "ap-northeast-2"
    use_lockfile = true
  }
}

key はバケットの中でこのプロジェクトの state が置かれるパスです。1 つのバケットに複数のプロジェクト・環境の state を key で区別して入れるのが一般的なので、プロジェクト/環境/terraform.tfstate のようなルールを最初から決めておくのが良いです。ブロックを追加して terraform init を実行すると、Terraform が既存のローカル state を発見して質問してきます。

実行結果
Initializing the backend...
Do you want to copy existing state to the new backend?
  Enter a value: yes

yes と答えるとローカル state が S3 にコピーされ、以後のすべての plan・apply は S3 の state を読み書きします。ローカルの terraform.tfstate はもう使われないので、確認したら削除して構いません。この時点から同僚も同じコードで init するだけで同じ state を見るようになり、問題 1 と 3 が解決しました。

use_lockfile — 同時実行のロック #

残っているのは同時実行です。上の設定の use_lockfile = true がその答えです。apply が始まると Terraform が state ファイルの隣に .tflock オブジェクトを作ってロックを示し、終わると削除します。ロックがある間に他の人が実行すると、このように拒否されます。

出力例
Error: Error acquiring the state lock
Lock Info:
  Who:       bob@laptop
  Created:   2026-08-30 09:12:44 UTC

この S3 ネイティブロックは Terraform 1.11 で正式(GA)になりました。それ以前はロック専用の DynamoDB テーブルを別に作って dynamodb_table 引数でつなぐ方式が標準で、検索するとまだその構成が多く出てきます。DynamoDB 方式は廃止予定の状態で、新しい構成では use_lockfile だけで十分です。チーム全員の Terraform が 1.11 以上である、という条件だけ確認すれば大丈夫です。

ロックエラーに遭遇したときの原則も知っておくべきです。ほとんどの場合は同僚の apply が進行中という正常なシグナルなので、待つのが正解です。apply が強制終了されてロックだけ残ることがまれにあり、そのときだけ terraform force-unlock <ロックID> で解除します。Lock Info の Who を確認して、実行中の人が本当にいないことを確かめてから使う最後の手段であって、エラーが出た瞬間に反射的に打つコマンドではありません。

state バケットのアクセス制御 #

state がチーム共有のストレージに上がった瞬間、#5 で見た平文の機密情報の問題がチームの問題になります。state バケットを読める人は、その中のデータベースパスワードも読めます。だから state バケットには最小アクセスの原則を適用します。パブリックブロックはもちろん、Terraform を実行するロールにだけ該当 key パスの読み書きを許可する IAM ポリシーで絞ります。機密情報を state にそもそも入れない設計(Secrets Manager 参照など)は、実践シリーズのシークレット回で扱います。

基礎シリーズを終えて #

9 本をまとめるとこうなります。インフラを宣言として書き(#1)、HCL で表現し(#2)、値を変数に切り出し(#3)、既存のものを読み込み(#4)、state という中心を理解し(#5)、依存と繰り返しを扱い(#6)、ライフサイクルを制御し(#7)、モジュールで構造化し(#8)、今日チーム協業の土台を作りました。文法と原理はそろい、残っているのは実際のインフラを立ち上げる経験です。

この記事で扱った内容です。

  • ローカル state は共有不可、同時実行の衝突、紛失リスクという 3 つの問題を持ち、リモートバックエンドがこれを一度に解決します
  • state バケットは Terraform の外で作り、バージョニングを必ず有効にします。事故時の以前バージョン復元が最後の保険です
  • backend "s3" ブロックを追加した後、init が既存 state のマイグレーションを案内します。key のパスルールは最初から決めます
  • ロックは use_lockfile = true が現行標準です(1.11 GA)。DynamoDB テーブル方式は廃止予定の状態で、force-unlock は実行者がいないことを確認した後の最後の手段です
  • state バケットは平文の機密情報を含むため、IAM でアクセスを最小化します

次は Terraform 実践シリーズです。VPC ネットワークからドメインと HTTPS まで、Web サービスのインフラ 1 つを 10 本かけて最初から最後までコードで立ち上げます。

X