前面已經有完整的 Kubernetes 架構:
Internet
│
▼
Ingress
│
▼
Service
│
▼
Deployment
│
▼
Pods
也知道 Backend 已經被包成 Docker Image:
backend:v1.0
但還有最後一個問題:
每次程式更新,都要人工 Build Image、Push Image,再手動修改 Kubernetes 嗎?
如果每次都要:
git pull
docker build
docker push
kubectl apply
那還是有很多人工操作。
CI/CD 的目的,就是把這些步驟自動化。
CI/CD 通常分成兩個部分。
CI:
Continuous Integration
持續整合。
主要負責:
程式碼檢查
測試
Build
CD 則常指:
Continuous Delivery
或:
Continuous Deployment
主要負責:
把新的 Application
部署到目標環境
所以可以簡化成:
Developer
│
│ git push
▼
Git Repository
│
▼
CI
│
├── Test
├── Build
└── Docker Image
│
▼
Container Registry
│
▼
CD
│
▼
Kubernetes
假設 Developer 修改 FastAPI:
@app.get("/products")
def get_products():
return {"data": []}
修改完成後:
git add .
git commit -m "feat: update product api"
git push origin main
原本如果沒有 CI/CD:
Git Push
│
▼
程式碼存在 Git
就結束了。
但是如果有 Pipeline:
Git Push
│
▼
觸發 CI/CD
例如:
GitHub Actions
GitLab CI
Jenkins
都可以監聽 Git Push。
Pipeline 啟動後,第一件事通常是把程式碼抓下來。
例如:
Git Repository
│
▼
CI Runner
Runner 可以理解成:
專門執行 Pipeline 工作的一台機器或執行環境。
它會取得:
main branch
最新程式碼。
流程:
git push
│
▼
CI Runner
│
▼
Checkout Code
因為是 FastAPI 專案,Pipeline 可能先準備 Python:
Python 3.12
然後:
pip install -r requirements.txt
這一步主要是為了讓後續測試可以執行。
例如:
FastAPI
SQLAlchemy
Pydantic
Pytest
都會被安裝。
接下來最重要的一步通常是:
pytest
例如:
100 tests
全部通過:
100 passed
Pipeline 才繼續。
如果:
3 failed
Pipeline 就停止。
也就是:
Git Push
│
▼
Test
│
├── Pass
│ │
│ ▼
│ Build
│
└── Fail
│
▼
Stop Pipeline
這就是 CI 很重要的功能:
不讓明顯有問題的程式直接進入部署流程。
測試通過後,就可以建立新的 Docker Image。
假設目前 Commit SHA 是:
a1b2c3d
可以 Build:
docker build \
-t registry.example.com/backend:a1b2c3d \
.
這時 Image 不只是:
backend:latest
而是:
backend:a1b2c3d
這非常重要。
因為這樣可以知道:
這個 Image
到底對應哪一次 Git Commit?
所以關係可以變成:
Git Commit
a1b2c3d
│
▼
Docker Image
backend:a1b2c3d
如果永遠使用:
backend:latest
會有一個問題:
今天的 latest
跟
昨天的 latest
名字一樣,但內容不同。
出問題時很難追:
Production 現在到底跑哪一版?
所以實務上通常會使用:
Git SHA
Version Tag
Build Number
例如:
backend:1.3.5
backend:a1b2c3d
backend:build-582
這樣才有版本追蹤能力。
Build 完成後,Image 目前只存在 CI Runner。
所以還需要:
docker push registry.example.com/backend:a1b2c3d
Push 到:
Container Registry
例如:
GitHub Container Registry
Amazon ECR
Google Artifact Registry
Docker Registry
流程:
CI Runner
│
│ Docker Push
▼
Container Registry
│
└── backend:a1b2c3d
這樣 Kubernetes 所在的 Node 才能 Pull。
前面的 Deployment 可能原本是:
containers:
- name: fastapi
image: registry.example.com/backend:v1.0
現在新的 Image 是:
backend:a1b2c3d
所以 CD 階段要把 Deployment 更新成:
image: registry.example.com/backend:a1b2c3d
可以有幾種做法。
最直覺的是:
kubectl set image \
deployment/fastapi-backend \
fastapi=registry.example.com/backend:a1b2c3d
意思就是:
fastapi-backend Deployment
裡面的 fastapi Container
請改用新 Image
原本:
Pod 1 → v1.0
Pod 2 → v1.0
Pod 3 → v1.0
Deployment 發現 Template 改成:
a1b2c3d
就會開始 Rolling Update。
例如:
Pod 1 → a1b2c3d
Pod 2 → v1.0
Pod 3 → v1.0
新的 Pod Readiness 通過後:
Pod 1 → a1b2c3d
Pod 2 → a1b2c3d
Pod 3 → v1.0
最後:
Pod 1 → a1b2c3d
Pod 2 → a1b2c3d
Pod 3 → a1b2c3d
這時部署完成。
整個流程就可以整理成:
Developer
│
│ git push
▼
Git Repository
│
▼
CI/CD Pipeline
│
├── Checkout
│
├── Install Dependencies
│
├── Run Tests
│
├── Docker Build
│
└── Docker Push
│
▼
Container Registry
│
▼
Update Deployment
│
▼
Kubernetes
│
▼
Rolling Update
│
▼
New Pods
這樣 Developer 就不需要:
SSH Production
也不需要:
手動 git pull
更不需要:
手動 restart FastAPI
這裡有一個很重要的觀念。
Kubernetes 通常不會:
Git Pull Source Code
然後直接跑。
而是:
Source Code
│
▼
Docker Image
│
▼
Container Registry
│
▼
Kubernetes
所以 Kubernetes 實際部署的是:
Image
不是:
Source Code
這也讓部署版本更加穩定。
很多團隊不一定:
Git Push
→ 直接 Production
而可能是:
Git Push
│
▼
CI
│
▼
Build Image
│
▼
Staging
測試完成後:
Manual Approval
再:
Production
例如:
main
│
▼
Build backend:a1b2c3d
│
▼
Staging Deploy
│
▼
QA
│
▼
Approval
│
▼
Production Deploy
這就比較接近:
Continuous Delivery
也就是:
系統已經準備好可以部署,但 Production 是否真的部署,還可以由人決定。
如果完全自動:
main push
│
▼
Test Pass
│
▼
Build
│
▼
Deploy Production
這比較接近:
Continuous Deployment
也就是:
程式碼只要通過 Pipeline,就自動進 Production。
這兩種方式沒有一定哪一個比較好。
要看:
團隊風險
測試成熟度
產業需求
Production 管理方式
例如:
name: Deploy FastAPI
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: |
pip install -r requirements.txt
- name: Run tests
run: |
pytest
- name: Build image
run: |
docker build \
-t registry.example.com/backend:${{ github.sha }} .
- name: Push image
run: |
docker push \
registry.example.com/backend:${{ github.sha }}
- name: Deploy
run: |
kubectl set image \
deployment/fastapi-backend \
fastapi=registry.example.com/backend:${{ github.sha }}
這份 Pipeline 概念就是:
Push main
│
▼
Test
│
▼
Build Image
│
▼
Push Registry
│
▼
Update Kubernetes
CI Runner 在:
docker push
之前,需要 Registry 認證。
例如:
REGISTRY_USERNAME
REGISTRY_PASSWORD
通常不會直接寫在 YAML:
password: 123456
而是存進 CI/CD Platform 的 Secret。
例如:
GitHub Secrets
GitLab CI Variables
Jenkins Credentials
Pipeline 再使用:
${REGISTRY_PASSWORD}
這樣敏感資料就不需要進 Git Repository。
Pipeline 要執行:
kubectl
就必須有權限連 Kubernetes Cluster。
也就是需要:
Kubeconfig
Service Account
Token
Cloud Identity
這些都不應直接放在 Repository。
應該透過:
CI Secret
IAM
Service Account
OIDC
提供。
因此:
Developer
不一定直接擁有 Production Cluster 的完整權限。
反而可能是:
CI/CD Service Account
負責部署。
CD 不應該只做:
kubectl set image
然後就假設成功。
還可以:
kubectl rollout status \
deployment/fastapi-backend
讓 Pipeline 等待 Deployment 狀態。
如果:
successfully rolled out
代表 Rolling Update 成功。
如果:
ImagePullBackOff
Readiness Failed
CrashLoopBackOff
Deployment 就可能失敗。
因此比較完整:
Deploy
│
▼
Wait Rollout
│
├── Success
│ │
│ ▼
│ Done
│
└── Failed
│
▼
Alert
假設:
backend:a1b2c3d
部署後有問題。
因為舊 Image:
backend:9x8y7z
還存在 Registry。
可以 Rollback:
kubectl rollout undo \
deployment/fastapi-backend
Kubernetes 就可以回到前一個 Deployment Revision。
所以:
Versioned Docker Image
+
Deployment History
讓 Rollback 比傳統 SSH 改程式容易很多。
FastAPI 專案可能還有:
Alembic
例如新版需要:
alembic upgrade head
這就不能簡單地讓每個 Pod 啟動時都執行。
因為如果 HPA 一次建立:
10 Pods
就可能變成:
Pod 1 → alembic upgrade
Pod 2 → alembic upgrade
Pod 3 → alembic upgrade
...
同時 Migration,容易出問題。
所以更常見的方式是:
CI/CD
│
▼
Migration Job
│
▼
Deployment Update
例如:
Run Alembic once
│
▼
成功
│
▼
Deploy New Backend
也可以在 Kubernetes 裡使用:
Job
專門執行一次性 Migration。
例如:
Database Migration
跟:
New Application
必須考慮相容性。
如果先把 Database Column 刪掉:
DROP COLUMN old_field
但是舊 Pod 還在使用:
old_field
Rolling Update 過程中就會爆掉。
所以 Production Migration 常常要設計成:
向前相容
例如先:
Add New Column
讓:
Old App
New App
都能跑。
等全部換新版後,再下一個 Release 清掉舊欄位。
這也是 CI/CD 往 Production 發展後會遇到的重要議題。
完整部署可以變成:
Git Push
│
▼
Test
│
▼
Build Image
│
▼
Push Registry
│
▼
Database Migration
│
▼
Update Deployment
│
▼
Kubernetes 建立 New Pods
│
▼
Readiness Probe
│
├── Pass
│ │
│ ▼
│ 加入 Service
│
└── Fail
│
▼
不接流量
舊 Pod 再逐步被替換。
因此可以做到:
低停機
甚至接近零停機部署
不會,因為兩者控制不同事情。
CI/CD:
我要把 Application
從 v1.0 更新到 v1.1
HPA:
我現在需要
3 個還是 10 個 Pod?
例如:
HPA
replicas = 7
此時 CI/CD 更新 Image:
v1.0
→
v1.1
Deployment 就會逐步把:
7 個 v1.0 Pod
換成:
7 個 v1.1 Pod
HPA 還是可以繼續根據流量調整。
再進階一點,還有另一種部署方式叫:
GitOps
剛才的方式是:
CI Pipeline
│
▼
直接執行 kubectl
│
▼
Kubernetes
GitOps 則比較像:
CI
│
▼
Build Image
│
▼
更新 Git 裡的 Kubernetes YAML
│
▼
GitOps Controller
│
▼
Kubernetes
例如 Deployment:
image: backend:a1b2c3d
直接提交到 Git。
然後:
Argo CD
Flux
這類工具看到 Git 變化後,再同步到 Kubernetes。
所以:
Git
變成 Deployment 狀態的主要來源。
傳統方式:
CI
│
▼
kubectl
│
▼
Cluster
Pipeline 本身需要操作 Cluster 的權限。
GitOps:
CI
│
▼
Git Repository
│
▼
Argo CD
│
▼
Cluster
CI 不一定需要直接碰 Cluster。
而且 Git 裡可以清楚看到:
Production 目前應該是哪一版
這在大型 Kubernetes 環境非常常見。
到這裡,一條比較成熟的 Pipeline 可以是:
Developer
│
│ git push
▼
Git Repository
│
▼
CI Pipeline
│
├── Lint
├── Unit Test
├── Integration Test
├── Security Scan
├── Docker Build
└── Push Image
│
▼
Container Registry
│
▼
Deployment Stage
│
├── DB Migration
│
▼
Kubernetes
│
▼
Rolling Update
│
▼
Health Check
│
▼
Smoke Test
│
▼
Success
如果失敗:
Alert
Rollback
一開始:
Developer
│
▼
SSH Server
│
▼
git pull
│
▼
restart
慢慢演進成:
Developer
│
│ git push
▼
Git
│
▼
CI
│
▼
Test
│
▼
Docker Build
│
▼
Container Registry
│
▼
CD
│
▼
Kubernetes Deployment
│
▼
Rolling Update
│
▼
Pods
外部流量則:
User
│
▼
Ingress
│
▼
Service
│
▼
Pods
│
├── Redis
│
└── Database
而系統負載增加時:
Metrics
│
▼
HPA
│
▼
More Pods
Node 不夠:
Cluster Autoscaler
│
▼
More Nodes
因此最後整個系統形成:
Developer
│
│ git push
▼
Git
│
▼
CI/CD
│
┌──────┴──────┐
▼ ▼
Testing Docker Build
│
▼
Registry
│
▼
Kubernetes
│
Deployment
│
┌────────────┼────────────┐
▼ ▼ ▼
Pod 1 Pod 2 Pod 3
│ │ │
└────────────┼────────────┘
│
Service
▲
│
Ingress
▲
│
User
這就是從「寫完程式後人工 SSH 部署」,演進成:
程式碼一旦 Push,後續的測試、打包、版本管理、部署、健康檢查與滾動更新,都盡可能交給自動化流程完成。
CI/CD 真正的價值並不是單純「部署比較快」,而是讓每一次部署都按照同一套可重複、可追蹤、可驗證的流程執行。