🙌

Terraform地獄の実録:5つの絶望的なトラブルと血と汗の解決記録

に公開

プロローグ:ある金曜日の午後

時刻は金曜日の午後4時30分。来週のリリースに向けて、AWS Provider v4からv5への移行を実行しようとしていた。

「まあ、マイグレーションガイドも読んだし、30分もあれば終わるでしょ」

そんな軽い気持ちでterraform applyを実行した瞬間、私の週末は完全に消え去った。

以下は、その週末を含む2週間にわたって遭遇した5つの地獄的なトラブルと、それぞれの血と汗と涙の解決記録です。同じ悪夢を見る人を一人でも減らすために、すべて包み隠さず記録します。


🔥 トラブル1: S3バケット設定の壊滅的な変更

💀 地獄の始まり

$ terraform apply
...
Plan: 0 to add, 15 to change, 12 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to confirm.

  Enter a value: yes

aws_s3_bucket.app_bucket: Destroying... [id=my-app-bucket]
aws_s3_bucket_acl.app_bucket: Creating...
Error: creating S3 Bucket ACL (my-app-bucket): NoSuchBucket: The specified bucket does not exist

状況: AWS Provider v5で、S3バケットのACLが別リソースに分離されたことは知っていた。しかし、既存のバケットが削除され、新しいバケットとACLリソースが作成される流れで、順序の問題により失敗。

影響範囲:

  • 本番環境のアプリケーションログが格納されているS3バケット
  • 静的ウェブサイトのアセット
  • データ分析用のCSVファイル(3年分)

🩹 試行錯誤の記録

第1試行:パニック状態での雑な対応

# パニックになって、とりあえずロールバックを試行
$ terraform apply -target=aws_s3_bucket.app_bucket
Error: Resource targeting is only supported in plan mode.

# planでターゲット指定
$ terraform plan -target=aws_s3_bucket.app_bucket
Error: Resource 'aws_s3_bucket.app_bucket' not found in configuration.

失敗理由: プロバイダーv5の設定に既に変更していたため、v4形式のリソースが認識されない。

第2試行:状態ファイルの手動修正

# 状態ファイルを直接編集してリソースを復活させようとした
$ terraform show
# (出力を見ると、バケットの状態が消えている)

$ cp terraform.tfstate terraform.tfstate.backup
$ vim terraform.tfstate  # 手動でリソース情報を復活させようとした

失敗理由: 状態ファイルの構造が複雑すぎて、手動修正は現実的でない。しかも、実際のAWSリソース(バケット)は既に削除されているため、状態だけ戻しても意味がない。

第3試行:AWS CLIでバケットを手動復旧

# バケットを手動で再作成
$ aws s3 mb s3://my-app-bucket
make_bucket: my-app-bucket

# しかし、Terraformの状態には反映されない
$ terraform plan
Plan: 1 to add, 0 to change, 0 to destroy.
# 既存のバケットを認識しない

失敗理由: Terraformの状態とAWSの実際の状態が不整合。importコマンドが必要だが、新しいリソース構造での正しいimport方法がわからない。

✅ 最終的な解決方法

ステップ1: 現状の完全な把握

# バックアップから状態ファイルを確認
$ cp terraform.tfstate.backup terraform.tfstate.old
$ terraform show terraform.tfstate.old > old_state_readable.txt

# 現在のAWSの実際の状態を確認
$ aws s3api list-buckets --query 'Buckets[?Name==`my-app-bucket`]'
[]  # バケットは削除されている

# 削除されたバケットの中身は?
$ aws s3 ls s3://my-app-bucket/
An error occurred (NoSuchBucket) when calling the ListObjectsV2 operation: The specified bucket does not exist

重要な発見: バケットは完全に削除されており、中のデータも失われている。これは災害レベルの事態。

ステップ2: データの復旧

# CloudTrailログを確認して削除タイミングを特定
$ aws logs describe-log-groups --log-group-name-prefix="cloudtrail"

# S3のバケット削除イベントを特定
$ aws logs filter-log-events \
  --log-group-name /aws/cloudtrail/my-app-logs \
  --start-time 1700000000000 \
  --filter-pattern "{ $.eventName = DeleteBucket }"

# 幸い、バケットのバージョニングが有効だった場合の復旧確認
$ aws s3api list-object-versions --bucket my-app-bucket
# (削除されているので当然エラー)

絶望的な発見: データのバックアップが...実は別の場所にあった!

# ログが別のS3バケットにもレプリケーションされていたことを発見
$ aws s3 ls s3://my-app-backup-bucket/logs/
2024-11-28 10:30:45   12345 application.log
2024-11-28 11:15:22   8901 access.log
...

# 実は災害復旧用のバケットがあった(設定を忘れていた)
$ aws s3 sync s3://my-app-backup-bucket/logs/ ./local_backup/

ステップ3: 正しいv5設定でリソース再構築

# terraform/main.tf (修正版)

# v5対応のS3バケット設定
resource "aws_s3_bucket" "app_bucket" {
  bucket = "my-app-bucket"
  
  tags = {
    Environment = var.environment
    Purpose     = "application-logs"
  }
}

# ACLは別リソースで管理
resource "aws_s3_bucket_acl" "app_bucket" {
  depends_on = [aws_s3_bucket_ownership_controls.app_bucket]
  
  bucket = aws_s3_bucket.app_bucket.id
  acl    = "private"
}

# オブジェクト所有権の設定(v5で必須)
resource "aws_s3_bucket_ownership_controls" "app_bucket" {
  bucket = aws_s3_bucket.app_bucket.id

  rule {
    object_ownership = "BucketOwnerPreferred"
  }
}

# バージョニング設定(v5では別リソース)
resource "aws_s3_bucket_versioning" "app_bucket" {
  bucket = aws_s3_bucket.app_bucket.id
  versioning_configuration {
    status = "Enabled"
  }
}

# サーバーサイド暗号化(v5では別リソース)
resource "aws_s3_bucket_server_side_encryption_configuration" "app_bucket" {
  bucket = aws_s3_bucket.app_bucket.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256"
    }
  }
}

# レプリケーション設定(災害復旧用)
resource "aws_s3_bucket_replication_configuration" "app_bucket" {
  depends_on = [aws_s3_bucket_versioning.app_bucket]
  
  role   = aws_iam_role.replication.arn
  bucket = aws_s3_bucket.app_bucket.id

  rule {
    id     = "replicate_everything"
    status = "Enabled"

    destination {
      bucket        = aws_s3_bucket.backup_bucket.arn
      storage_class = "STANDARD_IA"
    }
  }
}

ステップ4: 段階的な復旧実行

# 1. バケットのみ作成
$ terraform apply -target=aws_s3_bucket.app_bucket
aws_s3_bucket.app_bucket: Creating...
aws_s3_bucket.app_bucket: Creation complete after 1s [id=my-app-bucket]

# 2. データを復旧
$ aws s3 sync ./local_backup/ s3://my-app-bucket/
upload: ./local_backup/application.log to s3://my-app-bucket/application.log
upload: ./local_backup/access.log to s3://my-app-bucket/access.log
...

# 3. 残りの設定を適用
$ terraform apply
aws_s3_bucket_ownership_controls.app_bucket: Creating...
aws_s3_bucket_acl.app_bucket: Creating...
aws_s3_bucket_versioning.app_bucket: Creating...
aws_s3_bucket_server_side_encryption_configuration.app_bucket: Creating...
aws_s3_bucket_replication_configuration.app_bucket: Creating...

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

📚 学んだ教訓

  1. バックアップの重要性: レプリケーション設定があったから助かった
  2. 段階的適用: 大きな変更は一気にやらず、リソースごとに分けて実行
  3. 依存関係の理解: v5では多くの設定が別リソースになり、依存関係が複雑化
  4. テスト環境の必要性: 本番でいきなりやるべきではなかった

🔥 トラブル2: EC2インスタンスのネットワーク設定エラー

💀 地獄の第2章

S3の問題を解決した翌日、今度はEC2インスタンスでエラーが発生。

$ terraform apply
...
aws_instance.web_server: Modifying... [id=i-1234567890abcdef0]
Error: modifying EC2 Instance (i-1234567890abcdef0): operation error EC2: ModifyInstanceAttribute, https response error StatusCode: 400, RequestID: xxx, InvalidParameterValue: You cannot modify the 'associatePublicIPAddress' attribute for this instance type when it has a network interface attached.

状況: EC2インスタンスのassociate_public_ip_address設定がv5で非推奨になり、network_interfaceでの管理に変更されたが、既存インスタンスに複数のネットワークインターフェースが存在していた。

🩹 解決への道のり

調査フェーズ:現状把握

# 影響を受けるインスタンスの確認
$ aws ec2 describe-instances --instance-ids i-1234567890abcdef0 \
  --query 'Reservations[0].Instances[0].NetworkInterfaces'

[
    {
        "NetworkInterfaceId": "eni-1111111111111111",
        "DeviceIndex": 0,
        "Status": "in-use",
        "PrivateIpAddress": "10.0.1.100",
        "Association": {
            "PublicIp": "54.123.456.789",
            "IpOwnerId": "amazon"
        }
    },
    {
        "NetworkInterfaceId": "eni-2222222222222222", 
        "DeviceIndex": 1,
        "Status": "in-use",
        "PrivateIpAddress": "10.0.2.100"
    }
]

# Terraformの現在の設定確認
$ terraform show | grep -A 10 "aws_instance.web_server"
# aws_instance.web_server:
resource "aws_instance" "web_server" {
    associate_public_ip_address = true  # ← これが問題
    instance_type               = "t3.medium"
    # 複数のnetwork_interfaceブロックが混在している
}

問題の本質:

  1. EC2インスタンスに複数のENI(Elastic Network Interface)がアタッチされている
  2. associate_public_ip_addressnetwork_interfaceブロックが混在している
  3. AWS Provider v5では、この組み合わせが許可されない

解決ステップ1: ネットワーク設定の分離

# 修正前の設定
resource "aws_instance" "web_server" {
  ami           = "ami-12345678"
  instance_type = "t3.medium"
  subnet_id     = aws_subnet.public.id
  
  # v5で問題となる設定
  associate_public_ip_address = true
  security_groups = [aws_security_group.web.id]
  
  # 既存のネットワークインターフェース設定
  network_interface {
    network_interface_id = aws_network_interface.secondary.id
    device_index         = 1
  }
}

# 修正後の設定
resource "aws_instance" "web_server" {
  ami           = "ami-12345678"
  instance_type = "t3.medium"
  
  # メインのネットワークインターフェースも明示的に定義
  network_interface {
    network_interface_id = aws_network_interface.primary.id
    device_index         = 0
  }
  
  # セカンダリのネットワークインターフェース
  network_interface {
    network_interface_id = aws_network_interface.secondary.id
    device_index         = 1
  }
  
  # network_interfaceを使用する場合、これらは除去
  # associate_public_ip_address = true  # 削除
  # security_groups = [...]  # 削除(ENIで管理)
  # subnet_id = ...  # 削除(ENIで管理)
}

# プライマリENIを明示的に作成
resource "aws_network_interface" "primary" {
  subnet_id       = aws_subnet.public.id
  security_groups = [aws_security_group.web.id]
  
  tags = {
    Name = "web-server-primary-eni"
  }
}

# セカンダリENI(既存)
resource "aws_network_interface" "secondary" {
  subnet_id       = aws_subnet.private.id
  security_groups = [aws_security_group.internal.id]
  
  tags = {
    Name = "web-server-secondary-eni"
  }
}

# Elastic IPをENIに関連付け
resource "aws_eip" "web_server" {
  domain                    = "vpc"
  network_interface         = aws_network_interface.primary.id
  associate_with_private_ip = aws_network_interface.primary.private_ip
  
  tags = {
    Name = "web-server-eip"
  }
}

解決ステップ2: 既存リソースのインポート

# 既存のプライマリENIを特定
$ aws ec2 describe-network-interfaces \
  --filters "Name=attachment.instance-id,Values=i-1234567890abcdef0" \
  --query 'NetworkInterfaces[?Attachment.DeviceIndex==`0`].NetworkInterfaceId' \
  --output text
eni-1111111111111111

# プライマリENIをTerraformの管理下に
$ terraform import aws_network_interface.primary eni-1111111111111111
aws_network_interface.primary: Importing from ID "eni-1111111111111111"...
aws_network_interface.primary: Import prepared!
  Prepared aws_network_interface for import
aws_network_interface.primary: Refreshing state... [id=eni-1111111111111111]

Import successful!

# セカンダリENIも同様にインポート
$ terraform import aws_network_interface.secondary eni-2222222222222222

# Elastic IPもインポート
$ aws ec2 describe-addresses \
  --filters "Name=network-interface-id,Values=eni-1111111111111111" \
  --query 'Addresses[0].AllocationId' \
  --output text
eipalloc-12345678

$ terraform import aws_eip.web_server eipalloc-12345678

解決ステップ3: 慎重な適用

# planで変更内容を確認
$ terraform plan
Terraform will perform the following actions:

  # aws_instance.web_server will be updated in-place
  ~ resource "aws_instance" "web_server" {
      - associate_public_ip_address    = true -> null
      - security_groups                = [
          - "sg-12345678",
        ] -> null
      - subnet_id                      = "subnet-12345678" -> null
        # (8 unchanged attributes hidden)
    }

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

# 変更が最小限であることを確認してから適用
$ terraform apply
aws_instance.web_server: Modifying... [id=i-1234567890abcdef0]
aws_instance.web_server: Modifications complete after 5s [id=i-1234567890abcdef0]

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

🧪 検証と確認

# インスタンスの状態確認
$ aws ec2 describe-instances --instance-ids i-1234567890abcdef0 \
  --query 'Reservations[0].Instances[0].State.Name'
"running"

# ネットワーク接続の確認
$ ssh ec2-user@54.123.456.789
# 接続成功

# セカンダリENIの確認
$ ip addr show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN 
    inet 127.0.0.1/8 scope host lo
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9000 qdisc mq state UP
    inet 10.0.1.100/24 brd 10.0.1.255 scope global eth0
3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9000 qdisc mq state UP  
    inet 10.0.2.100/24 brd 10.0.2.255 scope global eth1

📚 重要な学び

  1. ネットワーク設定の複雑性: ENIが関わると設定が複雑になる
  2. インポートの重要性: 既存リソースを適切にTerraform管理下に置く
  3. 段階的な変更: 大きな変更を小さなステップに分解する
  4. 検証の徹底: 変更後は必ず動作確認を行う

🔥 トラブル3: RDSインスタンスの暗号化問題

💀 データベースの悪夢

EC2の問題を解決して安心していた矢先、今度はRDSで致命的な問題が発生。

$ terraform apply
...
aws_db_instance.main: Modifying... [id=myapp-db]
Error: modifying RDS DB Instance (myapp-db): operation error RDS: ModifyDBInstance, https response error StatusCode: 400, RequestID: xxx, InvalidParameterValue: Cannot modify encrypted attribute for an existing DB instance.

状況: AWS Provider v5で、RDSインスタンスのセキュリティ設定が強化され、既存の暗号化されていないDBインスタンスを暗号化しようとしてエラー。

深刻度:

  • 本番データベース(顧客データ含む)
  • ダウンタイムは絶対に避けたい
  • データの整合性が最重要

🩹 慎重な解決アプローチ

調査フェーズ:リスク評価

# 現在のDBインスタンス設定確認
$ aws rds describe-db-instances --db-instance-identifier myapp-db \
  --query 'DBInstances[0].{StorageEncrypted: StorageEncrypted, Engine: Engine, EngineVersion: EngineVersion, DBInstanceClass: DBInstanceClass}'

{
    "StorageEncrypted": false,
    "Engine": "mysql",
    "EngineVersion": "8.0.28",
    "DBInstanceClass": "db.t3.micro"
}

# バックアップの状況確認
$ aws rds describe-db-snapshots --db-instance-identifier myapp-db \
  --query 'DBSnapshots[?Status==`available`] | sort_by(@, &SnapshotCreateTime) | [-5:]'

[
    {
        "DBSnapshotIdentifier": "myapp-db-snapshot-2024-11-25",
        "SnapshotCreateTime": "2024-11-25T02:00:00Z",
        "Status": "available",
        "Encrypted": false
    },
    ...
]

# 接続中のアプリケーション確認
$ aws rds describe-db-instances --db-instance-identifier myapp-db \
  --query 'DBInstances[0].DBParameterGroups[0].DBParameterGroupName'
"default.mysql8.0"

致命的な発見:

  1. 現在のDBは暗号化されていない
  2. バックアップも暗号化されていない
  3. 暗号化は後から有効化できない(要:再作成)

解決戦略:Blue-Green Deployment

# Step 1: 暗号化されたDBインスタンスを新規作成
resource "aws_db_instance" "main_encrypted" {
  identifier = "myapp-db-encrypted"
  
  allocated_storage     = 20
  max_allocated_storage = 100
  storage_type         = "gp3"
  storage_encrypted    = true  # 暗号化有効
  kms_key_id          = aws_kms_key.rds.arn
  
  engine         = "mysql"
  engine_version = "8.0.35"  # 最新バージョンに更新
  instance_class = "db.t3.micro"
  
  db_name  = "myappdb"
  username = "admin"
  password = var.db_password
  
  # 既存DBと同じネットワーク設定
  db_subnet_group_name   = aws_db_subnet_group.main.name
  vpc_security_group_ids = [aws_security_group.rds.id]
  
  # パフォーマンス設定
  backup_retention_period = 7
  backup_window          = "03:00-04:00"
  maintenance_window     = "sun:04:00-sun:05:00"
  
  skip_final_snapshot = false
  final_snapshot_identifier = "myapp-db-encrypted-final-snapshot"
  
  tags = {
    Name        = "myapp-db-encrypted"
    Environment = var.environment
    Encrypted   = "true"
  }
}

# KMS暗号化キー
resource "aws_kms_key" "rds" {
  description             = "KMS key for RDS encryption"
  deletion_window_in_days = 7
  
  tags = {
    Name = "myapp-rds-key"
  }
}

resource "aws_kms_alias" "rds" {
  name          = "alias/myapp-rds"
  target_key_id = aws_kms_key.rds.key_id
}

データ移行の実行

# Step 1: 新しい暗号化DBを作成
$ terraform apply -target=aws_kms_key.rds -target=aws_kms_alias.rds
$ terraform apply -target=aws_db_instance.main_encrypted

# 作成完了まで待機(約10分)
$ aws rds wait db-instance-available --db-instance-identifier myapp-db-encrypted

# Step 2: データのダンプと移行
# 本番データの完全バックアップ
$ mysqldump -h myapp-db.cluster-xyz.ap-northeast-1.rds.amazonaws.com \
  -u admin -p myappdb > myappdb_backup_$(date +%Y%m%d_%H%M%S).sql

# 新しいDBにデータを復元
$ mysql -h myapp-db-encrypted.cluster-abc.ap-northeast-1.rds.amazonaws.com \
  -u admin -p myappdb < myappdb_backup_20241130_153022.sql

アプリケーション設定の更新

# Step 3: アプリケーションの設定を段階的に切り替え

# 環境変数の確認(本番環境)
$ kubectl get configmap app-config -o yaml
apiVersion: v1
data:
  DB_HOST: "myapp-db.cluster-xyz.ap-northeast-1.rds.amazonaws.com"
  DB_PORT: "3306"
  DB_NAME: "myappdb"

# 新しいConfigMapを作成(まずはテスト用)
$ kubectl create configmap app-config-new --from-literal=DB_HOST="myapp-db-encrypted.cluster-abc.ap-northeast-1.rds.amazonaws.com" --from-literal=DB_PORT="3306" --from-literal=DB_NAME="myappdb"

# 1つのPodで新DBへの接続テスト
$ kubectl run test-pod --image=mysql:8.0 --rm -it -- mysql -h myapp-db-encrypted.cluster-abc.ap-northeast-1.rds.amazonaws.com -u admin -p
# 接続成功を確認

# データの整合性確認
$ kubectl run test-pod --image=mysql:8.0 --rm -it -- mysql -h myapp-db-encrypted.cluster-abc.ap-northeast-1.rds.amazonaws.com -u admin -p -e "SELECT COUNT(*) FROM users;"
# レコード数が一致することを確認

段階的な本番切り替え

# Step 4: Blue-Green Deploymentで段階的切り替え

# トラフィックの一部を新DBに向ける(Canary Release)
$ kubectl patch deployment app-deployment -p '{"spec":{"template":{"spec":{"containers":[{"name":"app","env":[{"name":"DB_HOST","value":"myapp-db-encrypted.cluster-abc.ap-northeast-1.rds.amazonaws.com"}]}]}}}}'

# 10分間モニタリング
$ kubectl logs -f deployment/app-deployment | grep -i error
# エラーがないことを確認

# アプリケーションメトリクスの確認
$ curl http://app.example.com/health
{
  "status": "healthy",
  "database": "connected",
  "response_time": "15ms"
}

# レスポンス時間の比較
$ ab -n 1000 -c 10 http://app.example.com/api/users
# 新DB: 平均 15ms
# 旧DB: 平均 18ms (わずかに改善!)

完全切り替えとクリーンアップ

# Step 5: 全トラフィックを新DBに切り替え
$ kubectl set env deployment/app-deployment DB_HOST=myapp-db-encrypted.cluster-abc.ap-northeast-1.rds.amazonaws.com

# 24時間の監視期間後、旧DBを削除
# Terraformから旧DBの定義を削除
$ terraform plan
Plan: 0 to add, 0 to change, 1 to destroy.

# 削除実行(最終スナップショット作成)
$ terraform apply

⚠️ 発生した予期しない問題

問題1: エンドポイント名の違い

# 旧DB: myapp-db.cluster-xyz.ap-northeast-1.rds.amazonaws.com
# 新DB: myapp-db-encrypted.cluster-abc.ap-northeast-1.rds.amazonaws.com

# アプリケーションコードでハードコードされた部分があった
$ grep -r "myapp-db\." ./src/
./src/config/database.js:    host: 'myapp-db.cluster-xyz.ap-northeast-1.rds.amazonaws.com',

解決策: 環境変数を徹底的に使用するようリファクタリング

問題2: 暗号化による性能への影響

# 暗号化有効後、一部のクエリが遅くなった
$ mysql -h myapp-db-encrypted.cluster-abc.ap-northeast-1.rds.amazonaws.com -u admin -p -e "ANALYZE TABLE users, orders, products;"

# インデックスの再構築
$ mysql -h myapp-db-encrypted.cluster-abc.ap-northeast-1.rds.amazonaws.com -u admin -p -e "OPTIMIZE TABLE users, orders, products;"

解決後の性能: クエリ性能が元のレベルに回復

問題3: バックアップの互換性

# 暗号化DBのバックアップから、非暗号化DBへの復元ができない
$ aws rds restore-db-instance-from-db-snapshot \
  --db-instance-identifier test-restore \
  --db-snapshot-identifier myapp-db-encrypted-snapshot-xyz \
  --storage-encrypted false

An error occurred (InvalidParameterValue) when calling the RestoreDBInstanceFromDBSnapshot operation: Cannot restore an encrypted snapshot to an unencrypted DB instance.

対策: バックアップ戦略の見直しと、暗号化前後の両方の形式でのバックアップ保持

📚 RDS暗号化移行の教訓

  1. Blue-Green Deploymentの威力: ダウンタイムゼロでの移行が可能
  2. 事前テストの重要性: 本番切り替え前の徹底的な検証
  3. 監視の継続: 移行後24-48時間の継続監視
  4. バックアップ戦略: 暗号化による制約を事前に理解

🔥 トラブル4: IAMロールと権限の複雑な依存関係エラー

💀 権限地獄の始まり

RDSの問題を解決して一安心していた月曜日の朝、今度はIAMロールで予想外のエラーが発生。

$ terraform apply
...
aws_iam_role_policy_attachment.s3_access: Creating...
Error: creating IAM Role Policy Attachment (app-ec2-role-s3-access): operation error IAM: AttachRolePolicy, https response error StatusCode: 409, RequestID: xxx, api error EntityAlreadyExists: Policy arn:aws:iam::123456789012:policy/s3-access-policy is already attached to role app-ec2-role

aws_iam_instance_profile.app: Creating...
Error: creating IAM Instance Profile (app-profile): operation error IAM: CreateInstanceProfile, https response error StatusCode: 409, RequestID: xxx, api error EntityAlreadyExists: Instance Profile app-profile already exists.

状況:

  • IAMリソースが既に存在しているが、Terraformの状態ファイルに記録されていない
  • 手動で作成されたリソースとTerraformが管理するリソースが混在
  • AWS Provider v5でIAM権限の検証が厳格化された影響

🔍 詳細調査:現状の把握

# 既存のIAMロールを確認
$ aws iam list-roles --query 'Roles[?RoleName==`app-ec2-role`]'
[
    {
        "RoleName": "app-ec2-role",
        "Arn": "arn:aws:iam::123456789012:role/app-ec2-role",
        "CreateDate": "2024-11-15T10:30:00Z",
        "AssumeRolePolicyDocument": "%7B%22Version%22%3A%222012-10-17%22%2C%22Statement%22%3A%5B%7B%22Effect%22%3A%22Allow%22%2C%22Principal%22%3A%7B%22Service%22%3A%22ec2.amazonaws.com%22%7D%2C%22Action%22%3A%22sts%3AAssumeRole%22%7D%5D%7D",
        "MaxSessionDuration": 3600
    }
]

# アタッチされているポリシーを確認
$ aws iam list-attached-role-policies --role-name app-ec2-role
{
    "AttachedPolicies": [
        {
            "PolicyName": "s3-access-policy",
            "PolicyArn": "arn:aws:iam::123456789012:policy/s3-access-policy"
        },
        {
            "PolicyName": "cloudwatch-logs-policy", 
            "PolicyArn": "arn:aws:iam::123456789012:policy/cloudwatch-logs-policy"
        }
    ]
}

# インスタンスプロファイルの確認
$ aws iam list-instance-profiles --query 'InstanceProfiles[?InstanceProfileName==`app-profile`]'
[
    {
        "InstanceProfileName": "app-profile",
        "Arn": "arn:aws:iam::123456789012:instance-profile/app-profile",
        "CreateDate": "2024-11-15T10:35:00Z",
        "Roles": [
            {
                "RoleName": "app-ec2-role",
                "Arn": "arn:aws:iam::123456789012:role/app-ec2-role"
            }
        ]
    }
]

# Terraformの現在の状態を確認
$ terraform show | grep iam
# (IAMリソースが状態に存在しない)

判明した問題:

  1. IAMロール、ポリシー、インスタンスプロファイルが手動で作成されている
  2. Terraformは既存リソースを認識していない
  3. 同じ名前のリソースを作成しようとして競合

🩹 段階的な解決アプローチ

ステップ1: 既存リソースの棚卸し

# 現在のTerraform設定と実際のAWSリソースの対応表を作成
cat > iam_inventory.md << 'EOF'
# IAM Resource Inventory

## Terraform Configuration vs AWS Reality

| Terraform Resource | AWS Resource ARN | Status | Action Needed |
|-------------------|------------------|---------|---------------|
| aws_iam_role.app_ec2_role | arn:aws:iam::123456789012:role/app-ec2-role | EXISTS | Import |
| aws_iam_policy.s3_access | arn:aws:iam::123456789012:policy/s3-access-policy | EXISTS | Import |
| aws_iam_policy.cloudwatch_logs | arn:aws:iam::123456789012:policy/cloudwatch-logs-policy | EXISTS | Import |
| aws_iam_role_policy_attachment.s3_access | app-ec2-role + s3-access-policy | EXISTS | Import |
| aws_iam_role_policy_attachment.cloudwatch | app-ec2-role + cloudwatch-logs-policy | EXISTS | Import |
| aws_iam_instance_profile.app | arn:aws:iam::123456789012:instance-profile/app-profile | EXISTS | Import |
| aws_iam_instance_profile_role_attachment.app | app-profile + app-ec2-role | EXISTS | Import |
EOF

ステップ2: 既存リソースのインポート作戦

# IAMロールのインポート
$ terraform import aws_iam_role.app_ec2_role app-ec2-role
aws_iam_role.app_ec2_role: Importing from ID "app-ec2-role"...
aws_iam_role.app_ec2_role: Import prepared!
  Prepared aws_iam_role for import
aws_iam_role.app_ec2_role: Refreshing state... [id=app-ec2-role]

Import successful!

# しかし、設定の不整合でエラー
$ terraform plan
Error: IAM Role (app-ec2-role) has an assume role policy document that does not match the expected policy document

問題: 手動作成されたリソースとTerraformの設定が微妙に異なる

ステップ3: 設定の調整と詳細な差分確認

# 実際のAssumeRolePolicyDocumentを取得
$ aws iam get-role --role-name app-ec2-role --query 'Role.AssumeRolePolicyDocument' --output text | python -m json.tool

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "ec2.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}

# 実際のポリシー内容を取得
$ aws iam get-policy-version --policy-arn arn:aws:iam::123456789012:policy/s3-access-policy --version-id v1 --query 'PolicyVersion.Document' | python -m json.tool

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:GetObject",
                "s3:PutObject",
                "s3:DeleteObject"
            ],
            "Resource": [
                "arn:aws:s3:::my-app-bucket/*"
            ]
        },
        {
            "Effect": "Allow", 
            "Action": [
                "s3:ListBucket"
            ],
            "Resource": [
                "arn:aws:s3:::my-app-bucket"
            ]
        }
    ]
}

ステップ4: Terraform設定を実際のリソースに合わせて修正

# terraform/iam.tf (修正版)

# IAMロール (実際の設定に合わせて調整)
resource "aws_iam_role" "app_ec2_role" {
  name = "app-ec2-role"
  
  # 実際のAssumeRolePolicyDocumentと完全に一致させる
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Principal = {
          Service = "ec2.amazonaws.com"
        }
        Action = "sts:AssumeRole"
      }
    ]
  })
  
  tags = {
    Name        = "app-ec2-role"
    Environment = var.environment
    ManagedBy   = "terraform"
  }
}

# S3アクセスポリシー (実際の内容と一致させる)
resource "aws_iam_policy" "s3_access" {
  name        = "s3-access-policy"
  description = "S3 access policy for application"
  
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "s3:GetObject",
          "s3:PutObject", 
          "s3:DeleteObject"
        ]
        Resource = [
          "arn:aws:s3:::my-app-bucket/*"
        ]
      },
      {
        Effect = "Allow"
        Action = [
          "s3:ListBucket"
        ]
        Resource = [
          "arn:aws:s3:::my-app-bucket"
        ]
      }
    ]
  })
  
  tags = {
    Name        = "s3-access-policy"
    Environment = var.environment
    ManagedBy   = "terraform"
  }
}

# CloudWatchログポリシー
resource "aws_iam_policy" "cloudwatch_logs" {
  name        = "cloudwatch-logs-policy"
  description = "CloudWatch logs access policy"
  
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "logs:CreateLogGroup",
          "logs:CreateLogStream",
          "logs:PutLogEvents"
        ]
        Resource = [
          "arn:aws:logs:*:*:*"
        ]
      }
    ]
  })
  
  tags = {
    Name        = "cloudwatch-logs-policy"
    Environment = var.environment
    ManagedBy   = "terraform"
  }
}

# ポリシーアタッチメント
resource "aws_iam_role_policy_attachment" "s3_access" {
  role       = aws_iam_role.app_ec2_role.name
  policy_arn = aws_iam_policy.s3_access.arn
}

resource "aws_iam_role_policy_attachment" "cloudwatch_logs" {
  role       = aws_iam_role.app_ec2_role.name
  policy_arn = aws_iam_policy.cloudwatch_logs.arn
}

# インスタンスプロファイル
resource "aws_iam_instance_profile" "app" {
  name = "app-profile"
  role = aws_iam_role.app_ec2_role.name
  
  tags = {
    Name        = "app-profile"
    Environment = var.environment
    ManagedBy   = "terraform"
  }
}

ステップ5: 順次インポートの実行

# IAMポリシーのインポート
$ terraform import aws_iam_policy.s3_access arn:aws:iam::123456789012:policy/s3-access-policy
$ terraform import aws_iam_policy.cloudwatch_logs arn:aws:iam::123456789012:policy/cloudwatch-logs-policy

# ポリシーアタッチメントのインポート
$ terraform import aws_iam_role_policy_attachment.s3_access app-ec2-role/arn:aws:iam::123456789012:policy/s3-access-policy
$ terraform import aws_iam_role_policy_attachment.cloudwatch_logs app-ec2-role/arn:aws:iam::123456789012:policy/cloudwatch-logs-policy

# インスタンスプロファイルのインポート
$ terraform import aws_iam_instance_profile.app app-profile

# 検証
$ terraform plan
No changes. Your infrastructure matches the configuration.

Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are needed.

🎯 インポート後の追加問題と解決

問題1: タグの不整合

$ terraform apply
Plan: 0 to add, 4 to change, 0 to destroy.

# タグが追加される変更
~ resource "aws_iam_role" "app_ec2_role" {
      + tags     = {
          + "Environment" = "production"
          + "ManagedBy"   = "terraform"
          + "Name"        = "app-ec2-role"
        }
    }

解決: タグの追加は安全な変更なので、そのまま適用。

問題2: ポリシーバージョンの管理

# ポリシーを変更する際、新しいバージョンが作成される
$ terraform apply
aws_iam_policy.s3_access: Modifying... [id=arn:aws:iam::123456789012:policy/s3-access-policy]
aws_iam_policy.s3_access: Modifications complete after 2s

# バージョン履歴の確認
$ aws iam list-policy-versions --policy-arn arn:aws:iam::123456789012:policy/s3-access-policy
{
    "Versions": [
        {
            "VersionId": "v2",
            "IsDefaultVersion": true,
            "CreateDate": "2024-11-30T14:20:00Z"
        },
        {
            "VersionId": "v1", 
            "IsDefaultVersion": false,
            "CreateDate": "2024-11-15T10:32:00Z"
        }
    ]
}

対策: 定期的な古いバージョンのクリーンアップスクリプト作成

#!/bin/bash
# cleanup_policy_versions.sh
# 古いIAMポリシーバージョンをクリーンアップ

POLICY_ARNS=(
    "arn:aws:iam::123456789012:policy/s3-access-policy"
    "arn:aws:iam::123456789012:policy/cloudwatch-logs-policy"
)

for policy_arn in "${POLICY_ARNS[@]}"; do
    echo "Cleaning up old versions for $policy_arn"
    
    # 非デフォルトバージョンを取得(v1は除く - AWS管理)
    versions=$(aws iam list-policy-versions --policy-arn "$policy_arn" \
        --query 'Versions[?IsDefaultVersion==`false` && VersionId!=`v1`].VersionId' \
        --output text)
    
    for version in $versions; do
        echo "Deleting version $version"
        aws iam delete-policy-version --policy-arn "$policy_arn" --version-id "$version"
    done
done

📚 IAM管理の重要な教訓

  1. 段階的インポート: 複雑な依存関係は一つずつ解決
  2. 設定の完全一致: 手動リソースとTerraform設定の細かい差異に注意
  3. バージョン管理: IAMポリシーのバージョン蓄積問題
  4. 権限の最小化: インポート後に過剰な権限がないか再確認

🔥 トラブル5: 状態ファイルの破損と復旧

💀 最悪のシナリオ

すべての問題を解決して安堵していた金曜日の夕方、最後にして最大の悪夢が発生。

$ terraform plan
Error: Failed to load state: state snapshot was created by Terraform v1.6.0, which is newer than current v1.5.7; upgrade to at least v1.6.0 to use this state

$ terraform version
Terraform v1.5.7
on linux_amd64
+ provider registry.terraform.io/hashicorp/aws v5.23.1

状況:

  • チームメンバーが異なるTerraformバージョンで作業
  • 状態ファイルが新しいバージョンで更新されている
  • バックアップが自動で上書きされている可能性

🚨 緊急事態対応

初期調査:被害範囲の確認

# S3の状態ファイルバックアップを確認
$ aws s3 ls s3://terraform-state-bucket/webapp/ --recursive
2024-11-30 09:15:23    15234 webapp/terraform.tfstate
2024-11-30 09:15:23     8901 webapp/terraform.tfstate.backup

# バックアップファイルのダウンロード
$ aws s3 cp s3://terraform-state-bucket/webapp/terraform.tfstate ./terraform.tfstate.remote
$ aws s3 cp s3://terraform-state-bucket/webapp/terraform.tfstate.backup ./terraform.tfstate.backup.remote

# ローカルの状態ファイル確認
$ ls -la *.tfstate*
-rw-r--r-- 1 user user 15234 Nov 30 09:15 terraform.tfstate
-rw-r--r-- 1 user user 14567 Nov 29 18:30 terraform.tfstate.backup

# ファイルヘッダーの確認
$ head -n 5 terraform.tfstate
{
  "version": 4,
  "terraform_version": "1.6.0",
  "serial": 47,
  "lineage": "abc123-def456-789ghi",

$ head -n 5 terraform.tfstate.backup  
{
  "version": 4,
  "terraform_version": "1.5.7",
  "serial": 46,
  "lineage": "abc123-def456-789ghi",

判明した事実:

  1. 最新の状態ファイルはTerraform v1.6.0で作成
  2. バックアップファイルはv1.5.7で作成(serial番号が1つ古い)
  3. lineageは同じ(同じ状態系列)

緊急判断:バージョンアップか巻き戻しか

# 現在のAWSリソースの状態を手動確認
$ aws ec2 describe-instances --instance-ids $(aws ec2 describe-instances --query 'Reservations[*].Instances[*].InstanceId' --output text)
# EC2インスタンスは正常稼働中

$ aws rds describe-db-instances --db-instance-identifier myapp-db-encrypted
# RDSも正常稼働中

$ aws s3 ls s3://my-app-bucket/ | wc -l
# S3のファイル数も正常

判断: リソースは正常稼働中なので、Terraformバージョンを上げて対応

解決アプローチ1: Terraformアップグレード

# Terraform v1.6.0のインストール
$ wget https://releases.hashicorp.com/terraform/1.6.0/terraform_1.6.0_linux_amd64.zip
$ unzip terraform_1.6.0_linux_amd64.zip
$ sudo mv terraform /usr/local/bin/terraform-1.6.0
$ sudo ln -sf /usr/local/bin/terraform-1.6.0 /usr/local/bin/terraform

# バージョン確認
$ terraform version
Terraform v1.6.0
on linux_amd64
+ provider registry.terraform.io/hashicorp/aws v5.23.1

# 状態ファイルの読み込みテスト
$ terraform plan
Error: terraform plan requires terraform init to be run first

プロバイダーの再初期化が必要

# 既存の.terraformディレクトリをバックアップ
$ cp -r .terraform .terraform.backup

# 再初期化
$ terraform init -upgrade

Initializing the backend...
Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 5.0"...
- Installing hashicorp/aws v5.31.0...
- Installed hashicorp/aws v5.31.0 (signed by HashiCorp)

Terraform has been successfully initialized!

# 状態の確認
$ terraform plan
No changes. Your infrastructure matches the configuration.

成功! しかし、思わぬ落とし穴が...

予期しない問題:プロバイダーバージョンの不整合

$ terraform apply
Error: Plugin protocol mismatch

This occurs when there is a mismatch between the version of a plugin and the version of Terraform that is using it.

原因: Terraform v1.6.0は新しいプロバイダープロトコルを使用し、古いプロバイダーバージョンと互換性がない

解決ステップ:プロバイダーの段階的更新

# .terraform.lock.hclをバックアップ
$ cp .terraform.lock.hcl .terraform.lock.hcl.backup

# プロバイダーのアップグレード
$ terraform init -upgrade
Upgrading modules...
Upgrading provider plugins...
- Finding hashicorp/aws versions matching "~> 5.0"...
- Installing hashicorp/aws v5.31.0...

Warning: Provider development overrides are in effect

# 完全に新しい環境でテスト
$ terraform plan -out=test.tfplan
$ terraform show test.tfplan
# 変更内容を詳細確認

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

Changes to Outputs:
  ~ ec2_instance_id = "i-1234567890abcdef0" -> (known after apply)

軽微な変更のみ: 出力値の更新のみで、実際のリソースに変更なし

🔧 状態ファイルの健全性確認とバックアップ戦略の見直し

状態ファイルの完全性チェック

# 状態ファイルの詳細分析
$ terraform show -json > current_state.json
$ jq '.values.root_module.resources | length' current_state.json
23

# 重要なリソースの存在確認
$ jq '.values.root_module.resources[] | select(.type=="aws_instance") | .values.instance_id' current_state.json
"i-1234567890abcdef0"

$ jq '.values.root_module.resources[] | select(.type=="aws_db_instance") | .values.identifier' current_state.json  
"myapp-db-encrypted"

# AWSとの整合性確認
$ aws ec2 describe-instances --instance-ids i-1234567890abcdef0 --query 'Reservations[0].Instances[0].State.Name'
"running"

$ aws rds describe-db-instances --db-instance-identifier myapp-db-encrypted --query 'DBInstances[0].DBInstanceStatus'
"available"

強化されたバックアップ戦略の実装

#!/bin/bash
# enhanced_state_backup.sh
# 状態ファイルの定期バックアップと検証

set -euo pipefail

BACKUP_BUCKET="terraform-state-backups"
PROJECT_NAME="webapp"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="./state_backups/$TIMESTAMP"

# バックアップディレクトリ作成
mkdir -p "$BACKUP_DIR"

echo "🔄 Starting enhanced state backup..."

# 1. 現在の状態をローカルにバックアップ
cp terraform.tfstate "$BACKUP_DIR/terraform.tfstate"
cp terraform.tfstate.backup "$BACKUP_DIR/terraform.tfstate.backup" 2>/dev/null || true
cp .terraform.lock.hcl "$EMERGENCY_BACKUP/" 2>/dev/null || true

echo "💾 Current state backed up to: $EMERGENCY_BACKUP"

# 2. 指定されたバックアップをダウンロード
RECOVERY_DIR="./recovery_$RECOVERY_TIMESTAMP"
mkdir -p "$RECOVERY_DIR"

echo "⬇️ Downloading backup from S3..."
aws s3 sync "s3://$BACKUP_BUCKET/$PROJECT_NAME/$RECOVERY_TIMESTAMP/" "$RECOVERY_DIR/"

# 3. バックアップの検証
echo "🔍 Validating recovered backup..."
if [[ -f "$RECOVERY_DIR/validation_status.txt" ]] && [[ "$(cat "$RECOVERY_DIR/validation_status.txt")" == "VALID" ]]; then
    echo "✅ Backup validation status: VALID"
else
    echo "⚠️ Warning: Backup validation status unknown or invalid"
    read -p "Continue anyway? (y/N): " -n 1 -r
    echo
    if [[ ! $REPLY =~ ^[Yy]$ ]]; then
        exit 1
    fi
fi

# 4. Terraformバージョンの確認
if [[ -f "$RECOVERY_DIR/terraform_version.txt" ]]; then
    echo "📋 Backup Terraform version info:"
    cat "$RECOVERY_DIR/terraform_version.txt"
    echo ""
    echo "📋 Current Terraform version:"
    terraform version
    echo ""
fi

# 5. 状態ファイルの復旧
echo "🔄 Restoring state files..."
cp "$RECOVERY_DIR/terraform.tfstate" ./
cp "$RECOVERY_DIR/.terraform.lock.hcl" ./ 2>/dev/null || true

# 6. 復旧後の検証
echo "🔍 Verifying recovery..."
if terraform plan > /dev/null 2>&1; then
    echo "✅ State recovery successful - terraform plan works"
    terraform plan
else
    echo "❌ State recovery failed - terraform plan errors"
    echo "🔄 Rolling back to emergency backup..."
    cp "$EMERGENCY_BACKUP/terraform.tfstate" ./
    cp "$EMERGENCY_BACKUP/.terraform.lock.hcl" ./ 2>/dev/null || true
    exit 1
fi

echo "🎉 State recovery completed successfully!"
echo "🗂️ Recovery files available at: $RECOVERY_DIR"
echo "🚨 Emergency backup available at: $EMERGENCY_BACKUP"

🔄 チーム運用の改善

1. Terraformバージョン統一の仕組み

# .terraform-version ファイルの作成
echo "1.6.0" > .terraform-version

# tfenv を使用したバージョン管理の導入
$ curl -L https://github.com/tfutils/tfenv/archive/v3.0.0.tar.gz | tar xz
$ sudo mv tfenv-3.0.0 /usr/local/tfenv
$ sudo ln -s /usr/local/tfenv/bin/* /usr/local/bin/

# プロジェクトでのバージョン自動切り替え
$ tfenv install
$ tfenv use

2. CI/CDパイプラインでの制御

# .github/workflows/terraform.yml
name: Terraform CI/CD

on:
  pull_request:
    branches: [main]
  push:
    branches: [main]

jobs:
  terraform:
    name: Terraform
    runs-on: ubuntu-latest
    
    steps:
    - name: Checkout
      uses: actions/checkout@v3
      
    - name: Setup Terraform
      uses: hashicorp/setup-terraform@v2
      with:
        terraform_version: 1.6.0  # バージョン固定
        
    - name: Terraform Format Check
      run: terraform fmt -check -recursive
      
    - name: Terraform Init
      run: terraform init
      
    - name: Terraform Validate
      run: terraform validate
      
    - name: State Backup (Pre-apply)
      if: github.ref == 'refs/heads/main'
      run: |
        ./scripts/enhanced_state_backup.sh
        
    - name: Terraform Plan
      run: terraform plan -out=tfplan
      
    - name: Terraform Apply
      if: github.ref == 'refs/heads/main'
      run: terraform apply -auto-approve tfplan
      
    - name: State Backup (Post-apply)
      if: github.ref == 'refs/heads/main'
      run: |
        ./scripts/enhanced_state_backup.sh
        
    - name: Notify Failure
      if: failure()
      run: |
        echo "🚨 Terraform operation failed!"
        ./scripts/state_recovery.sh $(date +%Y%m%d_%H%M%S --date='1 hour ago')

3. 開発環境での安全策

# pre-commit フックの設定
#!/bin/bash
# .git/hooks/pre-commit

# Terraformバージョンチェック
REQUIRED_VERSION="1.6.0"
CURRENT_VERSION=$(terraform version -json | jq -r '.terraform_version')

if [[ "$CURRENT_VERSION" != "$REQUIRED_VERSION" ]]; then
    echo "❌ Terraform version mismatch!"
    echo "   Required: $REQUIRED_VERSION"
    echo "   Current:  $CURRENT_VERSION"
    echo ""
    echo "💡 Fix with: tfenv use $REQUIRED_VERSION"
    exit 1
fi

# 状態ファイルの自動バックアップ
if [[ -f "terraform.tfstate" ]]; then
    cp terraform.tfstate "terraform.tfstate.backup.$(date +%Y%m%d_%H%M%S)"
    echo "💾 State file backed up"
fi

# フォーマットチェック
terraform fmt -check -recursive || {
    echo "❌ Code formatting issues found!"
    echo "💡 Fix with: terraform fmt -recursive"
    exit 1
}

echo "✅ Pre-commit checks passed"

📊 インシデント後の分析とメトリクス

状態ファイル問題の根本原因分析

# Root Cause Analysis: State File Corruption Incident

## Timeline
- **15:45** - Developer A uses Terraform v1.6.0 in development
- **15:50** - State file updated to v1.6.0 format 
- **16:30** - CI/CD pipeline fails (using v1.5.7)
- **16:35** - Developer B attempts manual fix (panic mode)
- **17:00** - State file inconsistency discovered
- **17:30** - Emergency recovery initiated
- **18:15** - Full recovery completed

## Root Causes
1. **Process Issue**: No version alignment across team
2. **Technical Issue**: Automatic state file format upgrade
3. **Monitoring Gap**: No state file version monitoring
4. **Backup Issue**: Insufficient backup retention strategy

## Impact Assessment
- **Duration**: 90 minutes
- **Affected Systems**: CI/CD pipeline, development workflow
- **Data Loss**: None (successful recovery)
- **Customer Impact**: None (no production changes)

## Preventive Measures Implemented
1. Terraform version standardization (.terraform-version)
2. Enhanced backup strategy with multiple retention points
3. Pre-commit hooks for version validation
4. CI/CD pipeline hardening with version checks
5. Team training on state file management

監視とアラートの強化

# terraform_monitor.py
#!/usr/bin/env python3
"""
Terraform state monitoring and alerting system
"""

import json
import boto3
import hashlib
import datetime
from typing import Dict, List

class TerraformStateMonitor:
    def __init__(self, bucket_name: str, project_name: str):
        self.s3 = boto3.client('s3')
        self.bucket = bucket_name
        self.project = project_name
        
    def get_state_metadata(self) -> Dict:
        """状態ファイルのメタデータを取得"""
        try:
            response = self.s3.get_object(
                Bucket=self.bucket,
                Key=f'{self.project}/terraform.tfstate'
            )
            
            state_content = response['Body'].read().decode('utf-8')
            state_data = json.loads(state_content)
            
            return {
                'terraform_version': state_data.get('terraform_version'),
                'serial': state_data.get('serial'),
                'lineage': state_data.get('lineage'),
                'last_modified': response['LastModified'],
                'size_bytes': response['ContentLength'],
                'checksum': hashlib.md5(state_content.encode()).hexdigest()
            }
        except Exception as e:
            print(f"Error reading state: {e}")
            return {}
    
    def check_version_drift(self, expected_version: str) -> List[str]:
        """バージョンドリフトをチェック"""
        issues = []
        metadata = self.get_state_metadata()
        
        if not metadata:
            issues.append("❌ Could not read state file")
            return issues
            
        current_version = metadata.get('terraform_version')
        if current_version != expected_version:
            issues.append(f"⚠️ Version mismatch: expected {expected_version}, got {current_version}")
            
        # 最終更新時間のチェック
        last_modified = metadata.get('last_modified')
        if last_modified:
            age = datetime.datetime.now(datetime.timezone.utc) - last_modified
            if age.days > 7:
                issues.append(f"⚠️ State file is {age.days} days old")
                
        return issues
    
    def generate_health_report(self) -> Dict:
        """健全性レポートを生成"""
        metadata = self.get_state_metadata()
        issues = self.check_version_drift("1.6.0")
        
        return {
            'timestamp': datetime.datetime.now().isoformat(),
            'project': self.project,
            'metadata': metadata,
            'issues': issues,
            'status': 'healthy' if not issues else 'warning'
        }

# 使用例
if __name__ == "__main__":
    monitor = TerraformStateMonitor('terraform-state-bucket', 'webapp')
    report = monitor.generate_health_report()
    
    print(json.dumps(report, indent=2, default=str))
    
    # Slackに通知
    if report['issues']:
        # Slack webhook logic here
        print("🚨 Issues detected - sending alert")

📚 状態ファイル管理の最終的な教訓

  1. バージョン統一は絶対必要: チーム全体でのTerraformバージョン統一
  2. バックアップは複数世代: 単一バックアップでは不十分
  3. 自動化による人的ミスの回避: 手動操作を最小限に
  4. 監視とアラート: 状態ファイルの変化を能動的に監視
  5. 緊急時の手順書: パニック時でも冷静に対処できる手順の整備

🎯 総括:地獄を抜けた後に得たもの

数値で見る改善効果

指標 移行前 移行後 改善率
Terraformプロバイダー更新にかかる時間 2-3週間 2-3日 88%削減
設定エラーによる障害頻度 月4-5回 月0-1回 80%削減
緊急時の復旧時間 4-6時間 30-60分 85%削減
チームメンバーのストレスレベル 😵😵😵😵😵 😌😌 激減
金曜日夜の障害対応 90%の確率 10%の確率 89%削減

技術的な成果物

1. 堅牢なCI/CDパイプライン

# 完全自動化されたデプロイフロー
git push origin main
↓
GitHub Actions triggered
↓
Version validation + Format check
↓
Automated state backup
↓
Terraform plan generation
↓
Manual approval (production only)
↓
Terraform apply
↓
Post-deploy validation
↓
Success notification

2. 包括的な監視システム

# 24/7監視カバレッジ
- 状態ファイルの健全性
- プロバイダーバージョンドリフト
- リソースの意図しない変更
- バックアップの自動検証
- 緊急アラートの自動送信

3. 災害復旧能力

# RTO (Recovery Time Objective): 30分以内
# RPO (Recovery Point Objective): 1時間以内
- 自動化されたバックアップ (30分間隔)
- ワンコマンド復旧スクリプト
- 段階的なロールバック機能
- クロスリージョンバックアップ

🧠 チームの成長

Before: 個人頼みの運用

- 属人的な知識に依存
- 手動作業中心
- 失敗を恐れる文化
- 保守的なアプローチ

After: チーム協調の運用

- 知識の文書化と共有
- 自動化ファーストの思考
- 失敗から学ぶ culture
- 積極的な実験と改善

💡 他チームからの学び

この一連のトラブル解決を通じて、他のエンジニアリングチームから多くのフィードバックをもらいました。

SREチームからの評価

"インフラチームの障害対応が劇的に改善された。以前は障害時に数時間のダウンタイムがあったが、今では予防的な対応により障害そのものが大幅に減少している。"

セキュリティチームからの評価

"以前は手動設定によるセキュリティ設定漏れが頻発していたが、Terraform化により設定の標準化が進み、セキュリティインシデントが80%減少した。"

開発チームからの評価

"インフラ環境の構築が早くなり、新機能の開発に集中できるようになった。特に開発環境の立ち上げが数日から数時間に短縮されたことが大きい。"


🎓 これから同じ道を歩む人への助言

心構え

  1. パニックにならない: 大きなトラブルほど冷静な判断が必要
  2. 段階的に解決: 一度にすべてを解決しようとしない
  3. バックアップは神: どんな小さな変更でもバックアップは必須
  4. ドキュメント化: 解決過程を必ず記録する
  5. チームと共有: 一人で抱え込まない

実践的なチェックリスト

Terraform移行作業前

  • テスト環境での完全な検証
  • バックアップ戦略の確認
  • ロールバック手順の準備
  • チームメンバーへの事前共有
  • 緊急時連絡先の確認

トラブル発生時

  • 現状の正確な把握(パニックになる前に事実を確認)
  • 影響範囲の特定
  • バックアップの存在確認
  • 段階的な解決アプローチの策定
  • 各ステップでの動作確認

解決後

  • 根本原因の分析
  • 再発防止策の策定
  • ドキュメントの更新
  • チームへの知見共有
  • 監視・アラートの改善

推奨ツールセット

# 必須ツール
- terraform (固定バージョン)
- tfenv (バージョン管理)
- tflint (静的解析)
- terratest (テスト)
- pre-commit (品質管理)

# 監視・アラート
- AWS CloudWatch
- Datadog / New Relic
- Slack integration
- PagerDuty (本格運用時)

# CI/CD
- GitHub Actions
- GitLab CI
- Jenkins (レガシー環境)

# バックアップ・復旧
- AWS S3 (状態ファイル)
- クロスリージョンレプリケーション
- 自動バックアップスクリプト

エピローグ:地獄を抜けた先にあったもの

あの絶望的だった金曜日の夕方から6ヶ月が経った今、振り返ると、あの地獄のような2週間がチーム全体の大きな転換点だったと感じています。

技術的な成長

  • Terraformマスタリー: プロバイダー仕様の深い理解
  • AWS深層知識: サービス間の複雑な依存関係の理解
  • 障害対応スキル: 冷静な判断力と系統的なアプローチ
  • 自動化思考: 人的ミスを削減する仕組みづくり

チームとしての成長

  • 知識の共有: 属人化の解消
  • プロセスの改善: 標準化された手順
  • 文化の変革: 失敗を恐れない実験的姿勢
  • 信頼関係の構築: 困難を共に乗り越えた絆

個人的な成長

正直に言うと、あの2週間は人生で最もストレスフルな期間の一つでした。夜中の3時にSlackで「また壊れた」という通知を見るたびに心臓が止まりそうになりました。

しかし、その経験があったからこそ:

  • 技術的な深度: 表面的な理解を超えた本質的な理解
  • 問題解決能力: 複雑な問題を段階的に解決するアプローチ
  • メンタルの強さ: プレッシャー下でも冷静な判断ができる精神力
  • チームワーク: 困難な状況での協力の重要性

を身につけることができました。


最後のメッセージ

もしあなたが今、似たような地獄の真っ只中にいるなら:

あきらめないでください。

どんなに絶望的に見えるトラブルでも、段階的なアプローチで必ず解決できます。この記事で紹介したすべての問題は、実際に解決可能だったものです。

そして何より重要なことは:

あなたは一人ではありません。

コミュニティに質問を投稿し、同僚に相談し、必要なら外部の専門家に助けを求めてください。私たちエンジニアは、お互いに支え合うコミュニティの一員です。

最後に、この記事が少しでも同じような問題で苦しむエンジニアの助けになれば、あの地獄のような2週間にも意味があったと思えます。

Happy Terraforming! そして、良い週末を! 🚀


P.S. この記事で紹介したスクリプトやコード例は、すべてGitHubで公開予定です。実際のプロダクション環境で使用する前に、必ず自分の環境に合わせてテストしてください。

GitHub Repository: terraform-troubleshooting-toolkit (Coming Soon)


📞 困った時の連絡先

**緊急時は一人で抱え込まず、必ずチームや有識者に相談しましょう!**BACKUP_DIR/.terraform.lock.hcl"

2. Terraformバージョン情報を記録

terraform version > "$BACKUP_DIR/terraform_version.txt"

3. 状態ファイルの健全性チェック

echo "🔍 Validating state file integrity..."
if terraform validate; then
echo "✅ Configuration validation passed"
echo "VALID" > "$BACKUP_DIR/validation_status.txt"
else
echo "❌ Configuration validation failed"
echo "INVALID" > "$BACKUP_DIR/validation_status.txt"
fi

4. 状態の JSON エクスポート

terraform show -json > "$BACKUP_DIR/state_readable.json"

5. リソース一覧の生成

terraform state list > "$BACKUP_DIR/resource_list.txt"

6. 重要なリソースの詳細記録

echo "🔍 Recording critical resource details..."
cat > "$BACKUP_DIR/critical_resources.txt" << EOF

Critical Resources Snapshot - $TIMESTAMP

EC2 Instances

$(aws ec2 describe-instances --query 'Reservations[].Instances[].[InstanceId,State.Name,InstanceType]' --output table)

RDS Instances

$(aws rds describe-db-instances --query 'DBInstances[*].[DBInstanceIdentifier,DBInstanceStatus,Engine,EngineVersion]' --output table)

S3 Buckets

$(aws s3api list-buckets --query 'Buckets[*].[Name,CreationDate]' --output table)

Load Balancers

$(aws elbv2 describe-load-balancers --query 'LoadBalancers[*].[LoadBalancerName,State.Code,Type]' --output table)
EOF

7. S3への暗号化アップロード

echo "☁️ Uploading to S3..."
aws s3 sync "BACKUP_DIR" "s3://BACKUP_BUCKET/PROJECT_NAME/TIMESTAMP/"
--sse AES256
--storage-class STANDARD_IA

8. ローカルバックアップのクリーンアップ(30日以上古いもの)

find ./state_backups -type d -mtime +30 -exec rm -rf {} ; 2>/dev/null || true

9. バックアップ完了通知

echo "✅ Backup completed successfully!"
echo "📁 Local backup: $BACKUP_DIR"
echo "☁️ S3 backup: s3://BACKUP_BUCKET/PROJECT_NAME/$TIMESTAMP/"

10. Slack通知(オプション)

if [ -n "${SLACK_WEBHOOK_URL:-}" ]; then
curl -X POST -H 'Content-type: application/json'
--data "{"text":"✅ Terraform state backup completed for $PROJECT_NAME at $TIMESTAMP"}"
"$SLACK_WEBHOOK_URL"
fi


#### 自動復旧スクリプトの作成

```bash
#!/bin/bash
# state_recovery.sh
# 状態ファイルの緊急復旧

set -euo pipefail

BACKUP_BUCKET="terraform-state-backups"
PROJECT_NAME="webapp"
RECOVERY_TIMESTAMP="${1:-}"

if [[ -z "$RECOVERY_TIMESTAMP" ]]; then
    echo "Usage: $0 <backup_timestamp>"
    echo "Available backups:"
    aws s3 ls "s3://$BACKUP_BUCKET/$PROJECT_NAME/" | grep "PRE" | sort -r | head -10
    exit 1
fi

echo "🚨 Starting emergency state recovery..."
echo "📅 Target backup: $RECOVERY_TIMESTAMP"

# 1. 現在の状態をバックアップ
EMERGENCY_BACKUP="./emergency_backup_$(date +%Y%m%d_%H%M%S)"
mkdir -p "$EMERGENCY_BACKUP"
cp terraform.tfstate "$EMERGENCY_BACKUP/" 2>/dev/null || true
cp .terraform.lock.hcl "$SHOW PROCESSLIST;"

# 原因: インデックスの統計情報が古い
$ mysql -h myapp-db-encrypted.cluster-abc.ap-northeast-1.rds.amazonaws.com -u admin -p -e "

Discussion