Terraform Module Writer
Эксперт по созданию высококачественных, переиспользуемых Terraform модулей с соблюдением industry best practices.
Module Structure
Standard Directory Layout
modules/
└── my-module/
├── main.tf # Primary resource definitions
├── variables.tf # Input variable declarations
├── outputs.tf # Output value definitions
├── versions.tf # Provider version constraints
├── locals.tf # Local values and computations
├── data.tf # Data source definitions
├── README.md # Module documentation
├── CHANGELOG.md # Version history
├── examples/
│ ├── basic/
│ │ ├── main.tf
│ │ └── outputs.tf
│ └── advanced/
│ ├── main.tf
│ └── outputs.tf
└── tests/
└── module_test.go
versions.tf Template
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0.0, < 6.0.0"
}
random = {
source = "hashicorp/random"
version = ">= 3.5.0"
}
}
}
Input Variable Design
Variable Declaration Best Practices
# variables.tf
# Required variables (no default)
variable "name" {
description = "Name prefix for all resources created by this module"
type = string
validation {
condition = can(regex("^[a-z][a-z0-9-]*$", var.name))
error_message = "Name must start with a letter and contain only lowercase letters, numbers, and hyphens."
}
}
variable "environment" {
description = "Environment name (e.g., dev, staging, prod)"
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be one of: dev, staging, prod."
}
}
# Optional variables with sensible defaults
variable "instance_type" {
description = "EC2 instance type for the application servers"
type = string
default = "t3.medium"
validation {
condition = can(regex("^[a-z][0-9][.][a-z]+$", var.instance_type))
error_message = "Instance type must be a valid AWS instance type format."
}
}
variable "enable_monitoring" {
description = "Enable detailed monitoring for resources"
type = bool
default = true
}
variable "min_instances" {
description = "Minimum number of instances in the Auto Scaling Group"
type = number
default = 2
validation {
condition = var.min_instances >= 1 && var.min_instances <= 100
error_message = "Minimum instances must be between 1 and 100."
}
}
variable "max_instances" {
description = "Maximum number of instances in the Auto Scaling Group"
type = number
default = 10
validation {
condition = var.max_instances >= 1 && var.max_instances <= 100
error_message = "Maximum instances must be between 1 and 100."
}
}
Complex Variable Types
# Object type with optional attributes
variable "vpc_config" {
description = "VPC configuration for the module"
type = object({
vpc_id = string
subnet_ids = list(string)
security_group_ids = optional(list(string), [])
assign_public_ip = optional(bool, false)
})
validation {
condition = length(var.vpc_config.subnet_ids) >= 2
error_message = "At least 2 subnet IDs required for high availability."
}
}
# Map with specific value types
variable "tags" {
description = "Additional tags to apply to all resources"
type = map(string)
default = {}
validation {
condition = alltrue([for k, v in var.tags : can(regex("^[a-zA-Z][a-zA-Z0-9:/_-]*$", k))])
error_message = "Tag keys must start with a letter and contain only valid characters."
}
}
# List of objects
variable "ingress_rules" {
description = "List of ingress rules for the security group"
type = list(object({
description = string
from_port = number
to_port = number
protocol = string
cidr_blocks = optional(list(string), [])
security_groups = optional(list(string), [])
}))
default = []
validation {
condition = alltrue([
for rule in var.ingress_rules :
rule.from_port >= 0 && rule.from_port <= 65535 &&
rule.to_port >= 0 && rule.to_port <= 65535
])
error_message = "Port numbers must be between 0 and 65535."
}
}
# Sensitive variables
variable "database_password" {
description = "Master password for the RDS instance"
type = string
sensitive = true
validation {
condition = length(var.database_password) >= 16
error_message = "Database password must be at least 16 characters."
}
}
Local Values
locals.tf Template
locals {
# Naming convention
name_prefix = "${var.name}-${var.environment}"
# Common tags applied to all resources
common_tags = merge(
var.tags,
{
Name = local.name_prefix
Environment = var.environment
ManagedBy = "terraform"
Module = "my-module"
Project = var.name
}
)
# Computed values
is_production = var.environment == "prod"
enable_encryption = local.is_production
backup_retention = local.is_production ? 30 : 7
instance_count = local.is_production ? var.max_instances : var.min_instances
# Derived configurations
monitoring_config = {
enabled = var.enable_monitoring || local.is_production
detailed = local.is_production
retention_days = local.is_production ? 90 : 30
alarm_threshold = local.is_production ? 80 : 90
}
# Dynamic block helpers
ingress_rules_map = {
for idx, rule in var.ingress_rules :
"${rule.protocol}-${rule.from_port}-${rule.to_port}" => rule
}
# Conditional resource naming
bucket_name = var.bucket_name != null ? var.bucket_name : "${local.name_prefix}-storage-${random_id.bucket.hex}"
}
Resource Definitions
main.tf Template
# main.tf
#------------------------------------------------------------------------------
# Security Group
#------------------------------------------------------------------------------
resource "aws_security_group" "this" {
name_prefix = "${local.name_prefix}-sg-"
description = "Security group for ${var.name} ${var.environment}"
vpc_id = var.vpc_config.vpc_id
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-sg"
})
lifecycle {
create_before_destroy = true
}
}
resource "aws_security_group_rule" "ingress" {
for_each = local.ingress_rules_map
type = "ingress"
security_group_id = aws_security_group.this.id
description = each.value.description
from_port = each.value.from_port
to_port = each.value.to_port
protocol = each.value.protocol
cidr_blocks = length(each.value.cidr_blocks) > 0 ? each.value.cidr_blocks : null
source_security_group_id = length(each.value.security_groups) > 0 ? each.value.security_groups[0] : null
}
resource "aws_security_group_rule" "egress" {
type = "egress"
security_group_id = aws_security_group.this.id
description = "Allow all outbound traffic"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
#------------------------------------------------------------------------------
# Launch Template
#------------------------------------------------------------------------------
resource "aws_launch_template" "this" {
name_prefix = "${local.name_prefix}-lt-"
description = "Launch template for ${var.name} ${var.environment}"
image_id = data.aws_ami.amazon_linux.id
instance_type = var.instance_type
network_interfaces {
associate_public_ip_address = var.vpc_config.assign_public_ip
security_groups = concat([aws_security_group.this.id], var.vpc_config.security_group_ids)
delete_on_termination = true
}
iam_instance_profile {
arn = aws_iam_instance_profile.this.arn
}
monitoring {
enabled = local.monitoring_config.detailed
}
metadata_options {
http_endpoint = "enabled"
http_tokens = "required" # IMDSv2
http_put_response_hop_limit = 1
}
block_device_mappings {
device_name = "/dev/xvda"
ebs {
volume_size = var.root_volume_size
volume_type = "gp3"
encrypted = local.enable_encryption
kms_key_id = local.enable_encryption ? var.kms_key_id : null
delete_on_termination = true
}
}
tag_specifications {
resource_type = "instance"
tags = local.common_tags
}
tag_specifications {
resource_type = "volume"
tags = local.common_tags
}
tags = local.common_tags
lifecycle {
create_before_destroy = true
}
}
#------------------------------------------------------------------------------
# Auto Scaling Group
#------------------------------------------------------------------------------
resource "aws_autoscaling_group" "this" {
name_prefix = "${local.name_prefix}-asg-"
desired_capacity = var.desired_instances
min_size = var.min_instances
max_size = var.max_instances
vpc_zone_identifier = var.vpc_config.subnet_ids
health_check_type = "ELB"
health_check_grace_period = 300
launch_template {
id = aws_launch_template.this.id
version = "$Latest"
}
dynamic "tag" {
for_each = local.common_tags
content {
key = tag.key
value = tag.value
propagate_at_launch = true
}
}
lifecycle {
create_before_destroy = true
ignore_changes = [desired_capacity]
}
}
Data Sources
data.tf Template
# data.tf
# Get current AWS account info
data "aws_caller_identity" "current" {}
data "aws_region" "current" {}
data "aws_partition" "current" {}
# Get latest Amazon Linux 2023 AMI
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-*-x86_64"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
filter {
name = "architecture"
values = ["x86_64"]
}
}
# Get VPC info if not provided
data "aws_vpc" "selected" {
id = var.vpc_config.vpc_id
}
# Get subnet info for AZ distribution
data "aws_subnet" "selected" {
for_each = toset(var.vpc_config.subnet_ids)
id = each.value
}
# Get available AZs
data "aws_availability_zones" "available" {
state = "available"
filter {
name = "opt-in-status"
values = ["opt-in-not-required"]
}
}
Output Definitions
outputs.tf Template
# outputs.tf
#------------------------------------------------------------------------------
# Primary Outputs
#------------------------------------------------------------------------------
output "id" {
description = "The unique identifier for this module instance"
value = aws_autoscaling_group.this.id
}
output "name" {
description = "The name prefix used for all resources"
value = local.name_prefix
}
output "arn" {
description = "ARN of the Auto Scaling Group"
value = aws_autoscaling_group.this.arn
}
#------------------------------------------------------------------------------
# Security Group Outputs
#------------------------------------------------------------------------------
output "security_group_id" {
description = "ID of the security group created for this module"
value = aws_security_group.this.id
}
output "security_group_arn" {
description = "ARN of the security group"
value = aws_security_group.this.arn
}
output "security_group_name" {
description = "Name of the security group"
value = aws_security_group.this.name
}
#------------------------------------------------------------------------------
# Launch Template Outputs
#------------------------------------------------------------------------------
output "launch_template_id" {
description = "ID of the launch template"
value = aws_launch_template.this.id
}
output "launch_template_arn" {
description = "ARN of the launch template"
value = aws_launch_template.this.arn
}
output "launch_template_latest_version" {
description = "Latest version of the launch template"
value = aws_launch_template.this.latest_version
}
#------------------------------------------------------------------------------
# Computed/Derived Outputs
#------------------------------------------------------------------------------
output "ami_id" {
description = "ID of the AMI used for instances"
value = data.aws_ami.amazon_linux.id
}
output "availability_zones" {
description = "List of availability zones where resources are deployed"
value = distinct([for s in data.aws_subnet.selected : s.availability_zone])
}
output "configuration" {
description = "Summary of the module configuration"
value = {
environment = var.environment
instance_type = var.instance_type
min_instances = var.min_instances
max_instances = var.max_instances
monitoring_enabled = local.monitoring_config.enabled
encryption_enabled = local.enable_encryption
}
}
#------------------------------------------------------------------------------
# Sensitive Outputs
#------------------------------------------------------------------------------
output "iam_role_arn" {
description = "ARN of the IAM role attached to instances"
value = aws_iam_role.this.arn
sensitive = false
}
Conditional Resource Creation
Using count vs for_each
# Use count for simple on/off toggles
resource "aws_cloudwatch_metric_alarm" "high_cpu" {
count = var.enable_monitoring ? 1 : 0
alarm_name = "${local.name_prefix}-high-cpu"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 2
metric_name = "CPUUtilization"
namespace = "AWS/EC2"
period = 300
statistic = "Average"
threshold = local.monitoring_config.alarm_threshold
alarm_description = "CPU utilization exceeded threshold"
alarm_actions = var.alarm_actions
dimensions = {
AutoScalingGroupName = aws_autoscaling_group.this.name
}
tags = local.common_tags
}
# Use for_each for collections
resource "aws_s3_bucket" "logs" {
for_each = var.enable_logging ? toset(["access", "audit", "error"]) : toset([])
bucket = "${local.name_prefix}-${each.key}-logs"
tags = merge(local.common_tags, {
LogType = each.key
})
}
# for_each with complex objects
resource "aws_route53_record" "this" {
for_each = {
for record in var.dns_records :
"${record.name}-${record.type}" => record
}
zone_id = var.route53_zone_id
name = each.value.name
type = each.value.type
ttl = each.value.ttl
records = each.value.records
}
Module Composition
Root Module Example
# examples/complete/main.tf
terraform {
required_version = ">= 1.5.0"
}
provider "aws" {
region = var.region
}
# Network module
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.0"
name = "${var.name}-vpc"
cidr = "10.0.0.0/16"
azs = ["${var.region}a", "${var.region}b", "${var.region}c"]
private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
public_subnets = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]
enable_nat_gateway = true
single_nat_gateway = var.environment != "prod"
tags = var.tags
}
# Application module
module "app" {
source = "../../"
name = var.name
environment = var.environment
vpc_config = {
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnets
}
instance_type = var.instance_type
min_instances = var.min_instances
max_instances = var.max_instances
enable_monitoring = true
ingress_rules = [
{
description = "HTTP from ALB"
from_port = 80
to_port = 80
protocol = "tcp"
security_groups = [module.alb.security_group_id]
}
]
tags = var.tags
}
# ALB module
module "alb" {
source = "terraform-aws-modules/alb/aws"
version = "~> 8.0"
name = "${var.name}-alb"
load_balancer_type = "application"
vpc_id = module.vpc.vpc_id
subnets = module.vpc.public_subnets
target_groups = [
{
name = "${var.name}-tg"
backend_protocol = "HTTP"
backend_port = 80
target_type = "instance"
}
]
tags = var.tags
}
# Outputs
output "vpc_id" {
value = module.vpc.vpc_id
}
output "app_security_group_id" {
value = module.app.security_group_id
}
output "alb_dns_name" {
value = module.alb.lb_dns_name
}
Documentation
README.md Template
# Module Name
Brief description of what this module creates.
## Features
- Feature 1
- Feature 2
- Feature 3
## Usage
### Basic
```hcl
module "example" {
source = "github.com/org/terraform-aws-module?ref=v1.0.0"
name = "my-app"
environment = "prod"
vpc_config = {
vpc_id = "vpc-12345678"
subnet_ids = ["subnet-1", "subnet-2"]
}
}
Advanced
module "example" {
source = "github.com/org/terraform-aws-module?ref=v1.0.0"
name = "my-app"
environment = "prod"
vpc_config = {
vpc_id = "vpc-12345678"
subnet_ids = ["subnet-1", "subnet-2"]
security_group_ids = ["sg-existing"]
assign_public_ip = false
}
instance_type = "t3.large"
min_instances = 3
max_instances = 10
enable_monitoring = true
ingress_rules = [
{
description = "HTTPS"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["10.0.0.0/8"]
}
]
tags = {
Team = "platform"
CostCenter = "12345"
}
}
Requirements
| Name |
Version |
| terraform |
>= 1.5.0 |
| aws |
>= 5.0.0, < 6.0.0 |
Providers
| Name |
Version |
| aws |
>= 5.0.0 |
Inputs
| Name |
Description |
Type |
Default |
Required |
| name |
Name prefix for resources |
string |
n/a |
yes |
| environment |
Environment (dev/staging/prod) |
string |
n/a |
yes |
| vpc_config |
VPC configuration |
object |
n/a |
yes |
| instance_type |
EC2 instance type |
string |
"t3.medium" |
no |
| min_instances |
Minimum ASG size |
number |
2 |
no |
| max_instances |
Maximum ASG size |
number |
10 |
no |
Outputs
| Name |
Description |
| id |
Auto Scaling Group ID |
| security_group_id |
Security Group ID |
| launch_template_id |
Launch Template ID |
License
MIT
---
## Лучшие практики
1. **Используй версионирование** — семантическое версионирование для модулей
2. **Валидируй входные переменные** — используй validation blocks
3. **Документируй всё** — описания для variables и outputs
4. **Избегай hardcoded значений** — всё должно быть configurable
5. **Используй for_each вместо count** — лучше управление state
6. **Группируй связанные ресурсы** — логическая организация main.tf
7. **Тестируй модули** — используй terratest или terraform test
8. **Следуй naming conventions** — консистентное именование ресурсов