跳到主要内容

实战案例:Web 应用基础设施

本章通过一个完整的实战案例,演示如何使用 Terraform 在 AWS 上部署一个典型的三层 Web 应用基础设施。这个案例涵盖了从网络配置到应用部署的完整流程。

项目概述

架构设计

我们将构建一个高可用的 Web 应用基础设施,包含以下组件:

  • 网络层:VPC、公有/私有子网、NAT 网关
  • 计算层:Auto Scaling 组中的 EC2 实例
  • 负载均衡:Application Load Balancer
  • 数据库层:RDS PostgreSQL
  • 存储层:S3 存储桶用于静态资源

项目结构

terraform-webapp/
├── environments/
│ └── dev/
│ ├── main.tf
│ ├── variables.tf
│ ├── outputs.tf
│ └── terraform.tfvars
├── modules/
│ ├── networking/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ ├── compute/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ ├── outputs.tf
│ │ └── user_data.sh
│ └── database/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
└── shared/
└── s3.tf

网络模块

网络模块负责创建 VPC 及其相关资源,包括子网、路由表和 NAT 网关。

模块定义

# modules/networking/variables.tf

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

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

variable "environment" {
description = "环境名称"
type = string
}

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

variable "private_subnet_cidrs" {
description = "私有子网 CIDR 列表"
type = list(string)
}
# modules/networking/main.tf

# VPC
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true

tags = {
Name = "${var.environment}-vpc"
Environment = var.environment
ManagedBy = "Terraform"
}
}

# 互联网网关
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id

tags = {
Name = "${var.environment}-igw"
Environment = var.environment
}
}

# 弹性 IP(用于 NAT 网关)
resource "aws_eip" "nat" {
count = length(var.availability_zones)
domain = "vpc"

tags = {
Name = "${var.environment}-nat-eip-${count.index + 1}"
Environment = var.environment
}

depends_on = [aws_internet_gateway.main]
}

# 公有子网
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 = {
Name = "${var.environment}-public-${var.availability_zones[count.index]}"
Environment = var.environment
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 = {
Name = "${var.environment}-private-${var.availability_zones[count.index]}"
Environment = var.environment
Type = "private"
}
}

# NAT 网关
resource "aws_nat_gateway" "main" {
count = length(var.availability_zones)

allocation_id = aws_eip.nat[count.index].id
subnet_id = aws_subnet.public[count.index].id

tags = {
Name = "${var.environment}-nat-${var.availability_zones[count.index]}"
Environment = var.environment
}

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 = {
Name = "${var.environment}-public-rt"
Environment = var.environment
}
}

# 私有路由表
resource "aws_route_table" "private" {
count = length(var.availability_zones)

vpc_id = aws_vpc.main.id

route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.main[count.index].id
}

tags = {
Name = "${var.environment}-private-rt-${var.availability_zones[count.index]}"
Environment = var.environment
}
}

# 路由表关联 - 公有子网
resource "aws_route_table_association" "public" {
count = length(aws_subnet.public)

subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.public.id
}

# 路由表关联 - 私有子网
resource "aws_route_table_association" "private" {
count = length(aws_subnet.private)

subnet_id = aws_subnet.private[count.index].id
route_table_id = aws_route_table.private[count.index].id
}
# modules/networking/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
}

计算模块

计算模块创建应用服务器和负载均衡器。

安全组配置

# modules/compute/main.tf

# ALB 安全组 - 允许 HTTP/HTTPS 入站
resource "aws_security_group" "alb" {
name_prefix = "${var.environment}-alb-"
vpc_id = var.vpc_id

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

ingress {
from_port = 443
to_port = 443
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 = {
Name = "${var.environment}-alb-sg"
Environment = var.environment
}

lifecycle {
create_before_destroy = true
}
}

# Web 服务器安全组
resource "aws_security_group" "web" {
name_prefix = "${var.environment}-web-"
vpc_id = var.vpc_id

# 仅允许来自 ALB 的流量
ingress {
from_port = var.app_port
to_port = var.app_port
protocol = "tcp"
security_groups = [aws_security_group.alb.id]
}

# 允许 SSH(仅限特定 IP)
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = var.allowed_ssh_cidrs
}

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

tags = {
Name = "${var.environment}-web-sg"
Environment = var.environment
}
}

负载均衡器

# Application Load Balancer
resource "aws_lb" "main" {
name = "${var.environment}-alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb.id]
subnets = var.public_subnet_ids

enable_deletion_protection = false

tags = {
Environment = var.environment
}
}

# 目标组
resource "aws_lb_target_group" "web" {
name = "${var.environment}-web-tg"
port = var.app_port
protocol = "HTTP"
vpc_id = var.vpc_id

health_check {
enabled = true
healthy_threshold = 2
interval = 30
matcher = "200"
path = "/health"
port = "traffic-port"
protocol = "HTTP"
timeout = 5
unhealthy_threshold = 2
}

tags = {
Environment = var.environment
}
}

# HTTP 监听器(重定向到 HTTPS)
resource "aws_lb_listener" "http" {
load_balancer_arn = aws_lb.main.arn
port = 80
protocol = "HTTP"

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

Auto Scaling 组

# 获取最新 Amazon Linux AMI
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["amazon"]

filter {
name = "name"
values = ["amzn2-ami-hvm-*-x86_64-gp2"]
}
}

# 启动模板
resource "aws_launch_template" "web" {
name_prefix = "${var.environment}-web-"
image_id = data.aws_ami.amazon_linux.id
instance_type = var.instance_type
key_name = var.key_name

vpc_security_group_ids = [aws_security_group.web.id]

user_data = base64encode(templatefile("${path.module}/user_data.sh", {
app_port = var.app_port
environment = var.environment
s3_bucket = var.s3_bucket_name
db_endpoint = var.db_endpoint
db_name = var.db_name
db_username = var.db_username
db_password = var.db_password
}))

iam_instance_profile {
name = aws_iam_instance_profile.web.name
}

monitoring {
enabled = true
}

metadata_options {
http_endpoint = "enabled"
http_tokens = "required"
http_put_response_hop_limit = 1
}

tag_specifications {
resource_type = "instance"
tags = {
Name = "${var.environment}-web"
Environment = var.environment
}
}
}

# IAM 实例配置文件
resource "aws_iam_instance_profile" "web" {
name = "${var.environment}-web-profile"
role = aws_iam_role.web.name
}

resource "aws_iam_role" "web" {
name = "${var.environment}-web-role"

assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "ec2.amazonaws.com"
}
}
]
})
}

# S3 只读访问策略
resource "aws_iam_role_policy_attachment" "s3_read" {
role = aws_iam_role.web.name
policy_arn = "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
}

# Auto Scaling 组
resource "aws_autoscaling_group" "web" {
name = "${var.environment}-web-asg"
vpc_zone_identifier = var.private_subnet_ids

min_size = var.min_instances
max_size = var.max_instances
desired_capacity = var.desired_instances

launch_template {
id = aws_launch_template.web.id
version = "$Latest"
}

target_group_arns = [aws_lb_target_group.web.arn]
health_check_type = "ELB"

# 实例保护(防止自动缩容时被终止)
protect_from_scale_in = true

tag {
key = "Environment"
value = var.environment
propagate_at_launch = true
}

lifecycle {
create_before_destroy = true
}
}

# 自动缩容策略
resource "aws_autoscaling_policy" "scale_up" {
name = "${var.environment}-scale-up"
scaling_adjustment = 1
adjustment_type = "ChangeInCapacity"
cooldown = 300
autoscaling_group_name = aws_autoscaling_group.web.name
}

resource "aws_autoscaling_policy" "scale_down" {
name = "${var.environment}-scale-down"
scaling_adjustment = -1
adjustment_type = "ChangeInCapacity"
cooldown = 300
autoscaling_group_name = aws_autoscaling_group.web.name
}

# CloudWatch 告警 - CPU 使用率
resource "aws_cloudwatch_metric_alarm" "high_cpu" {
alarm_name = "${var.environment}-high-cpu"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 2
metric_name = "CPUUtilization"
namespace = "AWS/EC2"
period = 120
statistic = "Average"
threshold = 80

dimensions = {
AutoScalingGroupName = aws_autoscaling_group.web.name
}

alarm_actions = [aws_autoscaling_policy.scale_up.arn]
}

resource "aws_cloudwatch_metric_alarm" "low_cpu" {
alarm_name = "${var.environment}-low-cpu"
comparison_operator = "LessThanThreshold"
evaluation_periods = 2
metric_name = "CPUUtilization"
namespace = "AWS/EC2"
period = 120
statistic = "Average"
threshold = 20

dimensions = {
AutoScalingGroupName = aws_autoscaling_group.web.name
}

alarm_actions = [aws_autoscaling_policy.scale_down.arn]
}

用户数据脚本

#!/bin/bash
# modules/compute/user_data.sh

set -e

# 参数由 Terraform 模板传入
APP_PORT=${app_port}
ENVIRONMENT=${environment}
S3_BUCKET=${s3_bucket}
DB_ENDPOINT=${db_endpoint}
DB_NAME=${db_name}
DB_USERNAME=${db_username}
DB_PASSWORD=${db_password}

# 更新系统
yum update -y

# 安装依赖
yum install -y python3 python3-pip nginx

# 创建应用目录
mkdir -p /opt/webapp
cd /opt/webapp

# 从 S3 下载应用代码
aws s3 cp s3://$S3_BUCKET/app/ ./ --recursive

# 安装 Python 依赖
pip3 install -r requirements.txt

# 配置环境变量
cat > .env << EOF
APP_PORT=$APP_PORT
ENVIRONMENT=$ENVIRONMENT
DB_HOST=$DB_ENDPOINT
DB_NAME=$DB_NAME
DB_USER=$DB_USERNAME
DB_PASSWORD=$DB_PASSWORD
EOF

# 创建 systemd 服务
cat > /etc/systemd/system/webapp.service << EOF
[Unit]
Description=Web Application
After=network.target

[Service]
Type=simple
WorkingDirectory=/opt/webapp
ExecStart=/usr/bin/python3 app.py
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

# 配置 Nginx 反向代理
cat > /etc/nginx/conf.d/webapp.conf << EOF
upstream webapp {
server 127.0.0.1:$APP_PORT;
}

server {
listen 80;

location / {
proxy_pass http://webapp;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
}

location /health {
proxy_pass http://webapp/health;
}
}
EOF

# 启动服务
systemctl daemon-reload
systemctl enable webapp nginx
systemctl start webapp nginx

# 输出启动日志
echo "Web application started successfully"

数据库模块

# modules/database/variables.tf

variable "environment" {
type = string
}

variable "vpc_id" {
type = string
}

variable "private_subnet_ids" {
type = list(string)
}

variable "db_name" {
type = string
}

variable "db_username" {
type = string
}

variable "db_password" {
type = string
sensitive = true
}

variable "instance_class" {
type = string
default = "db.t3.micro"
}

variable "allocated_storage" {
type = number
default = 20
}
# modules/database/main.tf

# 数据库安全组
resource "aws_security_group" "database" {
name_prefix = "${var.environment}-db-"
vpc_id = var.vpc_id

ingress {
from_port = 5432
to_port = 5432
protocol = "tcp"
security_groups = [var.web_security_group_id]
}

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

tags = {
Name = "${var.environment}-db-sg"
Environment = var.environment
}
}

# 数据库子网组
resource "aws_db_subnet_group" "main" {
name = "${var.environment}-db-subnet"
subnet_ids = var.private_subnet_ids

tags = {
Name = "${var.environment}-db-subnet-group"
Environment = var.environment
}
}

# RDS PostgreSQL 实例
resource "aws_db_instance" "main" {
identifier = "${var.environment}-postgres"

engine = "postgres"
engine_version = "15.4"
instance_class = var.instance_class

allocated_storage = var.allocated_storage
max_allocated_storage = 100
storage_type = "gp3"
storage_encrypted = true

db_name = var.db_name
username = var.db_username
password = var.db_password

vpc_security_group_ids = [aws_security_group.database.id]
db_subnet_group_name = aws_db_subnet_group.main.name

backup_retention_period = 7
backup_window = "03:00-04:00"
maintenance_window = "Mon:04:00-Mon:05:00"

multi_az = true
publicly_accessible = false
skip_final_snapshot = false
final_snapshot_identifier = "${var.environment}-final-snapshot"

performance_insights_enabled = true
monitoring_interval = 60

tags = {
Environment = var.environment
}
}
# modules/database/outputs.tf

output "endpoint" {
description = "数据库端点"
value = aws_db_instance.main.endpoint
}

output "db_name" {
description = "数据库名称"
value = aws_db_instance.main.db_name
}

output "username" {
description = "数据库用户名"
value = aws_db_instance.main.username
}

output "security_group_id" {
description = "数据库安全组 ID"
value = aws_security_group.database.id
}

环境配置

将所有模块组合起来:

# environments/dev/main.tf

terraform {
required_version = ">= 1.0"

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

backend "s3" {
bucket = "my-terraform-state"
key = "dev/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-locks"
encrypt = true
}
}

provider "aws" {
region = var.aws_region

default_tags {
tags = {
Environment = "development"
Project = "webapp"
ManagedBy = "Terraform"
}
}
}

# 网络模块
module "networking" {
source = "../../modules/networking"

vpc_cidr = var.vpc_cidr
availability_zones = var.availability_zones
environment = "dev"
public_subnet_cidrs = var.public_subnet_cidrs
private_subnet_cidrs = var.private_subnet_cidrs
}

# 数据库模块
module "database" {
source = "../../modules/database"

environment = "dev"
vpc_id = module.networking.vpc_id
private_subnet_ids = module.networking.private_subnet_ids
db_name = var.db_name
db_username = var.db_username
db_password = var.db_password
instance_class = "db.t3.micro"
}

# 计算模块
module "compute" {
source = "../../modules/compute"

environment = "dev"
vpc_id = module.networking.vpc_id
public_subnet_ids = module.networking.public_subnet_ids
private_subnet_ids = module.networking.private_subnet_ids

instance_type = "t2.micro"
key_name = var.key_name
app_port = 8080
min_instances = 1
max_instances = 3
desired_instances = 2

s3_bucket_name = aws_s3_bucket.assets.id
db_endpoint = module.database.endpoint
db_name = module.database.db_name
db_username = module.database.username
db_password = var.db_password
allowed_ssh_cidrs = var.allowed_ssh_cidrs

depends_on = [module.database]
}

# S3 静态资源存储桶
resource "aws_s3_bucket" "assets" {
bucket = "${var.project_name}-assets-${var.environment}"

tags = {
Environment = "dev"
}
}

resource "aws_s3_bucket_versioning" "assets" {
bucket = aws_s3_bucket.assets.id
versioning_configuration {
status = "Enabled"
}
}

resource "aws_s3_bucket_server_side_encryption_configuration" "assets" {
bucket = aws_s3_bucket.assets.id

rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
# environments/dev/variables.tf

variable "aws_region" {
type = string
default = "us-east-1"
}

variable "project_name" {
type = string
default = "webapp"
}

variable "vpc_cidr" {
type = string
default = "10.0.0.0/16"
}

variable "availability_zones" {
type = list(string)
default = ["us-east-1a", "us-east-1b"]
}

variable "public_subnet_cidrs" {
type = list(string)
default = ["10.0.1.0/24", "10.0.2.0/24"]
}

variable "private_subnet_cidrs" {
type = list(string)
default = ["10.0.10.0/24", "10.0.20.0/24"]
}

variable "db_name" {
type = string
default = "webapp"
}

variable "db_username" {
type = string
default = "dbadmin"
sensitive = true
}

variable "db_password" {
type = string
sensitive = true
}

variable "key_name" {
type = string
}

variable "allowed_ssh_cidrs" {
type = list(string)
default = ["10.0.0.0/8"]
}
# environments/dev/outputs.tf

output "alb_dns_name" {
description = "负载均衡器 DNS 名称"
value = module.compute.alb_dns_name
}

output "rds_endpoint" {
description = "数据库端点"
value = module.database.endpoint
}

output "s3_bucket_name" {
description = "S3 存储桶名称"
value = aws_s3_bucket.assets.id
}

output "vpc_id" {
description = "VPC ID"
value = module.networking.vpc_id
}
# environments/dev/terraform.tfvars

aws_region = "us-east-1"
project_name = "mywebapp"
key_name = "my-key-pair"
db_password = "ChangeThisToASecurePassword123!"
allowed_ssh_cidrs = ["10.0.0.0/8", "192.168.0.0/16"]

部署步骤

1. 初始化项目

cd environments/dev
terraform init

2. 预览变更

terraform plan -out=tfplan

3. 应用配置

terraform apply tfplan

4. 验证部署

# 获取负载均衡器地址
terraform output alb_dns_name

# 测试应用
curl http://$(terraform output -raw alb_dns_name)/health

5. 销毁资源(可选)

terraform destroy

最佳实践总结

这个实战案例展示了以下 Terraform 最佳实践:

  1. 模块化设计:将基础设施按功能划分为独立的模块
  2. 环境隔离:使用目录结构分离不同环境的配置
  3. 变量管理:使用变量文件管理环境特定的值
  4. 状态管理:使用远程后端存储状态文件
  5. 安全配置:敏感信息使用变量并标记为敏感
  6. 标签策略:为所有资源添加一致的标签
  7. 生命周期管理:使用 lifecycle 块控制资源行为

通过这个案例,你应该对如何使用 Terraform 构建生产级别的基础设施有了清晰的认识。可以根据实际需求扩展和定制这个架构。