📌 這是 WeaMind 系列 的第 8 篇。
本系列以真實世界專案為背景,記錄重要技術實作與經驗分享。

〈用 Side Project 學 CI:WeaMind 的 CI 實作策略〉介紹了 WeaMind CI 的 7 個自動化項目,讓每次 push 都能自動檢查程式碼品質與安全性。

那 CI 跑完之後呢?

當時的 WeaMind 還是單機部署,CI 只驗證 image 能否成功建置,不需要實際把它推送到 container registry。

後來,我把 WeaMind 的應用層(LINE Bot)從單機 VM 遷移到 Kubernetes,並新增 weamind-infra repo,管理 Kubernetes 部署等基礎設施設定。

隨著部署方式的改變,CI/CD 流程也需要擴充了。關於目前的基礎設施,可以參考〈WeaMind Infra 介紹:VM、Load Balancer 與雲端架構〉。

本文將從 CI 的延伸講起,補完上一篇沒有涵蓋到的部分,然後進入 CD 的核心概念與實作。如果你沒看過前兩篇也沒關係,但建議先讀一下〈GitHub Actions 入門:自動化 Lint、Format 與 Type Check〉了解 CI 的基礎概念。

系列:GitHub Actions CI/CD 實戰

  1. GitHub Actions 入門:自動化 Lint、Format 與 Type Check
  2. 用 Side Project 學 CI:WeaMind 的 CI 實作策略
  3. WeaMind CD 實戰:從 Docker Image 建置與發布到 Kubernetes 部署

WeaMind CI 擴充:自動 Build & Push Image

上一篇的 CI 已經包含 Docker Build 驗證,用來確認 image 能否成功建置——這就是 CI 的最後一步。

當時 WeaMind 採用單機部署:我會先在遠端主機上透過 Git 同步程式碼,再執行 docker compose up,由 Docker Compose 在主機上建置 image 並啟動服務。因此,部署時使用的是在主機上建置的 image。

遷移到 Kubernetes 後,我把部署設定都放到新的 weamind-infra repo,並改由叢集直接拉取已發布的 image。

因此,我們得擴充 WeaMind 的 CI:當 pull request(PR)合併到 main、CI 通過後,透過 GitHub Actions 的 workflow_run 自動建置 image,並推送到 GitHub Container Registry(GHCR)。

完整的 GitHub Actions 設定可以參考 publish-ghcr.yml。

為什麼部署需要 Immutable Tag?

Build 完 image 之後要推到 registry,接下來的問題是:部署時該用哪個 image tag?

前面提到,CI 通過後會自動建置並推送 image。WeaMind 為這個 image 加上 latest tag,每次推送都會更新它指向的 image。

latest 方便取得最新版,但用於部署有幾個問題:

  • 版本不明確:只看 tag,無法知道它指向哪個版本
  • Rollback 較麻煩:要回到之前的版本,需要先找到當時使用的 image
  • 可能使用快取:環境的 image 拉取設定可能讓部署使用本機已有的 image,而不是重新拉取

因此,部署時需要一個固定指向特定版本的 tag,也就是 Immutable Tag。只要不覆寫同名 tag,就能用它辨認部署的版本,必要時也能指定舊版本去 rollback。

WeaMind 部署使用的是完整的語意化版本號,例如 1.2.3,而不是 latest。這些版本 tag 如何發布,以及如何更新到部署設定,後面會再介紹。

Multi-platform Build:同時支援 x86 與 ARM

ARM 架構的 VM 越來越普及。AWS 有 Graviton,Hetzner 有 CAX 系列,本地開發可能用 Apple Silicon。如果你的 image 只支援 x86,在這些環境跑起來會有問題。

我自己就遇到過——GHCR 自動發布剛上線時,在 Apple Silicon 的 Mac 上 docker pull,直接得到以下錯誤:

no matching manifest for linux/arm64/v8 in the manifest list entries

原因是 GitHub Actions 的 runner 是 x86,預設只會 build linux/amd64 的 image,registry 裡根本沒有 ARM 版本 image 可以拉。

解法是用 Docker Buildx 做 multi-platform build:

platforms: linux/amd64,linux/arm64

一次 build 兩個平台,不用維護兩套 workflow。Docker 會自動根據 runtime 環境選擇對應的 image。

加上這行之後,在 Mac 上再 pull 一次就成功了,docker image inspect 看到的 Architecture 是 arm64。

如此一來,本地用 Apple Silicon 開發,部署環境不管是 x86 還是 ARM,都能使用同一個 image。


CD 是什麼?從 Artifact 開始的旅程

到這裡為止,我們做的事情都還是 CI 的延伸:確保程式碼品質、build image、推到 registry。這些步驟的產出物——Docker image——就是所謂的 artifact。

什麼是 Artifact?

Artifact 不只有 Docker image。依照專案類型,它也可能是編譯後的執行檔、Java 的 JAR 檔案,或打包好的前端網站檔案。它們都是建置流程產出的檔案,可供後續部署使用。換言之:

  • CI 負責「程式碼 → Artifact」
  • CD 負責「Artifact → 環境」

Artifact 就是 CI 與 CD 之間的橋梁。

持續交付與持續部署

CD 可以指 Continuous Delivery(持續交付),也可以指 Continuous Deployment(持續部署)。兩者都仰賴自動化建置與驗證,差別在於通過驗證後,是否會自動部署到生產環境:

  • Continuous Delivery:讓軟體維持在可以隨時安全發布的狀態;何時部署到生產環境,可以由人決定。
  • Continuous Deployment:通過驗證的變更會自動部署到生產環境,不需要人工觸發每次部署。

關於持續交付的概念與實踐,可以參考〈Continuous Delivery 介紹與實踐指南〉。

WeaMind 目前的發布流程是:建立 Git tag 並 push 到 GitHub 後,自動建置、發布 image,並在 Infra repo 開啟 PR。之後由我決定何時合併 PR,以及何時部署。

因此,WeaMind 的流程比較接近持續交付:發布流程有自動化,但部署到生產環境仍由我決定。


推送 Tag 自動發布:GitHub Release + 語意化版本

當你在 WeaMind repo 建立並推送一個 v* 的 tag,例如 v1.2.3,GitHub Actions 會同時觸發兩個 workflow,分別完成:

  1. 自動建立 GitHub Release
  2. Build 並推送語意化版本的 image

PR 合併到 main、CI 通過後,會自動發布 latest image;而推送 Git tag 代表我決定發布一個正式版,例如 v1.2.3 表示準備發布 1.2.3 版。

下面是 WeaMind 目前從 CI 到部署的流程:

 +------------------------------+
 | Push main                    |
 +------------------------------+
                 |
                 v
 +------------------------------+
 | CI: lint, test, type check   |
 +------------------------------+
                 |  成功
                 v
 +------------------------------+
 | Build image                  |
 | - latest                     |
 +------------------------------+
                 |  人決定發布
                 v
 +------------------------------+
 | Push tag v1.2.3              |
 +------------------------------+
         |               |
         v               v
 +------------------+  +----------------+
 | Build & push     |  | GitHub Release |
 | 1.2.3, 1.2, 1    |  +----------------+
 +------------------+
         |
         v
 +------------------------------+
 | Open PR to weamind-infra     |
 +------------------------------+
                 |  人工 merge
                 v
 +------------------------------+
 | Deploy                       |
 +------------------------------+
                 |  出問題時
                 v
 +------------------------------+
 | Rollback                     |
 +------------------------------+

自動建立 GitHub Release

建立 GitHub Release 的流程可以透過 GitHub Actions 自動完成。以下是簡化後的 workflow 設定,完整版本可參考 auto-release.yml:

name: Auto Release
on:
  push:
    tags: [ 'v*' ]

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
        with:
          fetch-depth: 0
      - uses: softprops/action-gh-release@v2
        with:
          generate_release_notes: true

generate_release_notes: true 會自動產生 release notes,列出合併的 PR、貢獻者與完整 changelog 的連結,方便追溯版本變更。

語意化版本 Tagging

推送 v1.2.3 這個 Git tag 後,會發布 1.2.3、1.2 和 1 三個 image tag:

tags: |
  ghcr.io/kyomind/weamind:1.2.3
  ghcr.io/kyomind/weamind:1.2
  ghcr.io/kyomind/weamind:1

這讓使用者可以選擇追蹤的穩定程度:

  • 1.2.3:鎖定特定版本,最穩定
  • 1.2:追蹤 1.2.x 的最新版,接受 patch 更新
  • 1:追蹤 1.x.x 的最新版,接受 minor 更新

App Repo 與 Infra Repo

WeaMind 分成兩個 repo:WeaMind 管理應用程式與 CI/CD workflow,weamind-infra 管理 Kubernetes manifests、Terraform 等基礎設施設定。

因此,發布新版本後,還需要更新 Infra repo 裡的 image 版本。接下來要介紹的,就是如何自動建立這個更新用的 PR。

跨 Repo 自動開 PR

WeaMind 發布版本 image 後,GitHub Actions 會執行 open_infra_version_pr.sh,自動到 weamind-infra 建立 PR。

Script 會更新 deployment.yaml 的 image 版本、推送 branch,再建立 PR。如果同版本的 PR 已經存在,就更新既有 PR,避免重複建立。

跨 repo 操作需要額外授權。我使用 GitHub 上的 Fine-grained PAT,授予它存取 weamind-infra 的 Contents: write 和 Pull requests: write 權限,再把 token 存為 WeaMind repo 的 GitHub Actions secret,供 workflow 使用。

PR 建立後,我可以先確認版本變更,再決定何時合併與部署。


Merge 之後:部署與 Rollback

PR merge 之後,weamind-infra 裡的 deployment.yaml 已經指向新版本的 image。剩下的就是把它套用到叢集上。

我把這一步寫成 weamind-infra 的 Makefile——其實就是 kubectl 指令:

deploy:
	kubectl apply -f manifests/
	kubectl rollout status deployment/weamind -n weamind

rollback:
	kubectl rollout undo deployment/weamind -n weamind

make deploy 主要套用 manifests,然後等 rollout 完成。rollout status 會持續顯示部署進度;如果新版本起不來,部署超過進度期限後,指令會回報失敗,讓你知道這次部署有問題。

Rollback 後,記得更新 Infra Repo

出問題時,make rollback 可以讓 Kubernetes 回到上一個部署版本。這也是使用固定版本 tag 的好處:系統能明確指向舊版 image,而不是依賴可能已經變動的 latest。

不過,rollback 只會改變叢集上的狀態,weamind-infra 的 deployment.yaml 仍然記錄著新版本。為了避免下次部署時又套用問題版本,還需要把 repo 裡的 image 版本改回來,例如用 git revert 撤銷更新版本的 commit。


系列前三篇回顧:從 CI 入門到 WeaMind CD

這個系列從 GitHub Actions 的基本用法開始,逐步擴充 WeaMind 的 CI,最後接上版本發布與部署。

第一篇:GitHub Actions 入門

GitHub Actions 入門:自動化 Lint、Format 與 Type Check

  • 認識 CI 的用途,以及 GitHub Actions 的基本概念。
  • 用 workflow 自動執行 lint、format 和 type check。
  • 從 side project 的實際需求出發,建立第一套自動化檢查。

第二篇:WeaMind 的 CI 實作

用 Side Project 學 CI:WeaMind 的 CI 實作策略

  • 整理 WeaMind 的 CI 項目,包括程式碼品質、測試覆蓋率和 Docker Build 驗證。
  • 加入 CodeQL、SonarCloud 和 Dependabot,處理安全掃描、品質分析與相依套件更新。
  • 介紹自動建立 GitHub Release 的流程。

第三篇:WeaMind 的版本發布與部署

也就是本文:從 CI 建置 image,接上版本發布、跨 repo PR 與 Kubernetes 部署。

回頭看這三篇,也能看到 WeaMind 的 CI 怎麼隨著專案的需求慢慢擴充:從最初的程式碼檢查,到後來的 image 發布與部署。


小結

最後回顧一下 WeaMind 目前的交付流程:

  1. Push code → CI 跑 lint、test、type check
  2. CI 成功 → 自動 build image,推 latest 到 GHCR
  3. 推送 tag → 自動建立 GitHub Release + 推語意化版本 image
  4. 推送 tag → 自動到 weamind-infra 開 PR,更新 deployment.yaml
  5. Merge PR → make deploy 部署,出問題就 make rollback

合併 Infra repo 的 PR 後,我還是得手動執行部署指令。

這感覺還是有點「手工藝」😅

下一篇,我們將介紹 GitOps,讓部署設定合併後,自動同步到 Kubernetes 叢集。這樣一來,就不用再手動執行部署指令了。


如果這篇文章對你有幫助,歡迎到 WeaMind GitHub 首頁 給我一個星星
想試用 WeaMind,可掃描下方 QR Code 或搜尋 LINE ID @370ndhmf 加入好友
你的支持是我持續分享的動力

WeaMind QR CodeWeaMind QR Code