iT邦幫忙

2026 iThome 鐵人賽

DAY 10
0
Kubernetes

探討k8s部署方式系列 第 10 篇

導入CI/CD讓K8s自動更新[Day10]

  • 分享至 

  • xImage
  •  

一百四十四、CI/CD 怎麼把 Git Push 變成 Kubernetes 自動更新?

前面已經有完整的 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/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

一百四十六、先從 Git Push 開始

假設 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。


一百四十七、第一步:Checkout Source Code

Pipeline 啟動後,第一件事通常是把程式碼抓下來。

例如:

Git Repository
      │
      ▼
CI Runner

Runner 可以理解成:

專門執行 Pipeline 工作的一台機器或執行環境。

它會取得:

main branch

最新程式碼。

流程:

git push
   │
   ▼
CI Runner
   │
   ▼
Checkout Code

一百四十八、第二步:Install Dependencies

因為是 FastAPI 專案,Pipeline 可能先準備 Python:

Python 3.12

然後:

pip install -r requirements.txt

這一步主要是為了讓後續測試可以執行。

例如:

FastAPI
SQLAlchemy
Pydantic
Pytest

都會被安裝。


一百四十九、第三步:Run Test

接下來最重要的一步通常是:

pytest

例如:

100 tests

全部通過:

100 passed

Pipeline 才繼續。

如果:

3 failed

Pipeline 就停止。

也就是:

Git Push
   │
   ▼
Test
   │
   ├── Pass
   │      │
   │      ▼
   │    Build
   │
   └── Fail
          │
          ▼
      Stop Pipeline

這就是 CI 很重要的功能:

不讓明顯有問題的程式直接進入部署流程。


一百五十、第四步:Build Docker Image

測試通過後,就可以建立新的 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

一百五十一、為什麼不建議只用 latest?

如果永遠使用:

backend:latest

會有一個問題:

今天的 latest

跟

昨天的 latest

名字一樣,但內容不同。

出問題時很難追:

Production 現在到底跑哪一版?

所以實務上通常會使用:

Git SHA
Version Tag
Build Number

例如:

backend:1.3.5

backend:a1b2c3d

backend:build-582

這樣才有版本追蹤能力。


一百五十二、第五步:Push 到 Container Registry

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。


一百五十三、第六步:更新 Kubernetes Deployment

前面的 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

一百五十四、Deployment 發現 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

這時部署完成。


一百五十五、完整 Git Push 到 Production 流程

整個流程就可以整理成:

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

一百五十六、CI/CD 不是直接把 Source Code 丟到 Kubernetes

這裡有一個很重要的觀念。

Kubernetes 通常不會:

Git Pull Source Code

然後直接跑。

而是:

Source Code
   │
   ▼
Docker Image
   │
   ▼
Container Registry
   │
   ▼
Kubernetes

所以 Kubernetes 實際部署的是:

Image

不是:

Source Code

這也讓部署版本更加穩定。


一百五十七、CI 與 CD 可以分開

很多團隊不一定:

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 是否真的部署,還可以由人決定。


一百五十八、Continuous Deployment

如果完全自動:

main push
   │
   ▼
Test Pass
   │
   ▼
Build
   │
   ▼
Deploy Production

這比較接近:

Continuous Deployment

也就是:

程式碼只要通過 Pipeline,就自動進 Production。

這兩種方式沒有一定哪一個比較好。

要看:

團隊風險
測試成熟度
產業需求
Production 管理方式

一百五十九、一個簡化的 GitHub Actions 範例

例如:

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

一百六十、但 Registry Login 怎麼辦?

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。


一百六十一、Kubernetes Credentials 也不能寫進 Git

Pipeline 要執行:

kubectl

就必須有權限連 Kubernetes Cluster。

也就是需要:

Kubeconfig
Service Account
Token
Cloud Identity

這些都不應直接放在 Repository。

應該透過:

CI Secret
IAM
Service Account
OIDC

提供。

因此:

Developer

不一定直接擁有 Production Cluster 的完整權限。

反而可能是:

CI/CD Service Account

負責部署。


一百六十二、Pipeline 部署後還要驗證

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 改程式容易很多。


一百六十四、Database Migration 怎麼處理?

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 發展後會遇到的重要議題。


一百六十六、Rolling Update 的 Pipeline

完整部署可以變成:

Git Push
   │
   ▼
Test
   │
   ▼
Build Image
   │
   ▼
Push Registry
   │
   ▼
Database Migration
   │
   ▼
Update Deployment
   │
   ▼
Kubernetes 建立 New Pods
   │
   ▼
Readiness Probe
   │
   ├── Pass
   │      │
   │      ▼
   │   加入 Service
   │
   └── Fail
          │
          ▼
      不接流量

舊 Pod 再逐步被替換。

因此可以做到:

低停機
甚至接近零停機部署

一百六十七、CI/CD 與 HPA 會不會衝突?

不會,因為兩者控制不同事情。

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

再進階一點,還有另一種部署方式叫:

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/CD 與 GitOps 的差別

傳統方式:

CI
 │
 ▼
kubectl
 │
 ▼
Cluster

Pipeline 本身需要操作 Cluster 的權限。

GitOps:

CI
 │
 ▼
Git Repository
 │
 ▼
Argo CD
 │
 ▼
Cluster

CI 不一定需要直接碰 Cluster。

而且 Git 裡可以清楚看到:

Production 目前應該是哪一版

這在大型 Kubernetes 環境非常常見。


一百七十、完整 Production Pipeline

到這裡,一條比較成熟的 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 真正的價值並不是單純「部署比較快」,而是讓每一次部署都按照同一套可重複、可追蹤、可驗證的流程執行。


上一篇
實際部署FastAPI試試看[Day9]
下一篇
上線到prod版本會遇到的問題[Day11]
系列文
探討k8s部署方式 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言