引言:打破手動部署的迷思
「為什麼我的 CI 已經產出
prod-0.54,卻還得手動去跑kubectl apply -f deployment.yaml?那不是多此一舉嗎?」
如果你也曾陷入這樣的疑問,本文將從根本理清 CI/CD 與 Kubernetes 之間的分工,並學會如何「一鍵從程式碼到雲端服務」完全自動化。
CI/CD vs. Kubernetes:各司其職的完美搭檔
在軟體開發的世界裡,GitLab CI/CD 和 Kubernetes 常常被搭在一起討論,卻扮演著截然不同的角色。
CI/CD 的職責:生產線
flowchart LR
A[原始碼] --> B[Build Image]
B --> C[Tag Version]
C --> D[Push to Registry]
D --> E[Docker Image Ready]
GitLab CI/CD 的工作內容:
- 建置(Build):將程式碼打包成 Docker 映像
- 標記(Tag):為映像貼上版本號標籤(例如
0.54、v1.0.0) - 推送(Push):把 Docker 映像推到映像庫(AWS ECR、Docker Hub)
Kubernetes 的職責:配送中心
Kubernetes 的工作內容:
- 部署(Deploy):在叢集裡建立 Pod 並執行容器
- 監控(Monitor):監控運行狀況,Pod 死掉自動重啟
- 更新(Update):滾動更新(Rolling Update)時保證服務不中斷
- 維運(Operate):調整副本數量、健康檢查、網路規則
分工比喻
如果把軟體交付比喻成流水線:
| 角色 | 比喻 | 職責 |
|---|---|---|
| CI/CD | 工廠組裝工人 | 把原料(程式碼)生產成成品(Docker 映像),打上編號(Tag) |
| Kubernetes | 物流配送中心 | 拿到成品後送到倉庫(叢集),確保正確分配、穩定運行 |
⚠️ 關鍵問題: 若只把「生產出映像」交給 CI/CD,卻沒有「派送到叢集裡面運行」的步驟,流程就會中斷——就好比你生產一箱箱可口可樂,卻一直放在廠區裡沒人去配送到超商。
完整自動化流程架構
讓我們先看看完整的自動化部署流程:
sequenceDiagram
participant Dev as 開發者
participant Git as GitLab
participant CI as CI Pipeline
participant ECR as AWS ECR
participant K8s as Kubernetes
Dev->>Git: Push 程式碼
Git->>CI: 觸發 Pipeline
Note over CI: Build Stage
CI->>CI: docker build
CI->>CI: 標記 commit SHA
Note over CI: Push Stage
CI->>ECR: docker push (with tag)
CI->>ECR: docker push (latest)
Note over CI: Deploy Stage
CI->>K8s: kubectl set image
K8s->>ECR: Pull new image
K8s->>K8s: Rolling Update
K8s-->>CI: Rollout 完成
CI-->>Dev: ✅ 部署成功
流程說明:
- 開發者 Push 程式碼到 GitLab
- CI Pipeline 自動觸發,執行 Build → Push → Deploy
- Kubernetes 從 ECR 拉取新映像,執行滾動更新
- 完成部署,服務零中斷升級
第一部分:GitLab CI Pipeline 設定
完整 .gitlab-ci.yml 範例
# .gitlab-ci.yml
stages:
- build
- push
- deploy
variables:
AWS_ACCOUNT_ID: 781267011388
AWS_REGION: ap-east-1
IMAGE_NAME: company-web
# ============================================
# Stage 1: Build Docker Image
# ============================================
build:
stage: build
image: docker:latest
services:
- docker:dind
script:
# 建置 Docker 映像,使用 commit SHA 作為 tag
- docker build -t $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$IMAGE_NAME:$CI_COMMIT_SHORT_SHA .
# 將 commit SHA 寫入檔案,供後續 stage 使用
- echo $CI_COMMIT_SHORT_SHA > image_tag.txt
artifacts:
paths:
- image_tag.txt
expire_in: 1 hour
# ============================================
# Stage 2: Push to AWS ECR
# ============================================
push:
stage: push
image: docker:latest
services:
- docker:dind
before_script:
# 安裝 AWS CLI(若映像中沒有)
- apk add --no-cache aws-cli
script:
# 1. 登入 AWS ECR
- aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com
# 2. 讀取 build stage 產生的 tag
- IMAGE_TAG=$(cat image_tag.txt)
# 3. 推送帶有 commit SHA 的映像
- docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$IMAGE_NAME:$IMAGE_TAG
# 4. 同時標記為 latest(可選)
- docker tag $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$IMAGE_NAME:$IMAGE_TAG $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$IMAGE_NAME:latest
- docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$IMAGE_NAME:latest
only:
- main
- master
# ============================================
# Stage 3: Deploy to Kubernetes
# ============================================
deploy:
stage: deploy
image: bitnami/kubectl:latest
before_script:
# 設定 kubeconfig(使用 GitLab CI/CD Variables)
- mkdir -p ~/.kube
- echo "$KUBE_CONFIG" > ~/.kube/config
script:
# 1. 讀取映像 tag
- IMAGE_TAG=$(cat image_tag.txt)
# 2. 更新 Deployment 的映像版本
- kubectl set image deployment/company-web company-web-prod=$AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$IMAGE_NAME:$IMAGE_TAG -n prod
# 3. 等待 Rollout 完成
- kubectl rollout status deployment/company-web -n prod --timeout=5m
# 4. 驗證部署結果
- kubectl get pods -n prod -l app=company-web
only:
- main
- master
Pipeline 各階段詳解
階段流程圖:
flowchart TD
Start[Git Push] --> Build[Build Stage]
Build --> |產生 image_tag.txt| Push[Push Stage]
Push --> |映像推送成功| Deploy[Deploy Stage]
Deploy --> Check{Rollout 成功?}
Check -->|是| Success[✅ 部署完成]
Check -->|否| Rollback[❌ 需要回滾]
1. Build 階段
- 使用
docker:dind(Docker in Docker)服務 - 以 commit SHA(例如
6651c41e)作為映像標籤 - 將 tag 寫入
image_tag.txt作為 artifact,供後續階段使用
2. Push 階段
- 登入 AWS ECR(使用 IAM credentials)
- 推送兩個版本:
company-web:6651c41e(精確版本)company-web:latest(測試環境使用)
3. Deploy 階段
- 使用
kubectl set image更新 Deployment - 等待 Rolling Update 完成(最多 5 分鐘)
- 驗證新 Pod 是否正常運行
第二部分:Kubernetes Deployment 設定
完整 Deployment YAML
# company_web_prod_deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: company-web
namespace: prod
labels:
app: company-web
env: prod
spec:
# 副本數量
replicas: 3
# 滾動更新策略
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # 最多可以多出 1 個 Pod
maxUnavailable: 0 # 更新時至少保持所有 Pod 可用
# Pod 選擇器
selector:
matchLabels:
app: company-web
env: prod
# Pod 模板
template:
metadata:
labels:
app: company-web
env: prod
spec:
containers:
- name: company-web-prod
image: 781267011388.dkr.ecr.ap-east-1.amazonaws.com/company-web:0.54
# 容器端口
ports:
- containerPort: 80
protocol: TCP
# 環境變數
env:
- name: NODE_ENV
value: "production"
- name: PORT
value: "80"
# 資源限制
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
# 存活探針(Liveness Probe)
livenessProbe:
httpGet:
path: /_health
port: 80
scheme: HTTP
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
# 就緒探針(Readiness Probe)
readinessProbe:
httpGet:
path: /_health
port: 80
scheme: HTTP
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
# ImagePullPolicy
imagePullPolicy: IfNotPresent
Deployment 關鍵設定說明
1. 副本數與高可用性
replicas: 3
- 確保至少有 3 個 Pod 同時運行
- 若某個 Pod 失敗,K8s 自動重建
- 擴展到 10 個?只需改成
replicas: 10並 apply
2. 滾動更新策略
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # 最多可以多出 1 個 Pod
maxUnavailable: 0 # 更新時至少保持所有 Pod 可用
滾動更新流程:
flowchart LR
A[3 個舊 Pod 運行中] --> B[建立 1 個新 Pod]
B --> C{新 Pod Ready?}
C -->|是| D[刪除 1 個舊 Pod]
D --> E[再建立 1 個新 Pod]
E --> F{全部更新完成?}
F -->|否| C
F -->|是| G[✅ 3 個新 Pod 運行中]
關鍵優勢:
- ✅ 零停機時間:至少 3 個 Pod 隨時可用
- ✅ 逐步驗證:每個新 Pod 通過 Health Check 才繼續
- ✅ 快速回滾:若新版本失敗,可立即 rollback
3. Label 與 Selector
selector:
matchLabels:
app: company-web
env: prod
template:
metadata:
labels:
app: company-web
env: prod
Label 的用途:
- 識別管理:Deployment 透過 label 識別哪些 Pod 屬於它
- Service 路由:Service 透過 label selector 決定流量要送到哪些 Pod
- 查詢過濾:
kubectl get pods -l app=company-web
4. Health Check 機制
flowchart TD
Start[Pod 啟動] --> Init[初始化 30 秒]
Init --> Liveness{Liveness Check}
Liveness -->|成功| Readiness{Readiness Check}
Liveness -->|失敗 3 次| Kill[Kill Pod & 重啟]
Readiness -->|成功| Ready[加入 Service]
Readiness -->|失敗| NotReady[從 Service 移除]
Ready --> Liveness
NotReady --> Liveness
Liveness Probe(存活探針)
- 目的:檢查容器是否「活著」
- 失敗後果:連續 3 次失敗 → K8s 自動 kill 並重建 Pod
- 使用場景:防止應用程式死鎖或進入不可恢復狀態
Readiness Probe(就緒探針)
- 目的:檢查容器是否「準備好接流量」
- 失敗後果:從 Service Endpoints 移除,不再接收流量
- 使用場景:應用啟動慢,需要初始化資料庫連線等
實際範例:
假設你的應用需要 20 秒初始化:
readinessProbe:
httpGet:
path: /_health
port: 80
initialDelaySeconds: 10 # 啟動後等 10 秒再檢查
periodSeconds: 5 # 每 5 秒檢查一次
failureThreshold: 3 # 連續失敗 3 次才算失敗
5. 資源管理
resources:
requests: # 最低保證資源
memory: "256Mi"
cpu: "250m"
limits: # 最大可用資源
memory: "512Mi"
cpu: "500m"
資源設定最佳實踐:
| 場景 | CPU Request | CPU Limit | Memory Request | Memory Limit |
|---|---|---|---|---|
| 小型服務 | 100m | 200m | 128Mi | 256Mi |
| 中型服務 | 250m | 500m | 256Mi | 512Mi |
| 大型服務 | 500m | 1000m | 512Mi | 1Gi |
⚠️ 注意: 若 Pod 超過 Memory Limit,會被 OOMKilled(Out of Memory Killed)
第三部分:完整自動化部署流程
端到端流程
flowchart TD
A[開發者 Commit] --> B[Push to main]
B --> C[GitLab CI 觸發]
C --> D[Build Stage]
D --> E[產生 image_tag.txt]
E --> F[Push Stage]
F --> G[登入 ECR]
G --> H[Push image:6651c41e]
H --> I[Push image:latest]
I --> J[Deploy Stage]
J --> K[kubectl set image]
K --> L[K8s Rolling Update]
L --> M{Health Check}
M -->|通過| N[新 Pod Ready]
M -->|失敗| O[回滾舊版本]
N --> P[刪除舊 Pod]
P --> Q[✅ 部署完成]
O --> R[❌ 部署失敗]
實際操作步驟
步驟 1:Commit & Merge
# 開發者本地修改程式碼
git add .
git commit -m "feat: 新增使用者驗證功能"
git push origin main
GitLab CI 自動觸發,開始 Build 階段。
步驟 2:CI Build 階段
# GitLab Runner 執行
docker build -t 781267011388.dkr.ecr.ap-east-1.amazonaws.com/company-web:6651c41e .
# 產生 artifact
echo 6651c41e > image_tag.txt
輸出結果:
Step 1/8 : FROM node:18-alpine
Step 2/8 : WORKDIR /app
Step 3/8 : COPY package*.json ./
Step 4/8 : RUN npm ci --only=production
Step 5/8 : COPY . .
Step 6/8 : EXPOSE 80
Step 7/8 : CMD ["node", "server.js"]
Step 8/8 : Successfully built abc123def456
Successfully tagged 781267011388.dkr.ecr.ap-east-1.amazonaws.com/company-web:6651c41e
步驟 3:CI Push 階段
# 登入 ECR
aws ecr get-login-password --region ap-east-1 | \
docker login --username AWS --password-stdin \
781267011388.dkr.ecr.ap-east-1.amazonaws.com
# 推送映像
docker push 781267011388.dkr.ecr.ap-east-1.amazonaws.com/company-web:6651c41e
docker push 781267011388.dkr.ecr.ap-east-1.amazonaws.com/company-web:latest
輸出結果:
The push refers to repository [781267011388.dkr.ecr.ap-east-1.amazonaws.com/company-web]
6651c41e: Pushed
latest: Pushed
步驟 4:CI Deploy 階段
# 更新 Deployment 映像
kubectl set image deployment/company-web \
company-web-prod=781267011388.dkr.ecr.ap-east-1.amazonaws.com/company-web:6651c41e \
-n prod
# 等待 Rollout 完成
kubectl rollout status deployment/company-web -n prod
輸出結果:
deployment.apps/company-web image updated
Waiting for deployment "company-web" rollout to finish: 1 out of 3 new replicas have been updated...
Waiting for deployment "company-web" rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for deployment "company-web" rollout to finish: 1 old replicas are pending termination...
deployment "company-web" successfully rolled out
步驟 5:驗證部署
# 查看 Pod 狀態
kubectl get pods -n prod -l app=company-web
輸出結果:
NAME READY STATUS RESTARTS AGE
company-web-7d4b8f9c5d-abc12 1/1 Running 0 2m
company-web-7d4b8f9c5d-def34 1/1 Running 0 2m
company-web-7d4b8f9c5d-ghi56 1/1 Running 0 1m
第四部分:常見問題診斷與解決
問題診斷流程圖
flowchart TD
Start[部署失敗] --> Check1{Pod 狀態?}
Check1 -->|ImagePullBackOff| Q1[映像拉取失敗]
Check1 -->|CrashLoopBackOff| Q2[容器啟動失敗]
Check1 -->|Pending| Q3[資源不足]
Q1 --> S1[檢查 ECR 映像是否存在]
Q1 --> S2[檢查 IAM 權限]
Q1 --> S3[檢查 imagePullPolicy]
Q2 --> S4[查看容器日誌]
Q2 --> S5[檢查 Health Check]
Q2 --> S6[檢查環境變數]
Q3 --> S7[檢查 Node 資源]
Q3 --> S8[檢查資源 requests/limits]
問題 1:ImagePullBackOff
症狀:
$ kubectl get pods -n prod
NAME READY STATUS RESTARTS AGE
company-web-7d4b8f9c5d-abc12 0/1 ImagePullBackOff 0 5m
可能原因:
- ECR 裡沒有該 tag 的映像
- Kubernetes Node 沒有 ECR 拉取權限
imagePullPolicy設定問題
診斷步驟:
# 1. 查看 Pod 詳細資訊
kubectl describe pod company-web-7d4b8f9c5d-abc12 -n prod
輸出範例:
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Pulling 3m (x4 over 5m) kubelet Pulling image "781267011388.dkr.ecr.ap-east-1.amazonaws.com/company-web:0.54"
Warning Failed 3m (x4 over 5m) kubelet Failed to pull image: rpc error: code = Unknown desc = Error response from daemon: manifest for 781267011388.dkr.ecr.ap-east-1.amazonaws.com/company-web:0.54 not found: manifest unknown
解決方案:
# 檢查 ECR 是否有該映像
aws ecr describe-images \
--repository-name company-web \
--image-ids imageTag=0.54 \
--region ap-east-1
# 若沒有,檢查 CI Pipeline 是否成功
# 若有,檢查 Node IAM Role 是否有 ECR 拉取權限
問題 2:CrashLoopBackOff
症狀:
$ kubectl get pods -n prod
NAME READY STATUS RESTARTS AGE
company-web-7d4b8f9c5d-abc12 0/1 CrashLoopBackOff 5 3m
可能原因:
- 應用程式啟動失敗(例如環境變數錯誤)
- Health Check 路徑錯誤
- 端口綁定失敗
診斷步驟:
# 查看容器日誌
kubectl logs company-web-7d4b8f9c5d-abc12 -n prod
# 查看前一次崩潰的日誌
kubectl logs company-web-7d4b8f9c5d-abc12 -n prod --previous
輸出範例:
Error: Cannot find module '/app/server.js'
at Function.Module._resolveFilename (internal/modules/cjs/loader.js:636:15)
at Function.Module._load (internal/modules/cjs/loader.js:562:25)
解決方案:
# 檢查 Dockerfile 是否正確複製檔案
# 檢查 CMD 指令是否正確
# 檢查環境變數是否完整
問題 3:ProgressDeadlineExceeded
症狀:
$ kubectl rollout status deployment/company-web -n prod
error: deployment "company-web" exceeded its progress deadline
可能原因:
- 新 Pod 一直無法通過 Readiness Probe
- 映像拉取時間過長
- 資源不足導致 Pod 無法調度
診斷步驟:
# 查看 Deployment 事件
kubectl describe deployment company-web -n prod
# 查看 ReplicaSet 狀態
kubectl get rs -n prod
解決方案:
# 回滾到前一個版本
kubectl rollout undo deployment/company-web -n prod
# 檢查並修復問題後,重新部署
kubectl rollout restart deployment/company-web -n prod
問題診斷 Checklist
| 檢查項目 | 指令 | 預期結果 |
|---|---|---|
| Pod 狀態 | kubectl get pods -n prod | Running 且 READY 1/1 |
| Deployment 狀態 | kubectl get deployment -n prod | READY 3/3 |
| ReplicaSet 狀態 | kubectl get rs -n prod | 新的 RS 有 3 個 Pod |
| Events | kubectl get events -n prod --sort-by='.lastTimestamp' | 無錯誤事件 |
| 容器日誌 | kubectl logs <pod-name> -n prod | 無錯誤訊息 |
| ECR 映像 | aws ecr describe-images --repository-name company-web | Tag 存在 |
第五部分:進階優化與最佳實踐
1. 自動回滾機制
在 .gitlab-ci.yml 中加入回滾邏輯:
deploy:
stage: deploy
image: bitnami/kubectl:latest
script:
- IMAGE_TAG=$(cat image_tag.txt)
- kubectl set image deployment/company-web company-web-prod=$AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$IMAGE_NAME:$IMAGE_TAG -n prod
# 等待 Rollout,若失敗則自動回滾
- |
if ! kubectl rollout status deployment/company-web -n prod --timeout=5m; then
echo "❌ Rollout 失敗,開始自動回滾"
kubectl rollout undo deployment/company-web -n prod
kubectl rollout status deployment/company-web -n prod --timeout=3m
exit 1
fi
- echo "✅ 部署成功"
2. Smoke Test(冒煙測試)
部署完成後自動驗證服務可用性:
stages:
- build
- push
- deploy
- verify
verify:
stage: verify
image: curlimages/curl:latest
script:
# 等待服務完全啟動
- sleep 10
# 檢查健康檢查端點
- |
for i in {1..5}; do
if curl -f http://company-web.prod.svc.cluster.local/_health; then
echo "✅ 健康檢查通過"
exit 0
fi
echo "⏳ 等待服務啟動... ($i/5)"
sleep 5
done
- echo "❌ 健康檢查失敗"
- exit 1
3. 多環境部署策略
# 使用 GitLab Environment 管理不同環境
deploy_staging:
stage: deploy
script:
- kubectl set image deployment/company-web company-web=$IMAGE:$TAG -n staging
environment:
name: staging
url: https://staging.company.com
only:
- develop
deploy_production:
stage: deploy
script:
- kubectl set image deployment/company-web company-web=$IMAGE:$TAG -n prod
environment:
name: production
url: https://company.com
only:
- main
when: manual # 需要手動觸發
4. 監控與通知
整合 Slack 通知:
notify_success:
stage: .post
image: curlimages/curl:latest
script:
- |
curl -X POST https://hooks.slack.com/services/YOUR/WEBHOOK/URL \
-H 'Content-Type: application/json' \
-d '{
"text": "✅ 部署成功",
"attachments": [{
"color": "good",
"fields": [
{"title": "環境", "value": "Production", "short": true},
{"title": "版本", "value": "'$CI_COMMIT_SHORT_SHA'", "short": true},
{"title": "提交者", "value": "'$GITLAB_USER_NAME'", "short": true}
]
}]
}'
when: on_success
notify_failure:
stage: .post
image: curlimages/curl:latest
script:
- |
curl -X POST https://hooks.slack.com/services/YOUR/WEBHOOK/URL \
-H 'Content-Type: application/json' \
-d '{
"text": "❌ 部署失敗",
"attachments": [{
"color": "danger",
"fields": [
{"title": "環境", "value": "Production", "short": true},
{"title": "Pipeline", "value": "'$CI_PIPELINE_URL'", "short": true}
]
}]
}'
when: on_failure
結論:完整自動化的最後一哩路
關鍵要點回顧
✅ CI/CD + Kubernetes 分工明確
- CI 負責「建置與上傳映像」
- Kubernetes 負責「拉映像並執行容器」
✅ 完整 Pipeline 四階段
- Build:
docker build+ 標記 commit SHA - Push:推送到 ECR(精確版本 + latest)
- Deploy:
kubectl set image更新 Deployment - Verify:健康檢查 + Smoke Test
✅ 零停機部署
- Rolling Update 策略
- Health Check 機制
- 自動回滾保護
✅ 問題診斷能力
- ImagePullBackOff → 檢查 ECR + IAM
- CrashLoopBackOff → 查看日誌 + Health Check
- ProgressDeadlineExceeded → 回滾並修復
完整流程檢查清單
部署成功的 Pipeline 應該看到:
✓ lint/test # 程式碼檢查與測試
✓ build # Docker 映像建置
✓ push # 推送到 ECR
✓ deploy # Kubernetes 更新
✓ verify # 服務驗證
✅ Deployment "company-web" successfully rolled out
下一步建議
- 實作 GitOps:使用 ArgoCD 或 Flux 管理 Kubernetes 設定
- 加強監控:整合 Prometheus + Grafana
- 優化建置:使用 Docker Layer Caching 加速
- 安全掃描:整合 Trivy 或 Snyk 掃描映像漏洞