為什麼你應該用 ECS 部署服務
# 軟體工程我很討厭在小型團隊用 AWS,但如果你不幸要用 AWS 架設網頁服務,那麼我會強烈建議使用 ECS。
至於為什麼不幸,我們下次再聊🥲。
ECS 是什麼
ECS(Elastic Container Service)是 AWS 的容器編排服務,負責幫你管理 Docker container 的生命週期。在開始之前,有幾個核心概念:
Cluster
一組運算資源的邏輯分群。可以把它想成機房,你所有的 container 都跑在這個 cluster 裡面。Cluster 本身不花錢,花錢的是裡面跑的運算資源
Task Definition
container 的規格書。定義了要用哪個 Docker image、分配多少 CPU 和記憶體、環境變數、port mapping、log 設定等。每次修改都會產生新的 revision,方便追蹤和 rollback
Task
根據 Task Definition 實際跑起來的 container instance。一個 Task Definition 可以同時跑很多個 Task。可分為常駐型或是單次型。
Service
管理 Task 的上層抽象。負責確保指定數量的 Task 持續運行,某個 Task 掛了會自動拉起新的。也負責跟 ALB 串接、網路、Security Group,把流量導到健康的 Task 上。
Fargate
AWS 提供的 serverless 運算引擎。用了 Fargate,你不需要自己管 EC2 instance,只要告訴 AWS 你的 container 需要多少 CPU 和記憶體,底層的機器 AWS 幫你搞定。另一個選項是用 EC2 launch type,自己管機器,彈性更大但維運負擔也更重
簡單來說,關係是這樣的:Cluster 包含多個 Service,每個 Service 根據 Task Definition 跑出多個 Task,而 Task 實際運行在 Fargate 或 EC2 上。
如果非得用 AWS,直接考慮 ECS
看到上面那串清單,直覺反應是直接開一台 EC2 跑起來更單純。
表面上是。但管一台 EC2 就像養一隻寵物,牠會生病、會老、需要你持續照顧(以下都是真實慘案😢):
- OS 安全性更新:kernel patch、glibc 漏洞、OpenSSL 更新,不更新就是在裸奔,更新了可能把你的 application 搞爛
- 記憶體管理:Swap 設太少 OOM kill,設太多拖慢效能。OOM 發生時你可能連 SSH 都進不去,只能在 console 上乾瞪眼
- Log rotation:忘了設定,半年後 disk 滿了,服務直接掛掉
- Process 管理:application crash 了誰幫你重啟?systemd?pm2?每一種都要另外設定
- SSH key 管理:誰有權限連進去?有人離職了 key 刪了嗎?(不要知道答案比較好)
- 部署方式:
rsync?scp?自己寫 deploy script?每次部署都在祈禱 - 環境漂移:跑了半年的 EC2 跟新開的不會一樣。有人手動裝了什麼、改了什麼設定,沒人記得,也沒人敢重建
Container 的哲學正好相反:每次部署都是從乾淨的 image 啟動,環境不會漂移(drift),壞了就砍掉重來。
有人會說 EC2 不是也可以透過 Launch Template 或 AMI 確保環境不會漂移,也可以透過 Ansible 確保每個 Instance 的狀態相同,然而實務上需要考量:
- ECS 本質是用 Docker image 啟動,而 Dockerfile 通常與 application code 在同一個 Repository,追蹤起來很容易,也可以在本地端 Debug;然而 Ansible 通常會分開管理,間接造成開發人員的心智負擔
- 為了管理 AMI / Launch Templates,又要再引入一個「管理 AMI」的工具,例如 Packer,對很多應用來說是 overkill
綜合我自己與周遭朋友的經驗(三年來的印象,四位朋友),現在規模比較大的公司幾乎都轉向容器化了。
設置得好有可能比 VPS Instance 更省錢。容器化在維運上也有很大的優勢,像是環境一致性、部署可重複性、資源利用率、水平擴展能力。
接下來的選擇是 ECS 或 EKS。
EKS 的門檻比你想的高
EKS 是 AWS 上的 managed Kubernetes。
聽起來很美好,但 Kubernetes 本身的複雜度就已經很高:Pod、Deployment、Service、Ingress、ConfigMap、Secret、Namespace、Helm chart、還有各種 CRD。EKS 只是幫你管了 control plane,剩下的你還是得自己來。
如果團隊有專職的 SRE 而且已經在用 Kubernetes,EKS 是合理的選擇。但對大部分 3-10 人的團隊來說,這是殺雞用牛刀。ECS 相對簡單得多,而且比起 EC2 有幾個優勢。
不想管機器,用 Fargate
ECS 搭配 Fargate,你只要定義 container 要跑什麼 image、需要多少 CPU 和記憶體,AWS 幫你處理底層的機器。上面講的那些 OS patch、disk 監控、SSH key 管理全部不用煩。你付的是 container 實際跑的時間,不是一台閒置的機器。
內建 Blue/Green 部署
Blue/Green 部署的概念很直覺:你現在跑在 production 上的版本是 Blue,新版本部署上去之後跑在另一組 container 上叫Green。兩組同時存在,但流量還是導向 Blue。你可以透過 test listener(例如開一個 8080 port)先驗證 Green 沒問題,確認之後再把流量切過去。
跟傳統的 rolling update 比,Blue/Green 最大的好處是:
- 切換前可以先測試:新版本已經跑在 production 環境裡,用一樣的資料庫、一樣的環境變數,你可以透過 test listener 確認功能正常,而不是部署完才發現爆了
- 流量切換可以漸進:CodeDeploy 支援 Canary 跟 Linear 策略。例如先切 10% 的流量觀察 5 分鐘,沒問題再全切,不是一次 all-in
- 出事一鍵 rollback:Green 爆了?CodeDeploy console 按一個按鈕就切回 Blue。不用
git revert、不用重跑 pipeline,也不用手動改 task definition
對比一下 EC2 上的 rollback:SSH 進去、手動切回舊版本、重啟 process、祈禱。Blue/Green 在這方面的體驗好太多了。
藍綠部署的核心精神可以參考這篇文章,這是優秀的前同事 Henry 分享他在 Hahow 如何實踐藍綠部署,儘管時隔 9 年,概念一樣可以套用。
Docker image 管理跟 ECR 整合
ECS 跟 ECR 是原生整合的,Task Definition 裡直接指定 ECR 的 image URI,IAM 權限設好就能拉。
重要的原則:不要用 latest tag。每次 build 都用 commit hash 當 tag,這樣你永遠知道 production 上跑的是哪個版本的 code,出事的時候才有辦法追溯。ECR 支援設定 tag immutability,從機制上防止有人覆蓋既有的 tag。
ECR 可以設定 Lifecyle Policy,可按照一定的規則保留 Docker images,將沒有用到或過期的自動刪除。
以安全性的角度為前提,開發環境與正式環境應該需要完全隔離。在 AWS 會以不同的 Account 來做區分,因此 ECR 也應該按照環境區分。
但是這樣一來等於同一份 Docker image 分別放到不同的 ECR,這是為了安全性考量而做的權衡。我自己更喜歡的是全部共用同一個 ECR 來源。沒有正確答案,而是根據團隊當前的目標而定。
Auto Scaling
ECS Service 原生支援 auto scaling,可以根據 CPU 使用率、記憶體、或 ALB 的 request count 自動調整 task 數量,也能自行設定,例如根據 SQS 的 queue 數量動態調整。
設定不複雜,定義一個 target tracking policy 就好。
EC2 也有 Auto Scaling Group,但你還得維護 AMI、launch template、確保新起來的 instance 跟現有的一致。ECS + Fargate 的 scaling 就是多跑幾個 container,乾淨俐落。
要注意的是 Blue/Green 部署期間 auto scaling 會暫停,可以透過在 pipeline 前後用 script 控制 scaling policy 來處理。
Logging 跟監控
ECS 原生支援把 container 的 stdout/stderr 送到 CloudWatch Logs,Task Definition 裡設定 awslogs log driver 就好。不用在 EC2 上裝 CloudWatch Agent、設定 log group、處理 log rotation——container 砍掉重啟,log 還是在 CloudWatch 上。
搭配 CloudWatch Container Insights,你可以直接看到每個 service、每個 task 的 CPU / 記憶體 / 網路使用量,不用自己裝 node exporter 或設定 Prometheus。
安全性考量
ECS + Fargate 在安全性上有一個常被忽略的優勢:沒有 SSH。
聽起來像是限制,但其實是好事。沒有 SSH 表示沒有人可以「暫時」登進去改東西、沒有人可以偷偷裝軟體、沒有人可以忘記登出。所有的變更都必須透過 Task Definition 跟 CI/CD pipeline 進行,天生就是 immutable infrastructure。
如果你需要 debug,可以用 ECS Exec(基於 SSM Session Manager)進入 container,而且每次 session 都有稽核紀錄。
小結
ECS 不是什麼讓人興奮的技術選擇,但它夠用、夠穩、學習曲線合理。在 AWS 的世界裡,無聊的選擇通常是最好的選擇(比較燒錢的選擇)。
從部署開始設計流程
如果你問我在 AWS 上建容器服務最重要的一件事是什麼,我會說:先搞定部署。先想清楚 code 怎麼從 git push 到跑在 production 上。
ECS 的部署,指的是將最新版本的 code 打包成 Docker Image 後,更新 Task Definition,並觸發 Deployment 的過程。
測試環境與正式環境的部署流程,應該依照目標分開考慮:
- 測試環境優先追求彈性,盡可能減少 code 合併之後到部署環境的時間與複雜性
- Staging 環境:盡可能保持與正式環境一樣的流程,確保任何錯誤都能在上線前被發現
- 正式環境:與測試環境嚴格分離,避免開發者直接接觸到正式環境
部署是整個開發生命週期裡影響最久的環節。網路設計做完基本上不太會動,監控可以後面慢慢補,但部署是你每天、每個 PR 都會碰到的事情。部署體驗差,整個團隊的開發效率都會被拖下去。
在 AWS 的世界裡,部署通常會走 CodePipeline + CodeBuild + CodeDeploy 這條路。如果擔心 vendor lock-in,也可以針對部分流程做調整。
像是用 GitHub Actions,AWS 也有提供官方的 Actions 可以串。但不管走哪條路,你都需要處理:
- Build image 並推到 ECR
- 更新 Task Definition 的 image URI
- 更新 ECS Service 觸發部署
- 等待 service 穩定
如果是 production 環境,你可能還需要 Blue/Green 部署(透過 CodeDeploy)、承認機制、跨帳號的 ECR image 同步。每多一層,就多一個出錯的機會。
越早導入 IaC 越好
這些 AWS 資源如果純手動在 Console 上點,你會活在恐懼之中。
哪天有人手滑改了 Security Group 的 inbound rule,或是 IAM Policy 被人「暫時」改了之後忘記改回來,你可能要花一整天才找到問題。更不用說當你要建第二個環境(staging)的時候,你會發現根本記不得 production 是怎麼設定的。
用 Terraform 管理 AWS 資源應該是第一天就做的事,不是「之後有空再來整理」。
這也是在追求部署的方便性與 IaC 時需要權衡的事。Terraform 的中心思想是透過聲明式的宣告讓 Infra 達到預期的狀態。然而在實際運用當中,ECS 的部署通常只會涉及:
- 宣告新的 task definition,套用本次要部署的 Docker image tag
- 更新 Service 並觸發部署
如果每次都要透過 Terraform 來部署稍嫌麻煩,因為基礎建設通常不會有變動。這邊提供我目前常做的設定。
Terraform 的 lifecycle block 可以讓你忽略 Task Definition 的 image URI 變更,讓 Terraform 管基礎設施、CI/CD pipeline 管應用程式部署,兩者不會互相打架。這是我覺得在 ECS 部署裡最實用的一個技巧:
resource "aws_ecs_service" "app" {
# ...
lifecycle {
ignore_changes = [task_definition]
}
}
沒有 IaC,你的基礎設施就是一個黑盒子,只有當初設定的那個人知道裡面長什麼樣子,而那個人通常已經離職了。
部署的複雜度如何吃掉你的錢
很多團隊覺得「部署慢一點沒關係」、「多幾個步驟而已」,但這些看似微小的摩擦力,累積起來的成本可以量化。
- 部署慢
- 每次部署需要的步驟很多
- 人工操作增加錯誤機率
- 開發者心智負擔增加
- 不敢做大幅更動
- 埋下更大的錯誤
把這些隱藏成本量化之後:
Time to Production
假設一個 5 人的團隊,每位工程師的年薪成本(含福利、設備)約 200 萬台幣,換算下來每小時成本大約 1,000 元。
| 指標 | 簡單部署 | 複雜部署 |
|---|---|---|
| 單次部署時間 | 10 分鐘 | 45 分鐘 |
| 每週部署次數 | 15 次 | 5 次 |
| 部署失敗率 | 3% | 15% |
| 失敗後修復時間 | 15 分鐘 | 2 小時 |
| 每週花在部署的總時間 | ~3 小時 | ~8 小時 |
光是部署本身,複雜部署每週就比簡單部署多花 5 小時。以 5 人團隊來算,每週浪費約 25 小時,一年下來就是 1,300 小時,換算成本約 130 萬台幣。
Bug 的隱藏成本
部署困難帶來的另一個隱藏成本是 bug 率上升。假設每次變更失敗需要額外 8 小時的調查與修復:
| 指標 | 高頻部署 | 低頻部署 |
|---|---|---|
| 每月部署次數 | 60 次 | 20 次 |
| 變更失敗率 | 5% | 30% |
| 每月失敗次數 | 3 次 | 6 次 |
| 每次修復成本(人時) | 8 小時 | 16 小時(問題通常更複雜) |
| 每月修復總成本 | 24 小時 | 96 小時 |
低頻部署每月多花 72 小時在修 bug 上,一年下來是 864 小時,約 86 萬台幣。
這還沒算上:
- 機會成本:工程師花在 debug 的時間,本來可以拿來開發新功能
- 使用者流失:bug 上 production 後影響使用者體驗,MAU 下降的損失很難量化但絕對存在
- 心理成本:每次部署都像拆炸彈的團隊,士氣不可能好
負的循環
老闆對技術不熟,覺得每次開發團隊都在出包。
當團隊要求改善部署流程時,短期內很難對產品有直接影響,因此傾向延後。部署流程沒有改善,成本直接由現場的開發者承擔,信任持續下降,老闆也不再相信開發團隊的提案。
這個循環的成本比想像中高得多:
- 延遲改善的複利成本:前面算出複雜部署每年隱藏成本約 225 萬。改善被延後一年,這筆錢就直接蒸發;延後兩年就是 450 萬
- 人員流動成本:士氣低落最直接的後果是離職。工程師的替換成本大約是年薪的 50%-150%(招募、面試、onboarding、產能爬坡期),以年薪成本 200 萬計算,每人替換成本約 100-300 萬。5 人團隊若每年因為部署體驗差而多走 1 人,額外成本就是 100-300 萬/年
- 信任赤字的決策成本:老闆不信任技術團隊,否決技術提案,技術債持續累積。假設每季有 1 個改善提案被擋,每個能省 50 萬/年,一年 4 個提案被擋就是少省下 ~200 萬
| 項目 | 年成本 |
|---|---|
| 部署隱藏成本持續發生 | 225 萬 |
| 額外人員流動 | 100-300 萬 |
| 技術改善延遲的機會成本 | ~200 萬 |
| 合計 | ~525-725 萬/年 |
這大概等於 2-3 位工程師的年薪,而且會隨時間惡化——越晚改善,累積的成本越高,能留住的人越少,惡性循環越難打破。
總計
保守估計,一個 5 人團隊因為複雜的部署流程,每年的隱藏成本大約在 200-250 萬台幣。這還不包含 NAT Gateway 每月默默燒掉的幾千塊、閒置資源的費用、以及你為了搞懂 AWS 帳單花的時間。
差不多等於一位 junior 工程師的年薪了。
部署流程值得在第一天就投資。越早把這件事做對,每一天、每一次 PR 省下的成本都在複利累積。
總結
如果你的團隊已經綁在 AWS 上,ECS 是我目前最推薦的容器服務選項。比 EC2 省心,比 EKS 務實,搭配 Fargate 之後維運負擔大幅降低。
但 ECS 本身只是拼圖的一塊。實際跑起來,你還得處理 VPC 的網路規劃、ALB 的流量分配、IAM 的權限控制、ECR 的 image 管理、CloudWatch 的監控設定。
這些元件彼此牽連,任何一個設定錯誤都可能讓你 debug 半天。選了 ECS 只是起點,把周邊的基礎設施一起規劃好才算完成。
最近剛好在數個專案當中都有類似需求,這篇文章算是統整了自己的想法,如果這篇文章收到讀者的回覆,我會介紹實務上如何設計 ECS 部署流程與架構。就算沒有人回覆我也會寫啦😂
或者,你可能不需要 AWS?