💪

手書きBicepをAVMに乗り換えたらPrivate Endpoint周りがこんなにシンプルになった

に公開

はじめに

以前、私はAzureのPrivate EndpointまわりのBicepを書くたびに「なんでこんなに長くなるんだろう」と感じていました。

VNetの定義、サブネット、Private Endpointリソース、DNS Zoneの作成、VNetリンク、DNSゾーングループ……それぞれ別々に書いて、IDを繋いで、ようやく動く。コードは正確でも、読み返したときに全体像が見えにくくなっていました。

Azure Verified Modules(AVM) に乗り換えてから、その悩みがかなり解消されました。この記事では実際に書き換えたコードを比較しながら、AVMのどこが便利なのかを紹介します。

Azure Verified Modules(AVM)とは

AVMはMicrosoftが提供するBicep/Terraformの公式モジュール集です。Bicep Public Registryに登録されているモジュールは現在AVM準拠のものだけとなっており、Microsoftの「One IaC」戦略の中核を担っています。

https://azure.github.io/Azure-Verified-Modules/

モジュールは以下の3種類があります。

種類 概要
Resource Module 単一のAzureリソース VNet、Storage Account など
Pattern Module 複数リソースのまとまり Hub-Spoke構成 など
Utility Module 型定義やヘルパー 共通型 など

今回使うのは Resource Module です。

今回作るアーキテクチャ(使用バージョン)

今回使うモジュールのバージョンです(2026年2月時点)。

モジュール バージョン GitHubリポジトリ
avm/res/network/virtual-network 0.7.0 リンク
avm/res/network/private-dns-zone 0.7.0 リンク
avm/res/storage/storage-account 0.19.0 リンク

今回作るアーキテクチャ

シンプルな構成でAVMの恩恵を実感します。

使うAVMモジュールはこの3つです。

  • br/public:avm/res/network/virtual-network:0.7.0
  • br/public:avm/res/network/private-dns-zone:0.7.0
  • br/public:avm/res/storage/storage-account:0.19.0

手書きBicepとの比較

まずPrivate Endpointを手書きで定義するとどうなるか振り返ります。

手書きBicepの場合(抜粋)

// (1) VNet
resource vnet 'Microsoft.Network/virtualNetworks@2023-11-01' = {
  name: vnetName
  location: location
  properties: {
    addressSpace: { addressPrefixes: [vnetAddressPrefix] }
    subnets: [
      { name: 'snet-general', properties: { addressPrefix: '10.0.0.0/24' } }
      { name: 'snet-pe', properties: { addressPrefix: '10.0.1.0/24', privateEndpointNetworkPolicies: 'Disabled' } }
    ]
  }
}

// (2) Private DNS Zone
resource privateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = {
  name: 'privatelink.blob.core.windows.net'
  location: 'global'
}

// (3) VNet リンク(DNSゾーンとVNetを紐付け)
resource vnetLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = {
  name: '${privateDnsZone.name}/link-${vnetName}'
  location: 'global'
  properties: {
    virtualNetwork: { id: vnet.id }
    registrationEnabled: false
  }
}

// (4) Storage Account
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
  name: storageAccountName
  location: location
  sku: { name: 'Standard_LRS' }
  kind: 'StorageV2'
  properties: {
    publicNetworkAccess: 'Disabled'
    networkAcls: { bypass: 'AzureServices', defaultAction: 'Deny' }
  }
}

// (5) Private Endpoint
resource privateEndpoint 'Microsoft.Network/privateEndpoints@2023-11-01' = {
  name: 'pep-${storageAccountName}-blob'
  location: location
  properties: {
    subnet: { id: '${vnet.id}/subnets/snet-pe' }
    privateLinkServiceConnections: [
      {
        name: 'blob-connection'
        properties: {
          privateLinkServiceId: storageAccount.id
          groupIds: ['blob']
        }
      }
    ]
  }
}

// (6) DNS Zone Group(PEとDNSゾーンを紐付け)
resource dnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2023-11-01' = {
  name: '${privateEndpoint.name}/blob-dns-zone-group'
  properties: {
    privateDnsZoneConfigs: [
      {
        name: 'blob-config'
        properties: { privateDnsZoneId: privateDnsZone.id }
      }
    ]
  }
}

リソースが 6つ 。それぞれのIDを正確に繋いでいく必要があり、書いている最中に「あれ、これどのIDを渡すんだっけ」となりがちです。

AVMに乗り換えた場合

// (1) VNet
module vnet 'br/public:avm/res/network/virtual-network:0.7.0' = {
  name: 'vnet-deployment'
  params: {
    name: vnetName
    location: location
    addressPrefixes: [vnetAddressPrefix]
    subnets: [
      { name: 'snet-general', addressPrefix: '10.0.0.0/24' }
      { name: 'snet-pe', addressPrefix: '10.0.1.0/24', privateEndpointNetworkPolicies: 'Disabled' }
    ]
    tags: tags
    enableTelemetry: false
  }
}

// (2) Private DNS Zone(VNetリンクも一緒に定義できる!)
module privateDnsZone 'br/public:avm/res/network/private-dns-zone:0.7.0' = {
  name: 'private-dns-zone-deployment'
  params: {
    name: 'privatelink.blob.${environment().suffixes.storage}'
    virtualNetworkLinks: [
      {
        name: '${vnetName}-link'
        virtualNetworkResourceId: vnet.outputs.resourceId
        registrationEnabled: false
      }
    ]
    tags: tags
    enableTelemetry: false
  }
}

// (3) Storage Account(Private Endpoint + DNS 登録まで全部ここで完結!)
module storageAccount 'br/public:avm/res/storage/storage-account:0.19.0' = {
  name: 'storage-account-deployment'
  params: {
    name: storageAccountName
    location: location
    skuName: 'Standard_LRS'
    kind: 'StorageV2'
    publicNetworkAccess: 'Disabled'
    networkAcls: { bypass: 'AzureServices', defaultAction: 'Deny' }
    blobServices: {
      containers: [{ name: 'data', publicAccess: 'None' }]
    }
    // ここが最大のポイント!
    // privateEndpoints を書くだけで、PEの作成・サブネット配置・DNS登録まで自動
    privateEndpoints: [
      {
        name: 'pep-${storageAccountName}-blob'
        service: 'blob'
        subnetResourceId: '${vnet.outputs.resourceId}/subnets/snet-pe'
        privateDnsZoneGroup: {
          privateDnsZoneGroupConfigs: [
            { privateDnsZoneResourceId: privateDnsZone.outputs.resourceId }
          ]
        }
      }
    ]
    tags: tags
    enableTelemetry: false
  }
}

モジュールが 3つ になりました。特にStorage AccountのAVMモジュールがすごくて、privateEndpoints パラメータを1ブロック書くだけで、Private Endpointの作成・サブネットへの配置・DNS Zoneへの登録まで全部やってくれます。手書きで6ステップかかっていた処理が、この1ブロックに収まります。

完全なコード

ファイル構成はこうなります。

.
├── main.bicep         # メインテンプレート
└── main.bicepparam    # パラメータファイル(dev環境用)

main.bicep(全文)

// ============================================================
// main.bicep
// AVM を使った Private Endpoint 付きアーキテクチャのサンプル
// ============================================================

targetScope = 'resourceGroup'

@description('リソースを作成するAzureリージョン')
param location string = resourceGroup().location

@description('リソース名のプレフィックス (例: myapp)')
param prefix string = 'myapp'

@description('VNet のアドレス空間')
param vnetAddressPrefix string = '10.0.0.0/16'

@description('汎用サブネットのアドレス範囲')
param generalSubnetPrefix string = '10.0.0.0/24'

@description('Private Endpoint 専用サブネットのアドレス範囲')
param peSubnetPrefix string = '10.0.1.0/24'

@description('AVMテレメトリを有効にするか')
param enableTelemetry bool = false

var vnetName = '${prefix}-vnet'
var storageAccountName = toLower('${prefix}st${uniqueString(resourceGroup().id)}')
var privateDnsZoneName = 'privatelink.blob.${environment().suffixes.storage}'
var tags = {
  Environment: 'dev'
  ManagedBy: 'Bicep-AVM'
}

module vnet 'br/public:avm/res/network/virtual-network:0.7.0' = {
  name: 'vnet-deployment'
  params: {
    name: vnetName
    location: location
    addressPrefixes: [vnetAddressPrefix]
    subnets: [
      { name: 'snet-general', addressPrefix: generalSubnetPrefix }
      { name: 'snet-pe', addressPrefix: peSubnetPrefix, privateEndpointNetworkPolicies: 'Disabled' }
    ]
    tags: tags
    enableTelemetry: enableTelemetry
  }
}

module privateDnsZone 'br/public:avm/res/network/private-dns-zone:0.7.0' = {
  name: 'private-dns-zone-deployment'
  params: {
    name: privateDnsZoneName
    virtualNetworkLinks: [
      {
        name: '${vnetName}-link'
        virtualNetworkResourceId: vnet.outputs.resourceId
        registrationEnabled: false
      }
    ]
    tags: tags
    enableTelemetry: enableTelemetry
  }
}

module storageAccount 'br/public:avm/res/storage/storage-account:0.19.0' = {
  name: 'storage-account-deployment'
  params: {
    name: storageAccountName
    location: location
    skuName: 'Standard_LRS'
    kind: 'StorageV2'
    publicNetworkAccess: 'Disabled'
    networkAcls: {
      bypass: 'AzureServices'
      defaultAction: 'Deny'
    }
    blobServices: {
      containers: [{ name: 'data', publicAccess: 'None' }]
    }
    privateEndpoints: [
      {
        name: 'pep-${storageAccountName}-blob'
        service: 'blob'
        subnetResourceId: '${vnet.outputs.resourceId}/subnets/snet-pe'
        privateDnsZoneGroup: {
          privateDnsZoneGroupConfigs: [
            { privateDnsZoneResourceId: privateDnsZone.outputs.resourceId }
          ]
        }
      }
    ]
    tags: tags
    enableTelemetry: enableTelemetry
  }
}

output vnetResourceId string = vnet.outputs.resourceId
output storageAccountResourceId string = storageAccount.outputs.resourceId
output storageAccountName string = storageAccount.outputs.name
output privateDnsZoneResourceId string = privateDnsZone.outputs.resourceId

main.bicepparam

using './main.bicep'

param location = 'japaneast'
param prefix = 'myapp'
param vnetAddressPrefix = '10.0.0.0/16'
param generalSubnetPrefix = '10.0.0.0/24'
param peSubnetPrefix = '10.0.1.0/24'
param enableTelemetry = false

デプロイ方法

# リソースグループを作成
az group create --name rg-avm-sample --location japaneast

# デプロイ(パラメータファイルを使う場合)
az deployment group create \
  --resource-group rg-avm-sample \
  --template-file main.bicep \
  --parameters main.bicepparam

# デプロイ(パラメータを直接渡す場合)
az deployment group create \
  --resource-group rg-avm-sample \
  --template-file main.bicep \
  --parameters prefix=myapp location=japaneast

デプロイが完了すると、以下のリソースが作成されます。

  • VNet(サブネット2つ)
  • Private DNS Zone(privatelink.blob.core.windows.net)+ VNetリンク
  • Storage Account(パブリックアクセス無効)
  • Blob用Private Endpoint(snet-peサブネットに配置)
  • DNS Aレコード(Private EndpointのIPを自動登録)

知っておくと便利なこと

バージョンは固定する

AVMモジュールは頻繁に更新されます。0.7.0 のように バージョンを固定して使う ことを強くおすすめします。latest の指定はできないので、使用前にGitHubやAzAdvertizerで最新バージョンを確認してください(記事末尾の参考リンクを参照)。

enableTelemetry について

AVMモジュールにはデフォルトで enableTelemetry: true が設定されており、匿名の利用統計がMicrosoftに送信されます。不要な場合は enableTelemetry: false を指定してください。

params: {
  // ...
  enableTelemetry: false  // テレメトリを無効化
}

VS Code の補完が効く

Bicep拡張機能を入れていれば、br/public: のエイリアスに対して補完が効きます。モジュールのパラメータ名や型を確認しながら書けるので、ドキュメントを別タブで開かなくても作業できます(記事末尾の参考リンクを参照)。

まとめ

AVMに乗り換えてよかったポイントをまとめます。

ポイント 内容
コード量が減る Private Endpoint関連で6リソース → 3モジュールに
設定漏れが減る privateEndpointNetworkPolicies など推奨設定が組み込み済み
可読性が上がる 各モジュールが「何をしているか」明確
更新が楽 Microsoftがモジュールをメンテしてくれる

特に「Storage AccountモジュールのparamにPrivate Endpoint情報を書くだけでDNS登録まで完結する」という体験は最初かなり感動しました。

今回はStorageを例にしましたが、Key VaultやCognitive Services(Azure OpenAI)でも同じパターンが使えます。ぜひ試してみてください。

参考


Discussion