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 など)が進化してる中で、どう選ぶかというのは常に悩みどころ。実際に比較表を作ってみると、こんな感じになる:
| 項目 | Terraform | AWS CDK | Pulumi | CloudFormation |
|---|---|---|---|---|
| 学習曲線 | 中 | 中 | 中~高 | 低~中 |
| マルチクラウド対応 | ★★★★★ | ☆☆☆ | ★★★★★ | ☆☆☆ |
| 言語選択 | 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 管理が怖い」って感じだったけど、運用フレームワークを整備したら、むしろコード品質とデプロイ信頼度が上がった。本当に。是非試してみてください。