平台工程不只要提供 Kubernetes、GitOps 或觀測工具,也要讓團隊知道程式碼改完後怎麼往下走:誰來確認它能編譯、測試有沒有通過、Container Image 從哪個 commit 建出來,又該由誰決定它何時進入環境?這些問題構成了持續整合與持續交付(Continuous Integration/Continuous Delivery,CI/CD)的工作。
CI 會在每次變更時重複執行 restore、test 與 build,及早找出不能合併的原始碼;CD 則依環境規則,將已核准的 Container Image 部署到指定環境。兩者不必由同一個工具或 job 完成,但交接要清楚:CI 留下能回查來源的檢查結果,CD 再決定要部署哪個版本。
這篇先用 GitHub Actions 建立 Todo 服務的 CI 檢查。GitOps 負責將 Git 中已核准的環境設定收斂到叢集;它不判斷原始碼能否編譯,也不執行測試。因此,每次 Pull Request(PR)變更服務時,CI 都要留下可重複執行的檢查結果。
CI 與 CD 常被放在同一條 pipeline,但它們處理的問題和需要的權限不同:
| 階段 | 要回答的問題 | 不該擁有的權限 |
|---|---|---|
| CI | 這份原始碼能 restore、test、build 嗎?產物來自哪個 commit? | Production Kubernetes 寫入權限 |
| GitOps CD | 已核准的設定是否應收斂到目標環境?資源是否健康? | 任意修改 application source 的權限 |
這個界線要先切清楚。CI 可以建置 Container Image,也可以建立更新 GitOps 設定的 PR;但它不需要拿著 kubeconfig 對 Production 執行 kubectl apply。把叢集寫入權留給叢集內受限的 GitOps controller,才能讓原始碼驗證和環境部署各自留下可追查的責任邊界。
目前的 Todo 範例有兩個 .NET 8 專案:Todo.Api 提供待辦事項 API,Todo.Bff 轉送瀏覽器送進來的 API 請求。兩個專案各自有 .csproj 與 Dockerfile,沒有 solution 檔,也尚未建立 unit test project。
因此,PR 檢查不應假裝已經涵蓋不存在的測試。現階段 workflow 仍執行 dotnet test,讓每個專案都走過相同的 test 入口;但沒有測試專案時,這一步不會驗證任何 unit test。真正補上測試後,CI 才能把測試失敗留在 PR,而不是等到 image 或叢集裡才發現問題。
pull request
│
├── restore:取得指定專案的 NuGet 相依套件
├── test:執行專案的測試入口
├── build:以 Release 設定編譯
└── 回饋:將失敗留在 PR
workflow 將 Todo.Api 和 Todo.Bff 放進 matrix。兩個 job 都使用相同的 .NET SDK、restore、test 與 build 步驟;其中一個失敗時,另一個仍會繼續執行,PR 可以一次看到兩個專案的結果。
name: todo-ci
on:
pull_request:
paths:
- "src/0-todo-microservices/**"
- ".github/workflows/todo-ci.yaml"
permissions:
contents: read
jobs:
validate:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
project:
- src/0-todo-microservices/Todo.Api/Todo.Api.csproj
- src/0-todo-microservices/Todo.Bff/Todo.Bff.csproj
steps:
- name: Check out source
uses: actions/checkout@v4
- name: Set up .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: Restore NuGet packages
run: dotnet restore ${{ matrix.project }}
- name: Run tests
run: dotnet test ${{ matrix.project }} --configuration Release --no-restore
- name: Build release artifacts
run: dotnet build ${{ matrix.project }} --configuration Release --no-restore
permissions 只給 contents: read,因此 workflow 可以讀取 repository,卻不能推送 image、寫入 GitOps 設定或操作 Kubernetes。後續需要發佈 image 的 job 應另行設定最小權限,不能因為 PR workflow 曾經需要讀取原始碼,就順手加上 packages: write 或 Kubernetes credential。
fail-fast: false 的作用是保留兩個專案的檢查結果,不是忽略失敗。只要任一 matrix job 失敗,整個 workflow 仍會失敗;PR 不會因此取得可合併的綠燈。
目前 repository 沒有 packages.lock.json,所以這個 workflow 沒有啟用 actions/setup-dotnet 的 NuGet cache。採用 lock file 後,才能以 lock file 作為 cache key,避免相同 commit 因為相依套件解析不同而得到不一致的結果。不能只開啟 cache,就把它當成相依版本可重現的保證。
CI 成功只表示指定的 checks 已通過,不能延伸成所有風險都已排除:
dotnet test 成功不代表功能已被測試。CI 先把原始碼層能立即發現的問題擋在前面。部署後的觀測資料與資源 Policy 分別處理問題調查和資源規則;它們不會因為 CI 綠燈就失去必要性。
GitHub Actions 的 runner 會執行下列命令。送出 PR 前可在 repository root 執行同一組指令:
dotnet restore src/0-todo-microservices/Todo.Api/Todo.Api.csproj
dotnet test src/0-todo-microservices/Todo.Api/Todo.Api.csproj --configuration Release --no-restore
dotnet build src/0-todo-microservices/Todo.Api/Todo.Api.csproj --configuration Release --no-restore
dotnet restore src/0-todo-microservices/Todo.Bff/Todo.Bff.csproj
dotnet test src/0-todo-microservices/Todo.Bff/Todo.Bff.csproj --configuration Release --no-restore
dotnet build src/0-todo-microservices/Todo.Bff/Todo.Bff.csproj --configuration Release --no-restore
驗收時,刻意讓其中一個專案無法編譯,確認對應 matrix job 顯示 compiler error,且 workflow 整體失敗。建立 unit test project 後,也應刻意讓一個測試失敗,確認 job 停在 test 步驟而不會把未驗證的程式碼當成可交付產物。
下一篇會將通過檢查的服務封裝成 Container Image,並釐清 release tag、Git SHA 與 immutable digest 分別適合處理什麼問題。