ルール
Place variables.tf in every module. Empty file is acceptable
(全てのモジュールにvariables.tfを配置する)
解説
全てのモジュールにvariables.tfを配置します。variableブロックは必ずこのファイルに記載します。
機械的にコードの位置を推測できるようになるためAIとの相性も良いです。
サンプルコード
# ./modules/networking/variables.tf
variable "environment" {
type = string
description = "環境名(development, staging, production)"
}
variable "project" {
type = string
description = "プロジェクト名"
}
variable "vpc_cidr" {
type = string
description = "VPCのCIDRブロック"
}
variable "public_subnet_cidrs" {
type = list(string)
description = "パブリックサブネットのCIDRブロックリスト"
}
variable "availability_zones" {
type = list(string)
description = "使用するアベイラビリティゾーンのリスト"
}
参考リンク
ルール
Always place README.md in every module. README.md should have module information auto-inserted by terraform-docs
(全てのモジュールにREADME.mdを配置する)
解説
README.mdには本来目的や概要を記すのですが、それらをSpec-Drivenではspec.mdに記します。
そのためterraform-docsによって自動生成されたドキュメントがREADME.mdのメインコンテンツとなります。
サンプルコード
# Networking Module
<!-- BEGIN_TF_DOCS -->
<!-- END_TF_DOCS -->
参考リンク
ルール
Place outputs.tf in every module. Empty file is acceptable
(全てのモジュールにoutputs.tfを配置する)
解説
全てのモジュールにoutputs.tfを配置します。もちろんoutputブロックは例外なくここに記載します。たとえ空ファイルであってもかまいません。
すべてのoutputブロックは必ずこのファイルを見れば把握できるようになることで、可読性が向上します。
サンプルコード
# ./modules/networking/outputs.tf
output "vpc_id" {
description = "作成されたVPCのID"
value = aws_vpc.main.id
}
参考リンク
ルール
Place main.tf in every module. Empty file is acceptable
(全てのモジュールにmain.tfを配置する)
解説
全てのモジュールに必ずmain.tfを配置します。ルートモジュールであればサブモジュール呼び出しの起点となります。
ただしサブモジュールの場合はやや浮いた立ち位置になりがちです。小さなサブモジュールならmain.tfにすべて書くためいいのですが、多くのサブモジュールは<リソース名>.tf(e.g., s3.tf)のようにリソースのまとまりごとにファイルを用意するためです。この場合でもmain.tfは空のまま放置します。何も書かないからと言って消したりしません。
サンプルコード
# ./modules/networking/main.tf
# ネットワーク関連のリソースを定義
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.environment}-${var.project}-vpc"
}
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.environment}-${var.project}-igw"
}
}
参考リンク
ルール
Save static files referenced but not executed by Terraform (like EC2 startup scripts) in files/. Basic placement is at caller location (e.g., ./modules/api/files/userdata.sh), but project-wide files go directly under root directory
(Terraformから参照されるが実行されない静的ファイル(EC2起動スクリプトなど)はfiles/に保存する。基本的に呼び出し元の場所に配置するが、プロジェクト全体で使用するファイルはルート直下に配置する)
解説
Terraformリソースから参照される静的ファイルには、EC2のユーザーデータスクリプト、Lambda関数のダミーのソースコード、設定ファイルなどがあります。
決めの問題でしかないのですが、こういうことも決めておかないとカオスになりますので、決めておきます。
サンプルコード
# ディレクトリ構造:
# modules/
# api/
# main.tf
# files/
# userdata.sh # EC2起動スクリプト
# app_config.json # アプリケーション設定ファイル
# files/
# dummy.py # セットアップ専用のダミーPythonコード
参考リンク
ルール
Avoid cross-referencing between reusable modules. Control references from the caller as much as possible
(再利用可能モジュール間の相互参照を避ける。参照は可能な限り呼び出し側で制御する)
解説
モジュール間の相互参照は取り回しを大きく損なうためできる限り避けます。
よく問題を起こすのがセキュリティグループルールで、呼び出し元と呼び出し先が別のモジュールでかつoutboundとinboundをきちんと指定するだけで相互参照になってしまいます。このようなケースではエッジモジュール(配線専用モジュール)を別に用意し、そのモジュール内でルールを設定します。カプセル化の観点からは好ましくないのですが、制約上いたしかたのない妥協と言えます。
サンプルコード
# main.tf (ルートモジュール)
module "networking" {
source = "./modules/networking"
vpc_cidr = var.vpc_cidr
}
module "database" {
source = "./modules/database"
# 呼び出し側で依存関係を明示的に制御
vpc_id = module.networking.vpc_id
subnet_ids = module.networking.private_subnet_ids
security_group_id = module.networking.db_security_group_id
}
# modules/database内で直接 module.networking を参照しない
参考リンク
ルール
Reusable modules are placed under ./modules/ (e.g., ./modules/networking)
(再利用可能なモジュールは ./modules/ 配下に配置する)
解説
複数の環境から参照されるサブモジュールはすべてmodules/に集めます。こうすることでモジュールの性格を推測しやすくなり、迷うことが減ります。
サンプルコード
# ディレクトリ構造の例
# ./modules/networking/main.tf - ネットワーク関連の再利用可能モジュール
# ./modules/compute/main.tf - コンピュート関連の再利用可能モジュール
# ./modules/database/main.tf - データベース関連の再利用可能モジュール
参考リンク
ルール
*Always encrypt sensitive information when writing to .tfvars.json.enc
(機密情報は必ず暗号化して*.tfvars.json.encに記述する)
解説
APIキーやパスワード、秘密鍵などの機密情報を平文でリポジトリに保存するのはたとえPrivateリポジトリであっても絶対厳禁です。保存自体を避けられるなら避け、避けられないとしても必ず暗号化します。当然ハードコーディングもNGです。
暗号化にはsopsを用います。json形式なら部分的な暗号化も可能なためGitでの管理が容易です。KMSでも暗号化は可能ですが、ファイル全体を暗号化するため差分管理をGithub上で行うことができなくなり不便です。
ただし、ローカルで復号するとうっかりCursorで開くだけで潜在的にアウトです。暗号化するだけでなく、復号用の鍵と復号した後のデータからAIを隔離しなければいけません。
サンプルコード
// terraform.tfvars.json (sopsで暗号化前)
{
"db_password": "ENC[AES256_GCM,data:xxx...]",
"api_key": "ENC[AES256_GCM,data:yyy...]",
"project": "practitioner"
}
# sopsで暗号化
sops -e terraform.tfvars.json > terraform.tfvars.json.enc
# sopsで復号化してterraform実行
sops exec-file terraform.tfvars.json.enc 'terraform apply -var-file={}'
参考リンク
ルール
In principle, write all resource blocks in main.tf. However, if large (guideline: 150 lines), extract to files named .tf (e.g., iam.tf)
(リソースブロックは原則main.tfに記述する。大きくなった場合(目安: 150行)は.tfという名前でファイルを分割する)
解説
リソースブロックはすべてmain.tf にいつも集めきるのが理想です。が、ファイルが大きくなりすぎると読みづらくなります。そのためある程度の規模をもつモジュールは、AWSサービスの名前を利用したファイルごとにリソースブロックを保存します(例: IAMリソースは iam.tf。RoleやPolicyなどを書く)。
分割の目安はおおざっぱに150行です。
サンプルコード
# main.tf (小規模な場合、すべてここに記述)
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
}
# 大規模になった場合は分割する
# iam.tf - IAMリソース
resource "aws_iam_role" "api" {
name = "${var.environment}-${var.project}-api"
# ...
}
参考リンク
ルール
Save helper scripts in helpers/ directory. Basically place directly under repository root directory, but can be placed under specific module if limited to that module
(ヘルパースクリプトはhelpers/ディレクトリに保存する。基本的にリポジトリルート直下に配置するが、特定モジュール専用の場合はそのモジュール配下に配置できる)
解説
ヘルパースクリプトとは、terraformから呼び出さない、インフラ構築・運用の支援用スクリプトです。テストコマンドをまとめたスクリプトなどが該当します。
スクリプトファイルはscripts/に置きたくなりますが、Terraformでscripts/はカスタムスクリプトと呼ばれるTerraformから呼び出すためのスクリプトを置くための場所として使うことになっています。そのため混同しないよう、Terraformから呼び出さないスクリプトはhelpers/に配置します。
サンプルコード
# プロジェクト全体で使用するヘルパースクリプト
# ディレクトリ構造:
# ./helpers/
# check_state.sh # 全モジュールの状態確認
# generate_docs.sh # ドキュメント生成
#
# 特定モジュール専用のヘルパースクリプト
# ./modules/database/helpers/
# test_connection.sh # このモジュールのDB接続テスト
# backup_verification.sh # このモジュールのバックアップ検証
参考リンク
ルール
*Save template files read by Terraform’s templatefile function as .tftpl in the caller’s templates/ (e.g., ./modules/api/templates/s3_bucket_policy.tftpl)
(Terraformのtemplatefileファンクションで読み込むテンプレートファイルは、*.tftpl形式で呼び出し元のtemplates/に保存する)
解説
Terraformのtemplatefile()で呼び出すテンプレートファイルは、templates/ディレクトリに、.tftpl拡張子を使って保存します。
決めの問題なのでAWSの推奨に従っています。
サンプルコード
# modules/api/main.tf
resource "aws_s3_bucket_policy" "main" {
bucket = aws_s3_bucket.main.id
# 同じモジュール内のtemplates/からテンプレートを読み込む
policy = templatefile("${path.module}/templates/s3_bucket_policy.tftpl", {
bucket_name = aws_s3_bucket.main.id
account_id = data.aws_caller_identity.current.account_id
})
}
# ディレクトリ構造:
# modules/
# api/
# main.tf
# templates/
# s3_bucket_policy.tftpl # S3バケットポリシーのテンプレート
# iam_policy.tftpl # IAMポリシーのテンプレート
参考リンク
ルール
Use snake_case for all directory and file names (e.g., awesome_module/awesome_specifications.md)
(すべてのディレクトリ名とファイル名にはsnake_caseを使用する)
解説
スネークケースは、Terraformのリソース名やモジュール名でよく用いられる命名規則です。
ファイル名についてはケバブケースが使われることも多いのですが、逐一スネークケースにすべきかケバブケースにすべきかを考えないといけなくなっていると混乱のもとのため、なるべくスネークケースによせています。
サンプルコード
# ディレクトリ構造の例:
# modules/
# awesome_module/ # snake_caseのディレクトリ名
# main.tf
# variables.tf
# awesome_specifications.md # snake_caseのファイル名
参考リンク
ルール
Avoid using custom scripts as much as possible. If necessary, create a scripts/ directory directly under the using module and save scripts there
(カスタムスクリプトは可能な限り使用しない。必要な場合は使用するモジュール直下にscripts/ディレクトリを作成し、そこにスクリプトを保存する)
解説
Terraformから呼び出すスクリプト(カスタムスクリプト)を使う場合はscripts/以下に置きます。ただしほとんど必要自体ないはずです。
むしろヘルパースクリプトと呼ばれる、Terraformから呼び出すのではない補助用のスクリプトをscripts/に置かないよう気を付けます。ヘルパースクリプトは、helpers/に保存します。こちらは通常リポジトリルート直下に保存します(e.g., <root_dir>/helpers/awesome.sh)
サンプルコード
参考リンク
ルール
Root modules serving as entry points are expressed using environment names. These are placed directly under the root directory (e.g., ./development ./production)
(エントリーポイントとなるルートモジュールは環境名で表現する)
解説
Terraformを実行する起点となるルートモジュールは環境ごとに独立したディレクトリとして表現します。
たとえばdevelopment/なら開発環境用です。これはTerraform公式が紹介している方式でもあります。
サンプルコード
# ディレクトリ構造の例
# ./development/main.tf - 開発環境用のエントリーポイント
# ./staging/main.tf - ステージング環境用のエントリーポイント
# ./production/main.tf - 本番環境用のエントリーポイント
# ./development/main.tf の内容例
module "networking" {
source = "../modules/networking"
environment = "development"
vpc_cidr = "10.0.0.0/16"
}
module "compute" {
source = "../modules/compute"
environment = "development"
instance_type = "t3.micro"
}
参考リンク
ルール
Write variable values in terraform.tfvars.json. Environment-specific values in root modules, common values for all environments in repository root. However, it should be placed at the highest appropriate level in a monorepo.
(terraform.tfvars.jsonに変数値を記述する。環境固有の値はルートモジュールに、全環境共通の値はリポジトリルートに配置する。ただしモノレポの場合はふさわしいディレクトリのうち最上位でかまわない。)
解説
変数値はterraform.tfvarsその他ではなくterraform.tfvars.jsonに書きます。これはsopsを用いた暗号化の都合もあります。
なおリポジトリルートに配置するのはTerraform専用のリポジトリを使っている場合の話で、モノレポでさまざまなものが詰まっている場合はふさわしい場所のうち一番上位に配置します。たとえば<repository_root>/infrastructure/ です。
サンプルコード
// ./terraform.tfvars.json (リポジトリルート - 全環境共通)
{
"project": "practitioner",
"region": "ap-northeast-1"
}
// ./production/terraform.tfvars.json (環境固有)
{
"environment": "production",
"instance_type": "t3.medium",
"instance_count": 3
}
// ./development/terraform.tfvars.json (環境固有)
{
"environment": "development",
"instance_type": "t3.micro",
"instance_count": 1
}
参考リンク
備考
The reason for using the less readable json format is for ease of use with sops
ルール
Place providers.tf and versions.tf only in the root directory
(providers.tfとversions.tfはルートディレクトリのみに配置する)
解説
プロバイダーはルートモジュール(terraform apply を実行する場所。development/など。)で一元管理します。言い方を変えると、サブモジュールでプロバイダを設定してはいけません。
サンプルコード
# ./production/providers.tf (ルートモジュール)
provider "aws" {
region = "ap-northeast-1"
default_tags {
tags = {
Environment = "production"
ManagedBy = "Terraform"
}
}
}
# ./production/versions.tf (ルートモジュール)
terraform {
required_version = "= 1.9.8"
required_providers {
aws = {
source = "hashicorp/aws"
version = "= 5.75.1"
}
}
}
参考リンク
ルール
Use locals.tf only when necessary
(locals.tfは必要な場合のみ使用する)
解説
locals ブロックは複雑な式の計算結果を一時的に保持したり、繰り返し使われる値に名前を付けるために使用しますが、過度に使用するとコードの可読性が低下します。そのためなるべく使わないようにします。
どうしても必要な場合は locals.tf に書きます。
サンプルコード
# locals.tf
locals {
# 複数箇所で使用される複雑な式
common_tags = merge(
var.default_tags,
{
Environment = var.environment
ManagedBy = "Terraform"
}
)
# 計算結果を保持
instance_count = var.environment == "production" ? 3 : 1
}
参考リンク