Azure DevOps Pipeline で何度も同じ処理をYAMLに書いていた場合、Templateを使うとまとめられて便利になります。
実際にビルドが多く重複した定義の多いプロジェクトに適用したところ、表向き300行 (template 含めると100行) 減らせて見通しは良くなりました。
# before
$ find . -type f | xargs cat | wc -l
788
# after
$ cat *.yml | wc -l
483
$ find . -type f | xargs cat | wc -l
628
今回は、Template について書いておきます。
目次
TL;DR
Template を使うとパラメーターだけ変えてほぼ同じ条件のビルド定義を簡単に複数作成できるのでおすすめ。
単一ビルドの場合はまとめ上げる単位によってはメリットが薄くなるので、逐次書いたほうがむしろわかりやすく選択しない選択が妥当なことが多い。(なんでもやればいいってものじゃない。)
サンプルリポジトリ
GitHub にサンプル構成を用意してあります。
リポジトリの内容に沿って説明します。
github.com
Azure DevOps Pipeline Build の構造
Azure DevOps Pipeline Build は、Stages/Jobs/Steps の3つを使って構造を分割しています。
構造を知っておくとTemplateの範囲が予想できるので、先にこれを簡単に説明しますがStages と Jobs は普段使っていると出てこないことが多いと思います。
AzureDevOps のWeb UI で自動生成される azure-pipelines.yml
でも省略されていることからも察してください。(つまり読み飛ばしてok)
trigger:
- master
pool:
vmImage: 'ubuntu-latest'
steps:
- script: echo Hello, world!
displayName: 'Run a one-line script'
- script: |
echo Add other tasks to build, test, and deploy your project.
echo See https://aka.ms/yaml
displayName: 'Run a multi-line script'
構造に興味ある人はYAML定義のリンクに詳細があります。
docs.microsoft.com
パイプラインの最も大きな分割単位で、複数のJobs を持つことができます。"build this app", "run these tests", and "deploy to pre-production" はわかりやすい分割例です(まとめてやらないだろうと思いつつ)
Stage の構造は次のとおりですが、正直 Stages はまずかかないです。
stages:
- stage: string
displayName: string
dependsOn: string | [ string ]
condition: string
variables: { string: string } | [ variable | variableReference ]
jobs: [ job | templateReference]
もしYAMLでstages: を指定しなくても、暗黙的に1つのstageが割り当てられて実行されます。(これも書かない要因です)
stages:
- stage: A
jobs:
- job: A1
- job: A2
- stage: B
jobs:
- job: B1
- job: B2
Stage は、ポーズしたり各種チェックやStage間の依存関係を指定することもできます。
stages:
- stage: string
dependsOn: string
condition: string
これを利用して fan-in/fan-out も組めます。
stages:
- stage: Test
- stage: DeployUS1
dependsOn: Test
- stage: DeployUS2
dependsOn: Test
- stage: DeployEurope
dependsOn:
- DeployUS1
- DeployUS2
Stage レベルで変数を持って、Job や Step で利用することもできます。
Jobs は Steps(実際にやる処理) の塊で中のStep が直列で実行されます。
Jobs の構造は次のとおりです。この Jobs もまず書かないです。
jobs:
- job: string
displayName: string
dependsOn: string | [ string ]
condition: string
strategy:
matrix:
parallel:
maxParallel: number
continueOnError: boolean
pool: pool
workspace:
clean: outputs | resources | all
container: containerReference
timeoutInMinutes: number
cancelTimeoutInMinutes: number
variables: { string: string } | [ variable | variableReference ]
steps: [ script | bash | pwsh | powershell | checkout | task | templateReference ]
services: { string: string | container }
もしYAMLでjobs: を指定しなくても、暗黙的に1つのjobが割り当てられて実行されます。(これも書かない要因です)
YAML に直接 container
と書いて、Docker Hub のイメージを使うことがあると思います。
あるいは、strategy
と書いて Matrix Build していることがあると思います。
これらはJobの機能を使っているということです。
pool:
vmImage: 'ubuntu-16.04'
strategy:
matrix:
ubuntu14:
containerImage: ubuntu:14.04
ubuntu16:
containerImage: ubuntu:16.04
ubuntu18:
containerImage: ubuntu:18.04
container: $[ variables['containerImage'] ]
steps:
- script: printenv
1つのビルドエージェントは1つのjob を実行できます。
そのため、並列にジョブを実行したい場合は、並列度の分だけエージェントが必要になります。
例えばこれは直列実行になります。
jobs:
- job: Debug
steps:
- script: echo hello from the Debug build
- job: Release
dependsOn: Debug
steps:
- script: echo hello from the Release build
これは依存を互いに持たないので並列実行です。
jobs:
- job: Windows
pool:
vmImage: 'vs2017-win2016'
steps:
- script: echo hello from Windows
- job: macOS
pool:
vmImage: 'macOS-10.13'
steps:
- script: echo hello from macOS
- job: Linux
pool:
vmImage: 'ubuntu-16.04'
steps:
- script: echo hello from Linux
多くの場合は、matrixで自動生成された job を 最大並列度を指定したりすることが多いでしょう。
jobs:
- job: BuildPython
strategy:
maxParallel: 2
matrix:
Python35:
PYTHON_VERSION: '3.5'
Python36:
PYTHON_VERSION: '3.6'
Python37:
PYTHON_VERSION: '3.7'
Job レベルで変数を持って、Step で利用することもできます。
steps では、一連の実際にやりたい処理をstepとして定義します。job の中に、1つのsteps があるはずです。(CircleCI や TravisCI、drone.ioなど他のCIサービスでやるときと同じですね。)
steps:
- script: echo This runs in the default shell on any machine
- bash: |
echo This multiline script always runs in Bash.
echo Even on Windows machines!
- task: ProvidedUsefulTask
ParamA: "This multiline script always runs in PowerShell Core."
ParamB: "Even on non-Windows machines!"
Azure DevOps は stepごとに1つのプロセスを起動して実行します。そのため、プロセス環境変数はstep間で共有されず、ファイルシステムやUser/Machineレベルの環境変数、Stages/Jobs/Steps ごとの変数で共有できます。
steps では、CIのために複数のステップを書くことになります。
中には1つのCIで10step 書くこともあるでしう。
steps:
- script: OpA
- script: OpB
- script: OpC
- script: OpD
- script: OpE
- script: OpF
- script: OpG
- script: OpH
- script: OpI
- script: OpJ
同じ処理を条件ごとに渡すパラメーターを変えて実行する
もし一度のトリガーで実行されたPipelineで、複数組み合わせのパラメーターで実行する場合 matrix を使うと思います。
matrixを使えば、自動的にmatrix定義した条件ごとにJobが生成され実行されるので、steps を複数書く必要がありません。
matrix:
linux:
imageName: 'ubuntu-16.04'
mac:
imageName: 'macos-10.13'
windows:
imageName: 'vs2017-win2016'
pool:
vmImage: $(imageName)
steps:
- task: NodeTool@0
inputs:
versionSpec: '8.x'
- script: |
npm install
npm test
- task: PublishTestResults@2
inputs:
testResultsFiles: '**/TEST-RESULTS.xml'
testRunTitle: 'Test results for JavaScript'
- task: PublishCodeCoverageResults@1
inputs:
codeCoverageTool: Cobertura
summaryFileLocation: '$(System.DefaultWorkingDirectory)/**/*coverage.xml'
reportDirectory: '$(System.DefaultWorkingDirectory)/**/coverage'
- task: ArchiveFiles@2
inputs:
rootFolderOrFile: '$(System.DefaultWorkingDirectory)'
includeRootFolder: false
- task: PublishBuildArtifacts@1
しかし、特定のパスやブランチをフックしてビルドするときなど、matrix が使えない条件で同じ処理でパラメーターだけ違うものを書くこともあります。
// azure-pipelines-debug.yml
steps:
- script: OpA
- script: OpB -c Debug
- script: OpC -c Debug
- script: OpD
// azure-pipelines-release.yml
steps:
- script: OpA
- script: OpB -c Release
- script: OpC -c Release
- script: OpD
こういった「渡すパラメーターだけが違って処理は同じ」時に便利なのが、Templates機能 です。
Templates
Templates は単純にいうと、処理を別YAMLに定義しておいて呼び出す機能です。呼び出すときにパラメーターを渡すことができるため、Templateでパラメーターを使って処理を書くことでパラメーターごとに同じ処理を呼び出すことができます。
YAML schema/Template Reference - Azure Pipelines | Microsoft Docs
CircleCI だと、Commands を外部YAMLに定義して利用するイメージに近いでしょう。
circleci.com
Templates は Stages/Jobs/Steps あとはVariables(変数) として持つことができます。一番使われやすい「JobとStepを取りまとめた」例でTemplateを見てみます。
docs.microsoft.com
まずはわかりやすい例で、npm の install/test だけを外に出してみましょう。
template を使わないときに次のように書いているイメージです。
jobs:
- job: macOS
pool:
vmImage: 'macOS-10.13'
steps:
- script: npm install
- script: npm test
- job: Linux
pool:
vmImage: 'ubuntu-16.04'
steps:
- script: npm install
- script: npm test
- job: Windows
pool:
vmImage: 'vs2017-win2016'
steps:
- script: npm install
- script: npm test
- script: sign
step で同じことをしているbuild/test処理を1つのYAMLに書き出して テンプレート化します。仮にsteps/build.yml
に置きます。
steps:
- script: npm install
- script: npm test
あとは、先程のazure-pipelines.yml で template
を使ってテンプートにしたbuild.ymlファイルを呼び出すだけです。
jobs:
- job:
displayName: macOS
pool:
vmImage: 'macOS-10.13'
steps:
- template: steps/build.yml
- job
displayName: Linux
pool:
vmImage: 'ubuntu-16.04'
steps:
- template: steps/build.yml
- job:
displayName: Windows
pool:
vmImage: 'vs2017-win2016'
steps:
- template: steps/build.yml
- script: sign
パラメーターを利用する
先程の処理で、Job の環境もほぼ同じに見えます。
Tempalte を呼び出すときにパラメーターを渡して共通化してみましょう。
パラメーターは、parameters
で定義して、${{ parameters.YOUR_PARAMETER_KEY }}
でテンプレート内部で参照します。
parameters:
name: ''
vmImage: ''
jobs:
- job:
displayName: ${{ parameters.name }}
pool:
vmImage: ${{ parameters.vmImage }}
steps:
- script: npm install
- script: npm test
実際にTemplate の利用時にパラメーターを渡すには、template: を指定したときにparameters
: で定義したパラメーターキー:渡したい値
とします。
jobs:
- template: templates/npm-with-params.yml
parameters:
name: Linux
vmImage: 'ubuntu-16.04'
- template: templates/npm-with-params.yml
parameters:
name: macOS
vmImage: 'macOS-10.13'
- template: templates/npm-with-params.yml
parameters:
name: Windows
vmImage: 'vs2017-win2016'
Steps でTemplateを利用する
先程 Job sでみてみましたが、step で利用するときも同様です。
「追加テストを一部だけでやりたい」時に、Template利用時に true/false を渡して実行するか決めてみます。
初期値だけ設定しておかないといけないことに気をつけてください。
parameters:
runExtendedTests: 'false'
steps:
- script: npm test
- ${{ if eq(parameters.runExtendedTests, 'true') }}:
- script: npm test --extended
Template の参照時に、true を渡したときだけ実行されます。
steps:
- script: npm install
- template: templates/steps-with-params.yml
parameters:
runExtendedTests: 'true'
サンプル
ASP.NET Core でビルドするときに、dotnet と docker の両方を用意してみました。
github.com
steps/dotnetcore_publish.yml
にdotnet でビルドするものを用意しておきます。
parameters:
ProjectName: ''
ExtraBuildArguments: ''
BuildConfiguration: 'Debug'
DotNetCoreInstall: '2.2.100'
steps:
- task: DotNetCoreInstaller@0
inputs:
packageType: 'sdk'
version: '${{ parameters.DotNetCoreInstall }}'
- task: DotNetCoreCLI@2
displayName: 'dotnet restore'
inputs:
command: restore
projects: '${{ parameters.ProjectName }}/**/*.csproj'
- task: DotNetCoreCLI@2
displayName: 'dotnet publish'
inputs:
command: publish
publishWebProjects: false
projects: '${{ parameters.ProjectName }}/**/*.csproj'
arguments: '-c ${{ parameters.BuildConfiguration }} -o $(Build.ArtifactStagingDirectory)'
zipAfterPublish: false
modifyOutputPath: false
あとは呼び出すだけです。気にするのはプロジェクト名のみなのは、チーム内のテンプレートとしては便利です。
trigger:
batch: true
branches:
include:
- '*'
jobs:
- job: build
displayName: dotnet core (Debug)
pool:
name: Hosted Ubuntu 1604
steps:
- checkout: self
- template: steps/dotnetcore_publish.yml
parameters:
ProjectName: 'WebApplication'
BuildConfiguration: 'Debug'
- task: PublishBuildArtifacts@1
displayName: 'Publish Artifact: drop'
同様に、Azure Container Regisotry に docker build > push するときもかけます。docker タスクでのビルドがめちゃめちゃ使いにくいのでちょっと楽です。
parameters:
Subscription: ''
SubscriptionId: ''
Registry: ''
DockerImageName: ''
DockerFile: ''
ResourceGroup: ''
ImageName: ''
steps:
- bash: 'docker build -t ${{ parameters.Registry }}.azurecr.io/${{ parameters.DockerImageName }}:$(Build.SourceVersion)_$(Build.BuildId) -t ${{ parameters.Registry }}.azurecr.io/${{ parameters.DockerImageName }}:latest -f ${{ parameters.DockerFile }} .'
displayName: 'docker build'
- task: Docker@0
displayName: 'Push an image'
inputs:
azureSubscription: ${{ parameters.Subscription }}
azureContainerRegistry: '{"loginServer":"${{ parameters.Registry }}.azurecr.io", "id" : "/subscriptions/${{ parameters.SubscriptionId }}/resourceGroups/${{ parameters.ResourceGroup }}/providers/Microsoft.ContainerRegistry/registries/${{ parameters.Registry }}"}'
action: 'Push an image'
imageName: '${{ parameters.ImageName }}'
includeLatestTag: true
あとは呼び出すだけです。
実際のプロジェクトでは、Subscription や SubscriptionIdは埋めてしまっていいでしょう。
trigger:
batch: true
branches:
include:
- "*"
jobs:
- job: build
displayName: web docker build (dev)
pool:
name: Hosted Ubuntu 1604
steps:
- checkout: self
- template: steps/docker_push.yml
parameters:
Subscription: 'your_azure_subscription'
SubscriptionId: '12345-67890-ABCDEF-GHIJKL-OPQRSTU'
Registry: 'your_azure_container_registry_name'
DockerImageName: 'webapplication'
DockerFile: 'WebApplication/Dockerfile'
ResourceGroup: 'your_azure_resource_group'
ImageName: 'webapplication:$(Build.SourceVersion)_$(Build.BuildId)'
Refs
YAML schema - Azure Pipelines | Microsoft Docs
Stages in Azure Pipelines - Azure Pipelines | Microsoft Docs
Jobs in Azure Pipelines, Azure DevOps Sever, and TFS - Azure Pipelines | Microsoft Docs
Job and step templates - Azure Pipelines | Microsoft Docs