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 にサンプル構成を用意してあります。 リポジトリの内容に沿って説明します。
Azure DevOps Pipeline Build の構造
Azure DevOps Pipeline Build は、Stages/Jobs/Steps の3つを使って構造を分割しています。 構造を知っておくとTemplateの範囲が予想できるので、先にこれを簡単に説明しますがStages と Jobs は普段使っていると出てこないことが多いと思います。
AzureDevOps のWeb UI で自動生成される azure-pipelines.yml
でも省略されていることからも察してください。(つまり読み飛ばしてok)
# Starter pipeline # Start with a minimal pipeline that you can customize to build and deploy your code. # Add steps that build, run tests, deploy, and more: # https://aka.ms/yaml 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定義のリンクに詳細があります。
Stages
パイプラインの最も大きな分割単位で、複数のJobs を持つことができます。"build this app", "run these tests", and "deploy to pre-production" はわかりやすい分割例です(まとめてやらないだろうと思いつつ)
Stage の構造は次のとおりですが、正直 Stages はまずかかないです。
stages: - stage: string # name of the stage, A-Z, a-z, 0-9, and underscore displayName: string # friendly name to display in the UI 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 # this stage runs after Test - stage: DeployUS2 dependsOn: Test # this stage runs in parallel with DeployUS1, after Test - stage: DeployEurope dependsOn: # this stage runs after DeployUS1 and DeployUS2 - DeployUS1 - DeployUS2
Stage レベルで変数を持って、Job や Step で利用することもできます。
Jobs:
Jobs は Steps(実際にやる処理) の塊で中のStep が直列で実行されます。
Jobs の構造は次のとおりです。この Jobs もまず書かないです。
jobs: - job: string # name of the job, A-Z, a-z, 0-9, and underscore displayName: string # friendly name to display in the UI dependsOn: string | [ string ] condition: string strategy: matrix: # matrix strategy, see below parallel: # parallel strategy, see below maxParallel: number # maximum number of agents to simultaneously run copies of this job on continueOnError: boolean # 'true' if future jobs should run even if this job fails; defaults to 'false' pool: pool # see pool schema workspace: clean: outputs | resources | all # what to clean up before the job runs container: containerReference # container to run this job inside timeoutInMinutes: number # how long to run the job before automatically cancelling cancelTimeoutInMinutes: number # how much time to give 'run always even if cancelled tasks' before killing them variables: { string: string } | [ variable | variableReference ] steps: [ script | bash | pwsh | powershell | checkout | task | templateReference ] services: { string: string | container } # container resources to run as a service 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
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に定義して利用するイメージに近いでしょう。
Templates は Stages/Jobs/Steps あとはVariables(変数) として持つことができます。一番使われやすい「JobとStepを取りまとめた」例でTemplateを見てみます。
まずはわかりやすい例で、npm の install/test だけを外に出してみましょう。 template を使わないときに次のように書いているイメージです。
# File: azure-pipelines.yml 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 # Extra step on Windows only
step で同じことをしているbuild/test処理を1つのYAMLに書き出して テンプレート化します。仮にsteps/build.yml
に置きます。
# File: steps/build.yml steps: - script: npm install - script: npm test
あとは、先程のazure-pipelines.yml で template
を使ってテンプートにしたbuild.ymlファイルを呼び出すだけです。
# File: azure-pipelines.yml jobs: - job: displayName: macOS pool: vmImage: 'macOS-10.13' steps: - template: steps/build.yml # Template reference - job displayName: Linux pool: vmImage: 'ubuntu-16.04' steps: - template: steps/build.yml # Template reference - job: displayName: Windows pool: vmImage: 'vs2017-win2016' steps: - template: steps/build.yml # Template reference - script: sign # Extra step on Windows only
パラメーターを利用する
先程の処理で、Job の環境もほぼ同じに見えます。 Tempalte を呼び出すときにパラメーターを渡して共通化してみましょう。
パラメーターは、parameters
で定義して、${{ parameters.YOUR_PARAMETER_KEY }}
でテンプレート内部で参照します。
# File: templates/npm-with-params.yml parameters: name: '' # defaults for any parameters that aren't specified vmImage: '' jobs: - job: displayName: ${{ parameters.name }} pool: vmImage: ${{ parameters.vmImage }} steps: - script: npm install - script: npm test
実際にTemplate の利用時にパラメーターを渡すには、template: を指定したときにparameters
: で定義したパラメーターキー:渡したい値
とします。
# File: azure-pipelines.yml jobs: - template: templates/npm-with-params.yml # Template reference parameters: name: Linux vmImage: 'ubuntu-16.04' - template: templates/npm-with-params.yml # Template reference parameters: name: macOS vmImage: 'macOS-10.13' - template: templates/npm-with-params.yml # Template reference parameters: name: Windows vmImage: 'vs2017-win2016'
Steps でTemplateを利用する
先程 Job sでみてみましたが、step で利用するときも同様です。
「追加テストを一部だけでやりたい」時に、Template利用時に true/false を渡して実行するか決めてみます。 初期値だけ設定しておかないといけないことに気をつけてください。
# File: templates/steps-with-params.yml parameters: runExtendedTests: 'false' # defaults for any parameters that aren't specified steps: - script: npm test - ${{ if eq(parameters.runExtendedTests, 'true') }}: - script: npm test --extended
Template の参照時に、true を渡したときだけ実行されます。
# File: azure-pipelines.yml steps: - script: npm install - template: templates/steps-with-params.yml # Template reference parameters: runExtendedTests: 'true'
サンプル
ASP.NET Core でビルドするときに、dotnet と docker の両方を用意してみました。
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