Terraform 模块化设计

Terraform 模块是代码复用的基础。一个设计良好的模块,可以在多个环境、多个项目中复用,减少重复代码,确保基础设施一致性。

但模块设计不好,反而会成为维护噩梦:参数太多、默认值混乱、隐藏的依赖、难以理解的输出……

好的模块设计,是把复杂性封装在内部,把简洁留给调用者

模块基础

模块定义

模块结构
module/
├── main.tf          # 资源定义
├── variables.tf     # 输入变量
├── outputs.tf       # 输出值
├── versions.tf      # 版本约束
├── data.tf          # 数据源
└── README.md        # 文档

基本模块示例

vpc-module/main.tf
# VPC 模块
resource "aws_vpc" "main" {
  cidr_block           = var.cidr_block
  enable_dns_hostnames = var.enable_dns_hostnames
  enable_dns_support   = var.enable_dns_support

  tags = merge(
    var.tags,
    {
      Name = var.name
    }
  )
}

# 子网
resource "aws_subnet" "public" {
  count = length(var.public_subnets)

  vpc_id                  = aws_vpc.main.id
  cidr_block              = var.public_subnets[count.index]
  availability_zone       = var.availability_zones[count.index]
  map_public_ip_on_launch = true

  tags = {
    Type = "public"
  }
}

resource "aws_subnet" "private" {
  count = length(var.private_subnets)

  vpc_id            = aws_vpc.main.id
  cidr_block        = var.private_subnets[count.index]
  availability_zone = var.availability_zones[count.index]

  tags = {
    Type = "private"
  }
}

# Internet Gateway
resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "${var.name}-igw"
  }
}
vpc-module/variables.tf
variable "name" {
  description = "VPC 名称"
  type        = string
}

variable "cidr_block" {
  description = "VPC CIDR 块"
  type        = string
  default     = "10.0.0.0/16"
}

variable "enable_dns_hostnames" {
  description = "启用 DNS 主机名"
  type        = bool
  default     = true
}

variable "enable_dns_support" {
  description = "启用 DNS 支持"
  type        = bool
  default     = true
}

variable "public_subnets" {
  description = "公有子网 CIDR 列表"
  type        = list(string)
  default     = []
}

variable "private_subnets" {
  description = "私有子网 CIDR 列表"
  type        = list(string)
  default     = []
}

variable "availability_zones" {
  description = "可用区列表"
  type        = list(string)
  default     = []
}

variable "tags" {
  description = "标签"
  type        = map(string)
  default     = {}
}
vpc-module/outputs.tf
output "vpc_id" {
  description = "VPC ID"
  value       = aws_vpc.main.id
}

output "vpc_cidr" {
  description = "VPC CIDR"
  value       = aws_vpc.main.cidr_block
}

output "public_subnet_ids" {
  description = "公有子网 ID 列表"
  value       = aws_subnet.public[*].id
}

output "private_subnet_ids" {
  description = "私有子网 ID 列表"
  value       = aws_subnet.private[*].id
}

output "public_subnet_cidrs" {
  description = "公有子网 CIDR 列表"
  value       = aws_subnet.public[*].cidr_block
}

output "private_subnet_cidrs" {
  description = "私有子网 CIDR 列表"
  value       = aws_subnet.private[*].cidr_block
}

output "igw_id" {
  description = "Internet Gateway ID"
  value       = aws_internet_gateway.main.id
}

使用模块

使用
module "vpc" {
  source = "./modules/vpc"

  name                = "prod-vpc"
  cidr_block          = "10.0.0.0/16"
  enable_dns_hostnames = true
  enable_dns_support   = true

  public_subnets = [
    "10.0.1.0/24",
    "10.0.2.0/24",
    "10.0.3.0/24"
  ]

  private_subnets = [
    "10.0.10.0/24",
    "10.0.11.0/24",
    "10.0.12.0/24"
  ]

  availability_zones = [
    "us-east-1a",
    "us-east-1b",
    "us-east-1c"
  ]

  tags = {
    Environment = "prod"
    ManagedBy   = "terraform"
  }
}

模块设计原则

原则 1:单一职责

flowchart TB
    subgraph Bad["不好的设计"]
        Mega["巨大模块:VPC + EC2 + RDS + S3 + CloudFront"]
    end

    subgraph Good["好的设计"]
        Vpc["vpc 模块"]
        Ec2["ec2 模块"]
        Rds["rds 模块"]
        S3["s3 模块"]
    end

    Good --> Composed["组合使用"]

原则 2:最小接口

变量过多的问题
# 糟糕的接口设计
module "ecs_cluster" {
  source = "./modules/ecs"

  # 太多参数,用户需要了解所有细节
  cluster_name           = "my-cluster"
  vpc_id                = "vpc-xxx"
  subnet_ids             = ["subnet-xxx"]
  instance_type          = "t3.medium"
  instance_count         = 3
  ami_id                = "ami-xxx"
  key_name              = "my-key"
  security_groups       = ["sg-xxx"]
  iam_instance_profile   = "ecs-profile"
  user_data             = "..."
  ebs_volume_size       = 100
  ebs_volume_type       = "gp3"
  # ... 30+ 参数
}
好的接口设计
variable "cluster_config" {
  description = "ECS 集群配置"
  type = object({
    name           = string
    instance_type  = string
    instance_count = number
  })
  default = {
    name           = "default"
    instance_type  = "t3.medium"
    instance_count = 2
  }
}

variable "autoscaling_config" {
  description = "自动扩展配置"
  type = object({
    enabled         = bool
    min_size       = number
    max_size       = number
    target_cpu_utilization = number
  })
  default = {
    enabled                = false
    min_size               = 2
    max_size               = 10
    target_cpu_utilization = 70
  }
}

原则 3:合理默认值

设置合理的默认值
variable "instance_type" {
  description = "EC2 实例类型"
  type        = string
  default     = "t3.micro"  # 小实例适合开发和测试

  validation {
    condition     = contains(["t3.micro", "t3.small", "t3.medium", "t3.large"], var.instance_type)
    error_message = "Instance type must be valid."
  }
}

variable "enable_monitoring" {
  description = "启用详细监控"
  type        = bool
  default     = true  # 监控应该是默认启用的
}

variable "deletion_protection" {
  description = "启用删除保护"
  type        = bool
  default     = true  # 生产环境应该默认保护
}

原则 4:输出所有需要的值

输出完整的信息
output "cluster_info" {
  description = "集群完整信息"
  value = {
    id               = aws_ecs_cluster.main.id
    arn              = aws_ecs_cluster.main.arn
    name             = aws_ecs_cluster.main.name
    security_group_id = aws_security_group.cluster.id
    instance_ids      = aws_instance.cluster[*].id
    autoscaling_group_id = aws_autoscaling_group.main.id
  }
}

模块版本管理

语义化版本

模块版本标签
# 模块版本 v1.0.0
git tag modules/vpc/v1.0.0

# 模块版本 v1.1.0
git tag modules/vpc/v1.1.0

# 模块版本 v2.0.0
git tag modules/vpc/v2.0.0

使用特定版本

使用特定版本
module "vpc" {
  source  = "git::https://github.com/myorg/terraform-aws-vpc.git?ref=v1.2.0"
  version = "~> 1.2.0"  # 支持 1.2.x

  name      = "prod-vpc"
  cidr_block = "10.0.0.0/16"
}

版本约束

versions.tf
terraform {
  required_version = ">= 1.6.0"

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

组合模块

应用模块示例

webapp-module/main.tf
resource "aws_security_group" "app" {
  name        = "${var.name}-app"
  vpc_id      = var.vpc_id

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = var.tags
}

resource "aws_instance" "app" {
  count = var.instance_count

  ami           = var.ami_id
  instance_type = var.instance_type
  subnet_id    = var.subnet_ids[count.index % length(var.subnet_ids)]

  vpc_security_group_ids = [aws_security_group.app.id]
  key_name              = var.key_name
  user_data             = templatefile("${path.module}/user_data.sh", {
    env = var.environment
  })

  root_block_device {
    volume_size = var.root_volume_size
    volume_type = var.root_volume_type
  }

  tags = merge(var.tags, {
    Name = "${var.name}-${count.index + 1}"
    Role = "webapp"
  })
}

resource "aws_lb" "app" {
  name               = var.name
  internal           = false
  load_balancer_type = "application"
  security_groups    = [aws_security_group.app.id]
  subnets            = var.subnet_ids

  enable_deletion_protection = var.deletion_protection

  tags = var.tags
}

resource "aws_lb_target_group" "app" {
  name     = var.name
  port     = 80
  protocol = "HTTP"
  vpc_id   = var.vpc_id
}

resource "aws_lb_listener" "app" {
  load_balancer_arn = aws_lb.app.arn
  port             = 80
  protocol         = "HTTP"

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.app.arn
  }
}

调用组合模块

使用应用模块
module "webapp" {
  source = "../../modules/webapp"

  name   = "prod-webapp"
  vpc_id = module.vpc.vpc_id

  subnet_ids = module.vpc.private_subnet_ids

  ami_id          = "ami-0c55b159cbfafe1f0"
  instance_type   = "t3.medium"
  instance_count = 3

  key_name = "prod-key"

  environment = "production"

  deletion_protection = true

  tags = {
    Environment = "production"
    Project     = "mywebapp"
    ManagedBy   = "terraform"
  }
}

本地模块 vs 远程模块

本地模块

本地模块
module "vpc" {
  source = "./modules/vpc"
  # ...
}

远程模块

Terraform
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.0"

  name = "my-vpc"
  # ...
}
Git
module "vpc" {
  source = "git::https://github.com/myorg/terraform-aws-vpc.git"
  # 或者指定分支
  source = "git::https://github.com/myorg/terraform-aws-vpc.git?ref=develop"
}
S3
module "vpc" {
  source = "s3::https://s3.amazonaws.com/my-bucket/modules/vpc.zip"
}

模块文档

modules/vpc/README.md
# VPC 模块

创建标准的 VPC 架构,包含公有子网、私有子网、NAT Gateway 等。

## 用法

```hcl
module "vpc" {
  source = "github.com/myorg/terraform-aws-vpc?ref=v1.0.0"

  name = "my-vpc"
  cidr_block = "10.0.0.0/16"

  availability_zones = ["us-east-1a", "us-east-1b"]

  public_subnets  = ["10.0.1.0/24", "10.0.2.0/24"]
  private_subnets = ["10.0.10.0/24", "10.0.11.0/24"]
}

变量

变量名类型描述默认值
namestringVPC 名称-
cidr_blockstringCIDR 块10.0.0.0/16

输出

输出名类型描述
vpc_idstringVPC ID
public_subnet_idslist(string)公有子网 ID
private_subnet_idslist(string)私有子网 ID

示例

高可用配置

module "vpc" {
  source = "..."

  name = "ha-vpc"
  cidr_block = "10.0.0.0/16"

  availability_zones = ["us-east-1a", "us-east-1b", "us-east-1c"]

  public_subnets  = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
  private_subnets = ["10.0.10.0/24", "10.0.11.0/24", "10.0.12.0/24"]
}

限制

  • 最多支持 3 个可用区
  • CIDR 块必须在 10.0.0.0/8 范围内

## 模块检查清单

| 检查项 | 说明 |
| --- | --- |
| 单一职责 | 一个模块只做一件事 |
| 最小接口 | 参数不要太多 |
| 合理默认值 | 降低使用门槛 |
| 完整输出 | 输出调用者需要的信息 |
| 版本管理 | 使用语义化版本 |
| 文档完善 | README 说明用法 |
| 版本约束 | 声明 Terraform 和 Provider 版本 |
| 单元测试 | 使用 Terratest 测试模块 |

好的模块设计可以显著提高基础设施的一致性和可维护性。记住核心原则:**把复杂性封装在模块内部,把简洁留给调用者**。