🦐

【Azure/Bicep】AI Foundry+PrivateEndpointをBicepからデプロイする方法

に公開

記事の内容

下記の構成でAI Foundryをデプロイする。
※下記のBicepでは、SubnetとNSGを余分に作成しています。用途に合わせて削ってください。

ディレクトリ構成

bicep-project/
├── deployments/
│   ├── main.bicep
│   └── rg.bicep
└── modules/
     ├── resourceGroup.bicep
     ├── vnet.bicep
     ├── NSG.bicep
     ├── aifoundry.bicep
     └── privateEndpoint.bicep

Bicep

rg.bicep/resourceGroup.bicep

rg.bicep

targetScope = 'subscription'
param rgName string = 'myProjectRG'
param location string = 'japaneast'

var tags = {
  Environment: 'dev'
}

module rgModule '../modules/resourceGroup.bicep' = {
  name: 'resourceGroup'
  params: {
    rgName: rgName
    location: location
    tags: tags
  }
}

resourceGroup.bicep

targetScope = 'subscription'
param rgName string
param location string
param tags object

resource RG 'Microsoft.Resources/resourceGroups@2022-09-01' = {
  name: rgName
  location: location
  tags: tags
}

output rgName string = RG.name
vnet.bicep

vnet.bicep

param vnetName string
param location string
param tags object
param vnetAddressPrefix string
param subnets array

resource vnet 'Microsoft.Network/virtualNetworks@2023-05-01' = {
  name: vnetName
  location: location
  tags: tags
  properties: {
    addressSpace: {
      addressPrefixes: [
        vnetAddressPrefix
      ]
    }
    subnets: [
      for (subnet, index) in subnets: {
        name: subnet.name
        properties: union(
          {
            addressPrefix: subnet.addressPrefix
            privateEndpointNetworkPolicies: 'Enabled'
            networkSecurityGroup: {
              id: resourceId('Microsoft.Network/networkSecurityGroups', subnet.nsgName)
            }
          },
          index == 2
            ? {
                delegations: [
                  {
                    name: 'delegation'
                    properties: {
                      serviceName: 'Microsoft.Web/serverFarms'
                    }
                  }
                ]
              }
            : {}
        )
      }
    ]
  }
}

output vnetId string = vnet.id
output vnetName string = vnet.name
output agwsubnetId01 string = vnet.properties.subnets[0].id
output pesubnetId01 string = vnet.properties.subnets[1].id
output vnetIntegrationSubnetId string = vnet.properties.subnets[2].id
output pesubnetId02 string = vnet.properties.subnets[3].id
NSG.bicep

NSG.bicep

param location string = 'japaneast'
param tags object = {
  environment: 'sample'
}
param subnets array = [
  {
    name: 'subnet-app'
    nsgName: 'nsg-app'
  }
  {
    name: 'subnet-pe'
    nsgName: 'nsg-pe'
  }
]

var nsgRules = {
  '${subnets[0].nsgName}': [
    {
      name: 'AllowAppSubnetInbound'
      priority: 100
      direction: 'Inbound'
      access: 'Allow'
      protocol: '*'
      sourcePortRange: '*'
      destinationPortRange: '*'
      sourceAddressPrefix: '10.1.0.0/24' // sample private range
      destinationAddressPrefix: '*'
    }
    {
      name: 'DenyAllOutbound'
      priority: 4096
      direction: 'Outbound'
      access: 'Deny'
      protocol: '*'
      sourcePortRange: '*'
      destinationPortRange: '*'
      sourceAddressPrefix: '*'
      destinationAddressPrefix: '*'
    }
  ]
  '${subnets[1].nsgName}': [
    {
      name: 'AllowHttpsOutbound'
      priority: 100
      direction: 'Outbound'
      access: 'Allow'
      protocol: 'Tcp'
      sourcePortRange: '*'
      destinationPortRange: '443'
      sourceAddressPrefix: '*'
      destinationAddressPrefix: 'Internet'
    }
    {
      name: 'DenyAllOutbound'
      priority: 4096
      direction: 'Outbound'
      access: 'Deny'
      protocol: '*'
      sourcePortRange: '*'
      destinationPortRange: '*'
      sourceAddressPrefix: '*'
      destinationAddressPrefix: '*'
    }
  ]
}

resource nsgs 'Microsoft.Network/networkSecurityGroups@2023-05-01' = [
  for subnet in subnets: {
    name: subnet.nsgName
    location: location
    tags: tags
    properties: {
      securityRules: [
        for rule in nsgRules[subnet.nsgName]: {
          name: rule.name
          properties: {
            priority: rule.priority
            direction: rule.direction
            access: rule.access
            protocol: rule.protocol
            sourcePortRange: rule.sourcePortRange
            destinationPortRange: rule.destinationPortRange
            sourceAddressPrefix: rule.sourceAddressPrefix
            destinationAddressPrefix: rule.destinationAddressPrefix
          }
        }
      ]
    }
  }
]

output appNsgId string = nsgs[0].id
output peNsgId string = nsgs[1].id

main.bicep

// 共通パラメータ (sample)
targetScope = 'subscription'
param rgName string = 'sampleRG'
param location string = 'japaneast'
param environment string = 'dev'
param projectCode string = 'sample'

param tags object = {
  Environment: 'dev'
}

// ネットワーク関連 (sample ranges)
var subnets = [
  {
    name: 'sample-agw-subnet01'
    addressPrefix: '10.1.0.0/24'
    nsgName: 'sample-agw-nsg01'
  }
  {
    name: 'sample-pe-subnet01'
    addressPrefix: '10.1.1.0/24'
    nsgName: 'sample-pe-nsg01'
  }
  {
    name: 'sample-integration-subnet01'
    addressPrefix: '10.1.2.0/24'
    nsgName: 'sample-integration-nsg01'
  }
  {
    name: 'sample-pe-subnet02'
    addressPrefix: '10.1.3.0/24'
    nsgName: 'sample-pe-nsg02'
  }
]

module nsgModule '../modules/NSG.bicep' = {
  name: 'NetworkSecurityGroup'
  scope: resourceGroup(rgName)
  params: {
    location: location
    tags: tags
    subnets: subnets
  }
}

module vnetModule '../modules/vnet.bicep' = {
  name: 'VirtualNetwork'
  scope: resourceGroup(rgName)
  params: {
    vnetName: 'sampleVnet01'
    location: location
    tags: tags
    vnetAddressPrefix: '10.1.0.0/16'
    subnets: subnets
  }
  dependsOn: [
    nsgModule
  ]
}

// AI Foundry (sample)
param aiFoundryName string = 'sample-ai-foundry'
param aiProjectName string = '${aiFoundryName}-project'

module aiFoundryModule '../modules/aifoundry.bicep' = {
  name: 'ai-foundry'
  scope: resourceGroup(rgName)
  params: {
    aiFoundryName: aiFoundryName
    aiProjectName: aiProjectName
    location: location
  }
}

// AI Foundry PrivateEndpoint (sample)
module aifoundryPrivateEndpoint '../modules/privateEndpoint.bicep' = {
  name: 'aifoundry-pe'
  scope: resourceGroup(rgName)
  params: {
    privateEndpointName: 'sample-aifoundry-pe01'
    location: location
    tags: tags
    subnetId: vnetModule.outputs.peSubnetId02
    targetResourceId: aiFoundryModule.outputs.aiFoundryId
    groupIds: ['account']
    privateDnsZoneNames: [
      'privatelink.cognitiveservices.azure.com'
      'privatelink.openai.azure.com'
      'privatelink.services.ai.azure.com'
    ]
    vnetId: vnetModule.outputs.vnetId
  }
  dependsOn: [
    vnetModule
    aiFoundryModule
  ]
}

aifoundry.bicep

param aiFoundryName string
param aiProjectName string
param location string

// AI Foundry アカウントの作成
resource aiFoundry 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' = {
  name: aiFoundryName
  location: location
  identity: {
    type: 'SystemAssigned'
  }
  sku: {
    name: 'S0'
  }
  kind: 'AIServices'
  properties: {
    // AI Foundry での動作に必要な設定
    allowProjectManagement: true // プロジェクト管理機能を有効化
    // 開発者API エンドポイントのサブドメイン名を定義
    customSubDomainName: aiFoundryName // カスタムサブドメイン(例:uniquename.cognitiveservices.azure.com)
    disableLocalAuth: false // ローカル認証を無効化(Azure AD認証のみ使用)
    publicNetworkAccess: 'Enabled' // パブリックネットワークアクセスを無効化
    networkAcls: {
      defaultAction: 'Allow' // デフォルトでアクセス拒否
      bypass: 'AzureServices' // Azureサービスは許可
    }
  }
}

// AI プロジェクトの作成
resource aiProject 'Microsoft.CognitiveServices/accounts/projects@2025-04-01-preview' = {
  name: aiProjectName
  parent: aiFoundry // 親の AI Foundry アカウントに紐づけ
  location: location
  identity: {
    type: 'SystemAssigned' // プロジェクト専用のマネージドIDを作成
  }
  properties: {} // 現在は追加プロパティなし
}

// モデルデプロイメントの作成(オプション)
resource modelDeployment 'Microsoft.CognitiveServices/accounts/deployments@2024-10-01' = {
  parent: aiFoundry // AI Foundry アカウントの子リソースとして作成
  name: 'gpt-4o' // デプロイメント名
  sku: {
    capacity: 1 // デプロイ容量(TPM:Tokens Per Minute の単位)
    name: 'GlobalStandard' // グローバル標準SKU(複数リージョンで負荷分散)
  }
  properties: {
    model: {
      name: 'gpt-4o' // デプロイするモデル名
      format: 'OpenAI' // OpenAI互換フォーマット
    }
  }
}

privateEndpoint.bicep

param privateEndpointName string
param location string
param tags object
param subnetId string
param targetResourceId string
param groupIds array
param privateDnsZoneNames array
param vnetId string

resource privateDnsZones 'Microsoft.Network/privateDnsZones@2020-06-01' = [
  for zoneName in privateDnsZoneNames: {
    name: zoneName
    location: 'global'
    tags: tags
  }
]

resource privateDnsZoneLinks 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = [
  for (zoneName, index) in privateDnsZoneNames: {
    parent: privateDnsZones[index]
    name: 'vnet-link-${index}'
    location: 'global'
    tags: tags
    properties: {
      registrationEnabled: false
      virtualNetwork: {
        id: vnetId
      }
    }
  }
]

resource privateEndpoint 'Microsoft.Network/privateEndpoints@2023-05-01' = {
  name: privateEndpointName
  location: location
  tags: tags
  properties: {
    subnet: {
      id: subnetId
    }
    privateLinkServiceConnections: [
      {
        name: '${privateEndpointName}-connection'
        properties: {
          privateLinkServiceId: targetResourceId
          groupIds: groupIds
        }
      }
    ]
  }
}

resource privateDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2023-05-01' = {
  parent: privateEndpoint
  name: 'default'
  properties: {
    privateDnsZoneConfigs: [
      for (zoneName, index) in privateDnsZoneNames: {
        name: replace(zoneName, '.', '-')
        properties: {
          privateDnsZoneId: privateDnsZones[index].id
        }
      }
    ]
  }
  dependsOn: [
    privateDnsZoneLinks
  ]
}

output privateEndpointId string = privateEndpoint.id
output privateEndpointName string = privateEndpoint.name
output privateDnsZoneIds array = [for (zoneName, index) in privateDnsZoneNames: privateDnsZones[index].id]

デプロイ手順

1. Azure CLIへのログイン

az login

2. リソースグループのデプロイ

az deployment sub create `
  --location japaneast `
  --template-file .\deployments\rg.bicep

3. メインリソースのデプロイ

az deployment sub create `
  --location japaneast `
  --template-file .\deployments\main.bicep

デプロイ時に起きた課題と解決案

  • AI Foundryのリソース不足

    • デプロイリージョン(location)を変更する(japaneast, eastusなど)
  • リージョンの不一致

    • AI Foundryをデプロイするリージョンとネットワーク関連(VNet,Subnet,NSG,PE)のリージョンは統一する
  • AI Foundryの名前被り

    • 一意の名前に変更してからデプロイする
  • プロビジョニングの問題

    • PrivateEndpointを別モジュールに作成+depends onで対応
// AI Foundry PrivateEndpoint (sample)
module aifoundryPrivateEndpoint '../modules/privateEndpoint.bicep' = {
 name: 'aifoundry-pe'
 scope: resourceGroup(rgName)
 params: {
   privateEndpointName: 'sample-aifoundry-pe01'
   location: location
   tags: tags
   subnetId: vnetModule.outputs.peSubnetId02
   targetResourceId: aiFoundryModule.outputs.aiFoundryId
   groupIds: ['account']
   privateDnsZoneNames: [
     'privatelink.cognitiveservices.azure.com'
     'privatelink.openai.azure.com'
     'privatelink.services.ai.azure.com'
   ]
   vnetId: vnetModule.outputs.vnetId
 }
 dependsOn: [
   vnetModule
   aiFoundryModule
 ]
}

参考

https://learn.microsoft.com/ja-jp/azure/ai-foundry/agents/concepts/model-region-support?tabs=global-standard

ヘッドウォータース

Discussion