やる気駆動型エンジニアの備忘録

WPF(XAML+C#)の話題を中心に.NET/Android/CI やたまに趣味に関するブログです

(Azure Pipelines) ビルドパイプラインの使用するリソースの値を表示する

Azure Pipelines ではパイプライン外でresourcesとして定義しているリソースの値があります。 例えば、repositoriesを使用すると別サービスのリポジトリをチェックアウトすることも可能になっており、他にもpipelinescontainersなどを利用することができます。

<resourcesスキーマ>

resources:
  pipelines: [ pipeline ]  
  builds: [ build ]
  repositories: [ repository ]
  containers: [ container ]
  packages: [ package ]
  webhooks: [ webhook ]

スキーマは以下のリンクから抜粋しています。

docs.microsoft.com

これらの値を定義することは簡単なのですが、どのようなプロパティがあり、どのような値が設定されているのかはドキュメントを読んでもイマイチピンときません。 今回は偶然?このリソースの値を表示する方法を見つけたので備忘録として残しておきます。

リソースって何?って方は以下のリンクを読んでください。

docs.microsoft.com

リソースの値を参照するyaml

結論から先に。 リソースの値を参照するには yaml で以下のように各リソースを JSON へ変換します。

variables:
  pipelineVar: $[ convertToJson(Pipeline) ]            # resources.pipeline properties
  resourcesVar: $[ convertToJson(resources) ]       # resources properties
  variablesData: $[ convertToJson(variables) ]       # variables properties

定義する場所は yaml のルートでもいいですし、stagejob配下でもいいです。

これを定義したビルドを実行すると、以下のようにログ画面からジョブ名→Job preparation parametersを選択するとリソースの値が JSON 形式で表示されます。

f:id:iyemon018:20211017174217p:plain

実際には以下のような値が表示されます。 今回はresources - repositoriesで別リポジトリを参照している yaml を利用しました。 なので、他のリソースの値は表示されていません。pipelinescontainersを設定している場合は同様にログに出力されます。

Job preparation parameters
Variables:
  variablesData:
    Parsing expression: <convertToJson(variables)>
    Evaluating: convertToJson(variables)
    Result: '{
  "resources.triggeringCategory": "",
  "resources.triggeringAlias": "",
  "variablesData": "$[ convertToJson(variables) ]",
  "pipelineVar": "$[ convertToJson(Pipeline) ]",
  "resourcesVar": "$[ convertToJson(resources) ]",
  "system": "build",
  "system.hosttype": "build",
  "system.servertype": "Hosted",
  "system.culture": "en-US",
  "system.collectionId": "xxxxxxx-xxxxxxx-xxxxx-xxxx-xxxx",
  "system.collectionUri": "https://dev.azure.com/<Organization>/",
  "system.teamFoundationCollectionUri": "https://dev.azure.com/<Organization>/",
  "system.taskDefinitionsUri": "https://dev.azure.com/<Organization>/",
  "system.pipelineStartTime": "2021-10-14 23:33:56+09:00",
  "system.teamProject": "Azure Pipelines Learning",
  "system.teamProjectId": "xxxxxxx-xxxxxxx-xxxxx-xxxx-xxxx",
  "system.definitionId": "21",
  "build.definitionName": "Azure Pipelines Learning",
  "build.definitionVersion": "1",
  "build.queuedBy": "<User Name>",
  "build.queuedById": "xxxxxxx-xxxxxxx-xxxxx-xxxx-xxxx",
  "build.requestedFor": "<User Name>",
  "build.requestedForId": "xxxxxxx-xxxxxxx-xxxxx-xxxx-xxxx",
  "build.requestedForEmail": "<User unique name>",
  "build.sourceVersion": "xxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "build.sourceBranch": "refs/heads/master",
  "build.sourceBranchName": "master",
  "build.reason": "Manual",
  "system.pullRequest.isFork": "False",
  "system.jobParallelismTag": "Private",
  "system.enableAccessToken": "SecretVariable",
  "MSDEPLOY_HTTP_USER_AGENT": "VSTS_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
  "AZURE_HTTP_USER_AGENT": "VSTS_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
  "build.buildId": "1214",
  "build.buildUri": "vstfs:///Build/Build/1214",
  "build.buildNumber": "20211014.20",
  "build.containerId": "6470472",
  "system.isScheduled": "False",
  "system.definitionName": "Azure Pipelines Learning",
  "system.planId": "xxxxxxx-xxxxxxx-xxxxx-xxxx-xxxx",
  "system.timelineId": "xxxxxxx-xxxxxxx-xxxxx-xxxx-xxxx",
  "system.stageDisplayName": "__default",
  "system.stageId": "96ac2280-8cb4-5df5-99de-dd2da759617d",
  "system.stageName": "__default",
  "system.stageAttempt": "1",
  "system.phaseDisplayName": "",
  "system.phaseId": "d768f2aa-2c4b-5810-be30-11cbf757b796",
  "system.phaseName": "Job1",
  "system.phaseAttempt": "1"
}'
  pipelineVar:
    Parsing expression: <convertToJson(Pipeline)>
    Evaluating: convertToJson(Pipeline)
    Result: '{
  "startTime": "2021-10-14 23:33:56+09:00"
}'
  resourcesVar:
    Parsing expression: <convertToJson(resources)>
    Evaluating: convertToJson(resources)
    Result: '{
  "repositories": {
    "self": {
      "id": "xxxxxxx-xxxxxxx-xxxxx-xxxx-xxxx",
      "name": "Azure Pipelines Learning",
      "ref": "refs/heads/master",
      "type": "Git",
      "url": "https://dev.azure.com/<Organization>/Azure Pipelines Learning/_git/Azure Pipelines Learning"
    },
    "REPOS1": {
      "id": "xxxxxxx-xxxxxxx-xxxxx-xxxx-xxxx",
      "name": "Repos1",
      "ref": "refs/heads/master",
      "type": "git",
      "url": "https://dev.azure.com/<Organization>/Azure Pipelines Learning/_git/Repos1"
    }
  },
  "containers": {}
}'
ContinueOnError: False
TimeoutInMinutes: 60
CancelTimeoutInMinutes: 5
Expand:
  MaxConcurrency: 0

variables の値も表示できる

地味にありがたいのがsystembuildの値が表示されることですね。 variablesJSON で出力できるのでコンパイル時点で出力可能な変数については確認することができます。

ドキュメントに記載されていないプロパティも確認できる

例えば、resourcesのプロパティって何が定義されているのかドキュメントに記載されていません。 yaml を書く際のプロパティはスキーマのドキュメントにはあるのですが、値を参照するときの情報は皆無で必要になった場合は大変困ります。

このあたりにちょろっと.refsとかは書かれていますが、その程度です。

docs.microsoft.com

この手法を使えばreftype, urlなどが取得可能なことがわかります。

  resourcesVar:
    Parsing expression: <convertToJson(resources)>
    Evaluating: convertToJson(resources)
    Result: '{
  "repositories": {
    "self": {
      "id": "xxxxxxx-xxxxxxx-xxxxx-xxxx-xxxx",
      "name": "Azure Pipelines Learning",
      "ref": "refs/heads/master",
      "type": "Git",
      "url": "https://dev.azure.com/<Organization>/Azure Pipelines Learning/_git/Azure Pipelines Learning"
    },
    "REPOS1": {
      "id": "xxxxxxx-xxxxxxx-xxxxx-xxxx-xxxx",
      "name": "Repos1",
      "ref": "refs/heads/master",
      "type": "git",
      "url": "https://dev.azure.com/<Organization>/Azure Pipelines Learning/_git/Repos1"
    }
  },

取得できない値

  • Agent
  • Environment
  • Build, Systemの一部の値

定義済みの変数で上記の値は存在することはわかっていますが、この手法で参照することはできません。

docs.microsoft.com


variablesの各変数がどのような値を参照しているかなどは yaml の動作検証やバグ発生時に役立つかもしれません。 あとはresources - repositoriesで別リポジトリを参照している場合に、リポジトリの情報を通知したいシーンなどで、どのプロパティがどのような値か確認する場合にも利用できます(私の場合はこちらの調査で偶然この方法を見つけました)。

(Azure Pipelines)手動トリガー時に Agent.Name を指定して実行する方法

ビルドタスクを Self hosted な環境で実行する場合に、たまに手動トリガーを使うことがあります。 何らかの障害原因調査などで特定のエージェントに対してビルドを実行するようなケースはあります。 そんな要件を満たすにはランタイム パラメータを使用します。

ここでは、ランタイム パラメータを使用して手動トリガー実行時にエージェント名を指定する方法についてまとめています。

Azure Pipelines の Runtime Parameters については以下のリンクを参照してください。

docs.microsoft.com

通常の手動トリガー

手動トリガーを使用する場合、パイプラインを実行しようとしても Demands を入力することができないため、特定のエージェントで実行することはできません。 Classic editor だと Demands を設定できるんですが、yaml ではそういった手段は存在しません。

# azure-pipelines.yml

trigger: none

jobs:
- job:

  steps:
  - script: echo Hello, world!
    displayName: 'Run a one-line script'

シンプルな yaml ですが、手動でパイプラインを実行しようとするとこのように Demands を入力することはできません。

f:id:iyemon018:20211004233150p:plain

Runtime parameters

ランタイムパラメータは、パイプラインを実行する際に値を渡すことができます。 この機能を使うことでパイプラインの実行時にユーザーが動作を制御することができます。

イメージとしては以下のリンクが参考になると思います。

docs.microsoft.com

Runtime parameters を使用して Agent.Name を指定する

パイプライン実行時に Agent.Name を指定するには以下のように yaml を書き換えます。 ポイントはparametersdemands部分です。

# azure-pipelines.yml

trigger: none

parameters:
  - name: agentName
    displayName: エージェント名(Option)
    type: string
    default: 'any'

jobs:
- job:
  pool:
   name: Self hosted pool
   demands:
   - ${{ if ne(parameters.agentName, 'any') }}:
     - Agent.Name -equals ${{ parameters.agentName }}

  steps:
  - script: echo Hello, world!
    displayName: 'Run a one-line script'

今回は、通常であればエージェントを指定することがなく、特定のシチュエーションのみエージェントを指定するような想定にしました。 人間が使用することを考慮してdisplayNameで分かりやすい表示名をつけておきましょう。 日本語も使用できるのでわかりやすさ重視で良いと思います。

f:id:iyemon018:20211004234059p:plain

parameters.agentNameの初期値はanyとし、それ以外の値が入力されていればエージェント名と判断しています。 なお、ランタイムパラメータは必須入力のため、defaultに何も指定しなかったり空文字だと、以下のように Run ボタンが非活性になります。 分かりやすいdefaultの値を設定しておくのが良いでしょう。

f:id:iyemon018:20211004234252p:plain

ルートの demands でランタイムパラメータを使用することはできない

yaml のルートにあるpool - demandsでランタイムパラメータを使おうとしても以下のようなメッセージが出力されて実行できません。 何故かはよくわかってないのですが、必ずjobあるいはstage配下のdemadsを使用しましょう。

/azure-pipelines.yml (Line: 16, Col: 5): A mapping was not expected

# azure-pipelines.yml

trigger: none

parameters:
  - name: agentName
    displayName: エージェント名(Option)
    type: string

# ここに書いても動かない。
pool:
  name: Self hosted pool
  demands:
  - ${{ if ne(parameters.agentName, 'any') }}:
    - Agent.Name -equals ${{ parameters.agentName }}

jobs:
- job:

  steps:
  - script: echo Hello, world!
    displayName: 'Run a one-line script'

手動トリガーはリリース時などによく使用されると思いますが、状況に応じて動作を制御したい場合にランタイムパラメータは必須になってきます。 他にもvaluesで選択肢を予め設定しておくこともできるので、要件に合わせて柔軟に対応しましょう。

(Azure Pipelines) どういったデータがループ(each in)処理に対応しているのか検証してみる

Azure Pipelines で複数回登場するようなタスクなどを Template にまとめると再利用できるため大変便利です。 Template ではeach inという構文で反復処理(ループ処理)を行うことができます。 ただ、公式ドキュメントはあまり情報が記載されていないため、どういった方法でループ処理ができるのかよくわからないため、いくつかのパターンでループ処理ができるかどうかを検証してみました。

なお、公式ドキュメントは以下を確認してください。

docs.microsoft.com

parametersに文字列配列を渡してループさせる

テンプレート側のparametersに文字列配列を渡してループさせることができるかどうかを見てみます。

先に結論から言うとこちらは可能です。

反復処理が可能なのは配列やKey-value pairstepsjobsなど)となっており、これを利用することで繰り返しタスクの実行が可能になっています。 (以下参考)

Templates [Iterative insertion] - Azure Pipelines | Microsoft Docs

ループする場合は配列も可能ということですが、例えば文字列配列をparametersでテンプレートに渡してループ処理を実行することは可能なのでしょうか?

まずは通常よくあるサンプルから見ていきます。 次のようにiterative-string-template.ymlparametersで文字列の配列(実際にはobject型)を指定してループするような構成です。 非常に単純な構成で、これは成功します。

parameters:
  - name: values
    type: object
    default:
      - "Azure"
      - "Pipelines"
      - "Learning"

steps:
- ${{ each value in parameters.values }}:
  - script: echo "call ${{ value }}."
    displayName: script ${{ value }}
trigger: none

pool:
  vmImage: windows-latest

jobs:
- job: 
  steps:
  - template: iterative-string-template.yml

f:id:iyemon018:20210926171855p:plain

ただ、この構成だとループする要素の数が限られているため、可変長の要素をループさせたいような要件には対応できません。 可変長の要素をループさせるような場合は以下のようにすれば良いのでは?と思い、試してみました。

テンプレート側は以下のように書き換えました。 変わったのはparametersdefaultです。

parameters:
  - name: values
    type: object
    default: []

steps:
- ${{ each value in parameters.values }}:
  - script: echo "call ${{ value }}."
    displayName: script ${{ value }}

呼び出し元の yaml は、以下のようにparametersに文字列配列を指定するように変更しています。

trigger: none

pool:
  vmImage: windows-latest

jobs:
- job: 
  steps:
  - template: iterative-string-template.yml
    parameters:
      values: ["Azure", "Pipelines", "Learning"]

結果は以下のように成功しています。 スクリプトの呼び出しだけなのでパット見はわかりませんが…

f:id:iyemon018:20210926172912p:plain

ただし、これが成功するのはtypeを指定した場合のみ

こちらの方法はあるパターンにおいては失敗します。 それはテンプレート側でparameterstypeが指定されていない場合です。

具体的には以下のようなものです。

parameters:
  - values: []

steps:
- ${{ each value in parameters.values }}:
  - script: echo "call ${{ value }}."
    displayName: script ${{ value }}

この状態でパイプラインを実行すると以下のようなメッセージが出力されます。yaml Editor で Validate を実行した場合も同様です。

/LearningTemplate/iterative-string-template.yml (Line: 2, Col: 5): Unexpected value 'values'

f:id:iyemon018:20210926231824p:plain

複雑なデータを渡してループすることもできる

文字列だけなど単一のデータを渡すこともあれば、もっと複雑なデータを渡したいケースはあるかもしれません。 例えば key value pair のような構造のデータを渡してループさせることも可能です。

parameters:
  - name: keyValuePairs
    type: object
    default: []

steps:
- ${{ each keyValuePair in parameters.keyValuePairs }}:
    - script: echo "call ${{ keyValuePair.value.Value }}."
      displayName: script ${{ keyValuePair.value.Key }}
trigger: none

pool:
  vmImage: windows-latest

jobs:
- job: 
  steps:
  - template: iterative-string-template.yml
    parameters:
      keyValuePairs:
        pair1:
          Key: "number1"
          Value: "Azure"
        pair2:
          Key: "number2"
          Value: "Pipelines"
        pair3:
          Key: "number3"
          Value: "Learning"

azure-pipelines.ymlからは、KeyValueという構造の変数を複数定義しています。 このとき、keyValuePairs変数は配列のように扱われているようです。

iterative-string-template.ymlでは、eachディレクティブでkeyValuePairsの反復処理を行い、keyValuePair.valueとすることでpair1pair2のデータを参照しています。 つまり、keyValuePair.value.Valueはループ1回目はkeyValuePair.pair1.Valueを参照し、ループ2回目はkeyValuePair.pair2.Valueを参照していることになります。

データ構造が複雑になるとその分 yaml も可読性が下がりますが、こういった機能が必要になるケースもあるかもしれません。


テンプレートを利用して反復処理をしたいようなケースはよくあると思うので、実際に要件にあった方法が実現できるかどうかは検証してみなければわかりません。 今回試した方法であれば割と柔軟に色々なパターンに適応できると思います。

(Azure Pipelines) 複数リポジトリのチェックアウトステップを試してみた

Azure Pipelines では、yamlファイルが含まれているリポジトリとは異なるリポジトリをチェックアウトすることができます。 あるリポジトリが別のリポジトリに依存するような仕組みの場合、この機能を使う必要性があります。 私自身、この機能を使ったことがなかったので使い方をまとめます。

使い方

docs.microsoft.com

基本的には上記リンクを参照するのが良いです。 これを書いている時点では以下の Git ホスティングサービスに対応しているようです。(ただし、Azure DevOps Server の場合は Azure Repos のみ対応) Azure Repos の場合、別 Project や別 Organization のリポジトリについても認証さえできればチェックアウトすることができるようです。

  • Azure Repos
  • GitHub
  • GitHubEnterprise
  • Bitbucket Cloud

基本的な構文

複数リポジトリを利用する上で必要な定義がいくつかあります。 ひとつはresources、もうひとつはcheckoutです。 複数リポジトリをチェックアウトする上で必須なのがcheckoutで、それを利用するためにresourcesリポジトリ定義が必要になるイメージです。 スキーマはそれぞれこちらを読めばわかります。

docs.microsoft.com

docs.microsoft.com

resourcesは、複数定義することもできてそれぞれ別サービスからリポジトリをチェックアウトすることもできるようです。

resources:
  repositories:
  - repository: <yaml で参照するための名称>
    type: <Azure Repos の場合は`git`。他にも`github`も指定することができる>
    name: <リポジトリの名称。`プロジェクト名/リポジトリ名`で指定する>
    ref: <ブランチ名。`refs/heads/main`のように指定する>
    endpoint: <異なる Organization の場合はここに接続サービスの名称を記載する>
    trigger: <CI トリガーが必要な場合はここに定義する>

基本的なresourcesの構文としては、これで事足りそうです。 repositoryに指定する名称にはアルファベット大文字・小文字、数字とアンダースコア_のみ使用できます。 nameに指定するリポジトリ名は空白スペースが含まれていても問題ありません。プロジェクト名も同様です。

次にcheckoutです。 checkoutstepsごとに指定することができます。 -checkout: selfの場合は、自分自身、つまりyamlファイルのあるリポジトリを指定することになります。 別リポジトリを指定する場合は、resources- repository:で指定した名称を設定します。

チェックアウトパス

チェックアウト後にリポジトリがどこにクローンされるかについては以下のページに記載されています。

docs.microsoft.com

挙動としては、リポジトリが単体か複数かによって異なります。 単体か複数かの判断基準はyamlに定義されたcheckoutに定義したリポジトリの数に依存するようです。

単体リポジトリ構成では変数の$(Agent.BuildDirectory)のサブフォルダであるC:\agent\_work\1\sにチェックアウトされます。 複数リポジトリ構成の場合は、$(Agent.BuildDirectory)のサブフォルダにC:\agent\_work\1\<リポジトリ名>というフォルダが作られ、その配下にチェックアウトされます。 ここの<リポジトリ名>resources- repository:で指定した名称が使われます。

複数リポジトリ構成でチェックアウトパスがデフォルトのままでは都合が悪い場合、pathを指定することで別フォルダへチェックアウトすることができます。

初回のリポジトリチェックアウトの認証

リポジトリをチェックアウトする際に CI を初めて実行すると、チェックアウトするための承認が必要になります。 詳細は以下のリンクを参照してください。

docs.microsoft.com

初回だけの操作にはなりますが、手動で実施しなければならないため注意してください。

PR トリガーの挙動

PR(Pull Request) トリガーをブランチに指定していた場合、PR が作成されるとブランチの source と target を merge した結果が使用されます。

チェックアウトステップを使用した場合でも、PR トリガーを指定したリポジトリが merge した結果を使用するかどうか確認してみました。 イメージとしては以下のような構成です。

f:id:iyemon018:20210905221250p:plain

PR トリガーを指定するのが Repos1 リポジトリで、yamlファイルは Repos2 で管理しているような構成です。 master ブランチに対して feature/xxx のようなブランチを作り、Pull Request を作成するようにしています。

この場合でも、Repos1 PR トリガー実行時には feature/xxx と master を merge していました。

f:id:iyemon018:20210905221647p:plain


試してみた結果、システム全体で複数のリポジトリを使用する場合に想定されそうな運用パターンは結構網羅できていそうです。 具体的な挙動については実際に動かして確認するしかありませんが、すでにあるリポジトリに後から CI を構成するような場合は重宝しそうです。

コンデンサマイクを購入しました

在宅勤務が長引き Web 会議にもなれてきた頃、1日の会議の数が増加してきました。 これまでイヤフォン型のマイクを使ってたのですが会議が長時間に渡ると耳が痛くなるのでコンデンサマイクを購入しました。

購入したもの

購入したのはこちら↓↓↓

f:id:iyemon018:20210412171254j:plain:w500

これを選択した理由としてはこちら。

  • レビュー数が多く、Amazon 以外のレビューがそれなりにある
  • バイスの相性不一致で失敗してもギリ妥協できる金額

開封の儀

f:id:iyemon018:20210412171307j:plain:w500

f:id:iyemon018:20210412171315j:plain:w250

また、合わせてポップガードとマイクスタンドを購入しました。

KC グースネック 卓上マイクスタンド 固定型 ブラック MDS-4/BK

KC グースネック 卓上マイクスタンド 固定型 ブラック MDS-4/BK

  • 発売日: 2016/02/22
  • メディア: エレクトロニクス

f:id:iyemon018:20210412171324j:plain:w500

設置してみた

設置するとこのように。

今回購入したマイクスタンドは MDS-4/BK という蛇腹型のものになっており、折りたたみ式と比較して短い反面、かなり自由に曲げることが可能です。 私のデスクでは大型モニタを使っているため、折りたたみ式スタンドではちょっと邪魔になるかも?と思い、このスタンドにしたのです。

おかげでこのようにモニタの下部からマイクを通すことができました。

f:id:iyemon018:20210412171343j:plain:w250 f:id:iyemon018:20210412171350j:plain:w500f:id:iyemon018:20210412171357j:plain:w500

f:id:iyemon018:20210412171406j:plain:w400f:id:iyemon018:20210412171417j:plain:w400

使用感

よく使用するコミュニケーションツールは Microsoft Teams と Zoom ですが、どちらも使う分には全く問題ありません。 相手にどのように聞こえているかまではわからないですが、声が小さく聞こえたりこもったりということはないようです。 また、毎日 USB を抜き差ししていますが特に接触不良なども起きてません。

全体的にとても満足な結果となっています。

管理

コンデンサマイクは精密機械なので管理方法どうしたら良いのかな~、とググってたら本来はデジケーターを使うようです。 が、流石にそこまでお金は掛けられないのでもうちょっと簡易な方法を取りました。

f:id:iyemon018:20210412171426j:plain:w250 f:id:iyemon018:20210412171245j:plain:w500

CanDo で見つけたポーチにマイクを入れて、ダイソーで見つけたパスタケースにポーチごと入れてます。 また、湿気対策としてシリカゲルも一緒に入れてます。

唯一面倒なのは業務開始前にマイクを接続して、業務終了後に切断・ケースに戻す作業でしょうか。まぁ、仕方ないと割り切っています。


在宅勤務用にコンデンサマイクをお探しの方はご参考にしてみてください。