Terraform/OpenTofu architecture patterns, conventions, and module design based on HashiCorp official style guide, Yevgeniy Brikman (Gruntwork/Terragrunt), Anton Babenko (terraform-aws-modules), and Nicki Watt's evolution patterns.
From eccnpx claudepluginhub tatematsu-k/ai-development-skills --plugin eccThis skill uses the workspace's default tool permissions.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Enables AI agents to execute x402 payments with per-task budgets, spending controls, and non-custodial wallets via MCP tools. Use when agents pay for APIs, services, or other agents.
Based on: HashiCorp Style Guide, Terraform Best Practices (Anton Babenko), Yevgeniy Brikman (Gruntwork), Nicki Watt (OpenCredo).
| ファイル | 用途 |
|---|---|
main.tf | Resources と data sources |
variables.tf | Input variables(アルファベット順) |
outputs.tf | Outputs(アルファベット順) |
locals.tf | Local values |
providers.tf | Provider ブロック(root module のみ) |
versions.tf / terraform.tf | required_version と required_providers |
backend.tf | Backend 設定 |
data.tf | Data sources(大規模な場合は分離) |
大規模な場合は論理グループで分割: network.tf, compute.tf, storage.tf
snake_case(resources, variables, outputs, locals)# BAD
resource "aws_instance" "web_aws_instance" {}
# GOOD
resource "aws_instance" "web" {}
terraform-<PROVIDER>-<NAME>_(アンダースコア)を使う。-(ハイフン)は使わない (Anton Babenko)this を使うパターン: {name}_{type}_{attribute}
output "vpc_id" {
description = "The ID of the VPC"
value = aws_vpc.this.id
}
enable_x、disable_x ではなく)description を含めるresource "aws_instance" "web" {
# 1. Meta-arguments first
count = var.instance_count
# 2. Standard arguments (= を揃える)
ami = data.aws_ami.ubuntu.id
instance_type = var.instance_type
subnet_id = var.subnet_id
# 3. Nested blocks (空行で区切る)
root_block_device {
volume_size = 50
encrypted = true
}
# 4. tags (最後の実引数)
tags = {
Name = "web-${count.index}"
Environment = var.environment
}
# 5. lifecycle / depends_on (最後)
lifecycle {
create_before_destroy = true
}
}
Rules:
= を揃えるterraform fmt -recursive と terraform validate を毎コミット前に実行# を使用(// や /* */ ではなく)variable "db_disk_size" {
type = number
description = "Size of the database disk in GB"
default = 100
sensitive = false
validation {
condition = var.db_disk_size >= 20
error_message = "Disk size must be at least 20 GB."
}
}
必須: type と description は全ての variable に必要。
output "instance_public_ip" {
description = "The public IP of the web instance"
value = aws_instance.web.public_ip
sensitive = false
}
必須: description は全ての output に必要。
terraform-aws-vpc/
├── README.md # Required for registry
├── main.tf
├── variables.tf
├── outputs.tf
├── versions.tf
├── modules/ # Nested modules
│ ├── public-subnet/ # README あり = public API
│ │ ├── README.md
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ └── internal/ # README なし = internal only
├── examples/
│ ├── basic/
│ └── complete/
└── tests/ # .tftest.hcl files
| レイヤー | 説明 | 例 |
|---|---|---|
| Resource Module | 関連リソースの集合。1つのアクションを実行 | terraform-aws-vpc |
| Infrastructure Module | Resource Modules を組み合わせたデプロイ単位 | terraform-aws-atlantis |
| Composition | Infrastructure Modules のルート構成 | live/ ディレクトリ |
Terralith → Multi-Terralith → Terramod → Terramod+Registry → Terraservice
(1ファイル) (環境別分割) (モジュール化) (バージョン管理) (コンポーネント別state)
プロジェクトの成長に合わせて進化させる。Terralith に留まり続けるのはアンチパターン。
# versions.tf (module 内)
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0"
}
}
}
Rule: 再利用可能なモジュールで provider 設定をハードコードしない。呼び出し元で設定させる。
for_each vs countcount を使う場合 | for_each を使う場合 |
|---|---|
| 0 or 1 インスタンス(boolean toggle) | リソースがキーで識別される |
| 全インスタンスが完全に同一 | リストの中間からアイテムが削除される可能性がある |
| 単純な数値スケーリング | 安定したリソースアドレスが必要 |
Critical anti-pattern: リストで count を使い、中間のアイテムを削除 → インデックスがずれて不要な destroy/recreate が発生。for_each + map/set で回避。
# BAD: count with list
variable "subnet_names" {
default = ["web", "app", "db"]
}
resource "aws_subnet" "this" {
count = length(var.subnet_names)
# Removing "app" shifts "db" from index 2 to 1 → recreate!
}
# GOOD: for_each with map
resource "aws_subnet" "this" {
for_each = toset(var.subnet_names)
cidr_block = cidrsubnet(var.vpc_cidr, 8, index(var.subnet_names, each.key))
tags = { Name = each.key }
}
チーム/本番環境では絶対にローカル state を使わない。
# backend.tf
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "prod/vpc/terraform.tfstate"
region = "ap-northeast-1"
encrypt = true
use_lockfile = true # Terraform 1.10+ (replaces DynamoDB locking)
}
}
環境ごと + コンポーネントごとに state を分離(blast radius の最小化):
state/
├── prod/vpc/terraform.tfstate
├── prod/ecs/terraform.tfstate
├── staging/vpc/terraform.tfstate
└── staging/ecs/terraform.tfstate
*.tfstate
*.tfstate.*
.terraform/
.terraform.tfstate.lock.info
crash.log
*.tfvars # If contains secrets
override.tf
override.tf.json
# Always commit
.terraform.lock.hcl
moved Blocks# Rename resource
moved {
from = aws_instance.old_name
to = aws_instance.new_name
}
# Move into module
moved {
from = aws_instance.web
to = module.compute.aws_instance.web
}
terraform state mv より宣言的で安全。moved ブロックは共有モジュールでは永続的に保持する。
infrastructure-modules/
├── modules/
│ ├── vpc/
│ ├── ecs-cluster/
│ └── rds/
└── README.md
Git tag でバージョニング。
infrastructure-live/
├── account-1/
│ ├── _global/ # アカウント全体 (IAM, Route53)
│ └── ap-northeast-1/
│ ├── _global/ # リージョン全体 (ECR)
│ ├── dev/
│ │ ├── vpc/terragrunt.hcl
│ │ └── app/terragrunt.hcl
│ └── prod/
│ ├── vpc/terragrunt.hcl
│ └── app/terragrunt.hcl
Rule: infrastructure-live では immutable なバージョンタグを参照。環境ごとにバージョンを昇格: dev → staging → prod
import {
to = aws_instance.web
id = "i-1234567890abcdef0"
}
# Config を自動生成
terraform plan -generate-config-out=generated.tf
# 確認後に apply
terraform apply
PR opened → terraform fmt -check → terraform validate → terraform plan → plan をPRコメントに投稿
PR merged → terraform plan → 手動承認ゲート → terraform apply
Rules:
terraform plan -refresh-only を定期実行| Anti-Pattern | 推奨 |
|---|---|
| ローカル state(チーム環境) | Remote backend + state locking |
| 1つの state に全リソース | コンポーネント別 + 環境別に分離 |
count + 可変リスト | for_each + map/set |
| 単一リソースのラッパーモジュール | 意味のある抽象化のみモジュール化 |
| モジュール内で provider をハードコード | 呼び出し元で設定 |
手動 terraform state mv | moved ブロック(宣言的) |
.tf ファイルにシークレット | Vault / Secrets Manager / sensitive = true |
| 長期間有効な static credentials | OIDC / dynamic credentials |
| variable の乱用 | デプロイ間で変わるもののみ公開 |
terraform.tfvars を全レベルで使用 | Composition レベルのみ |
| 深いモジュールネスト | フラットに保つ(1-2レベルまで) |
| コンソールでの手動変更 | GitOps: main ブランチ = source of truth |
Sources: