以下の内容はhttps://tech.guitarrapc.com/entry/2019/05/07/210549より取得しました。


Azure DevOpsのTemplateを利用してビルドする

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について書いておきます。

概要

Templateを使うとパラメーターだけ変えてほぼ同じ条件のビルド定義を簡単に複数作成できるのでおすすめ。 単一ビルドの場合はまとめ上げる単位によってはメリットが薄くなるので、逐次書いたほうがむしろわかりやすいこともあります。(なんでもやればいいってものじゃない。)

サンプルリポジトリ

GitHubにサンプル構成を用意してあります。 リポジトリの内容に沿って説明します。

https://github.com/guitarrapc/azuredevops-lab

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定義のリンクに詳細があります。

YAML schema reference for Azure Pipelines

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やTravis CI、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に定義して利用するイメージに近いでしょう。

https://circleci.com/docs/2.0/reusing-config/

TemplatesはStages/Jobs/Steps/Variables(変数) として持つことができます。一番使われやすい「JobとStepを取りまとめた」例でTemplateを見てみます。

Template usage reference

まずはわかりやすい例で、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の両方を用意してみました。

https://github.com/guitarrapc/azuredevops-lab

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




以上の内容はhttps://tech.guitarrapc.com/entry/2019/05/07/210549より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14