Terraform Code Conventions
terraform/
Module Structure#
terraform/
├── main.tf # Module composition and orchestration
├── variables.tf # Map-based variables (per environment)
├── config.auto.tfvars # Actual value configuration
├── outputs.tf # Output variables
├── backend.tf # S3 + DynamoDB remote state
├── moved.tf # State migration
└── modules/
├── shared/ # Created once (VPC, ALB, IAM, SG)
├── environment/ # Per environment (EC2, RDS, Redis, S3, DNS)
├── message/ # Push notifications (SQS, SNS, Lambda)
├── pdf_processing/ # PDF processing (Step Functions, Lambda)
└── batch_processing/ # Batch processing (SQS, Lambda)
Variable Design Patterns#
Map-Based Variables (Per-Environment Configuration)#
# CORRECT: Map for per-environment settings
variable "instance_types" {
type = map(string)
default = {
"production" = "t4g.medium"
"staging" = "t4g.small"
"development" = "t4g.small"
}
}
# WRONG: Separate variables per environment
variable "production_instance_type" { ... }
variable "staging_instance_type" { ... }
Passing Variables to Modules (lookup pattern)#
# CORRECT: Safe access with lookup
deeplink_subdomain = lookup(var.deeplink_config, "production", null) != null ? var.deeplink_config["production"]["subdomain"] : ""
# WRONG: Direct access (errors if key is missing)
deeplink_subdomain = var.deeplink_config["production"]["subdomain"]
Feature Toggle Pattern#
# Environment activation
locals {
is_production_enabled = var.enable_production_server
is_staging_enabled = var.enable_staging_server
is_development_enabled = var.enable_development_server
}
# Module count condition
module "production_env" {
count = local.is_production_enabled ? 1 : 0
# ...
}
# Service + environment combination condition
module "message_production" {
count = var.enable_push_notifications && local.is_production_enabled ? 1 : 0
# ...
}
Route53 Resource Patterns#
count Condition Rules#
| Record Type | count Condition |
|---|---|
| Primary CNAME | enable_route53 && enable_primary_dns && subdomain != "" |
| Secondary CNAME | enable_route53 && secondary_domain != null && subdomain != "" |
| Top domain A |
enable_route53 && enable_primary_dns && enable_top_domain_alias && length(ips) > 0
|
| Production-only | Additional local.is_production condition |
Subdomain Naming#
# CORRECT: Production has no prefix, others have suffix
name = local.is_production ? "console" : "console-${local.env_short_suffix}"
# env_short_suffix: "stg" or "dev"
Lambda ZIP Path Rules#
Relative Paths Based on path.module#
# Reference Lambda ZIP within module
locals {
lambda_base_path = "${path.module}/../../../lambda/python"
router_zip_path = "${local.lambda_base_path}/pdf_router/build/pdf-router.zip"
}
Caution: path.module is relative to the module .tf file location:
modules/pdf_processing/../../../=deploy/aws/- NOT the
terraform/execution directory
fileexists() Guard Pattern#
# CORRECT: Guard with fileexists()
locals {
zip_exists = fileexists(local.zip_path)
}
resource "aws_lambda_function" "example" {
count = local.zip_exists ? 1 : 0
source_code_hash = local.zip_exists ? filebase64sha256(local.zip_path) : null
}
# WRONG: Direct call without guard (plan fails if file missing)
resource "aws_lambda_function" "example" {
source_code_hash = filebase64sha256(local.zip_path)
}
State Management#
Remote Backend#
backend "s3" {
bucket = "kobic-tfstate-bucket"
key = "terraform.tfstate"
region = "ap-northeast-2"
dynamodb_table = "kobic-tfstate-lock"
encrypt = true
}
Workspace Usage#
# List workspaces
terraform workspace list
# Switch workspace
terraform workspace select production
This project manages environments with a workspace + enable_*_server flag combination. Within each workspace, feature flags conditionally create per-environment resources.
Tag Rules#
tags = {
Project = var.project_name
Environment = var.environment
ManagedBy = "terraform"
}