在Azure DevOps中建立共用的Yaml Pipeline Template

圖片

為何有這種需求

最近在上 Azure DevOps 課程,下課後,一群學員突然跑來問了個問題。

「老師,我們公司有三十幾個 Azure DevOps 專案,每個專案都有自己的 repo,每個 repo 都有自己的 pipeline yaml 檔…」他頓了一下,臉上帶著某種「歷盡滄桑」的表情,「有沒有辦法讓這些 pipeline 共用同一份 template?改一次就全部生效的那種?」

我其實急著下課,並沒有聽得很仔細,腦袋裡還在糾結:「為何要讓所有的 repo 都共用同一份 yaml template,每個開發專案不是應該有自己的 pipeline design 嗎?為何要為了統一控管搞得這麼複雜?」

於是我追問:「為什麼會有這種需求呢?」

他苦笑著說「每當公司想要在 pipeline 裡加入一些共用的步驟,比如說安全掃描、程式碼品質檢查、部署前的驗證等等,我們就得一個一個 repo 去改 pipeline yaml 檔。三十幾個專案,每個專案又可能會有好幾個 repo,改起來超級累人。」

教室裡其他學員也紛紛露出「我懂我懂」的表情。這種痛,做 DevOps 的人應該都瞭。

更慘的是,當你好不容易改完三十幾份檔案,過兩個月主管又說:「欸,我們現在還要加入程式碼品質檢查。」然後你又要再改一輪。如果哪天發現其中一個步驟寫錯了,想改?好啊,三十幾個 repo 再巡一次。

這就像是複製貼上了三十幾份同樣的食譜,結果發現鹽放太多了,然後你得把三十幾本筆記本都翻出來,一本一本改。真的累。

「所以…」學員問,「有沒有辦法讓這些 pipeline 共用同一份 template?改一次就全部生效?」

我當下趕著回家,沒多想,也來不及具體回答。

但回家後仔細思量:「這個問題其實挺實際的,應該是有解決方案的吧?」稍微翻了一下,果然發現 Azure DevOps pipeline 原本就支援引用外部的 yaml template。

如何實現?

Azure DevOps 其實早就想到這個問題了。它提供了一個機制,讓你可以把 pipeline 的某些步驟抽出來,放到另一個 repo 裡當作「模板」,然後其他專案的 pipeline 就可以「引用」這個模板。

聽起來很簡單對吧?但實際上第一次設定的時候,總會有那種「欸這樣寫對嗎?」的不確定感。

我也是這樣,看著官方文件,然後照著做,結果 pipeline 一開始是連儲存都失敗,後來是跑起來一直紅燈。改了兩三次才發現是路徑寫法錯了。

所以在好不容易把範例做完之後,順手寫了這篇文章,一步一步紀錄一下整個流程。
不廢話,直接做。

具體實作

我建了兩個 Azure DevOps 專案來示範,分別叫 TestShardYmlA 和 TestShardYmlB。

想像一下,TestShardYmlA 是「模板庫」,專門用來放那些可以共用的 pipeline 步驟。而 TestShardYmlB 是「實際專案」,它的 pipeline 會去引用模板庫裡的東西。

這種設計的好處是,以後如果你的企業有更多專案,它們都可以指向同一個模板。改一次模板庫裡的模板,所有專案全部生效。

第一步:建立模板 Repo

在 TestShardYmlA 專案裡,我建了一個 repo,然後在裡面放了一個共用的 pipeline template,位置是這樣的:

/SharedTemplates/templates/dotnet-unit-test.yml

圖片

看到這個資料夾結構了嗎?這就是模板庫的樣子。
假設,我將片段的 yml 內容撰寫如下:

# 檔案:ProjectA / SharedTemplates / templates/dotnet-unit-test.yml
# 功能:共用的 .NET Unit Test 步驟

steps:
- task: DotNetCoreCLI@2
  displayName: Test
  inputs:
    command: test
    projects: $(BuildParameters.TestProjects)
    arguments: --configuration $(buildConfiguration)

就這樣。

這個 template 做的事情很單純:跑 .NET 的 unit test。
就這麼幾行,但它未來會做為所有專案共用的標準測試步驟。(假設你的專案是用 .NET Core 開發的)

如果哪天你想在測試後加點什麼,比如說檢查測試覆蓋率,或是輸出測試報告,改這裡就好。所有套用到這個 template 的專案,全部會自動更新。

第二步:在實際專案中引用 Template

接下來,我們看 TestShardYmlB,這是實際在跑的專案。

在它的 repo 裡,我建了一個 azure-pipelines.yml,這是該專案主要的 pipeline 定義檔。內容如下:

# 檔案:ProjectB / <你的 WebApp Repo> / azure-pipelines.yml
# ASP.NET Core
# Build and test ASP.NET Core projects targeting .NET Core.

trigger:
- main

pool:
  vmImage: ubuntu-latest

# 👉 引用 ProjectA 的共用 template Repo 👈
resources:
  repositories:
  - repository: sharedTemplates                  # 在這個 yml 裡的暱稱
    type: git
    name: TestShardYmlA/TestShardYmlA            # 格式:<ProjectA 名稱>/<Repo 名稱>
    #ref: refs/heads/main                        # 或 main,看你那邊的 branch 名稱

variables:
- name: buildConfiguration
  value: 'Release'
- name: BuildParameters.RestoreBuildProjects
  value: '**/*.csproj'
- name: BuildParameters.TestProjects
  value: '**/*[Tt]ests/*.csproj'

name: $(date:yyyyMMdd)$(rev:.r)

jobs:
- job: Job_1
  displayName: Agent job 1
  pool:
    vmImage: ubuntu-latest
  steps:
  - checkout: self

  - task: DotNetCoreCLI@2
    displayName: Restore
    inputs:
      command: restore
      projects: $(BuildParameters.RestoreBuildProjects)

  - task: DotNetCoreCLI@2
    displayName: Build
    inputs:
      projects: $(BuildParameters.RestoreBuildProjects)
      arguments: --configuration $(buildConfiguration)

  # 👉  這裡改成使用 ProjectA 的共用 Unit Test template 👈
  - template: SharedTemplates/templates/dotnet-unit-test.yml@sharedTemplates

  - task: DotNetCoreCLI@2
    displayName: Publish
    inputs:
      command: publish
      publishWebProjects: true
      projects: $(BuildParameters.RestoreBuildProjects)
      arguments: --configuration $(buildConfiguration) --output $(build.artifactstagingdirectory)
      zipAfterPublish: true

  - task: PublishBuildArtifacts@1
    displayName: Publish Artifact
    condition: succeededOrFailed()
    inputs:
      PathtoPublish: $(build.artifactstagingdirectory)
      TargetPath: '\\my\share\$(Build.DefinitionName)\$(Build.BuildNumber)'

關鍵在這兩個地方

第一個是 resources: 區段:

resources:
  repositories:
  - repository: sharedTemplates
    type: git
    name: TestShardYmlA/TestShardYmlA

這段是在告訴 Azure DevOps:「嘿,我要用另一個專案裡的 repo,它叫 TestShardYmlA/TestShardYmlA,在這個 pipeline 裡我給它取個暱稱叫 sharedTemplates。」

格式很重要:<專案名稱>/<Repo 名稱>。如果寫錯了,pipeline 會找不到,然後就報錯了。

第二個是引用 template 的那一行:

- template: SharedTemplates/templates/dotnet-unit-test.yml@sharedTemplates

這行的意思是:「去 sharedTemplates 這個 repo(就是我們剛剛取的暱稱),找到 SharedTemplates/templates/dotnet-unit-test.yml 這個檔案,然後把裡面的 steps 插到這裡。」

就是這麼簡單。
但關鍵就藏在這麼簡單的兩個步驟裡。

現在,如果你有十個、二十個、甚至五十個專案,它們都可以用同樣的方式引用這個 template。以後要改測試步驟?改一個地方就好。不用再一個 repo 一個 repo 巡了。

結論

自動化的本質,說穿了就是「減少重複勞動」。

你寫的每一行 code、每一個 pipeline、每一個設定檔,都應該問自己:「這東西會不會重複用到?」如果答案是「會」,那就該想辦法抽出來、共用它。

Azure DevOps 的 pipeline template 就是這種思維的呈現。它並非什麼高深的技術,就是把「可以重複用的東西」拉到一個地方,然後讓所有需要它的人都能輕鬆取用。

但這背後的價值,可不只是「少打幾行 code」這麼簡單。

當你的團隊有了共用的 template,代表著:

  • 標準化:所有專案的 pipeline 都遵循同樣的規範,不會有人自己亂搞。
  • 可追溯性:template 放在 git 裡,每次改動都有紀錄,出事了可以回溯。
  • 快速迭代:要推廣新的實踐(比如安全掃描、測試覆蓋率檢查),改一次就全部生效。

更重要的是,當有一天你的主管又跑來說「我們要在所有 pipeline 加入 XXX 步驟」的時候,你可以淡定地回答:「好,我五分鐘改給你。」

然後看著他驚訝的表情,內心暗爽。

這才是 DevOps 該有的樣子。😁


參考資料:
https://learn.microsoft.com/en-us/azure/devops/pipelines/process/templates?view=azure-devops

相關課程:
https://www.studyhost.tw/NewCourses/ALM

留言

這個網誌中的熱門文章

開啟 teams 中的『會議轉錄(謄寫)』與Copilot會議記錄、摘要功能

VS Code的字體大小

使用 Render.com 作靜態網站的自動化佈署

關於Silverlight中的RichTextBox以及文字處理

迎向2011,面對軟體開發的未來