模块基础与设计

学习如何创建、使用和管理 Terraform 模块。

一、什么是模块

模块是 Terraform 配置的容器,用于组织和复用代码。

1.1 模块的优势

  • 复用性:一次编写,多次使用
  • 组织性:将复杂配置分解为逻辑单元
  • 抽象性:隐藏实现细节
  • 维护性:集中管理和更新
  • 共享性:团队和社区共享

1.2 模块的类型

根模块(Root Module):

project/
├── main.tf
├── variables.tf
└── outputs.tf

子模块(Child Module):

project/
├── main.tf
└── modules/
    └── vpc/
        ├── main.tf
        ├── variables.tf
        └── outputs.tf

二、创建第一个模块

2.1 VPC 模块结构

modules/vpc/
├── main.tf           # 主要资源定义
├── variables.tf      # 输入变量
├── outputs.tf        # 输出值
└── README.md         # 文档

2.2 定义输入变量

modules/vpc/variables.tf:

variable "vpc_name" {
  description = "VPC 名称"
  type        = string
}

variable "vpc_cidr" {
  description = "VPC CIDR 块"
  type        = string
  default     = "10.0.0.0/16"
  
  validation {
    condition     = can(cidrhost(var.vpc_cidr, 0))
    error_message = "必须是有效的 CIDR 块。"
  }
}

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

variable "public_subnet_cidrs" {
  description = "公有子网 CIDR 列表"
  type        = list(string)
}

variable "private_subnet_cidrs" {
  description = "私有子网 CIDR 列表"
  type        = list(string)
}

variable "enable_nat_gateway" {
  description = "是否启用 NAT 网关"
  type        = bool
  default     = true
}

variable "tags" {
  description = "资源标签"
  type        = map(string)
  default     = {}
}

2.3 实现资源

modules/vpc/main.tf:

# VPC
resource "aws_vpc" "main" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true
  enable_dns_support   = true
  
  tags = merge(
    var.tags,
    {
      Name = var.vpc_name
    }
  )
}

# Internet Gateway
resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id
  
  tags = merge(
    var.tags,
    {
      Name = "${var.vpc_name}-igw"
    }
  )
}

# 公有子网
resource "aws_subnet" "public" {
  count             = length(var.public_subnet_cidrs)
  vpc_id            = aws_vpc.main.id
  cidr_block        = var.public_subnet_cidrs[count.index]
  availability_zone = var.availability_zones[count.index]
  
  map_public_ip_on_launch = true
  
  tags = merge(
    var.tags,
    {
      Name = "${var.vpc_name}-public-${count.index + 1}"
      Type = "Public"
    }
  )
}

# 私有子网
resource "aws_subnet" "private" {
  count             = length(var.private_subnet_cidrs)
  vpc_id            = aws_vpc.main.id
  cidr_block        = var.private_subnet_cidrs[count.index]
  availability_zone = var.availability_zones[count.index]
  
  tags = merge(
    var.tags,
    {
      Name = "${var.vpc_name}-private-${count.index + 1}"
      Type = "Private"
    }
  )
}

# NAT Gateway EIP
resource "aws_eip" "nat" {
  count  = var.enable_nat_gateway ? length(var.public_subnet_cidrs) : 0
  domain = "vpc"
  
  tags = merge(
    var.tags,
    {
      Name = "${var.vpc_name}-nat-eip-${count.index + 1}"
    }
  )
  
  depends_on = [aws_internet_gateway.main]
}

# NAT Gateway
resource "aws_nat_gateway" "main" {
  count         = var.enable_nat_gateway ? length(var.public_subnet_cidrs) : 0
  allocation_id = aws_eip.nat[count.index].id
  subnet_id     = aws_subnet.public[count.index].id
  
  tags = merge(
    var.tags,
    {
      Name = "${var.vpc_name}-nat-${count.index + 1}"
    }
  )
  
  depends_on = [aws_internet_gateway.main]
}

# 公有路由表
resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id
  
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.main.id
  }
  
  tags = merge(
    var.tags,
    {
      Name = "${var.vpc_name}-public-rt"
    }
  )
}

# 公有子网路由表关联
resource "aws_route_table_association" "public" {
  count          = length(var.public_subnet_cidrs)
  subnet_id      = aws_subnet.public[count.index].id
  route_table_id = aws_route_table.public.id
}

# 私有路由表
resource "aws_route_table" "private" {
  count  = length(var.private_subnet_cidrs)
  vpc_id = aws_vpc.main.id
  
  dynamic "route" {
    for_each = var.enable_nat_gateway ? [1] : []
    content {
      cidr_block     = "0.0.0.0/0"
      nat_gateway_id = aws_nat_gateway.main[count.index].id
    }
  }
  
  tags = merge(
    var.tags,
    {
      Name = "${var.vpc_name}-private-rt-${count.index + 1}"
    }
  )
}

# 私有子网路由表关联
resource "aws_route_table_association" "private" {
  count          = length(var.private_subnet_cidrs)
  subnet_id      = aws_subnet.private[count.index].id
  route_table_id = aws_route_table.private[count.index].id
}

2.4 定义输出

modules/vpc/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 "nat_gateway_ips" {
  description = "NAT 网关公网 IP"
  value       = aws_eip.nat[*].public_ip
}

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

三、使用模块

3.1 基本用法

# main.tf
module "vpc" {
  source = "./modules/vpc"
  
  vpc_name             = "production-vpc"
  vpc_cidr             = "10.0.0.0/16"
  availability_zones   = ["us-west-2a", "us-west-2b", "us-west-2c"]
  public_subnet_cidrs  = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
  private_subnet_cidrs = ["10.0.11.0/24", "10.0.12.0/24", "10.0.13.0/24"]
  enable_nat_gateway   = true
  
  tags = {
    Environment = "Production"
    ManagedBy   = "Terraform"
  }
}

# 使用模块输出
resource "aws_instance" "app" {
  ami           = "ami-xxx"
  instance_type = "t3.micro"
  subnet_id     = module.vpc.private_subnet_ids[0]
  
  tags = {
    Name = "app-server"
  }
}

output "vpc_id" {
  value = module.vpc.vpc_id
}

3.2 多次使用模块

# 开发环境 VPC
module "dev_vpc" {
  source = "./modules/vpc"
  
  vpc_name             = "dev-vpc"
  vpc_cidr             = "10.1.0.0/16"
  availability_zones   = ["us-west-2a", "us-west-2b"]
  public_subnet_cidrs  = ["10.1.1.0/24", "10.1.2.0/24"]
  private_subnet_cidrs = ["10.1.11.0/24", "10.1.12.0/24"]
  enable_nat_gateway   = false  # 开发环境不需要 NAT
  
  tags = {
    Environment = "Development"
  }
}

# 生产环境 VPC
module "prod_vpc" {
  source = "./modules/vpc"
  
  vpc_name             = "prod-vpc"
  vpc_cidr             = "10.0.0.0/16"
  availability_zones   = ["us-west-2a", "us-west-2b", "us-west-2c"]
  public_subnet_cidrs  = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
  private_subnet_cidrs = ["10.0.11.0/24", "10.0.12.0/24", "10.0.13.0/24"]
  enable_nat_gateway   = true
  
  tags = {
    Environment = "Production"
  }
}

四、模块源

4.1 本地路径

module "vpc" {
  source = "./modules/vpc"
}

module "vpc_relative" {
  source = "../shared-modules/vpc"
}

module "vpc_absolute" {
  source = "/absolute/path/to/modules/vpc"
}

4.2 Terraform Registry

# 官方模块
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.0.0"
  
  name = "my-vpc"
  cidr = "10.0.0.0/16"
}

# 私有 Registry
module "vpc" {
  source  = "app.terraform.io/my-org/vpc/aws"
  version = "1.0.0"
}

4.3 Git 仓库

# HTTPS
module "vpc" {
  source = "git::https://github.com/username/terraform-modules.git//vpc?ref=v1.0.0"
}

# SSH
module "vpc" {
  source = "git::ssh://git@github.com/username/terraform-modules.git//vpc?ref=v1.0.0"
}

# 分支或标签
module "vpc" {
  source = "git::https://github.com/username/terraform-modules.git//vpc?ref=main"
}

4.4 HTTP URL

module "vpc" {
  source = "https://example.com/terraform-modules/vpc.zip"
}

五、模块设计原则

5.1 单一职责

# ✅ 好的设计:VPC 模块只负责网络
modules/vpc/

# ✅ 好的设计:应用模块只负责计算资源
modules/app/

# ❌ 不好的设计:一个模块做所有事情
modules/infrastructure/  # 包含 VPC、数据库、应用等

5.2 合理的抽象层次

# ✅ 高层模块:业务导向
module "web_application" {
  source = "./modules/web-app"
  
  app_name    = "myapp"
  environment = "prod"
}

# ✅ 低层模块:技术导向
module "load_balancer" {
  source = "./modules/alb"
  
  name            = "my-alb"
  subnets         = var.subnet_ids
  security_groups = var.security_group_ids
}

5.3 清晰的接口

# ✅ 好的接口:必需参数少,有合理默认值
module "vpc" {
  source = "./modules/vpc"
  
  name = "my-vpc"  # 唯一必需参数
  
  # 其他参数都有默认值
}

# ❌ 不好的接口:太多必需参数
module "vpc" {
  source = "./modules/vpc"
  
  # 需要设置 20+ 个参数...
}

5.4 文档完善

modules/vpc/README.md:

# VPC 模块

创建 AWS VPC,包含公有和私有子网。

## 使用示例

\`\`\`hcl
module "vpc" {
  source = "./modules/vpc"
  
  vpc_name            = "production-vpc"
  vpc_cidr            = "10.0.0.0/16"
  availability_zones  = ["us-west-2a", "us-west-2b"]
  public_subnet_cidrs = ["10.0.1.0/24", "10.0.2.0/24"]
  private_subnet_cidrs = ["10.0.11.0/24", "10.0.12.0/24"]
}
\`\`\`

## 输入变量

| 名称 | 描述 | 类型 | 默认值 | 必需 |
|------|------|------|--------|------|
| vpc_name | VPC 名称 | string | - | 是 |
| vpc_cidr | VPC CIDR 块 | string | 10.0.0.0/16 | 否 |

## 输出

| 名称 | 描述 |
|------|------|
| vpc_id | VPC ID |
| public_subnet_ids | 公有子网 ID 列表 |

六、模块版本管理

6.1 语义化版本

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.0.0"  # 精确版本
}

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.0"  # >= 5.0.0 且 < 6.0.0
}

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = ">= 5.0.0, < 6.0.0"  # 版本范围
}

6.2 Git 标签

module "vpc" {
  source = "git::https://github.com/username/modules.git//vpc?ref=v1.0.0"
}

七、模块测试

7.1 示例配置

modules/vpc/
├── main.tf
├── variables.tf
├── outputs.tf
└── examples/
    ├── basic/
    │   ├── main.tf
    │   └── README.md
    └── complete/
        ├── main.tf
        └── README.md

examples/basic/main.tf:

module "vpc" {
  source = "../../"
  
  vpc_name            = "example-vpc"
  availability_zones  = ["us-west-2a", "us-west-2b"]
  public_subnet_cidrs = ["10.0.1.0/24", "10.0.2.0/24"]
  private_subnet_cidrs = ["10.0.11.0/24", "10.0.12.0/24"]
}

output "vpc_id" {
  value = module.vpc.vpc_id
}

7.2 自动化测试

使用 Terratest(Go):

// test/vpc_test.go
package test

import (
    "testing"
    "github.com/gruntwork-io/terratest/modules/terraform"
    "github.com/stretchr/testify/assert"
)

func TestVPCModule(t *testing.T) {
    terraformOptions := &terraform.Options{
        TerraformDir: "../examples/basic",
    }
    
    defer terraform.Destroy(t, terraformOptions)
    terraform.InitAndApply(t, terraformOptions)
    
    vpcId := terraform.Output(t, terraformOptions, "vpc_id")
    assert.NotEmpty(t, vpcId)
}

小结

模块设计的关键要点:

  • 结构清晰:输入、处理、输出
  • 职责单一:一个模块做好一件事
  • 接口简洁:最少必需参数
  • 文档完善:README + 注释
  • 版本管理:语义化版本
  • 可测试性:提供示例和测试

下一章我们将学习如何发布和共享模块。