引言:打破手動部署的迷思

「為什麼我的 CI 已經產出 prod-0.54,卻還得手動去跑 kubectl apply -f deployment.yaml?那不是多此一舉嗎?」

如果你也曾陷入這樣的疑問,本文將從根本理清 CI/CD 與 Kubernetes 之間的分工,並學會如何「一鍵從程式碼到雲端服務」完全自動化。


CI/CD vs. Kubernetes:各司其職的完美搭檔

在軟體開發的世界裡,GitLab CI/CDKubernetes 常常被搭在一起討論,卻扮演著截然不同的角色。

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 的工作內容:

  1. 建置(Build):將程式碼打包成 Docker 映像
  2. 標記(Tag):為映像貼上版本號標籤(例如 0.54v1.0.0
  3. 推送(Push):把 Docker 映像推到映像庫(AWS ECR、Docker Hub)

Kubernetes 的職責:配送中心

Kubernetes 的工作內容:

  1. 部署(Deploy):在叢集裡建立 Pod 並執行容器
  2. 監控(Monitor):監控運行狀況,Pod 死掉自動重啟
  3. 更新(Update):滾動更新(Rolling Update)時保證服務不中斷
  4. 維運(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: ✅ 部署成功

流程說明:

  1. 開發者 Push 程式碼到 GitLab
  2. CI Pipeline 自動觸發,執行 Build → Push → Deploy
  3. Kubernetes 從 ECR 拉取新映像,執行滾動更新
  4. 完成部署,服務零中斷升級

第一部分: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 RequestCPU LimitMemory RequestMemory Limit
小型服務100m200m128Mi256Mi
中型服務250m500m256Mi512Mi
大型服務500m1000m512Mi1Gi

⚠️ 注意: 若 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

可能原因:

  1. ECR 裡沒有該 tag 的映像
  2. Kubernetes Node 沒有 ECR 拉取權限
  3. 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

可能原因:

  1. 應用程式啟動失敗(例如環境變數錯誤)
  2. Health Check 路徑錯誤
  3. 端口綁定失敗

診斷步驟:

# 查看容器日誌
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

可能原因:

  1. 新 Pod 一直無法通過 Readiness Probe
  2. 映像拉取時間過長
  3. 資源不足導致 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 prodRunningREADY 1/1
Deployment 狀態kubectl get deployment -n prodREADY 3/3
ReplicaSet 狀態kubectl get rs -n prod新的 RS 有 3 個 Pod
Eventskubectl get events -n prod --sort-by='.lastTimestamp'無錯誤事件
容器日誌kubectl logs <pod-name> -n prod無錯誤訊息
ECR 映像aws ecr describe-images --repository-name company-webTag 存在

第五部分:進階優化與最佳實踐

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 四階段

  1. Builddocker build + 標記 commit SHA
  2. Push:推送到 ECR(精確版本 + latest)
  3. Deploykubectl set image 更新 Deployment
  4. 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

下一步建議

  1. 實作 GitOps:使用 ArgoCD 或 Flux 管理 Kubernetes 設定
  2. 加強監控:整合 Prometheus + Grafana
  3. 優化建置:使用 Docker Layer Caching 加速
  4. 安全掃描:整合 Trivy 或 Snyk 掃描映像漏洞

參考資源