🐥

Azure Managed DevOps Pools(MDP)をBicepで構築する

に公開

はじめに

Azure Managed DevOps Pools (MDP) を使用すると、Microsoft がホストする Azure DevOps エージェント プールを簡単に作成および管理できます。Azure コンポーネントをデプロイすると、Azure DevOps Organizationと統合され、残りの作業は Microsoft 側で処理を行います。

MDPのアーキテクチャ

作業を簡単にするために、Azure Bicep テンプレートを作成しました。

https://github.com/yutaka-art/mdp-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 リソース プロバイダーを登録する

https://learn.microsoft.com/ja-jp/azure/devops/managed-devops-pools/prerequisites?view=azure-devops&tabs=azure-portal#configure-your-azure-subscription

こちらに従い登録します。

マネージド 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

パラメータについてはそれぞれ調整して利用してください。

main.bicepparam
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'
main.bicep
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
}
modules/mdp.bicep
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 を取得します。

ipcheck.yaml
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リソース内の「エージェント」セクションでステータスを確認できます。

https://github.com/yutaka-art/mdp-bicep

リファレンス

https://learn.microsoft.com/ja-jp/azure/devops/managed-devops-pools/overview?view=azure-devops

https://learn.microsoft.com/ja-jp/azure/devops/managed-devops-pools/quickstart-azure-portal?view=azure-devops

https://learn.microsoft.com/ja-jp/azure/devops/managed-devops-pools/pricing?view=azure-devops

GitHubで編集を提案

Discussion