Terraformで3年分の失敗から学んだ、State管理とAI検証の正解

Terraform運用で全リソースを一つのStateで管理して大失敗した話。1.9の分割管理とCDK、AI検証を組み合わせて本番環境を堅牢にした実装例を共有します。

Terraformでハマったこと、そして学んだこと

正直なところ、3年前のうちのプロジェクトは Terraform の State 管理でめちゃくちゃ苦労した。当初は全リソースを一つの State ファイルで管理していて、ちょっと変更するたびに他のリソースに影響が出るんじゃないかってヒヤヒヤしながら terraform apply を実行してた。今思うと、あれは本当に危なかったな…。

最近、プロジェクトを新規立ち上げする機会があったから、ここ 2-3 年で大きく進化した Terraform の環境を一から設計し直してみた。2026年時点で、Terraform 1.9.x と CDK for Terraform、さらに AI アシスト検証を組み合わせることで、かなり堅牢で保守性の高いインフラコードを実現できるようになってるんですよ。今日はそこで学んだことを共有したい。

Terraform 1.9 の State 分割管理とモジュール設計

前のプロジェクトの最大の反省点が、State ファイルの粒度だった。全部一つだと、VPC 周りの変更が RDS に影響しないかとか、IAM ロール変更が ECS のデプロイと干渉しないかとか、そういう不安が常にあった。

で、今回は Terraform の workspace と module を組み合わせた階層的な State 管理にしてみた。基本的には下記のような構成になってる:

terraform/
├── modules/
│   ├── networking/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── outputs.tf
│   ├── compute/
│   ├── database/
│   └── iam/
├── environments/
│   ├── dev/
│   │   ├── terraform.tfvars
│   │   ├── main.tf
│   │   └── backend.tf
│   ├── staging/
│   └── prod/
└── .terraform/

こうすることで、各環境の State ファイルを完全に分離できるんですよ。うちの場合は AWS S3 + DynamoDB を使った remote state で、環境ごとに State ファイルを異なるキーに保存している。

terraform {
  required_version = ">= 1.9.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.40"
    }
  }

  backend "s3" {
    bucket         = "my-terraform-state"
    key            = "prod/terraform.tfstate"
    region         = "ap-northeast-1"
    encrypt        = true
    dynamodb_table = "terraform-locks"
  }
}

環境ごとに backend.tf を分けることで、誤った環境への apply を防げるし、State ロックの競合も減った。特に DynamoDB のロック機構が、複数人で同時に terraform apply しようとするのを自動的に防いでくれるのは本当にありがたい。地味だけど、これがないと本番環境で大惨事ですよ。

AI 検証統合と Policy as Code(Sentinel / OPA)

去年の半ばくらいから、チーム内で Terraform コードの品質をもっと自動化したいという話が出ていた。従来のレビューだけだと、どうしても人間の見落としが出てくるし、セキュリティルールの統一も難しい。

そこで試してみたのが、HashiCorp Sentinel と OPA(Open Policy Agent)を組み合わせた自動検証。Sentinel は Terraform Cloud/Enterprise 専用なんだけど、OPA は open source で、ローカルでも CI/CD パイプラインでも実行できるから本当に便利。

実装例として、こういう検証ポリシーを書いた。EC2 インスタンスに CloudWatch monitoring が有効になってることを強制するものだ:

# Security Policy: EC2 instances must have monitoring enabled

import "tfplan/v2" as tfplan

ec2_monitoring = rule {
  all tfplan.resource_changes.aws_instance as _, instances {
    instances.after.monitoring == true
  }
}

main = rule {
  (ec2_monitoring) else false
}

これを terraform plan 出力に対して実行することで、本番環境に CloudWatch monitoring なしの EC2 が混入するのを事前に防げる。ローカル開発でも CI/CD パイプラインでも同じポリシーが適用されるから、環境による差異がなくなるんだ。

さらに、OpenAI API を使った Terraform コード品質スコアリングも試してみた。terraform plan の出力と tfplan JSON を Claude や GPT-4 に送って、「このリソース構成は本当に最適化されてるか?」「セキュリティ面で漏れはないか?」みたいな分析をやってもらう。正直まだ検証中で、false positive も多いけど、設計段階での引っかかりを減らすには有効な感じがしている。

Terraform Testing Framework と自動検証パイプライン

Terraform 1.6 以降で testing framework が入ったのが本当に大きい転機だった。以前はインフラコード自体にテストを書く習慣がなかったけど、いまは unit test + integration test を階層化して回せるようになった。

うちのチームでやってるのはこんな感じ:

# tests/unit/networking_test.tf

variables {
  environment = "test"
  cidr_block  = "10.0.0.0/16"
}

run "vpc_creation" {
  command = plan

  assert {
    condition     = aws_vpc.main.cidr_block == "10.0.0.0/16"
    error_message = "VPC CIDR block must match expected value"
  }

  assert {
    condition     = aws_vpc.main.enable_dns_hostnames == true
    error_message = "DNS hostnames must be enabled"
  }
}

run "security_group_rules" {
  command = plan

  assert {
    condition     = length(aws_security_group.main.ingress) > 0
    error_message = "Security group must have ingress rules"
  }
}

terraform test コマンドで実行すると、定義したすべてのアサーションをチェックしてくれる。これを GitHub Actions の CI/CD パイプラインに組み込んでいるから、PR が出たら自動的に実行される。

name: Terraform Tests

on:
  pull_request:
    paths:
      - 'terraform/**'

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: 1.9.x
      - run: terraform fmt -check -recursive .
      - run: terraform validate
      - run: terraform test -json > test-results.json
      - name: Comment test results
        if: always()
        uses: actions/github-script@v7
        with:
          script: |
            const fs = require('fs');
            const results = JSON.parse(fs.readFileSync('test-results.json', 'utf8'));
            // Parse and comment results

これにより、terraform apply する前の段階で構文エラーだけじゃなく、ロジックレベルのバグを検出できるんだ。特に networking と security group の組み合わせミスを早期に見つけられるようになったのは大きい。なにしろ本番で気づくと本当に大変ですから。

CDK for Terraform(CDKTF)と言語統合

うちのチームは Go と Python も使ってるんだけど、HCL だけで全部書くのって実は結構つらい。条件分岐や複雑なループ処理は、どうしても HCL の制約に引っかかる。

そこで最近導入したのが CDK for Terraform。これは TypeScript や Python で Terraform リソースを定義できるフレームワークで、最終的に HCL に生成される。つまり HCL の読みやすさと、プログラミング言語の表現力を両立できるってわけだ。

import { Construct } from 'constructs';
import { App, TerraformStack } from 'cdktf';
import { AwsProvider, Ec2Instance, SecurityGroup } from '@cdktf/provider-aws';

export class MyStack extends TerraformStack {
  constructor(scope: Construct, id: string) {
    super(scope, id);

    new AwsProvider(this, 'aws', {
      region: 'ap-northeast-1',
    });

    const sg = new SecurityGroup(this, 'main', {
      name: 'my-sg',
      vpcId: 'vpc-xxxxx',
      ingress: [
        {
          fromPort: 443,
          toPort: 443,
          protocol: 'tcp',
          cidrBlocks: ['0.0.0.0/0'],
        },
      ],
    });

    // TypeScript で複雑なロジックが書ける
    const instanceConfigs = [
      { name: 'web-1', ami: 'ami-xxxxx', type: 't3.medium' },
      { name: 'web-2', ami: 'ami-xxxxx', type: 't3.medium' },
      { name: 'api-1', ami: 'ami-yyyyy', type: 't3.large' },
    ];

    instanceConfigs.forEach((config, idx) => {
      new Ec2Instance(this, `instance-${idx}`, {
        ami: config.ami,
        instanceType: config.type,
        securityGroups: [sg.id],
        tags: {
          Name: config.name,
          Environment: 'prod',
        },
      });
    });
  }
}

const app = new App();
new MyStack(app, 'my-infrastructure');
app.synth();

cdktf synth で HCL に変換されるんだけど、生成された HCL は完全に読み取り可能で、git で管理できる。TypeScript の型安全性と HCL の可読性を両立できるのが気に入ってる。

たぶん大事な点として、CDKTF でも結局最後は HCL になるから、従来の Terraform ワークフロー(plan → review → apply)は変わらない。ただしロジック面での複雑さが減るし、言語的な生産性が上がるんですよ。

State 管理の落とし穴と運用Tips

ここまで良い話ばかり書いてきたけど、正直 State 管理にはハマりどころがいっぱいある。実際に本番で困ったことをいくつか紹介しておきたい。

1. State ファイル削除問題

あるときチームメンバーが誤って State ファイルを削除してしまった。結果、Terraform は「このリソース、俺の管理下じゃなくなった」と判断して、実際には存在する AWS リソースを削除しようとした。慌てて terraform refresh で State を再構築したけど、本当に怖かった。

今は S3 バージョニングを有効化してるし、定期的に State の snapshot を取ってる。

# Backup Terraform state
aws s3 sync s3://my-terraform-state terraform-state-backup/
# Restore if needed
aws s3 sync terraform-state-backup/ s3://my-terraform-state

2. State Lock の timeout

DynamoDB を使ったロック機構は便利だけど、たまに apply が途中で失敗して lock が残ったままになる。その場合、次の apply はずっと待機状態になるんだ。

terraform force-unlock <LOCK_ID> で強制解除できるけど、本当に lock が不要な場合だけに限定したい。むやみに解除するとまた競合の問題が出てくる。

3. Sensitive data の扱い

Terraform state には DB パスワードとか API キーとか、平文で入る。sensitive = true をつけると State 出力から隠せるけど、State ファイル自体には平文で保存されるんだ。

必ず S3 暗号化と IAM policies で State へのアクセスを制限する必要がある。うちの場合は AWS Secrets Manager 連携にして、sensitive data は Terraform から読み込まないようにしてる。

data "aws_secretsmanager_secret_version" "db_password" {
  secret_id = aws_secretsmanager_secret.db_password.id
}

resource "aws_db_instance" "main" {
  password = data.aws_secretsmanager_secret_version.db_password.secret_string
  # その他設定...
}

Terraform と他のツール連携

うちのチームでは Terraform と以下のツールを組み合わせて、トータルな IaC パイプラインを構築してる。

CloudFormation との共存

まだ一部レガシーな CloudFormation Stack があるから、Terraform から参照する必要がある。data.aws_cloudformation_stack で既存 Stack の出力を取得して、Terraform リソースと統合してる。

data "aws_cloudformation_stack" "legacy" {
  name = "legacy-app-stack"
}

resource "aws_security_group_rule" "from_legacy" {
  type              = "ingress"
  from_port         = 8080
  to_port           = 8080
  protocol          = "tcp"
  cidr_blocks       = [data.aws_cloudformation_stack.legacy.outputs.VpcCidr]
  security_group_id = aws_security_group.app.id
}

Ansible との連携

インフラリソースの作成が Terraform で、その上でのアプリ設定が Ansible、みたいなハイブリッド運用をしてる。Terraform で EC2 instance を作ったあと、自動的に Ansible playbook が実行されるように、Terraform で dynamic inventory を生成してる。

resource "local_file" "ansible_inventory" {
  content = templatefile("${path.module}/inventory.tpl", {
    web_servers = aws_instance.web[*].private_ip,
    api_servers = aws_instance.api[*].private_ip,
  })
  filename = "${path.module}/../ansible/inventory.ini"
}

Cost 可視化と制約

Terraform plan の段階でコスト予測をしたいというニーズが強い。Infracost という OSS ツールを使って、terraform plan 出力から変更前後のコストを自動計算してる。

infracost breakdown --terraform-dir . --format json > costs.json

これを GitHub Actions で実行して、PR コメントにコスト差分を表示することで、「この変更でインフラコストが 50 万円増えます」みたいなことを事前に認識できるんだ。エンジニア視点だと見落としがちなコスト面を、自動で拾えるのは本当に価値がある。

2026年時点でのインフラコード選択肢比較

Terraform 以外のインフラコード選択肢(CloudFormation、AWS CDK、Pulumi など)が進化してる中で、どう選ぶかというのは常に悩みどころ。実際に比較表を作ってみると、こんな感じになる:

項目TerraformAWS CDKPulumiCloudFormation
学習曲線中~高低~中
マルチクラウド対応★★★★★☆☆☆★★★★★☆☆☆
言語選択HCL のみTypeScript/Python多言語対応JSON/YAML
State 管理明示的(複雑)不要自動化不要
本番運用の難易度
IDE サポート改善中優秀優秀IDE 依存
2026 推奨度★★★★★★★★★★★★★★

個人的には、マルチクラウド対応が必要 なら Terraform、AWS のみで深掘りしたい なら AWS CDK、Python でサクッと なら Pulumi、という使い分けがいいと思う。ただし Terraform の成熟度は他の追随を許さないから、迷ったら Terraform で間違いない。

まとめ

実装レベルで Terraform を改善したいなら、この 3 つを優先度順に取り組むことをお勧めします。

1. State 分割管理の導入

環境ごと・関心ごとに State を分割。Remote state + DynamoDB lock は必須。失敗リスクが劇的に下がるんですよ。本当に。

2. Terraform Testing Framework と CI/CD 自動化

Unit test を書く習慣がつくと、本番手前での問題検出精度が上がる。plan 出力の自動コメント化も PR レビューの負荷を減らせるし、チーム全体のスピードが上がる。

3. Policy as Code(OPA/Sentinel)とコスト可視化

セキュリティ・コンプライアンス・コストを自動で監視することで、人的レビューの範囲を最適化できる。AI 補助検証も今後の鍵になると思う。

うちのチームも最初は Terraform に対して「複雑すぎる」「State 管理が怖い」って感じだったけど、運用フレームワークを整備したら、むしろコード品質とデプロイ信頼度が上がった。本当に。是非試してみてください。

U

Untanbaby

ソフトウェアエンジニア|AWS / クラウドアーキテクチャ / DevOps

10年以上のIT実務経験をもとに、現場で使える技術情報を発信しています。 記事の誤りや改善点があればお問い合わせからお気軽にご連絡ください。

関連記事