Three Tier Architecture with AWS ECS
在日常生活中的應用程式,例如電子商務應用,天然擁有三個層級
- 接受流量的入口層
- 處理邏輯的計算層
- 存儲數據的持久化層
這三者的擴容和維護都不同,安全邊界不同(只有入口層對互聯網暴露),其生命週期也不一樣(持久化層的數據庫很少需要重建,而應用程式的代碼可能需要頻繁部署)。因此,將三個層級分開是非常有必要的。
而 AWS 提供了多種服務來實現這些層級的架構,以下是如何使用 AWS ECS(Elastic Container Service)來構建三層架構的示例:

這是一個可以讓容器化應用程式部署在aws上的三層架構,具備部分生產環境的特徵:多可用區高可用,網絡層嚴格隔離,HTTPS加密,自動擴縮容,基礎設施使用代碼管理。
在本架構中,三層架構的主要作用如下:
- 入口層:接收用戶HTTPS請求,做TLS卸載和流量分發。流量高峰時不需要變化
- 計算層:運行邏輯業務的容器,流量高峰的時候需要橫向拓展,低谷時縮回以節約成本
- 持久化層:運行數據庫,數據庫的生命週期和應用程式的生命週期不同,通常不需要頻繁重建,需要最高級別安全保護
整個架構部署在us-east-1區域的兩個可用區內,每個az有三個子網,公有子網放ALB和NAT Gateway,私有子網放ECS容器,私有數據子網放RDS數據庫和Redis數據庫。
選型以及Trade-off:
ECS Farget Vs EKS: 選擇Farget是因為目前的應用程式規模不大,而且只有一個應用程式在上面運行,不需要k8s的複雜性和靈活性,Farget的無服務器特性可以簡化運帷。如果未來要運行多個應用程式或者涉及到更精細的資源管理,會考慮遷移到EKS。
Cloudflare VS Route 53: 筆者在CloudFlare上註冊了域名,而Cloudflare Register不支持把nameserver改到第三方,所以使用ACM證書驗證,方法是手動在CloudFlare面板添加CNAME記錄。
tfvars VS Secrets Manager: 開發階段使用tfvars文件存儲敏感信息,生產環境使用Secrets Manager,這樣可以在開發過程中更方便地管理配置,而在生產環境中則能提供更高的安全性。只需要在ECS task definition中添加指向Secrets Manager的ARN即可。
項目結構
retail-store-infra/
├── main.tf # 調用各Module,定義資源依賴
├── variables.tf # 項目級別變量
├── outputs.tf # ALB DNS、RDS endpoint 等
├── providers.tf # AWS provider + 版本約束
├── backend.tf # S3 遠程狀態 + DynamoDB 鎖
├── terraform.tfvars # 環境變量值(不提交到 Git)
│
├── modules/
│ ├── vpc/ # VPC、6 個子網、IGW、NAT GW、路由表
│ ├── alb/ # ALB、Listener、Target Group、ACM 證書
│ ├── ecs/ # ECS 集群、Service、Task Definition、IAM
│ ├── rds/ # RDS 實例、子網組、參數組
│ └── redis/ # ElastiCache 複製組、子網組
│
├── bootstrap/ # S3 + DynamoDB(Terraform 狀態管理)
└── README.md

本項目模塊拆分的邏輯是按照生命週期以及所有權進行劃分的,VPC和子網一起創建,一起銷毀,所以放在同一個模塊,ALB的listener可能會頻繁變化而VPC不變,所以兩者分開。安全組跟著各自的組件走,而不是集中在一個文件夾中。
模塊之間的數據流
各模塊通過outputs導出數據,根目錄的main.tf集中調用
module "ecs"{
source = "./modules/ecs"
aws_region = var.aws_region
project = var.project
private_subnet_ids = module.vpc.private_app_subnet_ids
alb_target_group_arn = module.alb.alb_target_group_arn
app_port = var.app_port
vpc_id = module.vpc.vpc_id
alb_sg_id = module.alb.alb_sg_id
redis_sg_id = module.redis.redis_sg_id
rds_sg_id = module.rds.db_sg_id
repo_url = module.ecr.repo_url
db_endpoint = module.rds.db_endpoint
redis_endpoint = module.redis.redis_endpoint
db_password = var.db_password
}
關鍵設計決策:
網絡隔離
每個AZ有三個子網,公有子網放置ALB和NAT Gateway,應用子網放置ECS容器,數據子網放置RDS和Redis。這樣可以確保只有ALB對互聯網暴露,應用層和數據層完全隔離,提高安全性。 子網使用cidrsubnet函數從VPC的CIDR塊中劃分出不同的子網,確保地址空間合理利用。
#Public Subnets
resource "aws_subnet" "public" {
count = 2
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index)
availability_zone = var.azs[count.index]
map_public_ip_on_launch = true
tags = {
Name = "public-${var.azs[count.index]}"
}
}
安全組設計
最小權限鏈路設計:
三個安全組形成一條鏈,每一層只允許來自上一層的特定流量
安全組之間的引用使用referenced_security_group_id而不是cidr,這意味著即使IP發生改變,規則仍然有效,因為它認證特定安全組的流量。
安全組採用新的aws_vpc_security_group_ingress_rule和aws_vpc_security_group_egress_rule資源來定義規則,而不是在aws_security_group中內聯定義,這樣可以更清晰地管理規則,並且支持更多的功能,例如描述和依賴關係。
resource "aws_security_group" "allow_tls" {
name = "allow_tls"
description = "Allow TLS inbound traffic and all outbound traffic"
vpc_id = aws_vpc.main.id
tags = {
Name = "allow_tls"
}
}
resource "aws_vpc_security_group_ingress_rule" "allow_tls_ipv4" {
security_group_id = aws_security_group.allow_tls.id
cidr_ipv4 = aws_vpc.main.cidr_block
from_port = 443
ip_protocol = "tcp"
to_port = 443
}
resource "aws_vpc_security_group_ingress_rule" "allow_tls_ipv6" {
security_group_id = aws_security_group.allow_tls.id
cidr_ipv6 = aws_vpc.main.ipv6_cidr_block
from_port = 443
ip_protocol = "tcp"
to_port = 443
}
resource "aws_vpc_security_group_egress_rule" "allow_all_traffic_ipv4" {
security_group_id = aws_security_group.allow_tls.id
cidr_ipv4 = "0.0.0.0/0"
ip_protocol = "-1" # semantically equivalent to all ports
}
resource "aws_vpc_security_group_egress_rule" "allow_all_traffic_ipv6" {
security_group_id = aws_security_group.allow_tls.id
cidr_ipv6 = "::/0"
ip_protocol = "-1" # semantically equivalent to all ports
}
ALB
ALB涉及到四個AWS資源:
- aws_lb: ALB在什麼地方
- aws_lb_listener: ALB聽什麼端口,使用什麼協議,收到請求後怎麼處理
- aws_lb_target_group: ALB把流量發送到哪裡,那裡有什麼東西
- aws_ecs_service.load_balancer: 把task註冊到target group裡,讓ALB能找到它
80端口的listener只是做重定向,真正處理流量的是443端口的listener,這樣可以確保所有流量都經過TLS加密。
HTTPS實現的全過程:
- Terraform創建ACM證書資源
- ACM生成一條CNAME記錄驗證記錄
- 手動在CloudFlare添加CNAME記錄,完成域名所有權驗證
- 創建ALB HTTPS listener,引用已經驗證的證書
如果域名在Route 53註冊,則可以實現自動化完成
成本分析
NAT Gateway是成本最高的資源,光是存在就要2.16$/天,省錢策略是開發階段使用terraform destroy銷毀資源,生產環境使用terraform apply創建資源,這樣可以在不需要的時候節約成本。
改進方向
- CI/CD:用CodePipeline + CodeBuild 實現代碼提交後自動部署
- Secrets Manager:把敏感信息從tfvars遷移到Secrets Manager,並在ECS task definition中引用
- 監控警告: 使用CloudWatch監控ECS和RDS的性能指標,設置警告通知
- WAF:在ALB前面添加AWS WAF,保護應用免受常見的網絡攻擊
- 完整微服務部署: 不僅部署一個應用程式,還可以部署多個微服務
- 基礎設施測試: 使用Terratest或類似工具對基礎設施進行自動化測試,確保部署的可靠性和穩定性
總結
筆者認為寫terraform代碼的過程中,最大的挑戰是理解AWS資源之間的依賴關係,每一個Association和Attachment都代表一個依賴關係,以及outputs和variables傳遞數據的方式。