如果你跟我一樣有能力在開發完成後手動進行打包上傳等部署工作,但是卻對於CI/CD 跑 Pipeline 要吃的 YAML 語法感到頭痛,這篇文章可以解決你大多數疑惑

以 GitLab 為例細看

這邊提供一份簡單的 YAML 檔案做為參考

stages:
  - deploy

deploy:
  stage: deploy
  image: ubuntu:22.04
  only:
    - main
  before_script:
    - apt-get update -qq && apt-get install -y -qq rsync curl openssh-client
    # 安裝 Hugo Extended
    - HUGO_VERSION="0.147.0"
    - curl -sL "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.tar.gz" | tar -xz -C /usr/local/bin hugo
    # 設定 SSH
    - mkdir -p ~/.ssh
    - |
      if [ -f "$VULTR_SSH_KEY" ]; then
        cp "$VULTR_SSH_KEY" ~/.ssh/id_rsa
      else
        echo "$VULTR_SSH_KEY" | tr -d '\r' > ~/.ssh/id_rsa
      fi
    - echo "" >> ~/.ssh/id_rsa
    - chmod 600 ~/.ssh/id_rsa
    - ssh-keygen -l -f ~/.ssh/id_rsa # 診斷:顯示金鑰指紋,確認金鑰格式是否正確
    - ssh-keyscan -H $VULTR_HOST >> ~/.ssh/known_hosts
  script:
    # 建置
    - hugo --minify
    # 部署到 Vultr(只傳差異檔案)
    - rsync -avz --delete ./public/ $VULTR_USER@$VULTR_HOST:$VULTR_ROOT/
    # Ping IndexNow(Bing、Yandex 等)
    - curl -s -o /dev/null -w "%{http_code}" "https://api.indexnow.org/indexnow?url=https://your-website.com/sitemap.xml&key=$INDEXNOW_KEY"
    - curl -s -o /dev/null -w "%{http_code}" "https://www.bing.com/indexnow?url=https://your-website.com/sitemap.xml&key=$INDEXNOW_KEY"

以 GitLab 部署 Hugo 為例,當我 push 後是如何透過 pipeline 部署的

  1. gitlab runner 依據 .gitlab-ci.yml 定義的 image 下載映像檔,並啟動容器(Container)建立乾淨且獨立的建置環境。
  2. 接著會在裡面執行 before_script 進行部署前的環境初始化。 2.1. 安裝必要的系統套件(如 apt 依賴或 hugo)。這一步通常很花時間,最佳做法是直接採用預裝 Hugo 的官方或自製映像檔(Custom Docker Image)以大幅縮短建置時間。 2.2. 將私鑰(SSH Private Key)寫入容器的 ~/.ssh/id_rsa 中,以供後續遠端驗證。(公鑰需自行先放到正式機中) 2.3. 並利用 ssh-keyscan 將目標主機的公鑰加入 known_hosts,防止 rsync 執行時因互動式確認提示(Prompt)而導致 Pipeline 而卡住
  3. script 階段執行 hugo 命令,產生(Generate)靜態網頁檔案。
  4. 將打包後 public/ 目錄 rsync 到正式主機
  5. … 部署後的連動 ex. 更新 indexNow、清除快取等…

💡每個 Stage 都是乾淨的

每一個 Stage 都會重新起一個 Container 得到乾淨的環境,因此前一次做過的東西想留下必須透過 Artifacts

例如:在 Stage 1 將產出放在 public/ 目錄並設為 Artifacts,那麼在 Stage 2 啟動時會將 public/ 自動下載回來(請注意是花時間下載

💡如果前面 Stage 產生的 Artifacts 太多可以利用 dependencies 限制下載範圍

stage: deploy
dependencies:
  - build_job  # 這樣它就只會下載 build_job 的東西,不會下載 Test 階段的東西

💡如果後面 Stage 不需要,可以不下載嗎?

可以!只要給 dependencies 一個空陣列即可

job_不需要檔案:
  stage: notify
  dependencies: []  # 明確告訴 GitLab:這一個 Job 什麼都不要下載
  script:
    - echo "我不需檔案也能執行"

💡Artifacts 預設會在 GitLab Server 上留存一段時間(大約30天)

這指的是 GitLab 的網頁介面與儲存空間。 用途: 當你打開 GitLab 網頁,進到該次 Pipeline 的畫面時,你會看到一個「Download Artifacts」的按鈕。這讓你可以手動下載那次打包出來的 public/ 壓縮檔,用來檢查內容。

⚠️注意: Artifacts 不會影響下一次 Pipline 的環境

GitLab 的 YAML 結構

一般人大腦的思考方式,在程式設計中叫做「由上而下」(Top-Down) 的樹狀結構,例如

graph TD
    subgraph "大腦直覺:樹狀結構"
    A[Stages] --> B[Build]
    A --> C[Deploy]
    B --> B1[Job_打包網頁]
    B --> B2[Job_同步主機]
    end
# ❌ 這不是 GitLab 的語法,但這是你大腦直覺的樹狀邏輯:
stages:
  build:
    - job_打包網頁:
        script: hugo
    - job_壓縮圖片:
        script: optipng
  deploy:
    - job_同步主機:
        script: rsync

但 GitLab 是採用了「扁平化標籤(Tagging)邏輯」,他把所有 Job 攤平在最外層,在讓 Job 自己去認領 Stage。

graph TD
    subgraph "GitLab 邏輯:扁平化標籤"
    D[job_打包網頁] -- Tag: Build --> E[Build Stage]
    F[job_壓縮圖片] -- Tag: Build --> E
    G[job_同步主機] -- Tag: Deploy --> H[Deploy Stage]
    end
stages:
  - build
  - deploy

job_打包網頁:
  stage: build
  script:
    ...

job_壓縮圖片:
  stage: build
  script:
    ...

job_同步主機:
  stage: deploy
  script:
    ...

為何 GitLab 要這樣設計?

GitLab 這樣設計,是為了解決「樹狀結構」在複雜自動化時的致命缺點:

  1. 為了支援超高自由度的「條件執行」 如果採用樹狀結構,當你想讓某個 Job 在特定條件下才執行(例如:只有 master 分支才跑部署),你的語法會變得極度臃腫,容易搞錯縮排。現在的設計可以讓每個 Job 變成獨立的「積木」,自己決定自己的命運:
  2. 方便「繼承」與「重複使用」(Extends) 當專案很大時,你可能會寫一個「範本 Job」。如果是平鋪的結構,其他 Job 可以直接繼承它,並改掛到不同的 Stage 去:
.bash_template:  # 範本
  before_script:
    - echo "初始化環境"

job_測試環境:
  extends: .bash_template
  stage: test    # 掛到測試 stage
  
job_正式環境:
  extends: .bash_template
  stage: deploy  # 掛到部署 stage

這個觀念一通,以下這三件事你就全懂了

  1. 為什麼可以「跨關卡打臉」?(DAG 異步執行)
job_部署網頁:
  stage: deploy
  needs: ["job_打包網頁"] # 標籤連線:只要打包好,我就要直接去 deploy,不管中間的 Test 關卡跑完了沒!

如果用樹狀結構,這根本做不到,因為 Deploy 必須死等整個 Test 樹狀分枝結束。但用標籤,個體之間可以直接通訊。

  1. 為什麼可以用環境變數隨便過濾?(Rules 觸發) 因為每個 Job 都是獨立個體,它們可以像在百貨公司抽獎一樣,看自己身上的標籤符不符合條件:
job_測試:
  stage: test
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"' # 只有 main 分支時,這個個體才活過來

核心組件快速對照表

關鍵字核心功能一句話理解
artifacts檔案傳遞把這關產出的東西「打包」傳給下一關
dependencies下載控制決定這關要從哪關「拿」東西過來
needsDAG 異步不用等整關跑完,前置 Job 好了就衝
extends繼承範本減少重複程式碼的「積木範本」
rules條件觸發像抽獎一樣,符合條件的 Job 才會執行

延伸:Kubernetes / Terraform 也是這套邏輯 未來如果去碰雲端、微服務(Kubernetes),你會發現它也是這樣:你不會把一個容器「寫在」某台伺服器下面。你只會起一個容器,給它貼上標籤 app: hugo-web,然後起一個流量分配器(Service),也貼上標籤去撈 app: hugo-web。它們就自己對上了。

結語

掌握了「扁平化標籤」與「獨立積木」的邏輯後,GitLab CI/CD 的 YAML 就不再是令人頭痛的語法拼圖。這套設計哲學不僅賦予了 Pipeline 極高的靈活性,也與現代雲原生架構(Cloud Native)的設計理念不謀而合。希望這篇文章能幫助你克服對自動化部署的恐懼,開始建構屬於你自己的高效開發工作流。