terraform-best-practices について
Terraform の設定を書く開発者向けの実践ガイドです。ファイル構成、命名、モジュール、状態管理、セキュリティ、CI/CDを扱い、バージョン固定や一般的な設定上のリスク回避を重視します。
主な機能
- 01Terraform のファイル構成規則
- 02リソースと変数の命名規則
- 03再利用可能なモジュールのパターン
- 04S3 の状態とリモート状態の参照
- 05セキュリティチェックとアンチパターン
- 06CI/CD ワークフローのルール
活用シーン
- 01新しい Terraform プロジェクトの構成設計
- 02モジュールと Provider 設定のレビュー
- 03状態保存とスタック間参照の設定
- 04インフラコードのセキュリティ規則の策定
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ c0x12c/ ai-toolkit --skill terraform-best-practicesインストール後、Claude Code で/terraform-best-practicesを入力して読み込まれたか確認します。
詳細
Terraform のベストプラクティス — クイックリファレンス
ファイル構成
terraform/
live/ # Orchestration — providers, backend, module calls
terraform.tf # backend + provider (ONLY place for providers)
variables.tf # all input variables
locals.tf # computed values, remote state refs
outputs.tf # exported values
{resource-group}.tf # module invocations grouped by concern
modules/{name}/ # Reusable — no providers, no hardcoded values
main.tf # locals, data sources
variables.tf # inputs with descriptions + types
outputs.tf # consumed values only
versions.tf # required_providers
{resource}.tf # one file per resource type
envs/{env}/ # Per-environment config
state.config # backend partial config
terraform.tfvars # non-sensitive values
secrets.tfvars # sensitive values (gitignored)
命名規則
| 対象 | 規則 | 例 |
|---|---|---|
| リソースのプレフィックス | {project}-{service}-{env} | acme-payments-prod |
| 変数 | snake_case | instance_class |
| ローカル値 | snake_case | name_prefix |
| 出力 | snake_case | repository_url |
| リソース | this(主要なリソース)または説明的な名前 | aws_db_instance.this |
| セキュリティグループ | name_prefix(nameではなく) | "${local.name_prefix}-app-" |
| ファイル | {resource-type}.tf | rds.tf、sg.tf、ecr.tf |
| モジュール | kebab-caseのディレクトリ | modules/ecs-service/ |
| タグ | PascalCase のキー | Project、Environment、ManagedBy |
モジュールのパターン
c0x12c Terraform Registryのモジュールを使用します。
各モジュールのソースはc0x12c/{name}/awsの形式です。利用可能なモジュールとバージョンについては、レジストリを参照してください。
# Calling a registry module — always version-pin
module "database" {
source = "c0x12c/rds/aws"
version = "~> 0.6.6"
name = "${local.name_prefix}-db"
vpc_id = local.vpc_id
subnet_ids = local.private_subnet_ids
tags = local.common_tags
}
# Inside a module — no provider, explicit interface
# versions.tf
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0"
}
}
}
# variables.tf — every var has description + type
variable "name" {
description = "Resource name prefix"
type = string
}
# outputs.tf — only what consumers need
output "endpoint" {
description = "Connection endpoint"
value = aws_db_instance.this.endpoint
}
ステート管理
# Backend config — S3 + DynamoDB locking
terraform {
backend "s3" {}
}
# envs/dev/state.config
bucket = "{project}-terraform-state"
key = "{service}/dev/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "{project}-terraform-locks"
encrypt = true
# Init with partial config
# terraform init -backend-config=../envs/dev/state.config
# Remote state for cross-stack references
data "terraform_remote_state" "infra" {
backend = "s3"
config = {
bucket = "{project}-terraform-state"
key = "infra/terraform.tfstate"
region = var.region
}
}
locals {
vpc_id = data.terraform_remote_state.infra.outputs.vpc_id
}
セキュリティチェックリスト
# Sensitive variables
variable "db_password" {
type = string
sensitive = true
}
# S3 — block public, encrypt, version
module "s3" {
versioning = true
server_side_encryption = { sse_algorithm = "aws:kms" }
block_public_access = {
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
}
# RDS — encrypt, private subnet, protect
resource "aws_db_instance" "this" {
storage_encrypted = true
deletion_protection = var.env == "prod"
publicly_accessible = false # ALWAYS false
}
# Security groups — source SG, not CIDR
resource "aws_security_group_rule" "app_to_db" {
source_security_group_id = aws_security_group.app.id # not cidr_blocks
from_port = 5432
to_port = 5432
}
# Default tags at provider level
provider "aws" {
default_tags {
tags = {
Project = var.project
Service = var.service
Environment = var.env
ManagedBy = "terraform"
}
}
}
よくあるアンチパターン
# WRONG — provider in module
# modules/rds/main.tf
provider "aws" { region = "us-east-1" } # NEVER in a module
# WRONG — no version pin
module "rds" {
source = "git::https://github.com/{project}/terraform-modules.git//rds"
# missing ?ref=vX.Y.Z
}
# WRONG — hardcoded values
resource "aws_s3_bucket" "assets" {
bucket = "acme-prod-assets" # use ${local.name_prefix}-assets
}
# WRONG — secrets in code
resource "aws_db_instance" "main" {
password = "hunter2" # use var.db_password (sensitive)
}
# WRONG — wildcard IAM
resource "aws_iam_policy" "app" {
policy = jsonencode({
Statement = [{ Action = "*", Resource = "*", Effect = "Allow" }]
})
}
# WRONG — public database
resource "aws_db_instance" "main" {
publicly_accessible = true # NEVER for databases
}
# WRONG — no state locking
terraform {
backend "s3" {
# missing dynamodb_table for locking
}
}
# WRONG — all resources in one file
# main.tf with 500+ lines of mixed RDS, S3, SQS, IAM...
# Split into rds.tf, s3.tf, sqs.tf, iam.tf
CI/CD のパターン
# Standard workflow
# PR: fmt check → validate → plan (comment on PR)
# Merge to main: init → plan → apply
# Key rules:
# - Never auto-apply on PR
# - Always post plan output as PR comment
# - Lock state during apply (DynamoDB)
# - Inject secrets via CI environment variables
# - Pin Terraform version in CI to match team
避けるべきこと
- モジュール内のプロバイダーブロック
- バージョンが固定されていないモジュール
- 名前、ID、アカウント番号のハードコード
.tfファイル内のシークレット、またはコミットされた.tfvars- ワイルドカード IAM ポリシー(
*に対する*) - パブリックなデータベースやキャッシュ
- ストレージの暗号化漏れ
- モノリシックなファイル(リソースの種類ごとに分割します)
- 自動化での
terraform import(importブロックを使用します) - 変数と出力の
description漏れ - ネストされた locals マップ(フラットに保ちます)
- 条件付きリソースでの
count(セットを使ったfor_eachを使用します)
コメント
Markdown 対応