Azure DevOpsのPipelines ymlまとめ
記事
Last updated on

Azure DevOpsのPipelines ymlまとめ


Azure DevOps PipelinesのYAMLでよく使う書き方をまとめました。stages/jobs/stepsの構造、3種類の変数展開構文($()・${{}}・$[])の使い分け、ステージを跨いだ変数の受け渡し、テンプレートによる共通化、条件と依存関係、成果物の発行とダウンロード、AWS連携タスクまで。

目次

はじめに

Azure DevOps の Pipelines におけるよく使う処理・タスクの書き方をまとめます。(随時追加予定)

もともとは自分用の備忘メモとして書き始めたものですが、「あれどう書くんだっけ」と毎回同じところを調べ直していたので、前提の説明を足して記事の形に整理し直しました。

GitHub Actions を触ったことがある人なら構造はすぐ掴めると思いますが、変数まわりの構文が 3 種類あったり、ステージを跨いだ値の受け渡しにクセがあったりと、Azure DevOps 特有のハマりどころがそれなりにあります。この記事では、そのあたりを実際に踏んだ順に並べています。

MS のドキュメント、探しにくいんよな・・・

こんなんあるし、英語で探したほうがいいと思います。

日本語ドキュメントの品質に関する注意書き。機械翻訳である旨が表示されている

日本語ページは機械翻訳のものが多く、コード例の変数名まで翻訳されていて動かない、といったこともあります。挙動が怪しいと思ったら URL の ja-jpen-us に置き換えて原文を読む、これがいちばん早いです。この記事でも、日本語ページが特に酷い箇所は英語版のリンクを併記しています。

前提

Pipelineを動作させるVMは、ubuntu を想定しています。

Windows や Mac は対象外です。(動くかもしれませんが、私は未検証です。)

また、この記事は YAML パイプライン(Multi-stage pipelines)を前提にしています。GUI で組む Classic pipelines については触れません。

Pipeline の基本構造

まず全体像から。Azure DevOps の YAML パイプラインは stages > jobs > steps の 3 階層でできています。

階層単位実行環境
stageデプロイ単位など-
jobエージェント 1 台1 job = 1 エージェント。job ごとに作業ディレクトリは別
stepコマンド / タスク同一 job 内は同じエージェント上で直列実行

重要なのは job が変わるとエージェントが変わるという点です。job をまたぐとファイルもディレクトリも引き継がれません。ビルド結果を次の job に渡したいなら、後述する成果物(artifact)の発行とダウンロードが必要になります。

最小構成はこんな感じです。

trigger:
  - main

pool:
  vmImage: "ubuntu-latest"

steps:
  - script: echo Hello

stagesjobs を省略すると、暗黙的に 1 stage / 1 job として扱われます。逆に stage を複数書くときは、job も明示する必要があります。

stages:
  - stage: Build
    jobs:
      - job: BuildJob
        pool: { vmImage: "ubuntu-latest" }
        steps:
          - script: npm ci && npm run build
  - stage: Deploy
    jobs:
      - job: DeployJob
        pool: { vmImage: "ubuntu-latest" }
        steps:
          - script: ./deploy.sh

stage は書いた順に直列実行されるのがデフォルトです。並列にしたい場合や順序を変えたい場合は、後述の dependsOn を明示します。

なお、複数 job を同時に走らせるには並列実行数の枠が必要です。Private project では初期状態で枠がゼロのことがあるので、その場合は別途申請が必要になります(この話はAzure DevOpsのPrivate projectでMicrosoft-hosted agentsを動かすにまとめています)。

トリガーを設定する

何をきっかけに動かすか、の設定です。

# CIトリガー(push時)
trigger:
  branches:
    include:
      - main
      - release/*
    exclude:
      - feature/*
  paths:
    include:
      - src/*
    exclude:
      - docs/*

# PRトリガー
pr:
  branches:
    include:
      - main

# 定期実行
schedules:
  - cron: "0 18 * * *" # UTC。JSTなら-9時間して書く
    displayName: Nightly build
    branches:
      include:
        - main
    always: true # 前回から変更がなくても実行する

ハマりどころが 3 つあります。

  1. cron は UTC 固定です。 JST の朝 9 時に動かしたいなら 0 0 * * * になります。ここを間違えると 9 時間ズレたまま気付かないやつです。
  2. always: true を書かないと、前回実行時からソースに変更がない場合スキップされます。 「毎晩必ず動いてほしい」用途では必須です。
  3. Azure Repos Git では YAML の pr トリガーが効きません。 GitHub や Bitbucket Cloud では動きますが、Azure Repos の場合はリポジトリ側のブランチポリシー(Build validation)で設定する必要があります。YAML に書いたのに PR で動かない、という場合はまずここを疑ってください。

また、トリガー自体を無効化したい(手動実行や、他パイプラインからの呼び出し専用にしたい)場合は none を指定します。

trigger: none
pr: none

変数と 3 つの展開構文

個人的にいちばん最初につまずいたのがここです。Azure DevOps には変数を展開する構文が 3 種類あり、それぞれ評価されるタイミングが違います

構文名前評価タイミング主な用途
${{ }}テンプレート式コンパイル時(パイプライン展開時)パラメータ、if/each による構造の出し分け
$( )マクロ構文ランタイム(各タスクの実行直前)スクリプトやタスクの入力値
$[ ]ランタイム式ランタイム(ステージ/ジョブの開始時)conditionvariables への動的な代入

使い分けの目安はこうです。

  • タスクの inputs やスクリプト内で値がほしい → $( )
  • YAML の構造そのもの(ステップを出すか出さないか等)を変えたい → ${{ }}
  • 実行してみないと決まらない値を variablescondition に入れたい → $[ ]

具体例で見たほうが早いです。

parameters:
  - name: env
    type: string
    default: dev

variables:
  # コンパイル時に確定する。パラメータを埋め込む
  APP_ENV: ${{ parameters.env }}
  # 実行時に確定する。他ステージの出力を受け取る
  HAS_CHANGE: $[ stageDependencies.PreCheck.check.outputs['detect.HAS_CHANGE'] ]

steps:
  # コンパイル時に評価され、devのときだけこのステップ自体が生成されない
  - ${{ if ne(parameters.env, 'dev') }}:
      - script: echo "本番系の追加チェック"

  # 実行時に値が埋め込まれる
  - script: echo "env=$(APP_ENV) / changed=$(HAS_CHANGE)"

${{ if }}そもそもステップが生成されないのに対し、conditionステップは存在するがスキップされるという違いがあります。パイプラインの実行画面での見え方も変わるので、意図に合わせて選んでください。

変数のスコープ

variables は root / stage / job の 3 レベルで定義でき、下位のレベルが上位を上書きします。

variables:
  REGION: ap-northeast-1 # 全体で有効

stages:
  - stage: DeployUS
    variables:
      REGION: us-east-1 # このステージ内だけ上書き
    jobs:
      - job: Deploy
        variables:
          LOG_LEVEL: debug # このジョブ内だけ

シークレット変数はスクリプトから直接見えない

これも定番のハマりどころです。パイプライン変数の画面で「Keep this value secret」にした変数は、環境変数として自動的には渡されません$(MY_SECRET) をタスクの inputs に書くぶんには効きますが、スクリプト内で $MY_SECRET として参照したい場合は env で明示的にマッピングする必要があります。

- script: |
    echo "長さ: ${#MY_SECRET}"   # 中身はログに出せない(***にマスクされる)
  env:
    MY_SECRET: $(mySecretVariable) # ここで明示的に渡す

なお、シークレットの値はログ出力時に自動で *** にマスクされます。ただし Base64 にしたり文字を挟んだりすると素通りするので、そこは過信しないほうがいいです。

よく使う定義済み変数

変数内容
$(Build.SourceBranch)refs/heads/main のような完全なref名
$(Build.SourceBranchName)main のような短い名前
$(Build.SourceVersion)コミットハッシュ
$(Build.BuildId)実行ごとの一意なID。成果物のタグ付けなどに便利
$(Build.Reason)IndividualCI / PullRequest / Schedule / Manual
$(System.DefaultWorkingDirectory)ソースがチェックアウトされる先
$(Build.ArtifactStagingDirectory)成果物の一時置き場。実行ごとにクリーンされる
$(Agent.OS)Linux / Windows_NT / Darwin

$(Build.SourceBranch) を条件に使うときに main と比較して動かない、というのはよくあるミスです。比較するなら refs/heads/main か、$(Build.SourceBranchName) のほうを使ってください。

条件を追加する

ステージ、ジョブ、ステップのそれぞれで条件を追加できます。

- stage: Test
  dependsOn: [PreTest]
  condition: succeeded()
  variables:
    HAS_HOGE: $[ stageDependencies.PreTest.hoge.outputs['hogehoge.HAS_HOGE'] ]
  jobs:
    - job: Fuga
      pool: { vmImage: "ubuntu-latest" }
      condition: eq(variables['HAS_HOGE'], 'true') # ステージ跨ぎの変数をステージ条件には使えないようなので、ジョブ条件で使うパターン
      steps:
        - script: echo "run"
          condition: succeeded()

condition を書いた瞬間、暗黙の succeeded() は消える

これはハマると原因が分かりにくいので強調しておきます。

condition を指定しない場合、暗黙的に succeeded() が適用されます。つまり「前が失敗したら実行しない」が既定の挙動です。ところが自分で condition を書くと、この暗黙の succeeded() は上書きされて消えます

# NG: 前のステップが失敗していても、HAS_HOGEがtrueなら実行されてしまう
- script: ./deploy.sh
  condition: eq(variables['HAS_HOGE'], 'true')

# OK: 成功していることを明示的に条件へ入れる
- script: ./deploy.sh
  condition: and(succeeded(), eq(variables['HAS_HOGE'], 'true'))

「ビルドがコケてるのにデプロイが走った」の大半はこれが原因です。

よく使う条件関数

関数内容
succeeded()依存先がすべて成功
failed()依存先が失敗
always()失敗・キャンセル問わず常に実行。後片付け用
succeededOrFailed()キャンセル以外。テスト結果の発行などに
canceled()キャンセルされた
eq() / ne()等価 / 非等価
and() / or() / not()論理演算
contains() / startsWith()部分一致 / 前方一致
in() / notIn()複数値との一致判定

後片付けや、失敗時もテスト結果だけは残したい、といったケースでは always() が定番です。

- task: PublishTestResults@2
  condition: always() # テストがコケてもレポートは発行する
  inputs:
    testResultsFiles: "**/junit.xml"

条件に使用する式(eq、ne など)はこちらを参照

日本語ページは酷い。

ステージに依存関係を追加する

dependsOn を書かない場合、stage は定義順に直列実行されます。明示すると順序を組み替えたり、並列にしたりできます。

stages:
  - stage: Test
  - stage: DeployUS1
    dependsOn: Test # Testの後に実行
  - stage: Test2
    dependsOn: [] # 空配列で「依存なし」=Testと同時に開始できる
  - stage: DeployUS2
    dependsOn: [DeployUS1, Test2] # 複数指定OK。両方の完了を待つ

dependsOn: [](空配列)が地味に便利です。既定の直列実行を打ち消して、そのステージを先頭から並列に走らせられます。

依存関係は job にも同じように書けます。同一ステージ内で job を並列に走らせつつ、最後に集約する、といった構成が組めます。

なお、dependsOn後述する変数の受け渡しの前提条件でもあります。値がほしいだけで実行順に興味がない場合も、依存を張らないと参照できません。

変数を別ステージで使いたい

スクリプトの中で計算した値を、後続のステージやジョブで使いたいケースです。

出力方法

##vso[...] というログコマンドを標準出力に流すことで、エージェントに変数を設定させます。

- job: hoge
  steps:
    - bash: |
        HOGE1="1"
        echo "##vso[task.setVariable variable=HOGE2;isOutput=true]$HOGE1"
      name: hogehoge # ★ステップにnameが必要

isOutput=trueが重要になります。

これがないと別ステージで参照できません。

あわせて、変数を出力するステップには name を付ける必要があります。参照側で outputs['ステップ名.変数名'] と書くため、名前がないと指定できません。ここが抜けていて参照できない、というのが一番多いミスだと思います。

isOutput=true を付けなかった場合は、同じジョブ内の後続ステップからのみ $(HOGE2) で参照できます。用途に応じて使い分けてください。

なお、setVariable を実行したステップ自身の中では、まだその変数は使えません。反映されるのは次のステップからです。

参照方法(ステージを跨ぐ場合)

- stage: TEST
  dependsOn: HOGE # 依存指定がないと取得されない
  variables:
    HOGE2: $[ stageDependencies.STAGE_NAME.JOB_NAME.outputs['task_name.HOGE2'] ]

STAGE_NAME、JOB_NAME は変数出力した stage と job の名称を指定します。task_name は上の例でいう hogehoge、つまりステップに付けた name です。

参照方法(同一ステージ内の別ジョブ)

同じステージ内の別ジョブから参照する場合は、stageDependencies ではなく dependencies を使います。ここも間違えやすいポイントです。

- stage: Build
  jobs:
    - job: hoge
      steps:
        - bash: echo "##vso[task.setVariable variable=HOGE2;isOutput=true]1"
          name: hogehoge
    - job: fuga
      dependsOn: hoge
      variables:
        HOGE2: $[ dependencies.hoge.outputs['hogehoge.HOGE2'] ]
      steps:
        - script: echo $(HOGE2)
参照元構文
同一ジョブ内$(HOGE2)isOutput 不要)
同一ステージの別ジョブ$[ dependencies.JOB.outputs['STEP.VAR'] ]
別ステージ$[ stageDependencies.STAGE.JOB.outputs['STEP.VAR'] ]

値が空になるときのチェックリスト

  • 参照側のステージ/ジョブに dependsOn を書いたか
  • 出力側のステップに name を付けたか
  • isOutput=true を付けたか
  • $[ ] を使っているか($( ) では評価されない)
  • $[ ] が値の全体になっているか(prefix-$[ ... ] のような書き方は不可)
  • 出力元のジョブがスキップされていないか(スキップされると値は空文字になる)

最後の項目は特に厄介で、条件でスキップされたジョブの出力を参照すると、エラーにはならず静かに空文字が入ります。参照側で空チェックを入れておくと安全です。

yml を分割し、共通化したい

使いまわしたいときに。

テンプレートには大きく 2 つの使い方があります。

  • extends: パイプライン全体をテンプレートに委ねる。呼び出し側はパラメータだけを渡す
  • template によるインクルード: steps / jobs / stages の一部だけを差し込む

extends(パイプライン全体を共通化)

参照元

extends:
  template: template/common-cicd.yml # 参照元から見たパス
  parameters:
    val1: "hoge"

参照先

parameters:
  - name: val1
    type: string

variables:
  VAL1: ${{ parameters.val1 }}

stages:
  - stage: TEST_STAGE
# 以降省力

パラメータを渡せるので、環境によってアクセス先などの設定を変えたい場合に使えます。

extends を使うと、呼び出し側の YAML には extends 以外をほとんど書けなくなります。これは制約であると同時に、呼び出し側が勝手なステップを差し込めないという利点でもあり、組織のセキュリティポリシーを強制する用途で使われることがあります。

template(部分的に差し込む)

一部のステップだけ共通化したい場合はこちら。

# steps/setup-node.yml
parameters:
  - name: nodeVersion
    type: string
    default: "20"

steps:
  - task: NodeTool@0
    inputs:
      versionSpec: ${{ parameters.nodeVersion }}
  - script: npm ci
# 呼び出し側
steps:
  - template: steps/setup-node.yml
    parameters:
      nodeVersion: "22"

steps / jobs / stages の各レベルで同じように書けます。ただし、差し込む階層と、テンプレート側のトップレベルキーは一致している必要がありますsteps: の下に jobs を定義したテンプレートは差し込めません。

パラメータの型

parameters には型が指定できます。string / number / boolean / object のほか、stepListjobList といった構造そのものを渡せる型もあります。

parameters:
  - name: envName
    type: string
    values: [dev, stg, prod] # 値を限定できる(実行画面でプルダウンになる)
  - name: runTest
    type: boolean
    default: true
  - name: regions
    type: object
    default:
      - ap-northeast-1
      - us-east-1
  - name: extraSteps
    type: stepList
    default: []

values を指定すると、手動実行時にプルダウンで選ばせられるうえ、想定外の値を弾けるので便利です。

each でループする

object 型のパラメータと ${{ each }} を組み合わせると、同じ形のステージやジョブをまとめて生成できます。

stages:
  - ${{ each region in parameters.regions }}:
      - stage: Deploy_${{ replace(region, '-', '_') }} # stage名に「-」は使えない
        jobs:
          - job: Deploy
            pool: { vmImage: "ubuntu-latest" }
            steps:
              - script: ./deploy.sh --region ${{ region }}

stage 名や job 名に使える文字は英数字とアンダースコアだけなので、リージョン名などをそのまま埋め込むとエラーになります。上の例のように replace() で置き換えてください。

ビルド成果物を発行し、ダウンロードする

job をまたぐとエージェントが変わるため、ビルド結果を渡すには成果物として発行する必要があります。

# 発行
steps:
- task: PublishBuildArtifacts@1
  inputs:
    PathtoPublish: hoge/fuga.json # フォルダーまたはファイル パス。 デフォルト $(Build.ArtifactStagingDirectory)
    ArtifactName: fuga
    publishLocation: 'Container'
# ダウンロード
steps:
  - download: current
    artifact: fuga
    displayName: download fuga
    continueOnError: true # エラーでも続ける

# 別ステージでの成果物をダウンロードする場合は、依存に追加
stages:
- stage: Test
  jobs:
    - job: build
      pool: {vmImage: 'ubuntu-latest'}
      steps:
        - publish: hoge/fuga.json
          artifact: fuga
- stage: DeployUS1
  dependsOn: Test
  jobs:
    - job: deploy
      pool: {vmImage: 'ubuntu-latest'}
      steps:
        - download: current
          artifact: fuga
          displayName: download fuga

Build artifacts と Pipeline artifacts

紛らわしいのですが、成果物の仕組みは 2 世代あります。

種類発行ダウンロードショートハンド
Build artifacts(旧)PublishBuildArtifacts@1DownloadBuildArtifacts@1-
Pipeline artifacts(新)PublishPipelineArtifact@1DownloadPipelineArtifact@2publish: / download:

新規に書くなら Pipeline artifacts のほうが推奨です。内部的に重複排除が効くので、特にファイル数が多いときの転送がかなり速くなります。publish: / download: というショートハンドはこの新しい方のエイリアスです。

上のサンプルは新旧が混ざっていますが、実際には片方に寄せたほうが混乱がありません。

ダウンロード先のパス

download でダウンロードした成果物は $(Pipeline.Workspace)/<artifact名> に展開されます。$(System.DefaultWorkingDirectory) ではないので注意してください。

- download: current
  artifact: fuga
- script: ls -al $(Pipeline.Workspace)/fuga

また、deployment ジョブでは成果物が自動でダウンロードされます。不要な場合は download: none で明示的に止められます。逆に通常の job では自動ダウンロードされないので、明示的に書く必要があります。

セキュアファイルダウンロード

Library 画面の Secure files にアップロードしたファイルをダウンロードできます。

steps:
  - task: DownloadSecureFile@1
    name: secureFileTask
    inputs:
      secureFile: ".env.dev"
  - script: |
      ls -al $(secureFileTask.secureFilePath)

name を付けておくと、$(<name>.secureFilePath) でダウンロード先の絶対パスを取得できます。ファイルは一時ディレクトリに置かれ、ジョブの終了時に削除されます。

証明書や .env のような、リポジトリに置きたくないファイルを渡す用途に向いています。パーミッションはデフォルトで所有者のみ読み取り可(600 相当)になっているので、別ユーザーで実行するプロセスから読ませたい場合は chmod が必要になることがあります。

なお、Secure file はパイプラインからの初回利用時に認可が必要です。「このパイプラインで使ってよいか」の承認画面が出て実行が止まることがあるので、初回はログを確認してください。

マトリックスで同じジョブを並列実行する

複数のバージョンや環境に対して同じ処理を回したい場合は strategy: matrix が使えます。

- job: Test
  pool: { vmImage: "ubuntu-latest" }
  strategy:
    matrix:
      node20:
        NODE_VERSION: "20"
      node22:
        NODE_VERSION: "22"
    maxParallel: 2
  steps:
    - task: NodeTool@0
      inputs:
        versionSpec: $(NODE_VERSION) # マトリックスのキーが変数として使える
    - script: npm ci && npm test

マトリックスの各要素はそれぞれ別のジョブとして起動します。つまり並列実行の枠を消費するので、枠が 1 つしかない環境では結局直列になります。maxParallel で同時実行数を絞ることもできます。

キャッシュで依存関係の取得を短縮する

npm cipip install のような、毎回同じものを取ってくる処理はキャッシュできます。

variables:
  npm_config_cache: $(Pipeline.Workspace)/.npm

steps:
  - task: Cache@2
    inputs:
      key: 'npm | "$(Agent.OS)" | package-lock.json'
      restoreKeys: |
        npm | "$(Agent.OS)"
      path: $(npm_config_cache)
    displayName: Cache npm
  - script: npm ci

key はキャッシュの識別子で、| 区切りで複数の要素を並べます。ファイル名を書くとその内容のハッシュが使われるので、package-lock.json が変われば自動的に別のキャッシュになります。

注意点として、キャッシュは完全一致したときのみ復元されます。部分的に一致するキャッシュも使いたい場合は restoreKeys にフォールバック先を書きます。また、一度作られたキャッシュのエントリは同じキーでは上書きされないので、キーの設計を間違えると古い内容が延々と使われ続けます。

チェックアウトの挙動を変える

steps を書くと、暗黙的に checkout: self が先頭に挿入されます。明示的に書くとオプションを指定できます。

steps:
  - checkout: self
    fetchDepth: 1 # 浅いクローンで高速化
    clean: true # 作業ディレクトリをクリーンにする
    persistCredentials: true # パイプラインからgit pushしたい場合に必要
    submodules: true

デプロイだけを行うジョブなど、ソースが不要な場合は checkout: none でクローン自体を省略できます。地味ですがリポジトリが大きいと効きます。

steps:
  - checkout: none

persistCredentials: true は、パイプラインの中でタグを打ったりコミットを push したりする場合に必要です。あわせて、対象リポジトリに対して Build Service アカウントへ「Contribute」権限を付与しておく必要があります。

デプロイと承認(deployment ジョブ)

本番デプロイの前に人の承認を挟みたい、というのはよくある要件です。これは YAML 側ではなく Environment 側で設定します。

- stage: Deploy
  jobs:
    - deployment: DeployProd
      environment: production # ここで指定した名前のEnvironmentに紐づく
      pool: { vmImage: "ubuntu-latest" }
      strategy:
        runOnce:
          deploy:
            steps:
              - script: ./deploy.sh

job ではなく deployment と書くのがポイントです。environment に指定した名前の Environment は、存在しなければ初回実行時に自動作成されます。

承認を要求するには、Pipelines > Environments から対象の Environment を開き、「Approvals and checks」で承認者を設定します。設定すると、そのステージの手前でパイプラインが停止し、承認されるまで待つようになります。

deployment ジョブには、前述のとおり成果物が自動ダウンロードされるという挙動の違いもあります。

スクリプトで AWS クレデンシャルを使用する

variables:
  AWS_CREDENTIAL: 'aws-credential' # Azure DevOpsのServiceConnectionに登録した名前
  AWS_REGION: 'ap-northeast-1'

# ~~~省略~~~

- task: AWSShellScript@1
  displayName: AWS Work
  inputs:
    awsCredentials: $(AWS_CREDENTIAL) # 変数OK
    regionName: $(AWS_REGION)
    scriptType: 'inline'
    inlineScript: |
      # 作業ディレクトリは通常のScriptと同じ
      pwd
      # profileの指定なしでOK
      # aws cliコマンド s3やssmなど

これらのタスクを使うには、事前に Marketplace から「AWS Toolkit for Azure DevOps」拡張機能を組織にインストールしておく必要があります。タスクが見つからないというエラーが出たらまずここを確認してください。

クレデンシャルはタスクの実行中だけ環境変数として渡されるので、後続の通常 script タスクには引き継がれません。AWS を触る処理はこのタスクの中にまとめるか、必要なら都度指定してください。

AWS コマンドを実行する

単発のコマンド実行であれば、こちらも使えます。

awsCommand、awsSubCommand、awsArgments は CLI での入力と同じです。

- task: AWSCLI@1
  displayName: put param
  inputs:
    awsCredentials: $(AWS_CREDENTIAL)
    regionName: $(AWS_REGION)
    awsCommand: ssm
    awsSubCommand: put-parameter
    awsArgments: --name "app/dev/latest-version" --type String --value "1.0.0" --overwrite

@1 とありますが、こちらはタスクのメジャーバージョンなので、AWS CLI が v1 というわけではないそう。

ubuntu(22.04)では AWS CLI は v2 が入っています。

心配であれば、aws --versionをタスク内で実行してみるといいと思います。

なお vmImage: 'ubuntu-latest' が指す実体は時期によって変わります(22.04 → 24.04 のように更新されていきます)。プリインストールされているツールのバージョンに依存する処理を書くなら、ubuntu-24.04 のように固定するのが安全です。イメージに何が入っているかは、runner-images のリポジトリで確認できます。

デバッグのやり方

うまく動かないときの手札です。

system.debug を有効にする

パイプライン変数に system.debugtrue で追加すると、詳細ログが出るようになります。変数の展開結果やタスクの入力値がすべて出るので、「変数が空になる」系の調査ではまずこれを入れます。

手動実行時に「Variables」から一時的に追加することもできるので、恒久的に YAML へ書く必要はありません。

展開後の YAML を確認する

テンプレートやループを多用していると、最終的にどんな YAML になっているのか分からなくなります。パイプラインの編集画面から「Download full YAML」を選ぶと、テンプレートを展開した後の完全な YAML を取得できます。${{ }} 系の問題はこれで一発で分かることが多いです。

とりあえず全部出す

原始的ですが強いです。

- bash: |
    echo "--- env ---"
    env | sort
    echo "--- pwd ---"
    pwd && ls -al
  displayName: debug dump
  condition: always()

シークレットはマスクされるので、env を丸ごと出しても値そのものは漏れません。

おわりに

Azure DevOps Pipelines の YAML は、GitHub Actions に比べると情報が見つけにくく、日本語ドキュメントの質も相まって、最初のうちは手探りになりがちです。

とはいえ、つまずくポイントは

  • 変数の 3 種類の構文と評価タイミング
  • dependsOn を張らないと値が取れない
  • condition を書くと暗黙の succeeded() が消える
  • job をまたぐとエージェントが変わる

このあたりにだいたい集約される印象があります。ここさえ押さえておけば、あとはドキュメント(英語版)を引きながら書けるはずです。

新しく踏んだものがあれば随時追記していきます。

リンクをコピーしました