いまさらながらAzure DevOps / Pipelinesでスタンドアロンアプリ開発CIを実践する
はじめに
皆様はDevOps、CI/CDは活用されてますでしょうか?
恥ずかしながら、私はソフトウェア開発に携わって10年近くになりますが、これまで本格的にDevOpsやCI/CDに向き合うことなく過ごしてきました。
正直なところ「ビルドして、テストして、お客さんも満足している挙動でシステムは動いてるんだから、CI/CDなんて別に必要ないじゃーん!」とさえ思う時期もありました。
しかしながら、携わるプロジェクトの規模が大きくなるにつれ、属人化を防ぐ観点からも、DevOpsやCI/CDの重要性を意識する機会が増えてきました。
そんなこんなで、以前にAzure Repos Gitを少し利用していたこともあり、改めてAzure DevOps / Pipelinesを用いたCI/CDについて整理してみたいと考えるようになりました。
情報整理を進める中で、Webアプリを本番環境に自動デプロイするような、かっこいいCI/CD事例はよく見かけますが、スタンドアロンアプリにおいてもCI/CDの恩恵は十分にあると考えます。少なくともCIまでは取り入れるべきだと感じています。
ということで、すごくいまさら感もあるのですが、自身の学びなおしも兼ねて、本記事作成に至ります。
DevOpsとは
開発 (Dev) と運用 (Ops) の複合語である DevOps とは、継続的に顧客に価値を届けるために人、プロセス、テクノロジをひとつにまとめることです。
要するに、開発と運用が協力し、文化と仕組みを習慣化することで、価値を継続的に早く安全に顧客へ届けよう、という考え方のことです。
Azure DevOps とは
Azure DevOps は、ソフトウェア開発チームに統合ツールを提供するクラウドベースのプラットフォームです。 これには、作業の計画、コードでの共同作業、アプリケーションのビルド、機能のテスト、運用環境へのデプロイに必要なものがすべて含まれます。
ソフトウェア開発PJにおいて、全部入りの開発プラットフォーム、という位置づけです。
Azure Pipelines とは
Azure Pipelines は Azure DevOps の一部であり、 継続的インテグレーション、 継続的テスト、 継続的デリバリー を組み合わせて、コード プロジェクトを任意の宛先に自動的にビルド、テスト、デプロイします。 Azure Pipelines では、すべての主要な言語とプロジェクトの種類がサポートされており、アプリがオンプレミスかクラウドかに関係なく、選択したテクノロジとフレームワークのワークフローを自動化できます。
コードの変更を起点にビルド・テスト・配布などを自動化する仕組み、という理解です。
Azure Pipelines のYAML構成
次の図は、Azure Pipelinesの主要なコンポーネントとアクションを示しています。
上記の図はAzure Pipelines実行の概念図です。
私の理解ですが、Azure Pipelinesが実行されると、後にも記述するpoolで指定したエージェントという仮想マシンが起動し、仮想マシンがソースコードをチェックアウトした上でビルドやテストを実行するイメージです。また、仮想マシンはステージごとやジョブごとに指定もできる認識です。
Azure Pipelines処理定義はYAMLで記載します。
以下は、Azure PipelinesのYAML構成の全体像例です。
この中から、適宜必要な要素を組み合わせてYAMLを構築します。
Pipeline # パイプライン全体
├── trigger / pr / schedules # トリガー設定(いつ実行されるか)
├── variables # 変数・変数グループ設定
├── pool # 全体で使用するエージェント(任意)
├── resources # 外部リポジトリ、パイプライン、コンテナ等
└── stages # ステージ:処理の大きな区切り(例: Build/Test/Deploy)
├── stage: Build # ステージ1:例.ビルド処理全体をまとめる
│ └── jobs # ジョブ:ステージ内の並列実行単位
│ ├── job: BuildJob # ジョブ1:例.ビルド処理を担当
│ │ └── steps # ステップ:ジョブ内で順番に実行される具体的処理(順次実行)
│ │ ├── checkout: self # コードの取得(Azure DevOps標準ステップ)
│ │ ├── script: npm install # 任意のスクリプトやコマンド実行
│ │ └── task: PublishBuildArtifacts@1 # Azure組み込みタスクの実行(成果物発行など)
│ │
│ └── job: LintJob # ジョブ2:例.コードの静的解析(Lintチェックなど)
│ └── steps # ステップ:同一ステージ内のジョブ1とはデフォルトで「並列」実行
│ ├── checkout: self # 例.ソースコード取得
│ └── script: npm run lint # 例.Lintチェックなどを実行
│
└── stage: Deploy # ステージ2:デプロイフェーズ(Build完了後に実行されることが多い)
└── jobs # デプロイステージ内のジョブ群(通常1つ)
└── job: DeployJob # ジョブ:デプロイを担当
└── steps # ステップ:デプロイ手順を順に実行(順次実行)
├── download: drop # 例.Buildステージで作成した成果物を取得
├── script: az login ... # 例.Azure CLIなどでログイン処理
└── script: az webapp deploy ... # 実際のデプロイ処理を実行
| 階層 | 意味 | 実行イメージ |
|---|---|---|
| Pipeline | パイプライン全体の定義 | YAMLファイル1本で構成される |
| trigger | 実行条件(どのブランチ・イベントで起動) | mainブランチ更新で自動実行など |
| variables | パイプライン全体の共通変数 | OSやビルド設定を定義 |
| resources | 外部テンプレートや他リポジトリの参照 | 再利用可能な構成を読み込み |
| stages | 処理の大枠(Build / Test / Deployなど) | 順番にまたは並列に実行 |
| jobs | ステージ内の並列実行単位 | 複数ジョブはデフォルトで並列 |
| pool | ジョブを実行するエージェント環境を指定 | Microsoft-hosted Agent(例: ubuntu-latest)やSelf-hosted Agentを選択 |
| steps | 実際の処理内容 | 順番に1つずつ実行(直列) |
やりたいこと
ということで、本題ですが、
Azure DevOps / Pipelinesを使用して、以下のようなCIを構築したいです。
- Gitのタグ付けでAzure Pipelinesをトリガー
- アプリのビルド
- 単体テストプロジェクトの実行
- 成果物(Artifacts)の生成
前提
Azure Pipelinesの使用プラン
本記事では、Azure Pipelinesの無償枠を使用します。
Azure DevOps料金体系について参考ページ
- Microsoft Azure DevOps の料金
- Qiita Azure Pipelines を無料枠で使おうとして躓いた話
ソースコード構成
CIの対象とするソースコードは以下のような構成とします。スタンドアロンアプリは.NETコンソールアプリ(ConsoleApp1)とし、単体テストプロジェクト(xUnitTestPrj)が含まれるソリューションとします。
この.NETコンソールアプリ(ConsoleApp1)を単体テストを成功させた上で、成果物(Artifacts)を生成するところまでが、今回の目的です。

以下はツリー図です。
DevOpsSmaple # リポジトリルート
├── .gitignore
├── azure-pipelines.yml # Azure Pipelines YAMLファイル
├── README.md
└── ConsoleApp1/ # アプリフォルダ
├── ConsoleApp1.sln
├── ConsoleApp1/ # .NET コンソールアプリプロジェクト
│ ├── Class1.cs
│ ├── ConsoleApp1.csproj
│ └── Program.cs
└── xUnitTestPrj/ # 単体テストプロジェクト
├── UnitTest1.cs
└── xUnitTestPrj.csproj
スタンドアロンアプリ(.NET コンソールアプリprj)
コンソールアプリ内のClass1.csには、単体テスト用の簡単なpublicメソッドGetHelloWorldString()を実装しています。

namespace ConsoleApp1;
public static class Class1
{
public static string GetHelloWorldString()
{
return "Hello, World!";
}
}
単体テスト(.NET xUnitテストPrj)
ユニットテストプロジェクトには、コンソールアプリ内のメソッドGetHelloWorldString()をテストするためのテストメソッドTest_GetHelloWorldString_ReturnsHelloWorld()を実装しているとします。

using ConsoleApp1;
namespace xUnitTestPrj;
public class UnitTest1
{
[Fact]
public void Test_GetHelloWorldString_ReturnsHelloWorld()
{
// Arrange
// Act
string result = Class1.GetHelloWorldString();
// Assert
Assert.Equal("Hello, World!", result);
}
}
Azure Repos Git
以下のように、Azure Repos Gitにはプッシュ済みとします。

Azure PipeLines実装
いざいざAzure Pipelinesを実装します。
Azure Pipelinesの作成
Azure DevOpsページ内のAzure Pipelinesメニューより最初のPipelinesを作成します。
Azure DevOpsメニューのPipelines画面を開き、「Create Pipelines」を押下します。

今回のGitリポジトリは、Azure Git Reposに作成しますので、「Azure Repos Git」を選択します。

Azure Pipelinesを作成するプロジェクトを選択します。

いろいろテンプレートがありますが、今回は空から手を加えていきますので、「Starter Pipeline」を選択します。

以下のようなymlファイル作成されます。
画面右上の「Save and run」を押下し、編集内容を保存します。

# 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:
- main
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'
mainブランチのプッシュをトリガーにして、実行され、script部分が実行されます。
displayName: 'Run a one-line script'
displayName: 'Run a multi-line script'
が実行されて、成功していることが確認できました。

1. Gitのタグ付けでAzure Pipelinesをトリガー
まずは、トリガーをタグ付けで起動するようにします。
確認のためのHello, world!のscriptのみ残し、トリガー定義は以下のように編集します。
trigger:
+ branches:
+ include:
+ - refs/tags/*
pool:
vmImage: ubuntu-latest
steps:
- script: echo Hello, world!
displayName: 'Run a one-line script'
実行結果
下記のようにタグ付けを行います。


タグ付けでトリガーされ、Azure Pipelinesが起動します。

echo Hello, World!が表示されました。

すべてのsuccessで、commit HistoyでもSuccess表示となりました。

無事、タグ付けで実行され、Hello,World!も表示されました。
2. アプリのビルド
続いて、ソリューションのビルドを行うStepを追加します。
以下のようにecho Hello, Wrold!scriptは削除し、以下のようにStageとビルド用のStepを追記します。
poolはvmImage: ubuntu-latestを使用している理由としては、vmImage: windows-latestよりも起動や実行時間が早く済むので、linuxとしています。WinFormsなど、Windows環境の依存関係が必要な場合は、vmImage: windows-latestを指定する必要があると考えます。たぶん。。
trigger:
branches:
include:
- refs/tags/*
pool:
vmImage: ubuntu-latest
+ variables:
+ buildConfiguration: 'Release'
+ artifactName: 'console-app'
+ # ------------------------------------------
+ # Stage: Build (ビルド & テスト & 発行)
+ # ------------------------------------------
+ stages:
+ - stage: Build
+ displayName: 'Build and Test Stage'
+ jobs:
+ - job: BuildJob
+ displayName: 'Build, Test, and Publish Job'
+ steps:
+ # === ソース取得 ===
+ - checkout: self
+ displayName: 'Checkout source code'
+ # === .NET SDK セットアップ ===
+ - task: UseDotNet@2
+ displayName: 'Install .NET SDK'
+ inputs:
+ packageType: 'sdk'
+ version: '9.0.x' # プロジェクトに合わせて変更
+ # === パッケージ復元 ===
+ - script: dotnet restore ./ConsoleApp1/ConsoleApp1.sln
+ displayName: 'Restore dependencies'
+ # === ビルド ===
+ - script: dotnet build ./ConsoleApp1/ConsoleApp1.sln --configuration $(buildConfiguration) --no-restore
+ displayName: 'Build console app'
※パッケージ復元はxUnitテストのNugetパッケージ復元用です。
実行結果
無事、以下のステップ実行はSuccessとなり、ビルドできることが確認できました。
-
Checkout source code:ソース取得 -
Install .NET SDK:.NET SDK セットアップ -
Restore dependencies:パッケージ復元 -
Build console app:ビルド

3. 単体テストプロジェクトの実行
次に、単体テストプロジェクト(xUnitTestPrj)を実行するステップを追加します。
以下をYAMLファイルに追記します。
また、condition: succeeded()を明示的に指定して、前ステップが成功した場合のみ実行することとします。(ビルドが通ってないにも関わらず、単体テストを実行するのは芳しくないため。)
また、csprojファイルの指定パスは、リポジトリルートからの相対パスである点に注意が必要です。
# ~省略~
+ # === ユニットテスト ===
+ - script: dotnet test ./ConsoleApp1/xUnitTestPrj/xUnitTestPrj.csproj --configuration $(buildConfiguration) --no-build --verbosity normal
+ displayName: 'Run unit tests'
+ condition: succeeded()
実行結果
無事、以下のユニットテストのステップ実行はSuccessとなり、単体テストも問題ないことが確認できました。
-
Run unit tests:ユニットテスト
4. 成果物(Artifacts)の生成
最後に、成果物の生成とArtifactsへのアップロードのステップを追記します。
以下のような「Publish」ステップと「Artifactsへアップロード」ステップをYAMLファイルに追記します。
- task: PublishBuildArtifacts@1はAzure Pipelinesで標準で用意されているタスクです。
# ~省略~
+ # === Publish (成果物生成) ===
+ - script: dotnet publish ./ConsoleApp1/ConsoleApp1/ConsoleApp1.csproj --configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)
+ displayName: 'Publish console app'
+ condition: succeeded()
+ # === Artifactsへアップロード ===
+ - task: PublishBuildArtifacts@1
+ displayName: 'Publish build artifacts to Azure DevOps'
+ condition: succeeded()
+ inputs:
+ pathToPublish: '$(Build.ArtifactStagingDirectory)'
+ artifactName: '$(artifactName)'
+ publishLocation: 'Container'
YAMLファイルの全体は以下です。
trigger:
branches:
include:
- refs/tags/* # タグでのトリガー
pool:
vmImage: ubuntu-latest # 使用するビルドエージェント
variables:
buildConfiguration: 'Release'
artifactName: 'console-app'
# ------------------------------------------
# Stage: Build (ビルド & テスト & 発行)
# ------------------------------------------
stages:
- stage: Build
displayName: 'Build and Test Stage'
jobs:
- job: BuildJob
displayName: 'Build, Test, and Publish Job'
steps:
# === ソース取得 ===
- checkout: self
displayName: 'Checkout source code'
# === .NET SDK セットアップ ===
- task: UseDotNet@2
displayName: 'Install .NET SDK'
inputs:
packageType: 'sdk'
version: '9.0.x' # プロジェクトに合わせて変更
# === パッケージ復元 ===
- script: dotnet restore ./ConsoleApp1/ConsoleApp1.sln
displayName: 'Restore dependencies'
# === ビルド ===
- script: dotnet build ./ConsoleApp1/ConsoleApp1.sln --configuration $(buildConfiguration) --no-restore
displayName: 'Build console app'
# === ユニットテスト ===
- script: dotnet test ./ConsoleApp1/xUnitTestPrj/xUnitTestPrj.csproj --configuration $(buildConfiguration) --no-build --verbosity normal
displayName: 'Run unit tests'
condition: succeeded()
# === Publish (成果物生成) ===
- script: dotnet publish ./ConsoleApp1/ConsoleApp1/ConsoleApp1.csproj --configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)
displayName: 'Publish console app'
condition: succeeded()
# === Artifactsへアップロード ===
- task: PublishBuildArtifacts@1
displayName: 'Publish build artifacts to Azure DevOps'
condition: succeeded()
inputs:
pathToPublish: '$(Build.ArtifactStagingDirectory)'
artifactName: '$(artifactName)'
publishLocation: 'Container'
実行結果
いよいよ一式のAzure Pipelinesを動作させます。
タグ付けを行い、Azure Pipelinesの起動をトリガーします。

無事、すべてのステップが成功しました。

成果物(Artifacts)もアップロードされています。


成果物(Artifacts)は、右端のケバブメニューアイコンからダウンロードすることができます。

期待通り、成果物(Artifacts)がアップロードされ、CIの構築ができました。はじめてすべてのステップがSuccessで通過して成果物ができたときは、うれしすぎて過呼吸になりました。。
単体テスト失敗時のパターン
余談ですが、単体テストが失敗したときの動作も確認しましたので、以下に記します。
ユニットテストの判定で、「Hello,World!」に「!」を一つ増やして意図的に失敗するようにしたうえで、Azure Pipelinesを実行させました。

期待通り、Run unit testsのステップで失敗となり後続のステップは実行していません。
まとめ
ということで、簡単ですが、.NET スタンドアロンアプリ(コンソールアプリ)をAzure DevOps / Pipelinesを使用したCIを構築しました。
DevOpsを使用したCI/CDはWebアプリ開発が主であるイメージが強かったですが、(いまさらながら)触ってみた感触では、スタンドアロンアプリ開発でも活用できそうと実感しております。
特に、特定のPCでないとビルド環境が再現できない、、などの属人化を減らすことに活用できればと考えております。
本記事が少しでも何かのお役に立てれば幸いです。
参考
参考ページ
- Microsoft Learn Azure DevOps ドキュメントの概要
- Microsoft Learn Azure Pipelines のドキュメント
- Microsoft Learn YAML schema reference for Azure Pipelines
- Microsoft Learn Azure Repos Git のドキュメント
- Qiita Microsoft Azure DevOps Pipelines 設定ファイルの書き方
- Zenn Azure DevOps の ビルドパイプラインとリリースパイプラインを使用して CI/CD を実現する

Discussion