Azure Managed DevOps Pools(MDP)をBicepで構築する
はじめに
Azure Managed DevOps Pools (MDP) を使用すると、Microsoft がホストする Azure DevOps エージェント プールを簡単に作成および管理できます。Azure コンポーネントをデプロイすると、Azure DevOps Organizationと統合され、残りの作業は Microsoft 側で処理を行います。
MDPのアーキテクチャ
作業を簡単にするために、Azure Bicep テンプレートを作成しました。
なお、本記事の Bicep を実行することで、作成されるAzure コンポーネントは以下のとおりです。
名前 | 種類 |
---|---|
devcenter-cloud-dev | デベロッパーセンター |
devcenter-cloud-project-dev | プロジェクト |
mdp-cloud-dev | Managed DevOps Pool |
ng-mdp-cloud-dev | NAT Gateway |
pip-ng-mdp-cloud-dev | パブリックIPアドレス |
vnet-mdp-cloud-dev | 仮想ネットワーク |
前提条件
Azure サブスクリプションに Managed DevOps Pools リソース プロバイダーを登録する
こちらに従い登録します。
マネージド DevOps プールを使用するには、次のリソース プロバイダーを Azure サブスクリプションに登録します。
リソースプロバイダー | 説明 |
---|---|
Microsoft.DevOpsInfrastructure | マネージド DevOps プールのリソース プロバイダー |
Microsoft.DevCenter | デベロッパー センターおよびデベロッパー センター プロジェクトのリソース プロバイダー |
これらの設定後、DevOpsInfrastructure
がサービスプリンシパルとして登録されます。
後続で利用するObjectIDを取得するため、以下のコマンドを発行し、Registered
となっていることを確認してください。
az provider show --namespace Microsoft.DevOpsInfrastructure --query "registrationState"
続いて、登録されたDevOpsInfrastructure
のサービスプリンシパルのオブジェクトIDを取得します。
az ad sp list --query "[?starts_with(displayName, 'DevOpsInfrastructure')].{displayName:displayName, objectId:id}" -o table
Bicepモジュール
ディレクトリは以下の構造にしてください。
mdp\
├─ main.bicep
├─ main.bicepparam
└─ modules\
└─ mdp.bicep
パラメータについてはそれぞれ調整して利用してください。
using 'main.bicep'
// Defing our input parameters
param __env__ = 'dev'
param __cust__ = 'cloud'
param __location__ = 'japaneast'
param __devOpsOrganizationName__ = 'osatest'
param __devOpsProjectName__ = '20250624-mdptest'
param __devOpsInfrastructureSpId__ = '23a9154a-ec72-4df5-acd0-191374f49b86'
metadata name = 'Managed DevOps Pools as Code <3'
metadata description = 'This Bicep code deploys Managed DevOps Pools including NAT gateway for forced egress via a public IP address.'
metadata owner = 'yutaka-art'
targetScope = 'subscription'
@description('Defing our input parameters')
param __env__ string
param __cust__ string
param __location__ string
param __devOpsOrganizationName__ string
param __devOpsProjectName__ string
param __devOpsInfrastructureSpId__ string
@description('Defining our variables')
var _mdpResourceGroupName_ = 'rg-mdp-${__cust__}-${__env__}'
var _devCenterName_ = 'devcenter-${__cust__}-${__env__}'
var _devCenterProjectName_ = 'devcenter-${__cust__}-project-${__env__}'
var _vnetManagedDevOpsPoolName_ = 'vnet-mdp-${__cust__}-${__env__}'
var _pipNatGatewayName_ = 'pip-ng-mdp-${__cust__}-${__env__}'
var _natGatewayName_ = 'ng-mdp-${__cust__}-${__env__}'
var _managedDevOpsPoolName_ = 'mdp-${__cust__}-${__env__}'
@description('Resource Group Deployment')
resource mdpResourceGroup 'Microsoft.Resources/resourceGroups@2024-11-01' = {
name: _mdpResourceGroupName_
location: __location__
}
@description('Module Deployment')
module mdp './modules/mdp.bicep' = {
name: 'mdp-module-deployment'
params: {
__location__: __location__
_devCenterName_: _devCenterName_
_devCenterProjectName_: _devCenterProjectName_
_managedDevOpsPoolName_: _managedDevOpsPoolName_
_vnetManagedDevOpsPoolName_: _vnetManagedDevOpsPoolName_
_pipNatGatewayName_: _pipNatGatewayName_
_natGatewayName_: _natGatewayName_
__devOpsOrganizationName__: __devOpsOrganizationName__
__devOpsProjectName__: __devOpsProjectName__
__devOpsInfrastructureSpId__: __devOpsInfrastructureSpId__
}
scope: mdpResourceGroup
}
param __location__ string
param _devCenterName_ string
param _devCenterProjectName_ string
param _managedDevOpsPoolName_ string
param _vnetManagedDevOpsPoolName_ string
param _pipNatGatewayName_ string
param _natGatewayName_ string
param __devOpsOrganizationName__ string
param __devOpsProjectName__ string
param __devOpsInfrastructureSpId__ string
@description('Dev Center Deployment')
resource devCenter 'Microsoft.DevCenter/devcenters@2025-02-01' = {
name: _devCenterName_
location: __location__
}
resource devCenterProject 'Microsoft.DevCenter/projects@2025-02-01' = {
name: _devCenterProjectName_
location: __location__
properties: {
devCenterId: devCenter.id
}
}
@description('Virtual Network Deployment')
resource virtualNetwork 'Microsoft.Network/virtualNetworks@2024-07-01' = {
name: _vnetManagedDevOpsPoolName_
location: __location__
properties: {
addressSpace: {
addressPrefixes: [
'10.20.30.0/24'
]
}
subnets: [
{
name: 'snet-mdp-prod'
properties: {
addressPrefix: '10.20.30.0/24'
natGateway: {
id: natGateway.id
}
delegations: [
{
name: 'Microsoft.DevOpsInfrastructure/pools'
properties: {
serviceName: 'Microsoft.DevOpsInfrastructure/pools'
}
}
]
}
}
]
}
}
@description('Role Assignments Deployment')
resource networkContributorRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
scope: virtualNetwork
name: guid(virtualNetwork.id, 'Network Contributor', '${__devOpsInfrastructureSpId__}')
properties: {
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4d97b98b-1d4f-4787-a291-c67834d212e7') // Network Contributor role ID
principalId: __devOpsInfrastructureSpId__
}
}
resource readerRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
scope: virtualNetwork
name: guid(virtualNetwork.id, 'Reader', '${__devOpsInfrastructureSpId__}')
properties: {
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7') // Reader role ID
principalId: __devOpsInfrastructureSpId__
}
}
@description('Public IP Address Deployment')
resource publicIPAddress 'Microsoft.Network/publicIPAddresses@2024-07-01' = {
name: _pipNatGatewayName_
location: __location__
properties: {
publicIPAllocationMethod: 'Static'
}
sku: {
name: 'Standard'
}
}
@description('NAT Gateway Deployment')
resource natGateway 'Microsoft.Network/natGateways@2024-07-01' = {
name: _natGatewayName_
location: __location__
properties: {
idleTimeoutInMinutes: 4
publicIpAddresses: [{
id: publicIPAddress.id
}]
}
sku: {
name: 'Standard'
}
}
@description('Managed DevOps Pool Deployment')
resource managedDevOpsPool 'Microsoft.DevOpsInfrastructure/pools@2025-01-21' = {
name: _managedDevOpsPoolName_
location: __location__
properties: {
agentProfile: {
kind: 'Stateful'
}
devCenterProjectResourceId: devCenterProject.id
fabricProfile: {
sku: {
name: 'Standard_B2ms'
}
kind: 'Vmss'
images: [
{
wellKnownImageName: 'windows-2022/latest'
}
]
networkProfile: {
subnetId: virtualNetwork.properties.subnets[0].id
}
}
maximumConcurrency: 1
organizationProfile: {
kind: 'AzureDevOps'
organizations: [
{
url: 'https://dev.azure.com/${__devOpsOrganizationName__}'
projects: [
__devOpsProjectName__
]
}
]
}
}
}
デプロイ
Azure CLIで自分のテナントへサインインします。
az login --tenant <your-tenantid>
パラメータなどを調整し以下のコマンドでデプロイしてください。
ロケーションなどはjapaneast
にしてますが適宜かえてください。
az deployment sub create --name mdp-deploy-prod --location japaneast --template-file main.bicep --parameters main.bicepparam
デプロイメントを約 5 分間待つと、MDP に必要なリソース グループ内のすべてのリソースが表示されます。
VNET 統合に必要な、VNET レベルの DevOpsInfrastructure SPN に対するロールの割り当て (ネットワーク共同作成者と閲覧者) が行われています。
MDP を VNET に挿入するには、Microsoft.DevOpsInfrastructure/pools
へのサブネット委任が必要です。
NAT ゲートウェイが snet-mdp-prod に接続されています。これは、送信インターネット トラフィックが NAT ゲートウェイのパブリック IP を経由してルーティングされることを意味しています。
Azure DevOps プロジェクトを確認すると、新しく作成された MDP を確認できます。これで、パイプラインでエージェント プールを使用する準備が整いました。
動作確認
Azure DevOps で MDP を使用するためのシンプルなパイプラインを設定しました。
このパイプラインは、パブリック IP アドレスをキャプチャし、エージェントが送信トラフィックに使用するパブリック IP を取得します。
trigger: none
pool:
name: mdp-cloud-dev
steps:
- task: PowerShell@2
inputs:
targetType: 'inline'
script: |
(Invoke-WebRequest -uri "http://ifconfig.me/ip").Content
パイプラインが正常に実行されると、IP アドレス 172.192.24.135
が表示されます。
この IP アドレスを NAT ゲートウェイの送信 IP と同一であることが確認できました。これにより、Azure DevOps パイプラインエージェントが、サブネットに接続された VNET 内の NAT ゲートウェイを使用してブレークアウトされることが保証されます。
スタンバイエージェントを使用すると、ジョブを即座に実行できるため、パイプラインをトリガーするとエージェントがすぐにジョブの処理を開始できます。手動でスケジュールを設定することで、スタンバイエージェントの毎日の可用性を設定できます。
スタンバイエージェントが実行中の場合、 MDPリソース内の「エージェント」セクションでステータスを確認できます。
リファレンス
Discussion