在先前兩個主題 <Infrastructure as Code : Terraform 基礎設施代碼化與版本管控> 與 <CI/CD 全自動化實作 - GitHub Actions × CodePipeline × CodeBuild>中,我們分別講述了 **「讓基礎設施變得可預測、可重複、可版本化」**
的重要性與 保護既有商業邏輯不被程式碼異動所破壞汙染
的積極性。兩者同樣都是為了解決 要進行檢測與部屬的時候沒有一個 穩定的環境 與 固定的商業邏輯驗證
的議題並確保我們 商業邏輯驗證
能夠在穩定的土壤中成長茁壯成為參天大樹(我說的不是台灣某銀行),簡單聊聊環境的重要性與對應痛點的解決方案後。接下來,我們將要討論 - 我需要什麼環境?
。
前陣子有幸跟朋友一同探店,前去台北晶華酒店的 Impromptu by Paul Lee 品嘗 2025 夏季風味,那是一個結合開放式廚房與座位區的獨特設計,用餐人士可以簡單且透明的看到主廚與他的團隊夥伴是怎麼將主廚對於 2025 的夏季用食材、香料與選酒呈現一幅美輪美奐、錦羅織景的絕妙風采。我個人非常喜歡這次的菜單設計,可以感覺到清泉瀝瀝的清朗與乾脆俐落的風華。
但話說回來,能夠摘下米其林星的佳餚是怎麼被設計出來的? 有個新的概念是件讓所有老饕興奮與歡欣鼓舞的消息,但假如直接在客人面前的料理台上邊做邊給客人吃,萬一結果失敗了呢?鹽放多了、火候錯了,輕則賠上口碑,重則客人吃壞肚子,明天就上新聞頭條餐廳名譽徹底掃地。
那我們退一步,在餐廳的「主廚房」後場,正式出餐前找個角落試做怎麼樣?
這是一個好方法,我們避免了第一時間客人立即品嘗到實驗性的菜餚,然而,雖然客人看不見,但我們可能會打亂廚房的既有正常運作。我們佔用了正在備料的砧板、需要醬料時發現被其他廚師用完了,甚至實驗油煙可能會影響到旁邊正在精心烹調的出餐。
這樣說來,在資金到位、環境允許的狀況下,我們在一個完全獨立的「研發廚房 (Test Kitchen)」裡去嘗試實現我們新的概念好像是一個最優解。 在這裡,我們有自己全套的鍋碗瓢盆、食材、烤箱,可以盡情地實驗、失敗、重來,完全不用擔心影響到外面的餐廳營運。
這就是 開發環境 (Development / Dev) 。它是一個完全隔離的沙盒 (Sandbox),讓每一位開發者可以自由地撰寫、修改、測試自己的程式碼,而不用擔心「弄壞」任何東西或影響到任何人。它避免了我們在 「主廚房」後場(共用環境) 中 影響到正在測試其他功能的同事,反之亦然。最重要的是極大程度的避免在生產環境 (Production / Prod) 直接寫程式碼,任何一個小失誤,比如一個錯誤的資料庫指令,都可能導致所有使用者服務中斷、資料遺失。 這是絕對、絕對禁止的行為。
在正式端上餐桌前,我們在我們自己的 研發廚房(開發環境) 把新菜色做出來了,味道驚為天人。下一步呢?直接把它放上菜單,賣給最重要的美食評論家嗎?
假如我們頭上沒有一個躲在廚師帽裡的大廚,以及端上桌的是法式燉菜的話(料理鼠王是個很好看的作品,有閒暇時間的話可以看看),這是一個非常冒險的舉動。
在正式推出前,我們需要一個 「最終演練」 的場所,這個場所,我們稱之為 「模擬廚房 (Staging Kitchen)」。 它的配置、爐具、動線,甚至連盤子的品牌,都必須和正式的「主廚房」一模一樣。 在這裡,我們要模擬真實的出餐流程 : 服務生要跑一次完整的點單上菜流程、廚師團隊要模擬在尖峰時段的壓力下能否依照設想流程穩定地做出這道菜,最後我們要測試這道新菜和其他經典菜色一起出餐時,會不會有流程上的衝突。
這就是 預備環境 (Staging / Pre-production) 的重要性,我們必須有一個環境全面且各種情境的去進行測試確保我們的商業邏輯是符合需求與品質認可。
現在,我們可以將軟體開發的生命週期抽象化為一個清晰的「進程 (Progression)」:
Dev => Staging => Prod
開發環境 (Dev): The Test Kitchen
預備環境 (Staging): The Staging Kitchen
生產環境 (Prod): The Michelin-Starred Restaurant
這個 Dev => Staging => Prod
的單向流動,就是軟體品質保證的 核心動線
。程式碼像水一樣,只能從低環境流向高環境,絕不逆流,每一次流動都是一次 「品質閘門 (Quality Gate)」
的檢驗,不論是為了 保護既有商業邏輯不被程式碼異動所破壞汙染
的 CI(Continuous Integration,持續整合) 流程中的 自動化檢查
又或是 <版本控制策略(PRReview strategy)> 中所提到的 同儕審核門檻 、 業務審核門檻 、 品質審核門檻 三方驗證,一旦 商業邏輯的實踐任一條件未達標
就必須立刻退回與緊急修復,我們必須有一個深刻且鮮明的認知 - 系統是商業抽象邏輯的實現
。
在現代軟體開發中,多環境管理是確保應用程式穩定性和可靠性的關鍵。接下來我們將深入探討如何在 AWS 上建構和管理 Dev/Staging/Prod 多環境架構,重點關注 Docker 容器化、ECS 叢集管理、EKS/K8S 編排等核心技術。
- 多環境架構設計原則 - 環境隔離策略與 IaC 實踐
- Docker 容器化策略 - 多階段建構與環境特定配置
- Amazon ECS 叢集管理 - 服務定義與自動擴縮配置
- Amazon EKS 與 Kubernetes 管理 - 叢集配置、應用部署與 Ingress 設定
- CI/CD 管道整合 - GitHub Actions 工作流程與藍綠部署
- 配置管理與秘密管理 - AWS Secrets Manager 與 K8s ConfigMap/Secret
- 監控與日誌管理 - CloudWatch 與 Prometheus/Grafana 整合
- 安全性最佳實踐 - 網路安全與 RBAC 配置
- 成本優化策略 - 資源自動化管理與 Spot Instance 整合
- 災難恢復與備份 - 跨區域備份與資料庫容錯移轉
我們上述談到,要把餐廳(我們的系統)分成「研發廚房 (Dev)」、「模擬廚房 (Staging)」和「旗艦總店 (Prod)」,我們來談談如何實際「劃定地界」並確保這三塊地的「建築規範」是一致的。
Production Environment (生產環境)
├── 高可用性配置
├── 自動擴縮容
├── 完整監控告警
└── 嚴格的部署流程
Staging Environment (預發布環境)
├── 生產環境鏡像
├── 完整功能測試
├── 效能測試環境
└── 整合測試驗證
Development Environment (開發環境)
├── 快速部署
├── 開發者友好
├── 資源成本優化
└── 彈性配置調整
首先最簡單的方式當然是在給每個廚房上鎖,並給具有對應職能需求的人專屬鑰匙。先鎖好門,避免有些人亂竄,自己人不小心調到參數、配置錯誤導致服務癱瘓不說,至少也要確保連鑰匙都沒有的小偷強盜來進都進不來,所以我們的第一步核心原則很簡單: 最小化爆炸半徑 (Minimize Blast Radius)
。 意思是,當一個環境發生災難時(比如被駭客入侵、配置錯誤導致服務癱瘓),絕對不能波及到其他環境。
在 AWS 上,實現這個目標的黃金標準,叫做 多帳號策略 (Multi-Account Strategy)
。
** 1. AWS Organizations:雲端集團總部 **
AWS Organizations
就是我們的「集團總部」,藉由他我們可以針對不同的 AWS 帳號進行管理與基礎政策的制定。就如同集團下的不同事業體有財務、業務、法務、人資、開發、維護一般,我們可以透過建立一個 DevOU
和一個 ProdOU
當作事業體的概念。 Account-Dev
(部門) 放在 DevOU
(事業體) 裡; Account-Staging
和 Account-Prod
則放在 ProdOU
裡。然後我們可以設定 實施集團法規 (Service Control Policies, SCPs)
限制旗下帳號 能 做什麼或 不能 做什麼。這就像是集團下達的「最高行政命令」,當事業體政策與集團總部出現衝突時,會優先且強制依循 SCPs
的規範。並且 AWS Organizations
還有一個好處,所有事業體的帳單與花費成本都匯總到總部,方便監控與管理。
SCPs
是從根源上防止了人為失誤或惡意行為,這比事後補救要有效得多。它定義了每個環境的「行為憲法」與「物理定律」。例如:
** 2. IAM 權限:不同身份的「通行證」 **
帳號隔離後,人要怎麼進去工作呢?答案是 IAM 角色 (IAM Roles),而不是給每個人在每個帳號都開一組帳密 (IAM User)。
這種方式讓權限集中管理,安全且清晰。
我們確保了環境之間有銅牆鐵壁,但如何保證「模擬廚房」和「旗艦總店」的內部裝潢、管線、爐具都一模一樣?這就是 基礎設施即程式碼 (Infrastructure as Code, IaC)
的威力所在,按照 藍圖(IaC)
我們可以快速建置出一模一樣的架構與環境出來。我們在<Infrastructure as Code : Terraform 基礎設施代碼化與版本管控>有提到藍圖的重要性,它讓基礎設施變得 可預測
、可重複
、可進化
,讓我們繼續用 Terraform 為例。
terraform/
├── modules/
│ ├── vpc/
│ │ ├── main.tf
│ │ └── variables.tf
│ └── ec2_instance/
│ ├── main.tf
│ └── variables.tf
│
└── environments/
├── dev/
│ ├── main.tf
│ └── terraform.tfvars
├── staging/
│ ├── main.tf
│ └── terraform.tfvars
└── prod/
├── main.tf
└── terraform.tfvars
在 environments/dev/main.tf
裡,我們用 fake code 呈現:
# 引用 VPC 模組
module "my_vpc" {
source = "../../modules/vpc"
cidr_block = var.vpc_cidr
}
# 引用 EC2 模組
module "web_server" {
source = "../../modules/ec2_instance"
instance_type = var.instance_type
vpc_id = module.my_vpc.id
}
注意到了嗎?這份 main.tf
在 dev, staging, prod 裡會長得幾乎一樣。真正的差別在於旁邊的 terraform.tfvars 參數檔:
environments/dev/terraform.tfvars
vpc_cidr = "10.10.0.0/16"
instance_type = "t3.micro" // 開發環境用小機器省錢
environments/prod/terraform.tfvars
vpc_cidr = "10.20.0.0/16"
instance_type = "m5.large" // 生產環境用大機器扛流量
當我們要部署 Dev 環境時,就進入 environments/dev
資料夾執行 terraform apply
。Terraform 會自動讀取 dev
的參數,套用到共用的模組,蓋出一個符合開發規格的環境,部署 Prod
也同理。
這樣就完美達成了我們 IaC 的目標:
我們已經用 IaC 畫好了三間廚房的建築藍圖,現在,我們要來標準化我們的「做菜工具」了。這就是 Docker 容器化策略要解決的問題。
我們一定聽過,甚至親身經歷過這個經典場景:
開發者 A: 「這個功能在我電腦上跑是好的啊!怎麼上線就壞了?」
這個「在我電腦上是好的 (It works on my machine)」的問題,就是 Docker 要根治的頑疾。問題的根源在於,開發者的電腦、測試伺服器、生產伺服器,這三者的「環境」存在著細微但致命的差異:
甚至全部都相同但就是有一台測試通過、一台不通過,OS 相同 windows(沒有偏見,但我的這是最有可能出現 底層差異
的地方),我們只能放乖乖、聖水或是讚美歐姆彌賽亞來乞求機魂大悅順利執行。
Docker
的核心思想就是將應用程式連同它的「整個執行環境」打包帶走。
與其給廚師一份「法式紅酒燉牛肉」的食譜,讓他用自己廚房裡的鍋子、爐具、調味料去做(這很可能因為每間廚房的差異而味道走樣),我們不如直接把 做好的半成品、搭配特製的醬汁、以及一個精準控溫的「標準化加熱盒」 一起交給他,唯一要做的,就是按下按鈕。
這個「標準化加熱盒」,就是 Docker 容器 (Container) ,這個盒子裡裝著:
無論這個「盒子」被拿到 Dev
環境的微波爐,還是 Prod
環境的頂級烤箱(只要它們都是合格的 Docker Host),加熱後拿出來的菜,味道都保證一模一樣。這就是容器化帶來的 可攜性 (Portability)
與 一致性 (Consistency)
。
我們把 Dockerfile 的建構過程分成兩個階段,就像一個流水線:
# Dockerfile.multi-stage
# Build stage
FROM node:18-alpine AS builder
WORKDIR /app
# 只複製建構所需的 package.json
COPY package*.json ./
# 只安裝生產環境必要的依賴
RUN npm ci --only=production
# Development stage
FROM builder AS development
RUN npm ci
COPY . .
EXPOSE 3000
CMD ["npm", "run", "dev"]
# Production stage
FROM node:18-alpine AS production
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY . .
EXPOSE 3000
USER node
CMD ["npm", "start"]
這樣做的好處是顯而易見的:
「最少必要」
的執行元件。一個盒子,如何適應不同廚房?
我們已經有了一個標準化的「加熱盒」,但現在有個新問題:
在 Dev 環境,這道菜需要連到 dev-database;在 Prod 環境,它需要連到 prod-database。
但我們的盒子(Docker Image)只有一個,要怎麼辦?
# docker-compose.dev.yml
version: '3.8'
services:
app:
build:
context: .
target: development
environment:
- NODE_ENV=development
- LOG_LEVEL=debug
volumes:
- .:/app
- /app/node_modules
ports:
- "3000:3000"
# docker-compose.prod.yml
version: '3.8'
services:
app:
build:
context: .
target: production
environment:
- NODE_ENV=production
- LOG_LEVEL=error
restart: unless-stopped
ports:
- "80:3000"
首先我們要記住,我們必須遵守一個鐵律、一個絕對法則
一次建構,到處執行 (Build once, run anywhere)
我們不能把密封好的便當盒因為不同環境的需求,就直接拆開,這樣妥妥的違反了 映像檔不變性 (Image Immutability)
,我們絕對不能為不同環境建構不同的 Image
(例如 my-app:dev
, my-app:prod
),這會摧毀我們前面建立的所有 一致性保證
。
從 Dev 測試通過的那個 Image,必須是原封不動的同一個 Image 被部署到 Staging 和 Prod。
但假如我們真的有 外部注入
的需求呢? 我們該怎麼辦?
所以我們的「標準加熱盒」上,預留了幾個插槽,像是「資料庫連線插槽」、「API 金鑰插槽」。盒子本身不帶這些東西,而是等它被放到特定廚房的指定位置時,由廚房的「自動化手臂」(容器編排系統)把對應的插頭插上去。
又或者就像是一些日本泡麵一樣,熱水孔與倒水孔是不同的孔位,分別對應各自不同的用途。你可以用熱水、熱茶(也不錯,我試過)甚至是熱奶茶(???,看過但尊重)倒入跟排出,但不變的是裡面的主體(麵體跟調料)沒有被變動。
所以回頭看一下剛剛的 yaml 示意,我們發現有一些端倪
# docker-compose.dev.yml
version: '3.8'
services:
app:
build:
context: .
target: development
environment:
- NODE_ENV=development
- LOG_LEVEL=debug
volumes:
- .:/app
- /app/node_modules
ports:
- "3000:3000"
# docker-compose.prod.yml
version: '3.8'
services:
app:
build:
context: .
target: production
environment:
- NODE_ENV=production
- LOG_LEVEL=error
restart: unless-stopped
ports:
- "80:3000"
# docker-compose.dev.yml
environment:
- NODE_ENV=development
- LOG_LEVEL=debug
# docker-compose.prod.yml
environment:
- NODE_ENV=production
- LOG_LEVEL=error
我們可以透過宣告 環境變數
來設定不同的接口,這是最標準、最符合十二要素應用程式 (12-Factor App) 理念的方法 - 應用程式碼不應該寫死任何配置,而是從環境變數中讀取。
容器啟動時 (由 ECS/Kubernetes 負責)時,我們會如下執行:
docker run -e DATABASE_HOST=dev.db.internal ... my-app
docker run -e DATABASE_HOST=prod.db.internal ... my-app
而對於更複雜的配置(例如一整個 settings.json
),可以由容器編排系統將對應環境的設定檔,在容器啟動時「掛載」到容器內的特定路徑(例如 /etc/config/settings.json
),我們的應用程式只需要固定去讀取這個路徑的檔案即可。
最後有一個更進階、更安全的方式。啟動時從配置中心拉取 (Fetching from Config Service)
容器啟動時,應用程式會利用被賦予的身份(例如 AWS ECS Task Role),去呼叫一個外部服務(如 AWS Secrets Manager 或 Parameter Store)來取得它需要的資料庫密碼、API Key 等。這樣連環境變數都不會明文出現,安全性最高。