しばやん雑記

ASP.NET Core とメイドさんが大好きなフリーランスのプログラマーのブログ

Azure Pipelines でよく使う YAML のチートシートを作った

Azure Pipelines は YAML を使ってスクリプトを書くだけではなく、予め用意されたタスクを使って面倒な処理をシンプルに書くことが出来ますが、地味にはまるポイントも多いのでよく使う定義をメモしておきます。

YAML Schema 読めば大体は理解できるはずですが、多少ドキュメントが古いタスクがあったりします。

この間、ディレクトリを指す変数はどれを使った方が良いか調べたエントリもあるので、こっちも一緒に参照して貰えるとよりスムーズにビルドパイプラインを定義できると思います。

タスクで使われるベースとなるディレクトリについても、メモがてら調べて追記しておきました。これで悩まずにパス周りの定義を書くことが出来そうです。

他にも見つけたら追記するかもしれません。とりあえず自分がよく使う範囲で書いています。

タスクで共通な部分

ファイルのパターンマッチ

内部では minimatch を使っているみたいなので、ドキュメント曰く多くのパターンが使えるはずです。

よく使われるのは *** ぐらいかと思います。! を付けると否定になります。

# 1 行で指定するパターン
searchPatternPack: '**/*.csproj;-:**/*.Tests.csproj'

# 複数行で指定するパターン
projects: |
  **/*.csproj
  !**/*.Tests.csproj

1 行で指定するパターンは内部でレガシー扱いになってるようだったので、先行きが怪しいです。

ツールバージョンの指定

一部のタスクではバージョンを柔軟に指定できます。以下のパターンを知っておけば大体困らないです。

# .NET Core の例
version: '2.x'
version: '2.1.x'
version: '3.0.100-preview7-012821'

# Node / Python / Ruby / NuGet の例
# タスクによって version と versionSpec と差があるので注意
version: '10.6.x'
versionSpec: '>= 2.4'

微妙にプロパティ名が統一されていないのが不満です。

GitHub Tag トリガーを判別

Azure Pipelines の各 Stage / Job は condition で実行条件を指定できますが、GitHub の Tag が打たれた時だけ実行という条件を組むのは少し面倒です。

最近は変数を使ってタグかどうかを判別するようにすることが多いです。

trigger:
  branches:
    include:
    - master
  tags:
    include:
    - v*

variables:
  isGitHubTag: ${{ startsWith(variables['Build.SourceBranch'], 'refs/tags/v') }}

この時 isGitHubTag は true / false となるので、後は condition で参照します。

condition: and(succeeded(), eq(variables['isGitHubTag'], 'true'))

カスタム関数が書ければもっと上手く書けるはずですが、今はこれで妥協しておきます。

checkout 系

checkout を無効化

steps:
- checkout: none

submodule を同時に checkout する

パッと見 submodule は true / false かと思いきや、recursive も受け付けるので注意。

# 直下の submodule だけ checkout する場合
steps:
- checkout: self
  submodule: true

# 再帰的に submodule を checkout する場合
steps:
- checkout: self
  submodule: recursive

checkout する履歴を減らす

履歴が大きなリポジトリをチェックアウトする際に時間がかかるのを改善出来ます。

steps:
- checkout: self
  fetchDepth: 1

ツールインストール系

.NET Core をバージョン指定でインストール

インストールするバージョンを変数にして strategy / matrix で使うことも出来ます。

steps:
- task: UseDotNet@2
  inputs:
    packageType: 'sdk'
    version: '2.2.x'
  displayName: 'Install .NET Core SDK 2.2.x'
steps:
- task: UseDotNet@2
  inputs:
    packageType: 'sdk'
    version: '3.0.100-preview7-012821'
  displayName: 'Install .NET Core SDK 3.0.100-preview7-012821'

Node.js をバージョン指定でインストール

.NET Core や Node.js に限らず、言語系は Use*** というタスクに変更されました。

steps:
- task: UseNode@1
  inputs:
    version: '10.x'
  displayName: 'Install Node.js 10.x'

NuGet CLI をバージョン指定でインストール

NuGet CLI のデフォルトは 4.1.0 と古いので、新しいバージョンを入れておいた方が良いです。

steps:
- task: NuGetToolInstaller@1
  inputs:
    versionSpec: '5.0.x'
  displayName: 'Install NuGet CLI 5.0.x'

.NET Core CLI 系

アプリケーションを発行

ビルド設定と出力先ディレクトリは arguments で指定する必要があります。

ASP.NET Core の場合は publishWebProjects を設定しなくても問題ないですが、Azure Functions の場合は false を設定する必要があります。

variables:
  BuildConfiguration: 'Release'

steps:
- task: DotNetCoreCLI@2
  inputs:
    command: 'publish'
    publishWebProjects: false
    projects: '**/*.csproj'
    arguments: '-c $(BuildConfiguration)-o $(Build.SourcesDirectory)/dist'

普通に dotnet publish で実行するのとは違って、発行するプロジェクトをワイルドカードで指定したり、発行後のディレクトリを自動で zip にしたりと便利な機能が使えます。

NuGet パッケージを作成

outputDir のベースとなるディレクトリは $(Build.SourcesDirectory) です。相対パスが楽です。

variables:
  BuildConfiguration: 'Release'

steps:
- task: DotNetCoreCLI@2
  displayName: 'dotnet pack'
  inputs:
    command: pack
    searchPatternPack: '**/*.csproj;-:**/*.Tests.csproj'
    outputDir: 'dist'

.NET Core CLI ではなく NuGet CLI を使って作成する場合でも、あまり変わらないです。以下の例では nuspec から NuGet パッケージを作成しています。

steps:
- task: NuGetCommand@2
  displayName: 'nuget pack'
  inputs:
    command: 'pack'
    packagesToPack: '.\Demo.nuspec'
    packDestination: 'dist'
    basePath: '.\build'

この時の packDestination などのベースとなるディレクトリは $(Build.SourcesDirectory) です。

NuGet パッケージの公開

.NET Core CLI と NuGet CLI の両方でパッケージの公開を行えます。機能的にも差はないです。パッケージを探すパターンのベースとなるディレクトリは $(Build.SourcesDirectory) です。

steps:
- task: DotNetCoreCLI@2
  displayName: 'dotnet nuget push'
  inputs:
    command: push
    searchPatternPush: '**/*.nupkg'
    nugetFeedType: external
    externalEndPoint: NuGet
steps:
- task: NuGetCommand@2
  displayName: 'nuget push'
  inputs:
    command: push
    packagesToPush: '**/*.nupkg'
    nuGetFeedType: external
    publishFeedCredentials: NuGet

多少 NuGet CLI の方が古い印象を受けますが、やっていることは同じなので好きな方を使えば良いです。

コンテナー系

Container jobs を使う

Windows と Ubuntu の場合は Docker Image の内部でタスクを実行出来ます。

pool:
  vmImage: 'ubuntu-latest'

container: 'mcr.microsoft.com/dotnet/core/sdk:2.2'

使える Docker Image には多少の制限があるので、何でも使えるわけではないです。

具体的には Bash と glibc ベースで、Node.js が実行可能かつ ENTRYPOINT が無いイメージじゃないとダメなようです。なので Alpine ベースだと動きませんでした。

ファイル操作系

ファイルコピー

targetFolder のベースとなるディレクトリは $(Build.SourcesDirectory) です。

steps:
- task: CopyFiles@2
  inputs:
    contents: |
      dist/**/*
      package*.json
    targetFolder: 'dist'

パターンは除外も書けるのでなかなか強力です。VM Image に依存しないコピーとして便利に使えます。

アーカイブ作成

rootFolderOrFilearchiveFile のベースとなるディレクトリは $(Build.SourcesDirectory) です。

steps:
- task: ArchiveFiles@2
  inputs:
    rootFolderOrFile: 'dist'
    includeRootFolder: false
    archiveType: 'zip'
    archiveFile: '$(Build.BuildNumber).zip'

Pipeline Artifacts のことを考えると、全体的に $(Build.SourcesDirectory) へ統一した方が楽です。

Artifacts 系

Pipeline Artifacts へプッシュ

ベースとなるディレクトリは $(Build.SourcesDirectory) です。artifact を省略した場合は自動で "Stage 名 + Job 名" という名前が付けられます。

steps:
- publish: dist
  artifact: nupkg

Pipeline Artifacts からダウンロード

ベースとなるディレクトリは $(Pipeline.Workspace) となります。artifact を省略した場合は全ての Artifacts をダウンロードします。

steps:
- download: current
  artifact: nupkg

ちなみに Deployment jobs を使うと自動で行われるので便利です。

デプロイ系

Azure Web App にデプロイ

Windows の Web App へのデプロイ時には Run From Package を使うようにした方が、デプロイが Atomic なので安定します。パッケージは $(Pipeline.Workspace) から拾ってくるケースが大半だと思います。

steps:
- task: AzureWebApp@1
  inputs:
    azureSubscription: 'AzureRMConnection'
    appType: 'webApp'
    appName: 'deploy-test'
    package: '$(Pipeline.Workspace)/**/*.zip'
    deploymentMethod: 'runFromPackage'

Deployment Slot に対してデプロイする場合は slotName でデプロイ先スロットを指定すれば良いです。

ドキュメントなどではリソースグループ名が必要っぽく書いてますが、実際には不要でした。

steps:
- task: AzureWebApp@1
  inputs:
    azureSubscription: 'AzureRMConnection'
    appType: 'webApp'
    appName: 'deploy-test'
    slotName: 'staging'
    package: '$(Pipeline.Workspace)/**/*.zip'
    deploymentMethod: 'runFromPackage'

リソースグループ名の設定は地味に面倒なので、アプリ名とスロット名だけでデプロイ出来るのは楽です。

Azure Function にデプロイ

Web App へのデプロイとほとんど同じですが、タスクと appType が異なっています。

steps:
- task: AzureFunctionApp@1
  inputs:
    azureSubscription: 'AzureRMConnection'
    appType: 'functionApp'
    appName: 'deploy-test'
    package: '$(Pipeline.Workspace)/**/*.zip'
    deploymentMethod: 'runFromPackage'

Azure Function の場合も Run From Package を使うことで、Consumption での Cold Start にかかる時間を削減できますし、デプロイ自体もやはり安定するのでもはや必須です。