ツナ缶雑記

ぐうたらSEのブログです。主にマイクロソフト系技術を中心に扱います。

Azure Pipelinesで複数のリポジトリを使ったビルドパイプラインを構築する

本記事は Azure DevOps Advent Calendar 2019 - Qiita 24日目の記事です。

2019年12月2日(Sprint 161)の更新で、複数のリポジトリを使ったビルドパイプラインの構築がサポートされるようになりました。

docs.microsoft.com

エンタープライズ開発において、ある程度まとまった単位*1でプロジェクトを分割するケースはよくると思います。 1つの製品を作るのに複数のリポジトリを使っていると、今まで結構面倒な手順が必要でした。 頑張って Git コマンドを叩いてみたり、サードパーティーのタスクを使ってみたり、下手をすると手作業を入れてみたり。。。 この機能を使うと、割と簡単に複数のリポジトリを1つのビルドパイプラインから扱えるようになります。

やること

1つのプロジェクト(MultiRepositoriesSampleプロジェクト)に2つの Git リポジトリ(Repo1, Repo2)を立てます。

f:id:masatsuna:20191220002645p:plain
リポジトリ

リポジトリには WPF Core のソリューションが入っています(それぞれ Application1、Application2)。 Application1、Application2 をビルドして、1つのディレクトリにまとめ、製品版としてリリースできるビルドパイプラインの YAML を Repo1 に追加します。

f:id:masatsuna:20191220003034p:plain
ファイル構成

今回はことを簡単にするため、製品版に見立てるアプリケーション一式は、 Pipeline Artifacts として公開するだけにします。 通常なら App Center に公開したり、インストーラーを作ってどこかに置いたり、ということをすると思いますが、そこまで踏み込みません。

f:id:masatsuna:20191220003357p:plain
最終的な目指す状態

ビルドパイプラインを作成する

全体的な構成

今回はプレビュー機能である [Multi-stage pipelines] を使っていきます*2。 1つ目の Stage では、アプリケーションのビルドを行います。 2つ目の Stage では、製品版としてディレクトリ構造を作っていく処理を行います。 YAML の全体骨格は以下のような形になります。

stages:
  - stage: ApplicationBuildStage
    # Application1 のビルド
    # Application2 のビルド

  - stage: PackagingApplicationStage
    # ApplicationBuildStageが成功したらディレクトリ構造を作る

Repo1(Aplication1)のビルドができるようにする

まずは Repo1 リポジトリに含まれているアプリケーション(Application1)のビルド処理を組み立てていきます。 今回はビルドしたアプリケーションを Pipeline Artifact として Azure Pipelines にアップロードします。 これらの処理を ApplicationBuildStage の下にぶら下げていきます。

  - stage: ApplicationBuildStage
    pool:
      vmImage: 'windows-latest'
    jobs:
      - job: Application1BuildJob
        steps:
          - checkout: self
          - task: DotNetCoreCLI@2
            inputs:
              command: 'build'
              projects: '**/*.sln'
              arguments: '-o $(Build.ArtifactStagingDirectory)/Application1'
          
          - task: PublishPipelineArtifact@1
            inputs:
              targetPath: '$(Build.ArtifactStagingDirectory)'
              publishLocation: 'pipeline'

やっていることは非常にシンプルです。 まず出てくるのは - checkout: self です。 このように記載しておくと、この YAML が配置されている Git リポジトリをチェックアウトできます。 通常このような記載がなくても、自動的に Git リポジトリのチェックアウトが行われます。 しかし、今回は複数のリポジトリを扱う関係上、明示的にどのリポジトリを使うか明示しています。

続いて - task: DotNetCoreCLI@2 のタスクで、 WPF Core のアプリケーションをビルドし、その成果物を $(Build.ArtifactStagingDirectory)/Application1 に配置しています。 前述した [最終的な目指す状態] の図を見てもわかる通り、今回は製品版のディレクトリ構造を以下のようにしています。

products
├ Application1
│  └ Application1.exe など
└ Application2
   └ Application2.exe など

この時点で [Application1] ディレクトリに配置するよう、出力ディレクトリを設定しているのがポイントです。 アプリケーションをビルドしたときに、最終的な目指す状態のディレクトリ構造を作りこんでおきましょう。 今回は products ディレクトリに相当するルートディレクトリを、 [$(Build.ArtifactStagingDirectory)] としています。 このディレクトリを製品のルートディレクトリと考えて、その中にビルド成果物を配置するようにしておくと、後で調整作業が不要になるのでオススメです。

最後に - task: PublishPipelineArtifact@1 のタスクで、 Pipeline Artifacts をアップロードしています。 せっかく作った構造を壊さないように、アップロードする [targetPath] には、ルートディレクトリである [$(Build.ArtifactStagingDirectory)] を指定しています。

以上で Repo1 のアプリケーションがビルドできるようになります。 実際にこのビルドパイプラインを実行すると、以下のような Artifacts が生成されます。

f:id:masatsuna:20191220010534p:plain
Repo1のPipeline Artifacts

Artifacts の名前は、 Stage と Job の名前から自動的につけられていることがわかります。

Repo2(Aplication2)のビルドができるようにする

続いて Repo2 に配置している Application2 もビルドできるようにしていきます。

Repo2 が読み込めるように定義する

今回作成しているビルドパイプラインの YAML ファイルは、Repo1 に定義されているので、 Repo2 のリポジトリを参照するには、明示的にリポジトリを指定してあげなければなりません。 別のリポジトリを参照するためには、 resources を使って、以下のように参照するリポジトリを定義することになります。

resources:
 repositories:
   - repository: Repo2
     type: git
     name: MultiRepositoriesSample/Repo2

resourcesYAML の最上位要素として定義しましょう。 その中に参照するリポジトリの情報を定義していきます。 今回は Azure Repos にある Git リポジトリを参照したいので、上記のように定義します。 現時点では type の指定によって、Azure Repos、GitHub、Bitbucket Cloud のリポジトリを選択できるようです。 定義方法の詳細についてはこちらを参照してください。

Repo2 をチェックアウトしてアプリケーションをビルドする

続いて Repo2 をチェックアウトして、アプリケーションをビルドしていきます。

stages:
  - stage: ApplicationBuildStage
    pool:
      vmImage: 'windows-latest'
    jobs:
      - job: Application1BuildJob

        # 中略(Application1 のビルド処理)

      - job: Application2BuildJob
        steps:
          - checkout: Repo2
          - task: DotNetCoreCLI@2
            inputs:
              command: 'build'
              projects: '**/*.sln'
              arguments: '-o $(Build.ArtifactStagingDirectory)/Application2'
          - task: PublishPipelineArtifact@1
            inputs:
              targetPath: '$(Build.ArtifactStagingDirectory)'
              publishLocation: 'pipeline'

先ほど作成した ApplicationBuildStage の Job として、ビルド処理を追加していきます。 Repo1 のソースコードをチェックアウトするときは - checkout: self を指定していました。 Repo2 をチェックアウトする場合は、定義したリポジトリリソースの名前を指定します。 リポジトリリソースに Repo2 という名前を付けているので、ここでは - checkout: Repo2 を指定しています。 これで Repo2 のソースコードをチェックアウトすることができます*3

あとは Application1 のビルドと同様、 Application2 をビルドしています。 こちらも同様に、この時点で [Application2] ディレクトリに配置するよう、出力ディレクトリを設定しています。

ここまで設定して実行すると、2つのアプリケーションがビルドされ、 Pipeline Artifacts に2つの Artifacts が格納された状態になります。

f:id:masatsuna:20191223183906p:plain
Artifacts の状態

最終的な目指す状態を作り上げる

最後に、ここまで構築してきたアプリケーションを収集して、最終的な目指す姿を作り上げていきましょう。 今回は Application1、Application2 を1つのディレクトリにまとめて、[products] という名前の Artifact を作成していきます。

このパッケージング処理は、今までとは別のステージ(PackagingApplicationStage)で実行するようにしていきます。

  # 前略
  - stage: PackagingApplicationStage
    dependsOn: ApplicationBuildStage
    condition: succeeded()
    pool:
      vmImage: 'windows-latest'
    jobs:
      - job: PackagingApplicationJob
        steps:
          - checkout: none
          - task: DownloadPipelineArtifact@2
            inputs:
              buildType: 'current'
              artifactName: 'ApplicationBuildStage.Application1BuildJob'
              targetPath: '$(Pipeline.Workspace)/products'
          - task: DownloadPipelineArtifact@2
            inputs:
              buildType: 'current'
              artifactName: 'ApplicationBuildStage.Application2BuildJob'
              targetPath: '$(Pipeline.Workspace)/products'
          - task: PublishPipelineArtifact@1
            inputs:
              targetPath: '$(Pipeline.Workspace)/products'
              publishLocation: 'pipeline'
              artifact: 'products'

パッケージングの処理は、 ApplicationBuildStage の後に実行しなければならないので、 dependsOn: ApplicationBuildStage を指定して、依存するステージを指定しています。 また前処理で何かエラーがあった場合、パッケージング処理は実行したくないので、 condition: succeeded() として正常に処理が完了したときのみ実行するように明示しています。

今回パッケージング処理では、 Repo1、Repo2 のリポジトリをチェックアウトする必要はありません。 そのため、 - checkout: none でチェックアウト処理をスキップするようにしています。

ApplicationBuildStage で作成した Artifacts は、 - task: DownloadPipelineArtifact@2 のタスクを使用してダウンロードしています。 ApplicationBuildStage で既にパッケージングするアプリケーションのディレクトリ構造を作りこんであるので、ここでは単純にパッケージングするディレクトリに Artifacts をダウンロードするだけです。 そして最後にアプリケーションを公開先に配置しています。

今回最終的なアプリケーションの配置先を Pipeline Artifacts にしていますが、 App Center や Blob Storage など、もっと適した場所があると思いますので、自由に組み合わせてみるとよいと思います。

まとめ

今回は Azure Pipelines で複数のリポジトリを使ったビルドパイプラインの作り方について解説しました。 この機能は、1つの製品で複数の Git リポジトリを運用しているチームにとって非常にうれしい機能で、私たちも早速使い始めています。 しかし、この機能は本来ビルドパイプラインを YAML 化するメリットを失うことにもつながる機能であると思います。

ビルドパイプラインをYAML化するメリットは、リポジトリの成長とビルドパイプラインの成長を同期しながら管理できることにあります。 この機能を使うと、管理単位がリポジトリを超えてしまうため、乱用すると運用の崩壊につながるように思います。 個人的には、製品のリリース処理など、どうしてもリポジトリをまたぐ必要のあるビルドパイプラインに限って利用するべきかな、と思います。 もしこの機能を乱用しなければならないケースがあるとしたら、それはリポジトリの単位が間違っていることを疑った方がよいかもしれませんね。

*1:よくあるのは発注する会社の単位や、サブシステムの単位などが当たります

*2:既にこの辺は解説記事がいろいろ出ているので、詳細はそちらを参照ください。

*3:今回は1つのリポジトリしかチェックアウトしていませんが、複数のリポジトリをチェックアウトすることもできます。その場合は checkout のタスクを複数回呼び出してください。