Terraform / OpenTofu、AWS CloudFormation / CDK、Pulumi、Bicep などのツールに共通して使える原則をまとめています。具体例は主に Terraform で示します。
目次
- 基本原則
- リポジトリとディレクトリ構成
- モジュール設計
- ステート管理
- 環境分離
- バージョン管理と依存関係の固定
- シークレット管理
- セキュリティとポリシー
- CI/CD パイプライン
- テスト
- ドリフト検知と運用
- 命名規則・タグ付け
- コード品質
- アンチパターン
- 導入チェックリスト
1. 基本原則
| 原則 | 内容 |
|---|
| 宣言的に書く | 「どうやって作るか」ではなく「最終的にどうあるべきか」を記述する |
| 冪等性 | 何度適用しても同じ結果になるようにする |
| すべてをコードに | 手作業(ClickOps)での変更を禁止し、変更は必ずコード経由で行う |
| Single Source of Truth | Git リポジトリを唯一の正とする |
| イミュータブル | 既存リソースを書き換えるより、新しいものを作って置き換える方針を優先する |
| 小さく変更する | 1 回の変更範囲(blast radius)を小さく保つ |
| レビュー可能にする | 変更は必ず Pull Request と plan 結果を伴ってレビューする |
2. リポジトリとディレクトリ構成
infra/
├── modules/ # 再利用可能なモジュール
│ ├── network/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ ├── outputs.tf
│ │ ├── versions.tf
│ │ └── README.md
│ └── database/
├── envs/ # 環境ごとのルートモジュール
│ ├── dev/
│ │ ├── network/
│ │ └── app/
│ ├── stg/
│ └── prod/
├── policies/ # OPA / Sentinel などのポリシー
├── .tflint.hcl
├── .pre-commit-config.yaml
└── README.md
ポイント
- ルートモジュールを小さく分割する: ネットワーク、データベース、アプリケーションなど、ライフサイクルや変更頻度が異なるものはステートを分ける。
- ファイルを役割で分ける:
main.tf / variables.tf / outputs.tf / versions.tf / providers.tf / locals.tf。
- モノレポかポリレポか: 小〜中規模ならモノレポで十分。共通モジュールを多チームで使う場合は、モジュール専用リポジトリに分けてバージョンタグで配布する。
3. モジュール設計
原則
- 単一責任: 1 モジュールは 1 つの論理的なまとまり(例: VPC 一式、RDS クラスタ一式)を担当する。
- 薄すぎるラッパーを作らない: リソース 1 つを包むだけのモジュールは価値が低い。
- 入力は最小限、デフォルトは安全側: 暗号化有効、パブリックアクセス無効などをデフォルトにする。
- 出力を明示する: 他モジュールが必要とする ID や ARN を
outputs で公開する。
- 入力バリデーションを書く。
variable "environment" {
type = string
description = "デプロイ先の環境名"
validation {
condition = contains(["dev", "stg", "prod"], var.environment)
error_message = "environment は dev / stg / prod のいずれかを指定してください。"
}
}
- プロバイダ設定はモジュール内に書かない: プロバイダはルートモジュールで設定し、モジュールへは
providers 引数で渡す。
- README と例を付ける:
terraform-docs で入出力の表を自動生成する。
- 公開モジュールは鵜呑みにしない: 使う場合もバージョンを固定し、中身をレビューする。
4. ステート管理
ステートファイルには機密情報が含まれることがあり、壊れると復旧が困難です。最も慎重に扱うべき資産です。
- リモートバックエンドを使う(S3、GCS、Azure Blob、Terraform Cloud / HCP など)。ローカルステートは個人の検証用途に限る。
- ロックを有効にする(S3 のネイティブロック、DynamoDB、GCS のロック機能など)。
- 暗号化とバージョニングを有効にする: 誤った上書き・削除から戻せるようにする。
- アクセスを最小限にする: ステートの読み取り権限は CI と一部の管理者に限定する。
- ステートを分割する: 1 つの巨大なステートは plan が遅く、影響範囲も大きい。
- 手動でのステート操作は記録を残す:
state mv / state rm / import は手順書化し、可能であれば moved / import / removed ブロックでコードとして表現する。
moved {
from = aws_instance.web
to = module.web.aws_instance.this
}
- 他スタックの値は
terraform_remote_state よりもデータソースや SSM Parameter Store 経由で参照することを検討する(ステート全体への読み取り権限が不要になる)。
5. 環境分離
- 環境ごとにステートを分ける。可能ならクラウドアカウント/プロジェクト/サブスクリプション自体も分ける。
- 同じモジュールを環境間で使い、差分は変数で表現する。環境ごとにコードをコピペしない。
- 本番に近い検証環境を用意する: stg と prod で構成差が大きいと検証の意味が薄れる。
- Workspaces の扱い: Terraform CLI の workspace は手軽だが、権限やバックエンドを分けられないため、本番を含む環境分離にはディレクトリ分割(または Terragrunt 等)が推奨されることが多い。
- プロモーションの流れを決める: dev → stg → prod の順で、同じコミット・同じモジュールバージョンを適用する。
6. バージョン管理と依存関係の固定
terraform {
required_version = "~> 1.9"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
- ロックファイル(
.terraform.lock.hcl)をコミットする。
- モジュールはタグやコミットハッシュで参照する(
?ref=v1.2.0)。ブランチ参照は避ける。
- ツールバージョンをチームで統一する:
tfenv、mise、asdf、.terraform-version などを使う。
- 依存更新を自動化する: Renovate / Dependabot でプロバイダやモジュールの更新 PR を作り、plan 結果を確認してからマージする。
7. シークレット管理
- シークレットをコードや
.tfvars に直接書かない。
- シークレットマネージャを使う: AWS Secrets Manager、GCP Secret Manager、Azure Key Vault、HashiCorp Vault など。
- IaC ではシークレットの「入れ物」だけ作り、値は別経路で投入する方法も有効。
sensitive = true を付ける(ただし出力の非表示であり、ステートには平文で残る点に注意)。
- ephemeral な値(一時的な値)をサポートするツール・バージョンでは活用し、ステートにシークレットを残さないようにする。
- CI の認証は OIDC で短命なクレデンシャルを使う: 長期のアクセスキーを CI に保存しない。
- シークレットスキャンを導入する:
gitleaks、trufflehog などを pre-commit と CI で実行する。
.gitignore を整備する: *.tfstate、*.tfstate.*、.terraform/、*.tfvars(シークレットを含む場合)など。
8. セキュリティとポリシー
最小権限
- IaC を実行するロールは必要な権限のみに絞る。
- plan 用(読み取り)と apply 用(書き込み)でロールを分ける。PR 時は plan ロール、main マージ後のみ apply ロールを使う。
- 本番 apply ロールは、保護されたブランチ/環境からしか引き受けられないようにする。
静的セキュリティスキャン
| ツール | 用途 |
|---|
| Checkov | 多くの IaC 形式に対応した設定ミス検出 |
| Trivy(旧 tfsec 統合) | IaC の設定ミス・脆弱性スキャン |
| KICS | 多形式対応のセキュリティスキャン |
| cfn-nag / cfn-guard | CloudFormation 向け |
Policy as Code
- OPA / Conftest、Sentinel、AWS CloudFormation Guard などで組織ルールをコード化する。
- 例: 「S3 バケットはパブリック禁止」「特定リージョン以外へのデプロイ禁止」「必須タグの付与」「高額インスタンスタイプの禁止」。
- plan 結果(JSON)に対してポリシーを評価し、違反時は CI を失敗させる。
削除保護
- 重要リソースには
prevent_destroy やクラウド側の削除保護を設定する。
resource "aws_db_instance" "main" {
# ...
deletion_protection = true
lifecycle {
prevent_destroy = true
}
}
9. CI/CD パイプライン
推奨フロー
PR 作成
├─ fmt チェック
├─ validate
├─ lint(tflint など)
├─ セキュリティスキャン
├─ plan(結果を PR にコメント)
├─ ポリシーチェック(plan JSON に対して)
└─ コスト見積もり(Infracost など)
↓ レビュー・承認
main へマージ
└─ apply(plan を保存したファイルで適用)
↓
本番は手動承認ゲートを挟む
ポイント
- ローカルからの本番 apply を禁止する: apply は CI からのみ実行する。
- plan ファイルを保存して、その plan を apply する: レビューした内容と実際の適用内容のズレを防ぐ。
- 並列実行を防ぐ: 同じステートに対するジョブは直列化する(ステートロックに加え、CI の concurrency 制御も使う)。
- 変更のあったディレクトリだけ plan/apply する: モノレポではパスフィルタを使う。
- PR ベースの運用ツール: Atlantis、Terraform Cloud / HCP、Spacelift、env0、Digger など。
- 破壊的変更を目立たせる: plan で
destroy / replace が含まれる場合は警告や追加承認を必須にする。
10. テスト
テストはコストと効果のバランスで段階的に導入します。
| レベル | 内容 | ツール例 |
|---|
| 静的解析 | 構文・フォーマット・リント | terraform fmt, validate, tflint |
| セキュリティ/ポリシー | 設定ミス・規約違反 | Checkov, Trivy, OPA/Conftest |
| ユニットテスト | plan に対するアサーション(実リソース不要) | terraform test(command = plan), CDK の assertions |
| 結合テスト | 実際にリソースを作って検証し、破棄する | terraform test(command = apply), Terratest |
| E2E / スモークテスト | デプロイ後のアプリ疎通確認 | 任意のテストフレームワーク |
# tests/network.tftest.hcl
run "subnet_count" {
command = plan
assert {
condition = length(aws_subnet.private) == 3
error_message = "プライベートサブネットは 3 つ必要です。"
}
}
- 共有モジュールには優先してテストを書く: 影響範囲が広いため。
- 結合テストは専用のアカウント/プロジェクトで実行し、確実に後片付けする。
11. ドリフト検知と運用
- 定期的に plan を実行してドリフトを検知する(例: 毎日
terraform plan -detailed-exitcode)。差分があれば通知する。
- ドリフトの原因を潰す: 手動変更を防ぐため、本番環境のコンソール書き込み権限を絞る(ブレークグラス用アカウントは別途用意)。
- 既存リソースは
import ブロックでコードに取り込む。
- 外部から変更される属性は
ignore_changes で明示する(オートスケーリングの台数など)。ただし多用しない。
- 変更履歴を追えるようにする: Git の履歴、CI のログ、クラウドの監査ログ(CloudTrail など)を紐付けられるようにする。
- ロールバック手順を用意する: 基本は「前のコミットを再 apply」。データを伴うリソースは別途バックアップ/リストア手順を用意する。
12. 命名規則・タグ付け
命名規則
- 一貫したパターンを決める:
{システム}-{環境}-{用途}-{連番} など。
- Terraform のリソースローカル名は
snake_case、リソース種別名を繰り返さない(aws_s3_bucket.logs、aws_s3_bucket.logs_bucket は冗長)。
- 1 つしかない場合は
this や main を使う慣習もある。
タグ付け
- 必須タグを定義する:
Environment、Owner、Project、CostCenter、ManagedBy = terraform など。
- プロバイダの default tags を使う:
provider "aws" {
region = "ap-northeast-1"
default_tags {
tags = {
Environment = var.environment
Project = "example"
ManagedBy = "terraform"
}
}
}
- ポリシーでタグの付与を強制する: コスト配分や棚卸しに必須。
13. コード品質
- フォーマッタを強制する:
terraform fmt -check を CI で実行する。
- pre-commit を導入する:
pre-commit-terraform で fmt / validate / tflint / docs / シークレットスキャンをコミット前に実行。
- ハードコードを避ける: リージョン、AMI ID、アカウント ID などは変数やデータソースから取得する。
count より for_each を優先する: 要素の追加・削除でインデックスがずれて、意図しない再作成が起きるのを防ぐ。
locals で計算ロジックをまとめる: 複雑な式をリソース定義に直接書かない。
- 動的ブロックは必要な場合だけ: 可読性が下がるため乱用しない。
- コメントは「なぜ」を書く: 何をしているかはコードで分かるので、設計判断や制約の理由を残す。
depends_on は最小限に: 参照で暗黙の依存関係が作れる場合はそちらを使う。
14. アンチパターン
| アンチパターン | 問題 | 対策 |
|---|
| コンソールでの手動変更 | ドリフト、再現不能 | 書き込み権限を絞る、ドリフト検知 |
| ローカルステート/ステートの Git コミット | 競合、シークレット漏えい | リモートバックエンド+ロック |
| 巨大な単一ステート | plan が遅い、影響範囲が大きい | ライフサイクル単位で分割 |
| 環境ごとのコピペ | 差分が管理不能になる | 共通モジュール+変数 |
| バージョン未固定 | ある日突然壊れる | バージョン制約+ロックファイル |
| シークレットのハードコード | 漏えい | シークレットマネージャ |
| 個人 PC からの本番 apply | 監査不能、事故 | CI からのみ apply |
plan を見ずに apply(-auto-approve の乱用) | 予期しない削除 | plan レビューと承認ゲート |
| ラッパーだけの浅いモジュール | 抽象化コストだけ増える | 意味のある単位でまとめる |
ignore_changes = all の乱用 | IaC で管理していないのと同じ | 対象属性を限定する |
15. 導入チェックリスト
最低限(まず整えるもの)
推奨(次に整えるもの)
発展(成熟度を上げるもの)