雲原生時代的軟體交付: 從 commit 到 deploy 的自動化工程
陳紹雲 / Shao-Yun Chen · TSMC/BSID/TPMP
TSMC / BSID / TPMP
讓你下課之後,可以在自己的 repo 寫出第一條 CI/CD pipeline。
搞懂 CI/CD 在雲原生工作流程裡的角色
看懂 GitHub Actions 的 workflow 結構
在 Codespaces 跑 Fastify 專案的 CI,用 act 本機模擬
把 app 容器化,加上 CD 自動部署,理解 branch 策略
雲原生時代的 CD 新範式
過去幾堂課我們學了:
把 app 跟它的環境打包在一起
大規模管理 container
把大系統拆成多個小服務
CNCF 對「雲原生」的定義包含四個技術支柱:
解決環境一致性問題
系統可拆解、可獨立部署
規模化管理
讓上面三個能持續、安全地交付
沒有 CI/CD 的雲原生 = 用跑車載貨,但每次都要手推上去
CI/CD 是把前面的雲原生概念串成一條真正能跑的路,也是通往後續進階主題的基石。

想像你公司有 20 個微服務跑在 K8s 上⋯⋯
手動 docker build → 手動 docker push → 手動 kubectl apply
用了不一樣版本的 base image,production 爆炸
在自己機器 build 的 image,沒人知道是哪個 commit
20 個服務手動部署一輪,凌晨 3 點⛔ 沒有人敢碰 production
任何一個軟體專案都會經歷以下階段:Analysis → Design → Development → Testing → Deployment → Maintenance。
從工程師日常視角,每天反覆做的就是這四步。一個微服務一天可能跑 5 次,30 個服務一天 150 次——沒有自動化,光這四步就把整個團隊吃光。
持續地把多人的 code 合進同一個專案,每次合進來都自動跑 Test 跟 Build
CI 產出的 build,自動 Release 到測試或正式環境
每次有人 push code,自動執行:
檢查 code style
CI 其實很笨,它只看一個東西:Exit Code
$ npm test
✓ all tests passed
$ echo $?
0 # ← 0 = 成功$ npm test
✗ 1 test failed
$ echo $?
1 # ← 非 0 = 失敗這就是 CI 擋下爛 code 的全部原理。換成 Python、Go、Rust 都一樣,因為這是 Unix 的通用約定。
市面上常見的 CI 工具:
老牌、功能強、要自己維運
跟 GitLab 綁在一起
微軟生態
今天要用這個
CI 工具品牌一堆,但核心模型都一樣——任何 pipeline 在描述兩件事:
學會這個抽象模型,換工具只是換語法
跟 GitHub 原生整合,push 就跑,零設定基礎建設
Public repo 完全免費,學生最友善
寫過一次就會,學習曲線低
很多現成的 action 可用,開箱即用。GitHub Marketplace

例如程式碼推送 (push)、合併請求 (pull_request) 或定時排程 (schedule),是觸發自動化流程的起點。
一個獨立的執行單元,由一系列 Step 組成,且總是在同一台 Runner 上執行。多個 Job 可以並行或依序執行。
Job 中的最小執行步驟,可以是執行一個指令 (run) 或使用一個別人寫好的 Action (uses)。
執行 Job 的虛擬機器或實體機器。GitHub 提供代管 Runner,你也可以自架 Runner 來滿足特定需求。
與 GitHub Actions 相似,GitLab CI 也有觸發事件、執行單元和執行環境,但多了一層 Stage 的概念來組織 Job。

GitLab CI 多了 Stage 這層,用來分組 Job 並控制執行順序。你可以想像 Stage 是「橫向群組」,Job 則是「群組裡的成員」。
Azure Pipelines 作為 Microsoft Azure DevOps 的核心組件,提供強大的 CI/CD 能力。它與 GitLab CI 概念相似,同樣透過 Stage 來組織 Job,但也有其獨特的詞彙。

不同的 CI 工具,對於概念的命名略有差異,但核心意義相似:
needs: 處理任務依賴關係。學會 GitHub Actions,讀懂 GitLab CI / Azure Pipelines 的設定也大致能理解其核心邏輯。
YAML = YAML Ain't Markup Language,一種給人讀的資料格式。
jobs:
test: # job 名字
runs-on: ubuntu-latest # 跑在哪台機器
steps: # 步驟列表
- run: npm test # - 開頭表示 list 的一個元素
- run: npm run lint都是 key-value 結構
用 - 開頭,縮排決定誰是誰的子層
name: Hello GitHub Actions # workflow 名字
on: # 什麼時候要跑
push:
branches:
- "**"
jobs: # 要做哪些事
hello-job:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4 # uses = 用現成 action
- name: Say Hello
run: echo "Hello World!" # run = 跑一行 shell放在 .github/workflows,push 到任意分支就會跑。
name: Node CI
on:
push:
branches:
- "**"
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm install
- run: npm testCI 跑完留下的中間產物,用於除錯或審計:
→ actions/upload-artifact@v4
雲原生時代,最終要部署到環境的東西:
→ Image 推到 Container Registry (ex. ghcr.io/ Docker hub/ Harbor)
確保不會把壞掉的 code merge 進主分支:
CODE · BUILD · TEST
驗證並打包 code
ARTIFACT
通過驗證的產物
DEPLOY · OPERATE · MONITOR
送到實際環境跑
CI 把 code 驗證好、打包好 → CD 把這個產物送到實際環境跑。
雲原生時代,這條路很標準化:
Container Image = 雲原生的 Deployment Artifact
不可變、可複製、隨處可跑
部署不只是「丟個檔案上去」,要考慮:
要部哪個版本?
一台 VM?K8s?Serverless?
開幾個 instance?
DB 連線、API key — 不能寫死在 code
DB 密碼、API key、Cloud credential⋯⋯寫死在 code 裡 = 推到 GitHub 那刻全世界都知道
- name: Deploy
env:
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
API_KEY: ${{ secrets.PROD_API_KEY }}
run: ./deploy.sh設定地方:Repo Settings → Secrets and variables → Actions
「我哪個 branch 該跑 CI?哪個該跑 CD?」
接下來,我們動手做 👇
用 Codespaces + GitHub Actions + Docker + act
跑出一條完整 pipeline
課程目標是理解 CI/CD pipeline 的本質,不是學雲端設定。
K8s + cloud registry + IAM 設好,可能就吃掉整堂課。
Docker Compose 跟 K8s 都是「描述要跑什麼 container」。
CD 流程只差最後一步——把 docker compose up 換成 kubectl apply 而已。
雲端開發環境,瀏覽器就能寫
一個簡單的 web app + Vitest 測試框架
CI/CD 平台
容器化 + 本機模擬 GitHub Actions
Lab 會有三層 container,debug 時要分清楚:
你的開發機,本身是容器,你在這裡敲指令
用來模擬 GitHub Actions runner
docker compose 跑起來的 app
Debug 時看到一個 log,先問自己「這是哪一層的?」
cicd-lab/
├── .github/
│ └── workflows/ # ← 一開始是空的
├── snippets/
│ ├── 01_hello.yaml # Lab-01 會用
│ ├── 02_run-test.yaml # Lab-02 會用
│ ├── ci.yaml # Lab-04 會用
│ └── cd.yaml # Lab-04 會用
├── src/
│ ├── app.ts # Fastify app (可測試)
│ └── server.ts # 啟動 server
├── test/
│ └── app.test.ts # Vitest 測試
├── Dockerfile
├── docker-compose.yml
└── package.json這個 Lab 專案模擬一個完整的 Node.js + Fastify 服務,包含原始碼、測試檔案和 Docker 設定。
這是實作的第一步,請依照以下步驟設定您的開發環境:
環境建立完成後,請驗證各工具的版本:
node --version
docker --version
docker compose version
act --version# 安裝依賴
npm ci
# 啟動服務
npm run build
npm run start開另一個 terminal 驗證:
curl http://localhost:3000/
curl http://localhost:3000/healthnpm test看到測試結果全部綠色就表示程式碼符合預期,沒有錯誤。
接著,請您故意修改 src/app.ts 檔案,將 /health 端點的回應內容改錯。再次執行 npm test,這時您應該會看到測試失敗(紅色)。
將位於 snippets/01_hello.yaml 的檔案複製到 .github/workflows/ 目錄下,並推送到新的分支:
git checkout -b feature/ci-observe
cp snippets/01_hello.yaml .github/workflows/
git add .
git commit -m "ci: add hello.yaml"
git push origin feature/ci-observe點進 Hello GitHub Actions workflow run,並點開每個 step 檢視 log:

GitHub Actions runner 會將您的程式碼儲存庫複製 (clone) 到執行環境中。
執行一個簡單的 echo "Hello World!" 命令,輸出至 log。
執行 ls -al 命令,列出 checkout 後的檔案結構,驗證程式碼已存在。
接下來,請將位於 snippets/02_run-test.yaml 的檔案複製到 .github/workflows/ 目錄下,並推送到您目前的分支:
cp snippets/02_run-test.yaml .github/workflows/
git add .
git commit -m "ci: add run-test.yaml"
git push origin feature/ci-observe完成推送後,點擊左側導航欄的 Actions 分頁,您將會看到以下兩個 Workflow 自動開始運行:


點進 Run Tests Workflow 的執行頁面。
點開 Run tests 步驟,仔細查看 Vitest 執行測試的輸出日誌。
滑動頁面到最底部,您會看到一個名為 Artifacts 的區塊。
在此區塊中,您會發現一個名為 test-report 的壓縮檔,下載並解壓後,內容是 vitest-junit.xml 格式的測試報告。
act 是一個強大的工具,能讓您在本地端執行 GitHub Actions workflow,對於除錯和快速迭代 CI/CD 流程非常有幫助。
act 工具不僅能模擬完整的 CI/CD 流程,還能模擬特定的事件類型(例如 push),讓您在本地端就能測試 Workflow,大幅提升開發效率。
您可以直接在分支上執行 act push 模擬推送事件:
act push若想只針對單一 Workflow 進行測試,可以加上 -W 參數:
act push -W .github/workflows/01_hello.yaml
act push -W .github/workflows/02_run-test.yaml進階用法:若不想切換分支,但希望模擬特定分支的推送事件,可以使用 --env GITHUB_REF 參數:
act push --env GITHUB_REF=refs/heads/your-feature-branch目前為止,hello.yml 和 run-test.yaml 已經讓您對 GitHub Actions 有了基本認識。但業界真實的 CI/CD pipeline 會比這兩個檔案執行更多複雜的任務。
接下來,我們將帶您解析 snippets/ci.yml 和 snippets/cd.yml 這兩個進階設定檔,了解它們如何處理更複雜的 CI/CD 流程。
在實際應用中,我們希望透過自動化流程達成以下目標:
確保每一次程式碼提交都能觸發 CI 流程,自動驗證程式碼品質與功能穩定性。
每一次提交都建構 Docker Image,確保版本可追溯,有助於後續的問題排查與管理。
利用 Image Tag 明確區分開發中的 Feature 版本與正式發佈的 Release 版本,確保部署的正確性。
嚴格限制只有來自 Release 分支的變更才能觸發正式環境部署,避免非預期的部署造成環境不穩定。
接下來,我們將深入解析 snippets/ci.yaml 與 snippets/cd.yaml,看看它們是如何實現這些進階目標的。
讓我們打開 snippets/ci.yml,從頭開始解析這個進階的 CI Workflow 設定。
name: ci
on:
push:
branches: [ '**' ] # 所有 branch 都跑
pull_request: # PR 也跑
permissions:
contents: read # 最小權限原則
on)這個 Workflow 會在每次程式碼被 push 到任何分支時自動觸發。同時,當有新的 pull_request 被建立或更新時,也會觸發 CI 流程,確保每次合併前程式碼都能通過測試。
permissions)這裡採用「最小權限原則」,僅賦予 Workflow 讀取儲存庫內容 (contents: read) 的權限。這是一個重要的安全實踐,避免因權限過大而造成潛在的資安風險。
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true同一 branch 連續 push,只跑最新那次,省 runner 時間
strategy:
fail-fast: false
matrix:
node-version: ['22', '24']同時用 Node 22 跟 24 各跑一次,驗證升級風險
- name: Checkout
uses: actions/checkout@v4 # 抓 code
- name: Setup Node.js
uses: actions/setup-node@v4 # 裝 Node
with:
node-version: ${{ matrix.node-version }}
cache: npm
- name: Install dependencies
run: npm ci # 裝依賴
# add your steps here, e.g. lint, test, build etc.
# - run: npm run typecheck # 型別檢查
# - run: npm run ci # lint + test- name: Compute image tag
id: meta
run: |
BRANCH_NAME="${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}"
if [[ "$BRANCH_NAME" == release/* ]]; then
VERSION="${BRANCH_NAME#release/}"
IMAGE_TAG="release-${VERSION}"
else
IMAGE_TAG="sha-${GITHUB_SHA::7}"
fisha-a1b2c3d
release-1.4.0
- name: Build Docker image
if: ${{ matrix.node-version == '24' }}
run: docker build -t my-app:${{ steps.meta.outputs.image_tag }} .
# 另一個 job
cd:
if: ${{ startsWith(github.ref, 'refs/heads/release/') }}
needs: ci
uses: ./.github/workflows/cd.ymlon:
workflow_dispatch: # 可手動觸發
workflow_call: # 可被 ci.yml 呼叫
jobs:
deploy:
if: ${{ startsWith(github.ref_name, 'release/') }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Compute release image tag
...
- name: Build and deploy with Docker Compose
run: docker compose up -d --build
- name: Verify health endpoint
run: curl -fsS http://localhost:3000/health首先,將進階版的 ci.yaml 和 cd.yaml 複製到您的 workflows 目錄:
cp snippets/ci.yaml .github/workflows/
cp snippets/cd.yaml .github/workflows/在功能分支上模擬 push 事件,觀察 CI 流程的行為:
git checkout -b feature/a
act push
docker images # 查看 build 出來的 image切換到發布分支,模擬 push 事件,觀察 CI/CD 流程的協同工作:
git checkout -b release/1.0.0
act push
docker images # 查看 release tag image剛才 docker compose up -d --build —CD 重 build 一次 image。但前面說 image 是 immutable artifact、build once run anywhere。
自相矛盾嗎?是的,這是為了 lab 簡單做的妥協。
在 .github/workflows/ci.yml 的 steps 加:
- name: Format check
run: npm run format:check
- name: Lint
run: npm run lintact push 會看到這兩步驟有跑修改 src/app.ts,加一個 /version endpoint:
app.get('/version', async () => {
return { version: process.env.APP_VERSION || 'dev' };
});在 test/app.test.ts 加對應的測試。
npm test 全綠act push 能完整跑過 CInpm install # 第一次
npm start # 跑起來
npm run dev # 熱重載npm run typecheck # 型別檢查
npm test # 跑測試
npm run lint # lint
npm run format:check# 模擬 push 事件(預設跑全部)
act push
# 只跑 ci.yml
act push -W .github/workflows/ci.yml
act pull_request # 模擬 PR 事件
act -j test # 只跑名為 test 的 job
act -l # 列出所有 workflow 跟 jobA: 點 Actions 分頁,啟用 workflow
A: 它要拉 docker image,正常的,之後會快
A: 點失敗的 step,看 log。同樣的指令在本機重跑(npm test)
A: 用 act 在本機先驗
剛才你做了什麼?
寫 code → CI 自動驗 → 推 release branch → CD 自動部署
CI/CD「主動推」變更到部署環境(Push 模型)
但如果有一天你要管 100 個服務、5 個 K8s cluster 呢?
Push 模型的問題會浮現:
核心精神:Git 就是 K8s cluster 的唯一真相
Git 長怎樣,cluster 長怎樣
每個變更都是 commit
有人手改 cluster,自動拉回來
git revert 就能 rollback
業界最紅的 GitOps CD 工具:
好處:改部署不觸發 build、獨立審核、降低誤操作
你今天用的只是其中一種,業界主流有這四家。沒有對錯,選一個跟團隊節奏搭得起來的就好。
最簡單
✅ 適合:純 SaaS、不需要版本號的服務
今天用的
✅ 適合:雲原生 SaaS、版本化發版、有 QA gate
最完整 (但對 SaaS 太重)
⚠️ 作者 2020 年自己說「對現代開發不適合」
✅ 適合:傳統軟體、發行週期長、版本化軟體
折衷
✅ 適合:多環境、需要 staging gate 的團隊
沒有對錯—重點是 CI/CD 的
if條件,要對應你選的 branching strategy。
CI/CD 之後可以再深入: