地基就緒後,依「地基 → 上層」的順序把實際承載業務的服務寫進 IaC。身分(IAM)網路(VPC / subnet)環境分離構成底層平面,這一層在它們之上描述資料庫、運算、儲存與入口 — 業務流量真正落地的地方。順序與依賴的表達方式決定了這層能不能被乾淨地重建、拆除與演進。共通原則是:描述服務的「身分與接線」,而非把每個執行期參數都塞進程式碼。

本篇先確立依賴圖怎麼驅動部署順序,再展開核心服務裡最需要謹慎描述的一類 — 資料庫。資料庫持有無法重建的狀態,它的 IaC 描述比其他 stateless 資源多出保護策略、連線管理與讀寫分流三個維度。

核心服務的部署順序

核心服務的部署順序由依賴方向決定:被依賴的先建,依賴別人的後建。網路與身分是幾乎所有上層服務的共同前置 — 資料庫要放進私有 subnet、運算要套用 IAM role 才能讀 S3、load balancer 要掛在公開 subnet 並引用 security group。這些底層平面若還沒成形,上層資源會在 apply 時因為找不到 subnet ID 或 role ARN 而失敗,或更糟,建在預設 VPC 裡繞過了所有隔離設計。

把順序交給 IaC 工具的依賴圖自動推導,比人工排序可靠。當運算資源的定義引用了 subnet 與 security group 的資源屬性,Terraform 會解析出「subnet 先於運算」的邊,apply 時自動排程。人工維護一份「先做 A 再做 B」的清單會隨資源增加而失準,依賴圖則隨程式碼本身演進。

四層依賴結構

依賴圖的典型展開順序呈現四層結構:

層次資源依賴來源
1VPC、subnet、security group、IAM role無(地基層,由模組二到四建立)
2RDS、ElastiCache、S3 bucket引用 subnet group、security group
3ECS service / EKS workload、RDS Proxy引用 subnet、IAM role、DB 端點
4ALB、listener、target group、ACM 憑證引用 public subnet、security group、ECS

這四層不需要手動編排。只要程式碼裡的引用關係正確,Terraform 就會自動按這個順序 apply。當 plan 輸出的順序看起來不合直覺 — 例如 ALB 先於 ECS — 通常代表某個引用斷了、兩者之間沒有依賴邊。

順序失控的徵兆

順序失控的早期徵兆是:某個上層資源的定義裡寫了一串 hardcode 的 subnet ID 或 VPC ID。

1# 硬編碼 ID — 依賴圖斷裂,底層重建時上層不會跟上
2resource "aws_db_subnet_group" "private" {
3  subnet_ids = ["subnet-0abc123", "subnet-0def456"]
4}

這段 code 跟底層的 subnet 資源沒有引用關係。底層一旦重建、ID 改變,上層不會自動跟上,state 與雲端現實之間的不一致(即 drift)就此產生。修法是把硬編碼的 ID 換成對底層資源屬性的引用:

1# 引用資源屬性 — 依賴圖自動推導,底層重建時上層自動取得新 ID
2resource "aws_db_subnet_group" "private" {
3  subnet_ids = [for s in aws_subnet.private : s.id]
4}

跨 state 的情境(網路地基與核心服務分屬不同 state)則用 data source 取代直接引用 — 這個取捨在服務依賴與跨 state 引用展開。

隱性依賴與 depends_on

自動推導涵蓋的是「引用屬性時產生的邊」。少數情況下兩個資源之間有依賴卻沒有屬性引用 — 例如一個 IAM policy attachment 必須在某個 role 被 ECS task 使用之前完成,但 task 引用的是 role ARN 而非 attachment 的輸出。這時用 depends_on 顯式宣告邊:

1resource "aws_ecs_service" "api" {
2  # ...
3  depends_on = [aws_iam_role_policy_attachment.ecs_task_s3]
4}

depends_on 應該只出現在自動推導覆蓋不了的場景。如果一個 module 裡到處都是 depends_on,通常代表引用關係寫得不夠明確,該把隱性依賴改成屬性引用。

資料庫(RDS)

資料庫是核心服務裡最需要謹慎描述的資源,因為它持有無法重建的狀態。IaC 定義它的 instance class、引擎版本、所在的 subnet group(決定它落在哪些私有 subnet)、套用的 parameter group 與 security group。連線端點不要硬編碼,改用資源 output 暴露給上層運算引用,這樣端點隨主庫 failover 或重建而改變時,上層引用自動更新。

 1resource "aws_db_instance" "primary" {
 2  identifier             = "app-${var.env}-primary"
 3  engine                 = "postgres"
 4  engine_version         = "16.3"
 5  instance_class         = var.db_instance_class
 6  allocated_storage      = 100
 7  storage_encrypted      = true
 8
 9  db_subnet_group_name   = aws_db_subnet_group.private.name
10  vpc_security_group_ids = [aws_security_group.db.id]
11
12  multi_az                  = var.env == "prod" ? true : false
13  backup_retention_period   = var.env == "prod" ? 14 : 1
14  backup_window             = "03:00-04:00"
15  deletion_protection       = var.env == "prod" ? true : false
16  skip_final_snapshot       = var.env == "prod" ? false : true
17  final_snapshot_identifier = var.env == "prod" ? "app-prod-final-${formatdate("YYYYMMDD", timestamp())}" : null
18
19  tags = { service = "payments" }
20}
21
22output "db_endpoint" {
23  value = aws_db_instance.primary.endpoint
24}

加密的不可逆性

storage_encrypted = true 確保磁碟層級的加密在資源建立時就生效。RDS 不支援事後對既有 instance 開加密 — 漏了只能重建。補救路徑是匯出快照、用加密 KMS key 複製快照成加密版本、再用加密快照還原成新 instance。這個過程需要停機或切換端點,對已經承載流量的 production 資料庫代價很高。prod 的 RDS 若 storage_encrypted 為 false,這筆技術債越早處理越便宜。

parameter group 的角色

parameter group 定義資料庫引擎層級的行為參數(如 max_connectionswork_memlog_min_duration_statement),是 RDS instance 的設定骨架。IaC 描述 parameter group 的好處是讓這些參數進版本控制 — 有人改了 max_connections 會出現在 PR diff 裡,而不是某天在 Console 改了沒人知道。

 1resource "aws_db_parameter_group" "postgres16" {
 2  family = "postgres16"
 3  name   = "app-${var.env}-pg16"
 4
 5  parameter {
 6    name  = "log_min_duration_statement"
 7    value = "1000"
 8  }
 9
10  parameter {
11    name  = "shared_preload_libraries"
12    value = "pg_stat_statements"
13  }
14}

修改 parameter group 的某些參數需要重啟 RDS instance(稱為 apply_method = "pending-reboot"),修改前要先確認這個參數屬於「立即生效」還是「要重啟」。在 Terraform plan 裡不會明確標示重啟,要靠 AWS 文件交叉比對。

連線管理

運算到資料庫之間有一段常被略過的接線:連線管理。無狀態運算水平擴張時,每個實例各自開連線,容易把資料庫的連線數打滿。一個 ECS service 從 5 個 task 擴到 50 個、每個 task 開 10 條連線,就從 50 條跳到 500 條 — 而一台 db.r6g.largemax_connections 預設約在 1600 左右,500 條已經吃掉三分之一。

出現「擴運算反而拖垮 DB」的訊號時,解法是引入連線池或受管的連線代理。RDS Proxy 是 AWS 的受管方案:它在運算與 RDS 之間當一層連線池,把下游的數百條短連線收斂成對 RDS 的少量長連線。在 IaC 裡一併定義,輸出 proxy 端點給運算引用:

 1resource "aws_db_proxy" "app" {
 2  name                   = "app-${var.env}-proxy"
 3  engine_family          = "POSTGRESQL"
 4  role_arn               = aws_iam_role.rds_proxy.arn
 5  vpc_subnet_ids         = [for s in aws_subnet.private : s.id]
 6  vpc_security_group_ids = [aws_security_group.db.id]
 7
 8  auth {
 9    auth_scheme = "SECRETS"
10    secret_arn  = aws_secretsmanager_secret.db_password.arn
11  }
12}
13
14output "db_proxy_endpoint" {
15  value = aws_db_proxy.app.endpoint
16}

運算端引用 db_proxy_endpoint 而非 db_endpoint,連線管理就從各 task 自己處理轉成由 proxy 統一收斂。RDS Proxy 同時提供 failover 的連線保持 — 主庫切換到 standby 時,proxy 維護的連線不會全部斷開重建,應用端感受到的是短暫延遲而非連線錯誤。

判讀是否需要 RDS Proxy 的訊號是連線數成長曲線:如果運算的擴縮範圍固定且連線數上限遠低於 max_connections,直連即可;如果運算會頻繁擴縮或連線數可能逼近上限,proxy 值得引入。proxy 本身有額外成本(按 vCPU 計費),不是所有環境都划算 — dev 環境通常直連就夠。

read replica

當讀流量遠大於寫、且能容忍副本的複寫延遲(通常是毫秒到秒級)時,read replica 是把讀請求導離主庫的下一步。replica 在 IaC 裡用獨立資源描述,引用主庫的 identifier:

 1resource "aws_db_instance" "read_replica" {
 2  identifier             = "app-${var.env}-replica"
 3  replicate_source_db    = aws_db_instance.primary.identifier
 4  instance_class         = var.db_replica_class
 5  vpc_security_group_ids = [aws_security_group.db.id]
 6}
 7
 8output "db_replica_endpoint" {
 9  value = aws_db_instance.read_replica.endpoint
10}

運算端依讀寫分流引用不同端點 — 寫走 db_endpoint(或 db_proxy_endpoint),讀走 db_replica_endpoint。這個分流邏輯屬於應用層的責任,infra 只負責把端點暴露出來。

read replica 的邊界要講清楚:它緩解讀流量對主庫的壓力,但它不是備份。replica 會同步複製主庫的所有變更 — 包括誤刪的資料。需要還原到某個時間點的保護由 backup retention 與 PITR(point-in-time recovery)提供,這兩者的 IaC 描述在 stateful 保護策略

引擎版本升級的取捨

RDS 引擎版本(engine_version)寫進 IaC 後,版本升級就成為一個需要 PR review 的變更。升級分 minor 和 major:minor 升級(16.2 → 16.3)通常向後相容、可在維護視窗自動套用;major 升級(15 → 16)可能有 breaking change,需要先在 dev 環境驗證、備份、排維護窗口。

在 IaC 裡把 engine_version 寫死是刻意的選擇 — 它阻止 AWS 在背景自動升級 major 版本,讓版本變更必須走 PR。代價是需要定期檢查是否有 EOL 版本還在用。如果 engine_version 指向的版本已經超過 AWS 的支援期限,Terraform apply 會在某天失敗(AWS 會強制升級),這比主動升級更不可控。

資料庫在規模放大後的治理維度也會改變。Netflix 把分散的 Aurora 叢集整併後成本降了 28%——多個團隊各自開的 RDS instance 加起來的閒置容量遠超一個整併後的叢集。infra 層的教訓是 RDS 的 IaC 描述不只管單一 instance 的設定,長期還要管叢集的分布與合併策略。另一個維度是合規需求驅動的資料落地:Hard Rock Digital 因為 Wire Act 法規要求資料留在特定州,用 AWS Outposts 在地端跑運算——這類情境下 infra 的 region 與可用區選擇由法規約束驅動,而非純技術決策。

跨分類引用