Terraform

69 件の記事

ルール

Avoid abbreviations, use full names (e.g., cgcoding_guidelines). However, abbreviations used as proper nouns (e.g., RDS, id) may be used.

(略語を避け、完全な名前を使用する(例: cgcoding_guidelines)。ただし、固有名詞として使用される略語(例: RDS, id)は使用可能)

解説

独自の略語は外部の人員や将来の担当者が意図を理解しにくくなるため極力利用を控えます。

ただし、RDSやidのような固有名詞に近いものは利用してかまいません。

サンプルコード

# 完全な名前を使用。ただしidのように半ば固有名詞として使われているものは可
variable "security_group_id" {
  type        = string
  description = "Security Group ID"
}

# このような略し方はNG
variable "sg_id" {
  type        = string
  description = "Security Group ID"
}

ルール

Do not use embedded attributes as much as possible. (e.g., ingress/egress within security groups)

(埋め込み属性をできるだけ使用しない(例: セキュリティグループ内の ingress/egress))

解説

埋め込み属性は可読性が悪い・ループなどの操作がしづらい・更新時に一時的に削除されることがあるなど、使いづらさが目立つため、極力使わないようにします。

サンプルコード

resource "aws_security_group" "web" {
  name        = "${var.environment}-${var.project}-web-sg"
  description = "Webサーバー用セキュリティグループ"
  vpc_id      = var.vpc_id
}

resource "aws_security_group_rule" "web_https_ingress" {
  type              = "ingress"
  from_port         = 443
  to_port           = 443
  protocol          = "tcp"
  cidr_blocks       = ["0.0.0.0/0"]
  security_group_id = aws_security_group.web.id
}

resource "aws_security_group_rule" "web_egress" {
  type              = "egress"
  from_port         = 0
  to_port           = 0
  protocol          = "-1"
  cidr_blocks       = ["0.0.0.0/0"]
  security_group_id = aws_security_group.web.id
}

参考リンク

ルール

Do not use variable blocks unless strictly necessary. They may be used only when there are actual parameter differences between environments (e.g., development vs. production).

(変数ブロックは厳密に必要な場合のみ使用する。環境間で実際にパラメータ差異がある場合(例:開発環境と本番環境)にのみ使用可能)

解説

生成AIはvariable blocksを必要もなく使いたがる傾向があるため、濫用を禁止しています。

ただし環境ごとに異なる値(CPUサイズ、インスタンス数など)を表現するためには必要ですから、全面禁止にはしていません。

サンプルコード

# 開発環境と本番環境でCPUサイズが異なる場合のみ変数化
variable "cpu_size" {
  type        = number
  description = "ECS task CPU size"
}

resource "aws_ecs_task_definition" "main" {
  family = "api"
  cpu    = var.cpu_size  # 環境により 512 または 2048
  memory = 1024

  # その他の固定値は直接記述
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
}

ルール

Define calls to other modules in main.tf.

(他モジュールの呼び出しはmain.tfに定義する)

解説

AWSの推奨に従っています。

モジュール呼び出しは必ずmain.tfにある、とわかっていれば、モジュールの呼び出し部分を迷わず見つけることが可能です。

サンプルコード

# main.tf
module "networking" {
  source = "./modules/networking"
  vpc_cidr = var.vpc_cidr
}

module "database" {
  source = "./modules/database"
  vpc_id = module.networking.vpc_id
  db_subnet_ids = module.networking.private_subnet_ids
}

module "application" {
  source = "./modules/application"
  vpc_id = module.networking.vpc_id
  db_endpoint = module.database.endpoint
}

参考リンク

ルール

Include units in numerical variable names (e.g., size_gb, ram_size_gb).

(数値変数名には単位を含める)

解説

単位を変数名に含めることで、単位の取り違えによるバグを防止します。

AWS推奨の設定方法です。

サンプルコード

variable "bandwidth_limit_mb" {
  type        = number
  description = "Bandwidth limit in MB per second"
}

参考リンク

ルール

Name boolean variables affirmatively (e.g., enable_external_access).

(真偽値変数は肯定形で命名する)

解説

コードの可読性を向上させるため、変数は肯定形で記載します。

否定形を使用した二重否定(e.g., disable_logging = false) は読みづらいため避けます。

サンプルコード

variable "enable_external_access" {
  type        = bool
  description = "Enable external access to the resource"
}

参考リンク

ルール

Do not reference variable blocks as values in output blocks.

(出力ブロックで変数ブロックを参照しない)

解説

AWSの推奨に従っています。

出力ブロックに変数をそのまま格納するのは無駄ですから普通しないと思いますが、念のため残しています。

サンプルコード(悪い例)

variable "name" {
  description = "Resource name"
  type = string
}

output "name" {
  description = "Name of Resource"
  value       = var.name
}

参考リンク

ルール

Implementation should consider the AWS Well-Architected Framework, though strict compliance is not required.

(実装はAWS Well-Architected Frameworkを考慮する。ただし厳格な準拠は要求しない)

解説

AWS Well-Architected Frameworkは大変優れた設計原則であり、何をおいてもこの考え方が設計の基本となります。 ただしその方針を具体化したルールの中には準拠が難しいものも含まれるため、参考に止めればよいとしています。

ルール

Fixed values may be coded directly in resource definitions. (e.g., t4g.micro)

(固定値はリソース定義に直接記述してよい(例:t4g.micro))

解説

生成AIはむやみやたらとvariablesを使いたがる傾向があります。そこで明示的にそれを抑制しています。

サンプルコード

resource "aws_instance" "web" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = "t4g.micro"  # 固定値を直接記述

  tags = {
    Name = "web-server"
  }
}

ルール

Common tags such as Environment and Service must be set only in aws_default_tags. Do not define them in any individual resource block.

EnvironmentServiceなどの共通タグはaws_default_tagsのみで設定する。個別のリソースブロックで定義しない)

解説

すべてのAWSリソースに共通して付与すべきタグはaws_default_tagsで一元管理します。個々のリソースブロックで設定してはならないのですが、AIはやりがちのため、明示的に禁止しています。

サンプルコード

# providers.tf
provider "aws" {
  region = "ap-northeast-1"

  default_tags {
    tags = {
      Environment = var.environment
      Service     = var.service
    }
  }
}

# main.tf - 個別リソースでは共通タグを定義しない
resource "aws_s3_bucket" "main" {
  bucket = "${var.environment}-${var.service}-data"
  # Environment、Serviceタグは自動的に付与される
}

ルール

Do not use variable blocks for values that remain the same across environments.

(環境間で同一の値には変数ブロックを使用しない)

解説

生成AIはvariable blocksを必要もなく使いたがる傾向があるため、濫用を禁止しています。

サンプルコード

# 全環境で同じ値は直接記述
resource "aws_ecs_task_definition" "main" {
  family                   = "api"
  network_mode             = "awsvpc"  # 全環境で同じ
  requires_compatibilities = ["FARGATE"]  # 全環境で同じ

  # 環境ごとに異なる値のみ変数化
  cpu    = var.cpu_size
  memory = var.memory_size
}

ルール

Use variable blocks when parameter values differ between environments (e.g., production → CPU 4096, development → CPU 1024).

(環境間でパラメータ値が異なる場合にvariableブロックを使用する(例: 本番環境 → CPU 4096、開発環境 → CPU 1024))

解説

生成AIはvariable blocksを必要もなく使いたがる傾向があるため、濫用を禁止しています。しかし全く使わないとそれはそれでおかしなコードができあがるため、利用して良いケースを明示しています。

サンプルコード

# variables.tf
variable "cpu_units" {
  type        = number
  description = "CPU units for ECS task"
}

ルール

Do not use plural forms in resource names.

(リソース名に複数形を使用しない)

解説

特にこだわる所ではないため、素直にAWSの推奨に従っています。

なぜこのようになっているかは存じませんが、リソースによって複数形だったり単数形だったりするよりは、片方だけに統一されているほうが好ましいと考えます。

サンプルコード

# 良い例: 単数形を使用
resource "aws_instance" "web_server" {
  count         = 3
  instance_type = "t3.micro"
  ami           = var.ami_id
}

参考リンク

ルール

Do not include the resource type name in the resource name (e.g., avoid aws_instance.ec2_instance).

(リソース名にリソースタイプ名を含めない)

解説

二つの理由により、リソース名にリソースタイプ名を含めるべきではありません。

  1. リソースタイプを見ればわかることは書くべきでない
  2. 命名が雑になりやすい

悪い例としてはaws_instance.ec2_instance のような命名が該当します。

AWS公式のベストプラクティスでもこの命名規則が推奨されています。

サンプルコード

# 良い例: リソースタイプを重複させない
resource "aws_instance" "web_server" {
  instance_type = "t3.micro"
  ami           = var.ami_id
}

resource "aws_security_group" "api" {
  name   = "${var.environment}-${var.project}-api"
  vpc_id = aws_vpc.main.id
}

参考リンク

ルール

The order of resource block attributes should be as follows: 1. If present, The count or for_each meta-argument, 2. Resource-specific non-block parameters, 3. Resource-specific block parameters, 4. If required, a lifecycle block, 5. If required, the depends_on parameter. No other constraints apply.

(リソースブロック属性の順序は以下の通りとする:1. 存在する場合、countまたはfor_eachメタ引数、2. リソース固有の非ブロックパラメータ、3. リソース固有のブロックパラメータ、4. 必要な場合、lifecycleブロック、5. 必要な場合、depends_onパラメータ。その他の制約は適用しない)

解説

Terraform公式スタイルガイドに従っています。

決めの問題ですが、ある程度フォーマットされているだけでも読みやすさが上がります。

サンプルコード

resource "aws_instance" "web" {
  # 1. メタ引数
  count = var.instance_count

  # 2. 非ブロックパラメータ
  ami           = data.aws_ami.ubuntu.id
  instance_type = "t4g.micro"
  subnet_id     = aws_subnet.main.id

  # 3. ブロックパラメータ
  tags = {
    Name = "web-server-${count.index}"
  }

  # 4. lifecycle
  lifecycle {
    create_before_destroy = true
  }

  # 5. depends_on
  depends_on = [aws_security_group.web]
}

参考リンク

ルール

Multiple resources within a module should be named according to their role (e.g., primary, read_replica).

(モジュール内の複数リソースは役割に応じて命名する)

解説

モジュール内に同じタイプのリソースが複数存在する場合は、primaryread_replica のようにそのリソースの役割を示す名称をつけます。

なお単一リソースの場合は mainとします。

サンプルコード

# modules/database/main.tf
# データベースの役割に応じて命名
resource "aws_db_instance" "primary" {
  identifier     = "${var.environment}-${var.project}-primary"
  engine         = "postgres"
  instance_class = "db.t3.medium"
}

resource "aws_db_instance" "read_replica" {
  identifier          = "${var.environment}-${var.project}-replica"
  replicate_source_db = aws_db_instance.primary.id
  instance_class      = "db.t3.small"
}

参考リンク

ルール

A single resource within a module should be named main (e.g., resource "aws_vpc" "main").

(モジュール内の単一リソースは main と命名する)

解説

モジュール内に特定のリソースタイプが一つしかない場合はリソース名を main とします。

採用の理由は「それがAWS推奨の手法だから」ですが、モジュール内の主要なリソース、またはそのリソースのための補助的な宣言であることが理解しやすいため従っておけばよいと考えます。

サンプルコード

# modules/vpc/main.tf
# VPCリソースが一つだけの場合は main と命名
resource "aws_vpc" "main" {
  cidr_block           = var.cidr_block
  enable_dns_hostnames = true
  enable_dns_support   = true
}

参考リンク

ルール

Apply terraform fmt to all .tf files.

(すべての.tfファイルにterraform fmtを適用する)

解説

「フォーマッタが正しい」これをプロジェクトのルールにします。

個人のスタイルの違いが出ないよう、ツールにすべて任せます。

サンプルコード

# terraform fmt 実行前
resource "aws_instance" "web" {
instance_type="t3.micro"
  ami=data.aws_ami.ubuntu.id
    tags={
  Name="web-server"
    }
}

# terraform fmt 実行後
resource "aws_instance" "web" {
  instance_type = "t3.micro"
  ami           = data.aws_ami.ubuntu.id

  tags = {
    Name = "web-server"
  }
}

参考リンク

ルール

Use MiB/GiB for storage units, and MB/GB (decimal units) for others.

(ストレージ単位にはMiB/GiBを使用し、その他にはMB/GBを使用する)

解説

ストレージ容量(ディスク/ボリューム/スナップショット等)はバイナリ単位を使用します。それ以外のメトリクス(ネットワーク転送量、帯域など)は10進数の単位を使用します。単位の微妙な違いによる勘違いを防止します。

サンプルコード

# ストレージには2進数(GiB/MiB)を使用
variable "ebs_volume_size_gib" {
  type        = number
  description = "EBS volume size in GiB"
}

# それ以外は10進単位を使用
variable "cw_alarm_network_out_threshold_mb" {
  type        = number
  description = "CloudWatch Alarm threshold for NetworkOut in MB"
}

参考リンク

ルール

Do not include provider or terraform in submodules.

(サブモジュールにproviderterraformブロックを含めない)

解説

プロバイダー設定はルートモジュールで一括管理します。そのためサブモジュールでproviderやterraformブロックを利用してはいけません。

Terraformの公式ベストプラクティスでも推奨されている設計原則です。

サンプルコード

# production/providers.tf(ルートモジュールで定義)
terraform {
  required_version = ">= 1.5.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = "ap-northeast-1"
}

ルール

Do not add comments describing what is already clear from the code.

(コードから明らかな内容を説明するコメントを追加しない)

解説

コードを見れば分かる内容を繰り返すコメントは害のほうが大きいため禁止しています。

サンプルコード

# 良い例
resource "aws_s3_bucket_lifecycle_configuration" "logs" {
  bucket = aws_s3_bucket.logs.id

  rule {
    id     = "delete_old_logs"
    status = "Enabled"

    # 監査要件により90日間保持
    expiration {
      days = 90
    }
  }
}

# 悪い例(参考)
# resource "aws_s3_bucket" "logs" {  # <- S3バケットを作成
#   bucket = "${var.environment}-${var.project}-logs"  # <- バケット名を設定
# }

ルール

  • Infrastructure directory structure:
infrastructure/
  scripts/
  terraform/
    development/
    production/
    modules/
      <module_name>/

(インフラストラクチャのディレクトリ構造を定めておく)

解説

ディレクトリ構造はあらかじめ定めておきます。露骨な言い方をすると、生成AIにディレクトリ構造を決めさせてはいけません。

生成AIはランダムな結果を返すものですから、なんのルールもないと、法則も設計もないディレクトリ構造を作成します。ランダムなので法則がなく、したがって理解しようもないディレクトリ構造は、運用を困難にします。

サンプルコード

infrastructure/
  scripts/
    deploy.sh
  terraform/
    development/
      main.tf
      variables.tf
      outputs.tf
      terraform.tfvars.json
    production/
      main.tf
      variables.tf
      outputs.tf
      terraform.tfvars.json
    modules/
      networking/
        main.tf
        variables.tf
        outputs.tf
      database/
        main.tf
        variables.tf
        outputs.tf
      compute/
        main.tf
        variables.tf
        outputs.tf

参考リンク

備考

infrastructure/
  scripts/
  terraform/
    development/
    production/
    modules/
      <module_name>/

ルール

Define variable blocks in variables.tf.

(variableブロックはvariables.tfに定義する)

解説

variableブロックの置き場所を定めておくことで可読性を向上させます。

サンプルコード

# variables.tf
variable "environment" {
  type        = string
  description = "Environment name (e.g., production, staging)"
}

variable "vpc_cidr" {
  type        = string
  description = "CIDR block for VPC"
}

variable "enable_nat_gateway" {
  type        = bool
  description = "Whether to create NAT Gateway for private subnets"
}

参考リンク

ルール

The order of variable block attributes should be as follows: 1. Type, 2. Description, 3. Default (optional), 4. Sensitive (optional), 5. Validation blocks

(variableブロックの属性順序は次の通り: 1. type、2. description、3. default(オプション)、4. sensitive(オプション)、5. validationブロック)

解説

HashiCorp公式の推奨に従っています。

統一された記述順序は可読性を向上させます。

サンプルコード

variable "instance_type" {
  type        = string
  description = "EC2インスタンスタイプ"
  default     = "t3.micro"

  validation {
    condition     = can(regex("^t3\\.", var.instance_type))
    error_message = "インスタンスタイプはt3系を指定してください"
  }
}

variable "db_password" {
  type        = string
  description = "データベースパスワード"
  sensitive   = true
}

参考リンク

ルール

Include type definitions in variable blocks.

(variableブロックに型定義を含める)

解説

AWSの推奨に従っています。

変数に型を明示することで誤った値の代入を防ぐ他、変数の性格を推測しやすくなります。

サンプルコード

# variables.tf
variable "environment" {
  type        = string
  description = "Environment name"
}

variable "instance_count" {
  type        = number
  description = "Number of instances to create"
}

参考リンク

ルール

Do not set default values in variable blocks.

(variableブロックにdefault値を設定しない)

解説

デフォルト値があると設定漏れに気づかずに意図しない値でインフラが構築されることがあります。

ほんの少し楽をしようとして事故を起こすのは割に合わないため、全面的に禁止しています。

サンプルコード

# variables.tf (良い例)
variable "instance_type" {
  type        = string
  description = "EC2 instance type"
  # defaultは設定しない
}

ルール

Include descriptions in variable and output blocks.

(variableとoutputブロックにdescriptionを含めること)

解説

変数と出力値に説明を付けることで、各パラメータの目的を誤解しづらくなります。人間も助かりますが、なによりAIの助けになります。

サンプルコード

# variables.tf
variable "instance_type" {
  type        = string
  description = "EC2 instance type for application servers"
}

参考リンク

ルール

Introduce TFLint to detect AWS best practice violations and unused declarations.

(TFLintを導入してAWSベストプラクティス違反と未使用宣言を検出する)

解説

人間のレビューを受ける前に機械的なチェックで済ませられることを極力済ませておきます。

これは人間のレビュー負荷軽減に貢献します。

サンプルコード

$ tflint

参考リンク

ルール

Use snake_case for resource names in terraform code (e.g., web_server).

(Terraformコードのリソース名はスネークケースを使用する)

解説

Terraformのリソース名には通常スネークケースが用いられます。

AWS公式のベストプラクティスでもスネークケースの使用が推奨されています。

サンプルコード

# リソース名にスネークケースを使用
resource "aws_instance" "web_server" {
  instance_type = "t3.micro"
  ami           = var.ami_id
}

参考リンク

ルール

Place terraform.tfvars.json in the root module (the module where terraform apply is executed) instead of submodules.

terraform.tfvars.jsonはルートモジュールに配置し、サブモジュールには配置しない)

解説

混乱防止のため、terraform.tfvars.jsonは必ずルートモジュールに置くルールです。

サンプルコード

# ディレクトリ構造
#
# terraform/
#   production/
#     main.tf
#     variables.tf
#     terraform.tfvars.json    # ← ここに配置
#   modules/
#     networking/
#       main.tf
#       variables.tf
#       # terraform.tfvars.json は配置しない

ルール

In the root module, ensure terraform.tfvars.json stores: 1) values that other modules might also reuse, 2) sensitive information that must never be hard-coded (e.g., private keys).

(ルートモジュールのterraform.tfvars.jsonには、1) 他のモジュールでも再利用する値、2) ハードコードしてはならない機密情報を格納する)

解説

平文の機密情報はなるべくTerraformを通さずに利用したいところですが、利用せざるを得ないケースもあります。その場合はコード内にハードコーディングせず、terraform.tfvars.jsonに記述してsopsで暗号化します。

なおこのアプローチをとる場合、そのリポジトリでCursorやClaudeCodeなどクラウドタイプのAIを利用してはいけません。

サンプルコード

# terraform.tfvars.json(良い例)
{
  "environment": "production",
  "project": "myapp",
  "region": "ap-northeast-1",
  "db_password": "encrypted_password_here",
  "ssl_certificate_private_key": "encrypted_key_here"
}

# variables.tf
variable "environment" {
  type        = string
  description = "Environment name"
}

variable "project" {
  type        = string
  description = "Project name"
}

variable "db_password" {
  type        = string
  sensitive   = true
  description = "Database password"
}

# main.tf
module "database" {
  source = "../modules/database"

  environment = var.environment
  project     = var.project
  password    = var.db_password
}

module "networking" {
  source = "../modules/networking"

  environment = var.environment
  project     = var.project
}

ルール

Perform syntax validation with terraform validate.

terraform validateで構文検証を実行する)

解説

生成AIが最初に出したコードには時折構文エラーが含まれます。 これらを自動的に検出し、自動で修正させるため、ツールによるバリデーションを行います。

人間のレビュー負荷軽減のための施策の一つです。

サンプルコード

# 構文検証の実行
$ terraform validate

参考リンク

ルール

Always set Name tags for resources that use Name tags as identifiers.

(Nameタグを識別子として使用するリソースには必ずNameタグを設定する)

解説

EC2インスタンスなど、Nameタグが事実上のリソース名として用いられるリソースがいくつかあります。

これらはNameタグが適切につけられていないと管理運用が難しくなるため、必ずセットします。

サンプルコード

resource "aws_instance" "api" {
  tags = {
    Name = "${var.environment}-${var.project}-api"
  }
}

参考リンク

ルール

Minimize the use of locals blocks. If used, define them in locals.tf.

localsブロックの使用を最小限にする。使用する場合はlocals.tfに定義する)

解説

AWSの推奨に従っています。

使用する際はlocals.tfに集約することで、ローカル変数の定義場所を一元化し可読性を維持します。

サンプルコード

# locals.tf
locals {
  # 複数の変数を組み合わせた共通タグ
  common_tags = {
    Environment = var.environment
    Project     = var.project
  }

  # 計算が必要な値
  common_prefix = "${var.environment}-${var.project}-"
}

参考リンク

ルール

Minimize the use of data blocks. If used, place them immediately above the resource block that references them.

dataブロックの使用を最小限にする。使用する場合は、参照するリソースブロックの直前に配置する)

解説

dataブロックは便利なのですが、AWS APIへの問い合わせが行われるため、多用するとTerraformの実行が遅くなります。 そのため使わなくてもよい場面ではDataブロックではなくvariablesを利用します。

ただし禁止ではないため、使用することもあります。その場合はなるべく読みやすいよう参照するリソースの直前に配置します。

サンプルコード

# 使用するリソースの直前にdataブロックを配置
data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"]  # Canonical

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
  }
}

resource "aws_instance" "web" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = "t3.micro"
}

参考リンク

ルール

Name AWS resource names (Name tags) in the format [environment]-[project]([-additional_info_if_any]) (e.g., production-practitioner-api). In HCL, use variables like ${var.environment}-${var.project}-api. ‘api’ is just an example, so be sure to replace it appropriately.

(AWSリソース名(Nameタグ)は[環境名]-[プロジェクト名][-追加情報]の形式で命名する)

解説

これはプロジェクト独自の命名規則です。

環境名を先頭に配置することですぐにどの環境のリソースかわかるようにし、環境の取り違えを防止します。

またどのリソース名でも同一の命名規則を徹底することで、スクリプトなどを使った一括処理を行いやすくします。

サンプルコード

resource "aws_instance" "api" {
  ami           = "ami-12345678"
  instance_type = "t3.micro"

  tags = {
    Name = "${var.environment}-${var.project}-api"
  }
}

参考リンク

ルール

*Keep comments in .tf files to a minimum. Avoid adding header blocks or documentation-level specifications in comments.

(*.tfファイルのコメントを最小限に保つ。ヘッダーブロックの追加禁止。またドキュメントに記載すべき仕様も書いてはならない)

解説

生成AIはしばしば大量のコメントやヘッダーブロックをコード内に埋め込もうとします。しかし過剰なコメントは可読性を落とし、コメントの保守の手間を増やし、トークン効率を悪化させます。そのため禁止しています。

サンプルコード

# 良い例: 最小限のコメント、自己文書化されたコード
variable "instance_type" {
  type        = string
  description = "EC2インスタンスタイプ。本番環境ではt3.mediumを推奨"
}

resource "aws_instance" "web" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = var.instance_type

  # セキュリティ要件により、IMDSv2を強制
  metadata_options {
    http_endpoint               = "enabled"
    http_tokens                 = "required"
  }
}

ルール

Priorities are: 1) Security and Reliability, 2) Operational Excellence and Performance Efficiency, 3) Cost Optimization, considering balance among the three

(優先順位は 1) セキュリティと信頼性、2) 運用上の優秀性とパフォーマンス効率、3) コスト最適化とし、3つのバランスを考慮する)

解説

システムは何をおいても動き続けなければなりません。そのためセキュリティと信頼性を最重視しています。 次に、システムは運営費が続かなくなって止まることも防がねばなりません。運用上の優秀性とパフォーマンス効率も無視するわけにいきません。 最後にコスト最適化ですが、これはセキュリティや信頼性と比較すれば優先順位は下がります。

ただしこれらはバランスを考慮しなければなりません。いくらセキュリティが大事だからと言っても、金額換算すれば10ドル程度のメリットのために、1000ドルもかかるような対策を打つべきではありません。

サンプルコード

ルール

Do not create modules that only wrap a single resource; define resources directly in the caller when needed

(単一リソースを包むだけのモジュールは作らない。必要な場合は呼び出し元で直接リソースを定義する)

解説

特定の設定をまとめただけのリソースブロックをモジュールにしないようにします。

粒度が小さすぎるモジュールは保守性をかえって悪化させます。

サンプルコード

参考リンク

ルール

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コード

参考リンク

ルール

Always log important resources and operations

(重要なリソースと操作は常にログを取る)

解説

トレーサビリティの確保にも、トラブルシューティングにも、ログは必要不可欠です。 なくても動くからとおろそかにせず、重要なリソースのログは必ず残します。代表例はCloudTrailです。

ただしプロジェクトの性格上重要ではないものはオミットして差し支えありません。

サンプルコード

ルール

Always backup important resources and data. DR strategy is at backup & restore level

(重要なリソースとデータは常にバックアップする。DR戦略はバックアップ&リストアレベルとする)

解説

重要なリソースとは自社保有の暗号鍵など、データとはデータベース上のレコードなどです。 これらの消失はサービス存続にも関わる大損害をもたらすため、必ずバックアップします。

ここではコスト面を重視しDR(災害対策)はバックアップ&リストア方式を採用していますが、プロジェクトの性格次第でそれ以外の戦略を取ってももちろんかまいません。

サンプルコード

なし

参考リンク

ルール

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   - データベース関連の再利用可能モジュール

参考リンク

ルール

Do not define provider blocks within shared modules (provider configuration should be done only once in root module)

(サブモジュール内で provider ブロックを定義しない。provider の設定はルートモジュールで一度だけ行う)

解説

プロバイダの設定はルートモジュールで一元管理します。サブモジュール内に provider ブロックを含めると混乱のもととなるため禁止します。

サンプルコード

# ルートモジュール: ./production/providers.tf
provider "aws" {
  region = "ap-northeast-1"

  default_tags {
    tags = {
      Environment = "production"
      Service     = "my-service"
    }
  }
}

# 共有モジュール: ./modules/networking/main.tf
# providerブロックは定義しない
resource "aws_vpc" "main" {
  cidr_block = var.vpc_cidr
}

参考リンク

ルール

Never open repositories containing sensitive information in LLM-integrated editors

(機密情報を含むリポジトリをLLM統合エディタで開かない)

解説

LLMを統合したエディタは、開いたファイル内容がクラウドサービスへ送信される可能性があります。うっかり機密情報を外部に流してしまわないよう、機密情報を含むリポジトリはLLM統合エディタでは開かないようにします。

暗号化していれば別ですが、その場合は復号するための鍵がLLM統合エディタの手の届かないようにします。もちろん復号した後の平文のテキストがLLM統合エディタから開ける状態もNGです。

サンプルコード

ルール

*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={}'

参考リンク

ルール

Group related resources logically and encapsulate by functional units like network foundation and data layer

(関連リソースを論理的にまとめ、ネットワーク基盤やデータ層のような機能単位でカプセル化する)

解説

モジュールは用途に沿ったリソースをまとめられるように勤めます。variableブロックを用いた外部への依存およびoutputブロックを用いた外部への情報提供は少ないに越したことはありません。

サンプルコード

# ネットワーク基盤を機能単位でまとめたモジュール
# modules/networking/main.tf
resource "aws_vpc" "main" {

}

resource "aws_subnet" "public" {

}

resource "aws_internet_gateway" "main" {

}

resource "aws_route_table" "public" {

}

参考リンク

ルール

Environment names are production, staging, development, and experimental

(環境名はproductionstagingdevelopmentexperimentalとする)

解説

productionは本番環境、stagingは本番環境と同等の構成で最終検証を行う環境、developmentは開発者が日常的に使用する開発環境、experimentalは新技術の検証や実験的な機能を試すための環境です。 表記ゆれがないよう明確に指定します。prd, prod, product, productionと同じ用途で微妙に異なる文字列が使われることは、美観を損なうだけでなく、リソースをまとめて管理したい際の妨げとなります。

サンプルコード

# ディレクトリ構造での使用例
# terraform/
#   production/
#   staging/
#   development/
#   experimental/

# リソース命名での使用例
resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"

  tags = {
    Name        = "${var.environment}-${var.project}-vpc"
    Environment = var.environment  # production, staging, development, experimental のいずれか
  }
}

ルール

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"
  # ...
}

参考リンク

ルール

Always expose reference values for resources within modules through outputs to make dependencies explicit

(モジュール内リソースの参照値は必ずoutputで公開し、依存関係を明示する)

解説

あるモジュール内で作成されたリソースの値を他のモジュールやリソースで使用する際は、outputブロックを通じて利用させます。 言い方を変えると、dataブロックやARNのハードコーディングを用いるコーディングは、保守性を落とすため避けるべきです。

サンプルコード

# outputで参照値を明示的に公開
# modules/networking/outputs.tf
output "vpc_id" {
  description = "The ID of the VPC"
  value       = aws_vpc.main.id
}

参考リンク

ルール

Keep module hierarchy to 1-2 levels and maintain flat inheritance

(モジュール階層を1〜2段階に保ち、フラットな継承を維持する)

解説

サブモジュールから別のサブモジュールを呼び出す構造は極力避けます。避けられない場合でも1度までとします。 コピペしたDRYではないベタ書きが増えるわけですが、Terraform用コードは結局それがバランスがいいようです。

サンプルコード

# フラットなモジュール階層
# main.tf(ルートモジュール)
module "networking" {
  source = "./modules/networking"

  vpc_cidr             = var.vpc_cidr
  availability_zones   = var.availability_zones
  public_subnet_cidrs  = var.public_subnet_cidrs
  private_subnet_cidrs = var.private_subnet_cidrs
}

module "application" {
  source = "./modules/application"

  vpc_id            = module.networking.vpc_id
  private_subnet_ids = module.networking.private_subnet_ids
  app_port          = var.app_port
}

# modules/networking/ と modules/application/ は
# さらに他のモジュールを呼ばず、直接リソースを定義

参考リンク

ルール

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のファイル名

参考リンク

ルール

AWS is used as the cloud platform

(クラウドプラットフォームとしてAWSを使用する)

解説

このプロジェクトでは、AWSを標準のクラウドプラットフォームとして採用しています。 AWSは手厚いサポートと情報提供、優秀なサービスラインナップ、安定したエコシステムを持ち、多くの企業での採用実績があります。

サンプルコード

参考リンク

ルール

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"
}

参考リンク

ルール

Infrastructure is built and operated using Terraform

(インフラストラクチャはTerraformを使用して構築および運用する)

解説

このプロジェクトでは、インフラをIaCツールのTerraformで管理します。 コードでインフラを管理することは、生成AIによるインフラ構築を容易にするばかりか、GitとGithubを使ったトレーサビリティの確保の道も開きます。 今や手作業だけでAWSの管理をすることは想像もし難いことです。

サンプルコード

参考リンク

ルール

Prepare service monitoring and alerting mechanisms

(サービスの監視とアラート機構を準備する)

解説

サービスの安定運用には、問題の早期発見と迅速な対応が不可欠です。 CloudWatchやDatadog等を使用して監視・発報の仕組みを用意しておきます。

サンプルコード

参考リンク

ルール

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

ルール

Declare required providers and version constraints in required_providers (e.g., = 4.0)

(required_providers で必要なプロバイダとバージョン制約を宣言する)

解説

プロバイダやTerraformのバージョンによって無視できない挙動の違いが出ることが稀にあります。 本番環境でそのようなことが起こらないよう、バージョンは「=」だけで完全に固定します。「以上(>=)」や「パッチバージョンの違いは許す"~>"」のようなあいまいな書き方はすべて禁止です。

サンプルコード

# versions.tf
terraform {
  required_version = "= 1.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "= 4.0"
    }
  }
}

参考リンク

ルール

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
}

参考リンク

ルール

Implementation follows AWS Well-Architected Framework

(AWS Well-Architected Framework に従って実装する)

解説

AWS Well-Architected Framework は、AWSでインフラを構築する際の基本となる設計原則です。 考慮すべき事項の抜け漏れ防止、あるいは関心事の深堀りなどその効用は多岐にわたります。生成AIに活用させるには若干漠然としすぎていますが、それでも指摘せぬわけにいかない重要事項です。

サンプルコード

参考リンク