CI/CD 到底是什麼?

AdSense

前言

CI/CD 是一種軟體開發的實踐方法,目的是透過自動化的流程,來頻繁地將程式碼整合、測試並部署到應用程式中。

簡單來說,它就像是一條自動化生產線

  1. 你寫好程式碼 (原料)。
  2. 機器自動幫你檢查有沒有語法錯誤、邏輯錯誤 (組裝與品管)。
  3. 機器自動幫你把程式打包好,送到客戶手上 (包裝與出貨)。

CI:持續整合 (Continuous Integration)

CI 的重點在於「頻繁」且「自動」地檢查程式碼品質

當多個開發者同時在開發同一個專案時,CI 伺服器會在開發者將程式碼上傳 (Push) 到儲存庫 (如 GitHub/GitLab) 後,自動執行以下動作:

  1. 還原套件 (Restore):檢查專案依賴的 NuGet 套件是否都能下載。
  2. 建置 (Build):執行 dotnet build,確保程式碼可以成功編譯成 DLL 或執行檔,沒有語法錯誤。
  3. 測試 (Test):執行 dotnet test,跑過所有的單元測試 (Unit Test),確保新的修改沒有壞掉原本的邏輯。

優點:

  • 提早發現錯誤:不用等到最後要發佈時才發現編譯失敗。
  • 解決合併衝突:強迫大家頻繁同步程式碼。

CD:持續交付/部署 (Continuous Delivery / Deployment)

CD 接續在 CI 之後,重點在於將通過測試的程式碼發佈出去。這裡分成兩個層次:

1. 持續交付 (Continuous Delivery)

自動化流程會將程式碼建置好,並部署到測試環境 (Staging)。但是,要發佈到正式環境 (Production) 時,通常需要人工審核 (Manual Approval)或是手動按下按鈕,這在企業級應用很常見。

2. 持續部署 (Continuous Deployment)

更進階的自動化。只要通過 CI 的所有測試,程式碼就會自動部署到正式環境給使用者使用,完全不需要人工介入。

C# 專案常見的 CD 動作:

  • 執行 dotnet publish 產出發佈檔案。
  • 將檔案複製到 IIS 伺服器。
  • 或是打包成 Docker Image 推送到雲端 (如 Azure App Service)。
  • 修改 web.configappsettings.json 的連線字串 (針對不同環境)。

C# 開發者的 CI/CD 工具

身為 .NET 開發者,我們最常用的工具有:

  • Azure DevOps (ADO):微軟自家的工具,對 .NET 支援度最好,企業最愛用。
  • GitHub Actions / GitLab CI:目前最流行的選擇,開源專案免費,設定檔 (YAML) 簡單直觀。
  • Jenkins:老牌的 CI/CD 工具,自由度高但設定較繁瑣。

CI 範例

GitHub Actions

假設我們有一個 .NET Core 的 Console 專案,一個簡單的 CI 設定檔 (.yml) 長得像這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
name: .NET Core CI

# 觸發條件:當 master 分支有 push 時
on:
push:
branches: [ master ]

jobs:
build:
runs-on: ubuntu-latest # 使用 Linux 環境執行

steps:
# 1. 下載程式碼
- uses: actions/checkout@v2

# 2. 安裝 .NET SDK
- name: Setup .NET
uses: actions/setup-dotnet@v1
with:
dotnet-version: 6.0.x

# 3. 還原 NuGet 套件
- name: Restore dependencies
run: dotnet restore

# 4. 建置專案 (CI 的核心)
- name: Build
run: dotnet build --no-restore

# 5. 執行測試 (確保邏輯沒壞)
- name: Test
run: dotnet test --no-build --verbosity normal

在 GitHub Actions 的 steps (步驟) 中,主要有兩種指令方式:

  • run:執行你在終端機 (Terminal) 會打的指令。
    • 例如:dotnet buildecho "Hello"
  • uses使用別人寫好的套件 (Action)
    • 這就像是 C# 中的 using 或是呼叫 NuGet 套件一樣。你不需要自己寫幾百行 Shell Script 來完成複雜的工作,直接「引用」別人做好的功能即可。

actions/checkout@v2 是一個官方提供的 Action,它的作用就是:「登入你的 GitHub 儲存庫,把你的程式碼 git pull 下來放到虛擬機的資料夾中。」當 GitHub Actions 啟動一台虛擬機 (Runner) 準備幫你跑 CI 時,這台虛擬機是空的、乾淨的,裡面完全沒有你的程式碼。如果你不執行這一步,直接跑 dotnet build,系統會報錯說:「找不到專案檔」,因為資料夾是空的。

GitLab CI (.gitlab-ci.yml)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 1. 指定環境 (Image)
# 直接使用微軟官方包好的 Docker Image,裡面已經裝好 .NET 6 SDK 了
image: mcr.microsoft.com/dotnet/sdk:6.0

# 2. 定義流程階段 (可以有很多階段,這裡我們先簡單定義一個)
stages:
- build_and_test

# 3. 定義工作 (Job)
build_job: # 自定義工作名稱
stage: build_and_test

# 指定只在 master 分支變動時執行
only:
- master

# 這裡就是執行的指令
script:
- echo "開始還原套件..."
- dotnet restore

- echo "開始建置專案..."
- dotnet build --no-restore

- echo "開始測試..."
- dotnet test --no-build --verbosity normal

比較:GitLab vs GitHub 的 CI 差異

這兩個範例做的事情是一模一樣的,但寫法有幾個很重要的不同的地方:

1. uses: actions/checkout 去哪了?

GitLab 自動幫你做了!
這是最大的不同。GitLab CI 在執行任何 Job 之前,預設會自動執行 git clone/fetch 把你的程式碼抓下來。所以你不需要像 GitHub 那樣顯式地寫一行 checkout 指令。

2. uses: actions/setup-dotnet 去哪了?

image: ... 取代了。

  • GitHub:是用 ubuntu-latest (通用環境),然後透過 steps 去安裝 .NET。
  • GitLab:更傾向直接指定一個 Docker Image (mcr.microsoft.com/dotnet/sdk:6.0)。這就像是直接租了一間「.NET 專用廚房」,進去的時候工具都已經掛在牆上了,不用再安裝。

3. 語法對照表

功能 GitHub Actions GitLab CI
執行指令 run: dotnet build script:
- dotnet build
使用外掛/套件 uses: ... 比較少用,通常靠 Docker Image 預裝
觸發條件 on: push: branches: ... only: - master
環境/虛擬機 runs-on: ubuntu-latest image: mcr.microsoft.com/...

從 CI 到 CD,以部屬到 AWS 為例

目前為止我們做的事情 (Restore, Build, Test),都只是在「工廠內部」把產品做出來並確保品質良好 (CI)。但是,如果產品做好了卻放在倉庫長灰塵,客戶 (使用者) 是看不到的。

CD (持續部署) 的過程,說穿了就是把這個做好的「產品包」(Artifact),自動搬運到 AWS 伺服器上。

要在 YAML 裡面實現部署到 AWS,通常需要兩個關鍵步驟:

  1. 身分驗證:CI/CD 機器人需要有權限才能操作你的 AWS 帳號 (就像你要給外送員大門鑰匙)。
  2. 傳遞產物 (Artifact):把 CI 階段編譯好的檔案 (.dll, .exe, .zip) 傳給 CD 階段。

以最常見的 AWS CLI (Command Line Interface) 方式來示範。想像一下,這就是讓機器人幫你輸入你平常在終端機打的指令。

前置作業:設定帳號、密碼和 tocken

在寫 YAML 之前,絕對不能把 AWS 的帳號密碼直接寫在程式碼裡!這是資安大忌。

你需要去 GitHub 或 GitLab 的後台設定 Variables (Secrets),把這兩個東西存進去:

  • AWS_ACCESS_KEY_ID (帳號)
  • AWS_SECRET_ACCESS_KEY (密碼)

CI/CD 執行時,會自動從後台讀取這些變數。

GitHub Actions

GitHub 的邏輯是拆成兩個 Job:builddeploy。因為 deploy 必須等 build 成功才能做,所以會用到 needs 關鍵字。

關鍵字:needsartifact

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
name: Deploy to AWS

on:
push:
branches: [ master ]

jobs:
# === 第一階段:CI (打包) ===
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup .NET
uses: actions/setup-dotnet@v1
with:
dotnet-version: 6.0.x

# 編譯並發佈到 output 資料夾
- name: Publish
run: dotnet publish -c Release -o output

# 【關鍵】:把 output 資料夾存起來,傳給下一個 Job 用
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: my-app-package # 包裹名稱
path: output/ # 要打包的路徑 (我要去哪裡抓檔案?)

# === 第二階段:CD (部署) ===
deploy:
runs-on: ubuntu-latest
needs: build # <--- 重點:一定要等 build 成功才執行

steps:
# 1. 下載剛剛打包好的包裹
- name: Download Artifact
uses: actions/download-artifact@v3
with:
name: my-app-package
path: app-files/ # 解壓縮後的檔案放置的路徑 (我要把檔案倒在哪裡?)

# 2. 設定 AWS 權限 (使用官方 Action)
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1

# 3. 執行部署指令 (這裡以複製到 S3 為例,或是 Elastic Beanstalk)
- name: Deploy to S3
run: |
aws s3 cp ./app-files s3://my-bucket-name/ --recursive
echo "部署完成!"

解析:

  • needs: build:這建立了依賴關係。如果你不寫這個,GitHub 會同時跑 Build 和 Deploy,那時候還沒編譯好,部署一定會失敗。
  • upload-artifact / download-artifact:這就像大隊接力。CI 跑完把棒子 (編譯好的檔案) 交出去,CD 接棒後繼續跑。因為 GitHub 的每個 Job 都是獨立的全新虛擬機,檔案不會自動保留,必須透過這個動作傳遞。
  • upload-artifactdownload-artifact 兩邊的 path 不一樣
    • 上傳階段 (upload-artifact) 是打包
      • 情境:你的 dotnet publish 指令剛剛把編譯好的檔案放在了 output 這個資料夾裡。
      • 動作:GitHub Actions 會去 output/ 資料夾把所有檔案抓出來,打包成一個叫做 my-app-package 的壓縮檔存到雲端。
    • 下載階段 (download-artifact) 是拆包
      • 情境:現在是一個全新的虛擬機 (Deploy Job),裡面空空如也。
      • 動作:GitHub Actions 從雲端把 my-app-package 下載下來,然後自動建立一個叫做 app-files/ 的資料夾,把內容物全部解壓縮進去。

GitLab CI

GitLab 的邏輯比較直觀,透過 stages 來控制順序。而且 GitLab 有一個很強大的特性:Artifacts (產物) 會自動傳遞給下一個 Stage,不用像 GitHub 那樣顯式地下載。

關鍵字:artifactsimage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 定義順序:先 Build 再 Deploy
stages:
- build
- deploy

# === 第一階段:CI (打包) ===
build_job:
stage: build
image: mcr.microsoft.com/dotnet/sdk:6.0
script:
- dotnet publish -c Release -o output_folder

# 【關鍵】:告訴 GitLab 這個資料夾要留給下一棒
artifacts:
paths:
- output_folder/
expire_in: 1 hour # 檔案只保留一小時,節省空間

# === 第二階段:CD (部署) ===
deploy_to_aws:
stage: deploy
# 換一個環境!這次我們不需要 .NET SDK,我們需要 AWS CLI 工具
image: registry.gitlab.com/gitlab-org/cloud-deploy/aws-base:latest

script:
# 這裡可以直接讀取到 output_folder,因為 GitLab 自動下載了
- echo "正在連線到 AWS..."
- aws s3 cp ./output_folder s3://my-bucket-name/ --recursive
- echo "部署完成!"

only:
- master # 只在 master 分支進行部署,避免開發分支誤傳

解析:

  • image 的切換
    • build_job 用的是微軟的 image(因為要編譯 C#)。
    • deploy_to_aws 換成了 AWS 的 image(因為要下 aws 指令)。這是 GitLab CI 最靈活的地方,每個步驟可以用最適合的工具箱。
  • 隱藏的 AWS 變數:GitLab 會自動把你在後台設定的 AWS_ACCESS_KEY_ID 注入到環境變數中,所以你在 script 裡不需要寫登入指令,aws 指令會自動讀取這些變數。

比較:GitLab vs GitHub 的 CD 差異

特性 GitHub Actions GitLab CI
流程控制 使用 needs: build 使用 stages 定義順序
needs 是用來無視 stages 的等待規則
檔案傳遞 upload-artifact (上傳)
download-artifact (下載)
artifacts (定義後自動傳遞)
AWS 工具 使用 uses: aws-actions/... 使用 image: .../aws-base
部署指令 手寫 run: aws ... 手寫 script: aws ...

部署到 AWS 的方式有很多種(S3, EC2, Elastic Beanstalk, Lambda),上面的範例是用最通用的 AWS CLI 指令。

實際工作上,那行 aws s3 cp ... 可能會換成:

  • aws elasticbeanstalk update-environment ... (如果是用 EB)
  • dotnet lambda deploy-function ... (如果是用 Lambda)

但不變的核心邏輯都是:CI 產出檔案 -> 傳遞檔案 -> CD 取得權限 -> CD 執行指令將檔案送上雲端

總結

  • CI (持續整合) = 自動 Build + 自動 Test。確保程式碼是「健康的」。
  • CD (持續交付/部署) = 自動 Publish + 自動 Deploy。確保程式碼能「送到使用者手上」。