儲存上 IaC — S3 bucket 的安全與生命週期
S3 bucket 描述的是物件儲存的存在、命名、加密設定、版本控制與存取政策。bucket 本身沒有重建代價意義上的狀態問題 — 困難在它「裝的東西」。空 bucket 可隨時重建,裝了正式資料的 bucket 與 RDS 一樣不可隨意 destroy。把安全設定與生命週期規則寫進 IaC,讓這些防線成為可版本控制、可審查的程式碼,而非散落在 Console 的隱性設定。
bucket 的四道安全防線
一個 S3 bucket 在 IaC 裡至少要描述四個獨立資源,各自對應一道防線。Terraform 把它們拆成獨立資源是設計選擇 — 每道防線可以單獨 review、單獨調整、單獨追蹤變更歷史。
1resource "aws_s3_bucket" "assets" {
2 bucket = "acme-${var.env}-assets"
3
4 tags = { service = "cdn-origin", env = var.env }
5}
6
7resource "aws_s3_bucket_versioning" "assets" {
8 bucket = aws_s3_bucket.assets.id
9 versioning_configuration { status = "Enabled" }
10}
11
12resource "aws_s3_bucket_server_side_encryption_configuration" "assets" {
13 bucket = aws_s3_bucket.assets.id
14 rule {
15 apply_server_side_encryption_by_default {
16 sse_algorithm = "aws:kms"
17 }
18 }
19}
20
21resource "aws_s3_bucket_public_access_block" "assets" {
22 bucket = aws_s3_bucket.assets.id
23 block_public_acls = true
24 block_public_policy = true
25 ignore_public_acls = true
26 restrict_public_buckets = true
27}versioning
versioning 讓物件的每次覆寫都保留前一版。誤覆寫時可以從版本歷史回退到前一個正確版本,誤刪時物件只是被標記為 delete marker、前一版仍然存在。這道防線對承載正式資料的 bucket 是必要的 — 沒有 versioning 的 bucket,一次誤操作就是資料永久遺失。
versioning 開啟後會累積歷史版本的儲存量。搭配生命週期規則設定 noncurrent_version_expiration 可以控制保留多少天的舊版本,避免儲存成本無限成長。這個天數是「保留能力」跟「儲存成本」的取捨 — 保留 30 天通常足以涵蓋發現問題到回退的時間差,受合規要求的資料則依規定延長。
server-side encryption
server_side_encryption 確保物件在 S3 落地時加密。aws:kms 使用 KMS 管理的金鑰,加密操作對應用程式透明 — 寫入時自動加密、讀取時自動解密,不需要改應用程式碼。選 aws:kms 而非 AES256(SSE-S3)的判斷依據是存取控制粒度:KMS 金鑰可以獨立設定 key policy,讓「誰能解密」這件事跟「誰能讀 bucket」分開管理,適合跨帳號或跨團隊的場景。
使用 KMS 加密的 bucket 在跨帳號存取時,目標帳號除了要有 bucket 的讀取權限,還需要 KMS key 的 kms:Decrypt 權限 — 少了這一步會拿到 AccessDenied,錯誤訊息通常指向 S3 權限而非 KMS,排查時容易走錯方向。
public access block
public_access_block 的四個布林全設 true,等於從 bucket 層級封死對外公開的可能。即使有人之後誤加了一條公開的 bucket policy 或 ACL,這個 block 也會擋住。它是一道兜底機制 — 擋的是設定錯誤,不是正常操作。
靜態掃描工具(checkov / tfsec)會標記缺少 public access block 的 bucket。這正是模組七:infra 走 PR 流程裡自動化護欄的典型攔截對象 — 漏設的 bucket 會在 PR 階段被擋下,而非部署到線上才發現。
定期用 CLI 掃一遍帳號內所有 bucket 的公開狀態,命中的每個 bucket 都要能回答「這個公開是故意的、理由是什麼」:
1aws s3api list-buckets --query 'Buckets[].Name' --output text | tr '\t' '\n' | \
2 while read b; do
3 status=$(aws s3api get-public-access-block --bucket "$b" 2>/dev/null | \
4 jq -r '.PublicAccessBlockConfiguration | to_entries[] | select(.value==false) | .key')
5 [ -n "$status" ] && echo "$b: $status"
6 done生命週期規則
儲存成本隨物件數量與保留時間線性成長。生命週期規則讓 IaC 描述「某類物件多久後搬到更便宜的儲存層、再多久後刪掉」,把成本控制變成可版本控制的設定。
1resource "aws_s3_bucket_lifecycle_configuration" "assets" {
2 bucket = aws_s3_bucket.assets.id
3
4 rule {
5 id = "archive-old-logs"
6 status = "Enabled"
7 filter { prefix = "logs/" }
8
9 transition {
10 days = 30
11 storage_class = "GLACIER_IR"
12 }
13 expiration { days = 365 }
14 }
15
16 rule {
17 id = "cleanup-old-versions"
18 status = "Enabled"
19 filter {}
20
21 noncurrent_version_expiration {
22 noncurrent_days = 30
23 }
24 }
25}儲存層的取捨
S3 提供多個儲存層,各自在存取延遲與儲存單價之間取捨:
| 儲存層 | 存取延遲 | 適用場景 |
|---|---|---|
| Standard | 毫秒級 | 頻繁讀取的熱資料 |
| Standard-IA | 毫秒級 | 不常存取但需要時立即讀到 |
| Glacier Instant | 毫秒級 | 每季存取一次的歸檔 |
| Glacier Flexible | 分鐘到小時級 | 稽核留存、年度查閱 |
| Glacier Deep Archive | 12 小時級 | 法規留存、極少存取 |
transition 規則的日數設定要回推自業務需求:log 在除錯期間需要即時讀取(Standard),超過 30 天後幾乎只在事故回顧時才翻(Glacier Instant Retrieval 或 Standard-IA),超過一年可以淘汰或移到更深的歸檔層。把這些規則寫進 IaC,「為什麼 logs 只留一年」就是一個能在 PR 上被討論的決定,而非某人在 Console 點了不知道大家知不知道的設定。
bucket policy 與跨帳號存取
bucket policy 描述誰能對這個 bucket 做什麼操作,是 bucket 層級的存取控制。它跟 IAM policy 的差別在施力點:IAM policy 貼在身分上、定義「這個身分能做什麼」;bucket policy 貼在資源上、定義「這個 bucket 允許誰來」。兩者同時生效 — 一個請求要同時被身分端和資源端允許才會放行(除非有顯式 deny)。
跨帳號存取是 bucket policy 最常見的使用場景。一個帳號的 S3 bucket 要讓另一個帳號的 IAM role 讀取,需要兩端同時授權:bucket policy 允許那個 role 的 ARN,對方帳號的 IAM policy 也允許對這個 bucket 操作。
1resource "aws_s3_bucket_policy" "cross_account_read" {
2 bucket = aws_s3_bucket.assets.id
3
4 policy = jsonencode({
5 Version = "2012-10-17"
6 Statement = [{
7 Sid = "AllowCrossAccountRead"
8 Effect = "Allow"
9 Principal = { AWS = "arn:aws:iam::111222333444:role/data-reader" }
10 Action = ["s3:GetObject", "s3:ListBucket"]
11 Resource = [
12 aws_s3_bucket.assets.arn,
13 "${aws_s3_bucket.assets.arn}/*"
14 ]
15 }]
16 })
17}bucket policy 的常見陷阱是 Principal: "*" — 允許任何人存取。這跟 security group 的 0.0.0.0/0 是同一類風險。除了做為 CloudFront Origin Access Control(OAC)的配合設定,幾乎沒有合理場景需要把 Principal 設成 wildcard。checkov 的 CKV_AWS_70 規則專門攔這個。
把 bucket policy 寫進 IaC 的好處是每一條授權都有 PR 紀錄 — 誰在什麼時候加了一條跨帳號存取、為什麼加、reviewer 同意了沒有。散落在 Console 的 bucket policy 沒有這些追蹤,某天發現一條不認得的授權時,只能去翻 CloudTrail 猜它是什麼時候加的。
事件通知
S3 事件通知讓 bucket 在物件被建立、刪除或還原時,自動觸發下游處理 — 寫入後自動縮圖、上傳後自動掃毒、刪除後自動通知。這些觸發關係寫進 IaC,讓「這個 bucket 會觸發什麼」成為可查詢的事實,而非散落在 Console 的隱性接線。
1resource "aws_s3_bucket_notification" "assets" {
2 bucket = aws_s3_bucket.assets.id
3
4 lambda_function {
5 lambda_function_arn = aws_lambda_function.thumbnail.arn
6 events = ["s3:ObjectCreated:*"]
7 filter_prefix = "uploads/"
8 filter_suffix = ".jpg"
9 }
10}
11
12resource "aws_lambda_permission" "allow_s3" {
13 statement_id = "AllowS3Invoke"
14 action = "lambda:InvokeFunction"
15 function_name = aws_lambda_function.thumbnail.function_name
16 principal = "s3.amazonaws.com"
17 source_arn = aws_s3_bucket.assets.arn
18}事件通知的兩個配置常被忽略。第一是權限:S3 要觸發 Lambda,Lambda 的 resource-based policy 必須允許 S3 呼叫它(上面的 aws_lambda_permission),少了這段 apply 會成功但事件不會觸發,除錯時不容易發現。第二是 filter:不設 prefix / suffix 的通知會對 bucket 裡每一個物件操作都觸發,包括生命週期搬遷產生的物件變動 — 流量遠超預期。用 filter 把觸發範圍收斂到需要處理的路徑與檔案類型。
事件通知也可以導向 SQS 或 SNS,適合需要非同步佇列處理或 fan-out 到多個消費者的場景。選擇依據是下游的消費模式:Lambda 適合輕量即時處理(毫秒級回應),SQS 適合需要 backpressure 和重試的批次處理,SNS 適合同一事件需要同時通知多個服務。
跨分類引用
- → 模組七:infra 走 PR 流程:checkov / tfsec 攔截缺少 public access block 或加密的 bucket
- → 模組八:治理好習慣:bucket 的 tagging 與成本歸因
- → 模組二:身分與憑證地基:bucket policy 與 IAM policy 的權限模型交集