Skip to content
Published on

Terraform State管理 完全ガイド — Remote Backend、Locking、マイグレーション実践

Authors
  • Name
    Twitter
Terraform State Management

はじめに

Terraformを一人で使う場合は、ローカルのterraform.tfstateファイルで十分です。しかし、チーム環境ではStateの競合同時修正Stateの消失といった問題が発生します。この記事では、Terraform Stateを安全に管理する方法を実践的な例とともに解説します。

Terraform Stateとは?

Terraform Stateは、インフラの現在の状態を追跡するJSONファイルです。

# Stateファイルの確認
cat terraform.tfstate | jq '.resources[0]'
{
  "mode": "managed",
  "type": "aws_instance",
  "name": "web",
  "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
  "instances": [
    {
      "attributes": {
        "id": "i-0abc123def456789",
        "ami": "ami-0c55b159cbfafe1f0",
        "instance_type": "t3.medium"
      }
    }
  ]
}

Stateが重要な理由

  1. マッピング: Terraformコードと実際のリソースのマッピング
  2. 依存関係の追跡: リソース間の依存関係を記録
  3. パフォーマンス: API呼び出しの最小化(まずStateで確認)
  4. 協業: チームメンバー間でインフラ状態を共有

Remote Backendの構成

AWS S3 + DynamoDB

最も一般的なRemote Backend構成です:

# backend.tf - S3 Backend設定
terraform {
  backend "s3" {
    bucket         = "my-terraform-state-bucket"
    key            = "prod/networking/terraform.tfstate"
    region         = "ap-northeast-2"
    encrypt        = true
    dynamodb_table = "terraform-state-lock"

    # 追加セキュリティ設定
    kms_key_id     = "alias/terraform-state"
  }
}

S3バケットの作成(ブートストラップ)

# bootstrap/main.tf - Stateストレージのブートストラップ
provider "aws" {
  region = "ap-northeast-2"
}

resource "aws_s3_bucket" "terraform_state" {
  bucket = "my-terraform-state-bucket"

  lifecycle {
    prevent_destroy = true
  }
}

resource "aws_s3_bucket_versioning" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id
  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id
  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm     = "aws:kms"
      kms_master_key_id = aws_kms_key.terraform_state.arn
    }
  }
}

resource "aws_s3_bucket_public_access_block" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

# State LockingのためのDynamoDB
resource "aws_dynamodb_table" "terraform_lock" {
  name         = "terraform-state-lock"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "LockID"

  attribute {
    name = "LockID"
    type = "S"
  }
}

# 暗号化のためのKMS Key
resource "aws_kms_key" "terraform_state" {
  description             = "KMS key for Terraform state encryption"
  deletion_window_in_days = 30
  enable_key_rotation     = true
}

resource "aws_kms_alias" "terraform_state" {
  name          = "alias/terraform-state"
  target_key_id = aws_kms_key.terraform_state.key_id
}
# ブートストラップの実行(まずローカルStateで作成)
cd bootstrap
terraform init
terraform apply

# ブートストラップ自体のStateもS3にマイグレーション可能

GCS(Google Cloud Storage)

terraform {
  backend "gcs" {
    bucket  = "my-terraform-state"
    prefix  = "prod/networking"

    # GCSは自体でState Lockingをサポート
    # DynamoDBのような別途のLockテーブル不要
  }
}
# GCSバケットの作成
gsutil mb -p my-project -l asia-northeast3 gs://my-terraform-state
gsutil versioning set on gs://my-terraform-state

Azure Blob Storage

terraform {
  backend "azurerm" {
    resource_group_name  = "terraform-state-rg"
    storage_account_name = "tfstate2026"
    container_name       = "tfstate"
    key                  = "prod/networking/terraform.tfstate"

    # Azure Blob Leaseによる自動Locking
  }
}

State Lockingの詳細

DynamoDB Lockの動作原理

# Lock取得のプロセス
# 1. terraform applyを実行
# 2. DynamoDBにLockID項目を作成(PutItem with ConditionExpression)
# 3. 他のユーザーがapplyを試みるとLock競合エラーが発生

# Lock状態の確認
aws dynamodb get-item \
  --table-name terraform-state-lock \
  --key '{"LockID": {"S": "my-terraform-state-bucket/prod/networking/terraform.tfstate"}}' \
  | jq '.Item.Info.S | fromjson'

Lockが保持されている場合

# Lock情報の確認
terraform force-unlock <LOCK_ID>

# 注意: force-unlockは本当にLockが不正に残っている場合のみ使用
# 他のユーザーが作業中の場合、Stateが破損する可能性がある

Lockタイムアウトの設定

# Lock待機時間の設定(デフォルト0s = 即座に失敗)
terraform apply -lock-timeout=5m

State分離戦略

環境別の分離

terraform/
├── modules/
│   ├── networking/
│   ├── compute/
│   └── database/
├── environments/
│   ├── dev/
│   │   ├── main.tf
│   │   └── backend.tf  # key = "dev/terraform.tfstate"
│   ├── staging/
│   │   ├── main.tf
│   │   └── backend.tf  # key = "staging/terraform.tfstate"
│   └── prod/
│       ├── main.tf
│       └── backend.tf  # key = "prod/terraform.tfstate"

Workspaceの活用

# Workspaceの作成
terraform workspace new dev
terraform workspace new staging
terraform workspace new prod

# Workspaceの切り替え
terraform workspace select prod

# 現在のWorkspaceを確認
terraform workspace show
# Workspaceに応じて設定を分岐
locals {
  env = terraform.workspace

  instance_type = {
    dev     = "t3.small"
    staging = "t3.medium"
    prod    = "t3.large"
  }
}

resource "aws_instance" "web" {
  instance_type = local.instance_type[local.env]
  # ...
}

TerragruntでDRYに管理

# terragrunt.hcl(ルート)
remote_state {
  backend = "s3"
  generate = {
    path      = "backend.tf"
    if_exists = "overwrite_terragrunt"
  }
  config = {
    bucket         = "my-terraform-state"
    key            = "${path_relative_to_include()}/terraform.tfstate"
    region         = "ap-northeast-2"
    encrypt        = true
    dynamodb_table = "terraform-state-lock"
  }
}
# environments/prod/networking/terragrunt.hcl
include "root" {
  path = find_in_parent_folders()
}

terraform {
  source = "../../../modules/networking"
}

inputs = {
  vpc_cidr = "10.0.0.0/16"
  environment = "prod"
}

Stateのマイグレーション

ローカルからRemoteへ

# 1. backend.tfにRemote Backend設定を追加
# 2. terraform initを実行
terraform init

# Terraformが自動的にマイグレーションを提案
# "Do you want to copy existing state to the new backend?"
# → yesを入力

State間のリソース移動

# Stateからリソースを削除(実際のインフラは維持)
terraform state rm aws_instance.old_web

# 別のStateにリソースをインポート
terraform import aws_instance.new_web i-0abc123def456789

# State間の移動(Terraform 1.1以降)
terraform state mv -state-out=../other/terraform.tfstate \
  aws_instance.web aws_instance.web

movedブロック(Terraform 1.1以降)

# リソース名変更時にStateを自動更新
moved {
  from = aws_instance.web
  to   = aws_instance.web_server
}

moved {
  from = module.old_vpc
  to   = module.networking
}

Stateのバックアップとリカバリ

S3バージョニングによるリカバリ

# Stateファイルのバージョン一覧を確認
aws s3api list-object-versions \
  --bucket my-terraform-state \
  --prefix prod/networking/terraform.tfstate \
  | jq '.Versions[:5] | .[] | {VersionId, LastModified, Size}'

# 特定のバージョンに復元
aws s3api get-object \
  --bucket my-terraform-state \
  --key prod/networking/terraform.tfstate \
  --version-id "abc123" \
  terraform.tfstate.backup

# 復元されたStateをpush
terraform state push terraform.tfstate.backup

Stateバックアップの自動化

#!/bin/bash
# backup-state.sh
DATE=$(date +%Y%m%d-%H%M%S)
BUCKET="my-terraform-state-backup"

# すべてのStateファイルをバックアップ
aws s3 sync s3://my-terraform-state/ s3://$BUCKET/$DATE/ \
  --include "*.tfstate"

echo "Backup completed: $BUCKET/$DATE"

セキュリティベストプラクティス

1. Stateファイルの暗号化

# S3 SSE-KMS暗号化(必須)
backend "s3" {
  encrypt    = true
  kms_key_id = "alias/terraform-state"
}

2. IAMポリシーの最小権限

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
      "Resource": "arn:aws:s3:::my-terraform-state/*"
    },
    {
      "Effect": "Allow",
      "Action": ["s3:ListBucket"],
      "Resource": "arn:aws:s3:::my-terraform-state"
    },
    {
      "Effect": "Allow",
      "Action": ["dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem"],
      "Resource": "arn:aws:dynamodb:*:*:table/terraform-state-lock"
    }
  ]
}

3. sensitiveな値の保護

# Stateファイルに機密データが含まれる可能性がある
output "db_password" {
  value     = aws_db_instance.main.password
  sensitive = true
}

# terraform.tfstateから直接確認は可能なため、
# Stateファイル自体のアクセス制御が重要

まとめ

Terraform State管理はIaCの基盤です。重要なポイント:

  1. Remote Backendは必須: S3/GCS + Lockingでチーム協業を安全に
  2. Stateの分離: 環境/サービス別にStateを分離してblast radiusを最小化
  3. バージョン管理: S3 VersioningでStateのリカバリを可能に
  4. セキュリティ: 暗号化 + IAM最小権限 + sensitiveマーキング

クイズ(6問)

Q1. Terraform Stateの主な役割3つは? リソースマッピング、依存関係の追跡、API呼び出しの最小化(パフォーマンス)

Q2. AWSでState Lockingに使用されるサービスは? DynamoDB

Q3. GCS Backendで別途のLockテーブルが不要な理由は? GCSが自体でObject Lockingをサポートしているため

Q4. terraform force-unlockはいつ使用すべきか? Lockが異常に残っている場合のみ使用。他のユーザーが作業中の場合は使用してはならない

Q5. movedブロックの用途は? リソース名の変更時にStateを自動更新し、リソースの再作成を防止する

Q6. Stateファイルにsensitiveな値が含まれる可能性があるため、どのようなセキュリティ対策が必要か? Stateファイル自体の暗号化(SSE-KMS)とIAMアクセス制御