ARM Templateで行っていた仮想マシンの展開をBicepに移行する-仮想マシン作成編-

はじめに

前回の記事では、ARM TemplateからBicepの移行を、リソースを限定したうえで実施して、いい結果を得ることが出来ました。

blog.jbs.co.jp

今度は、仮想マシン作成用のテンプレートを使い、Azure Compute GalleryからBicepで仮想マシンを展開してみたいと思います。

ARM Templateの確認

作成されるリソース

作成イメージはこんな感じです。

実際にデプロイした様子です。

テンプレートファイル

パラメーター、変数、ループ、依存関係などがあるのは前回同様ですが、仮想マシンや仮想マシン作成後のカスタムスクリプトの実行、Azure Key Vaultの参照など、より複雑になっています。

かなり長いので、参照時はクリックして展開したうえで見てください。

テンプレートファイル(クリックで展開)

{
    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "numberOfInstances": {
            "defaultValue": 1,
            "minValue": 1,
            "maxValue": 20,
            "type": "Int",
            "metadata": {
                "description": "Number of VM Sets to deploy"
            }
        },
        "adminPassword": {
            "type": "SecureString",
            "metadata": {
                "description": "Password for the Virtual Machine."
            }
        },
        "labName": {
            "type": "String",
            "metadata": {
                "description": "Unique Prefix. ex) ad0717"
            }
        },
        "scriptURI": {
            "type": "String"
        }

    },
    "variables": {
        "adminUsername": "(管理者のID)",
        "addressPrefix": "10.0.0.0/16",
        "subnet1Name": "Subnet-1",
        "subnet1Prefix": "10.0.0.0/24",
        "imageReference": {
            "id": "(Azure Compute ImageのイメージID)"
        },
        "location": "westus2",
        "privateIP-vm1" : "10.0.0.5"
    },
    "resources": [
        {
            "type": "Microsoft.Network/publicIPAddresses",
            "apiVersion": "2018-11-01",
            "name": "[concat(parameters('labName'),'-PIP-VM1-',copyindex())]",
            "location": "[variables('location')]",
            "properties": {
                "publicIPAllocationMethod": "Dynamic",
                "dnsSettings": {
                    "domainNameLabel": "[concat(parameters('labName'),'-vm1-',copyindex())]"
                }
            },
            "copy": {
                "name": "pip-vm1-Loop",
                "count": "[parameters('numberOfInstances')]"
            }
        },
        {
            "type": "Microsoft.Network/networkSecurityGroups",
            "apiVersion": "2019-08-01",
            "name": "[concat(parameters('labName'),'-NSG-',copyindex())]",
            "location": "[variables('location')]",
            "properties": {
                "securityRules": [
                    {
                        "name": "default-allow-RDP",
                        "properties": {
                            "priority": 1000,
                            "access": "Allow",
                            "direction": "Inbound",
                            "destinationPortRange": "3389",
                            "protocol": "Tcp",
                            "sourceAddressPrefix": "*",
                            "sourcePortRange": "*",
                            "destinationAddressPrefix": "*"
                        }
                    }
                ]
            },
            "copy": {
                "name": "nsg-Loop",
                "count": "[parameters('numberOfInstances')]"
            }
        },
        {
            "type": "Microsoft.Network/virtualNetworks",
            "apiVersion": "2016-03-30",
            "name": "[concat(parameters('labName'),'-VNET-',copyindex())]",
            "location": "[variables('location')]",
            "dependsOn": [
                "[resourceId('Microsoft.Network/networkSecurityGroups', concat(parameters('labName'),'-NSG-',copyindex()))]"
            ],
            "properties": {
                "addressSpace": {
                    "addressPrefixes": [
                        "[variables('addressPrefix')]"
                    ]
                },
                "subnets": [
                    {
                        "name": "[variables('subnet1Name')]",
                        "properties": {
                            "addressPrefix": "[variables('subnet1Prefix')]",
                            "networkSecurityGroup": {
                                "id": "[resourceId('Microsoft.Network/networkSecurityGroups', concat(parameters('labName'),'-NSG-',copyindex()))]"
                            }
                        }
                    }
                ]
            },
            "copy": {
                "name": "vnet-Loop",
                "count": "[parameters('numberOfInstances')]"
            }
        },
        
        {
            "type": "Microsoft.Network/networkInterfaces",
            "apiVersion": "2016-03-30",
            "name": "[concat(parameters('labName'),'-NIC-VM1-',copyindex())]",
            "location": "[variables('location')]",
            "dependsOn": [
                "[concat(parameters('labName'),'-VNET-',copyindex())]",
                "pip-vm1-Loop"
            ],
            "properties": {
                "ipConfigurations": [
                    {
                        "name": "ipconfig1",
                        "properties": {
                            "privateIPAllocationMethod": "Static",
                            "privateIPAddress": "[variables('privateIP-vm1')]",
                            "publicIPAddress": {
                                "id": "[resourceId('Microsoft.Network/publicIPAddresses',concat(parameters('labName'),'-PIP-VM1-',copyindex()))]"
                            },
                            "subnet": {
                                "id": "[resourceId('Microsoft.Network/virtualNetworks/subnets',concat(parameters('labName'),'-VNET-',copyindex()),variables('subnet1Name'))]"
                            }
                        }
                    }
                ]
            },
            "copy": {
                "name": "nic-vm1-Loop",
                "count": "[parameters('numberOfInstances')]"
            }
        },
        {
            "type": "Microsoft.Compute/virtualMachines",
            "apiVersion": "2022-03-01",
            "name": "[concat(parameters('labName'),'-VM1-',copyindex())]",
            "location": "[variables('location')]",
            "dependsOn": [
                "nic-VM1-Loop"
            ],
            "properties": {
                "hardwareProfile": {
                    "vmSize": "Standard_B2s"
                },
                "osProfile": {
                    "computerName": "[concat(parameters('labName'),'-VM1-',copyindex())]",
                    "adminUsername": "[variables('adminUsername')]",
                    "adminPassword": "[parameters('adminPassword')]"
                },
                "storageProfile": {
                    "imageReference": "[variables('imageReference')]",
                    "osDisk": {
                        "createOption": "FromImage",
                        "managedDisk": {
                            "storageAccountType": "Standard_LRS"
                        }
                    }
                },
                "networkProfile": {
                    "networkInterfaces": [
                        {
                            "id": "[resourceId('Microsoft.Network/networkInterfaces',concat(parameters('labName'),'-NIC-VM1-',copyindex()))]"
                        }
                    ]
                }
            },
            "copy": {
                "name": "vm-VM1-Loop",
                "count": "[parameters('numberOfInstances')]"
            }
        },
        {
            "type": "Microsoft.Compute/virtualMachines/extensions",
            "apiVersion": "2019-12-01",
            "name": "[concat(parameters('labName'),'-VM1-',copyindex(),'/', 'Install-ADDS')]",
            "location": "[variables('location')]",
            "dependsOn": [
                "[concat('Microsoft.Compute/virtualMachines/',parameters('labName'),'-VM1-',copyindex())]"
            ],
            "properties": {
                "publisher": "Microsoft.Compute",
                "type": "CustomScriptExtension",
                "typeHandlerVersion": "1.7",
                "autoUpgradeMinorVersion": true,
                "settings": {
                    "fileUris": [
                        "[parameters('scriptURI')]"
                    ],
                    "commandToExecute": "powershell.exe -ExecutionPolicy Unrestricted -File adds.ps1"
                }
            },
            "copy": {
                "name": "vm-VM1-dsc-Loop",
                "count": "[parameters('numberOfInstances')]"
            }
        }
    ]
}

パラメーターファイル

前出のテンプレートとセットで使用するパラメーターファイルです。

パラメーターファイル(クリックで展開)

{
    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "adminPassword": {
            "reference": {
                "keyVault": {
                    "id": "(Azure Key VaultのID)"
                },
                "secretName": "(Azure Key Vaultのシークレット名)"
            }
        },
        "numberOfInstances": {
            "value": 2
        },
        "scriptURI": {
            "value": "(ストレージアカウント上のスクリプトファイルのURI)"
        }
    }
}

デプロイ用のPowershellスクリプト

テンプレートファイルとパラメーターファイルを使ってデプロイするためのスクリプトです。前回の記事と同じですが一応こちらにも書いておきます。

$labName = "(任意の英数字。lab1201など)"

$location = "westus2"
$today=Get-Date -Format "yyyy-MM-dd-hh-mm"
$deployName = $labName + "-Deploy-" + $today

$SubscriptionID = "(サブスクリプションID)"

Connect-AzAccount

$context = Get-AzSubscription -SubscriptionId $SubscriptionID
Set-AzContext $context

New-AzResourceGroup -Name $labName -Location $location
New-AzResourceGroupDeployment -Name $deployName -ResourceGroupName $labName `
  -TemplateFile "(テンプレートファイル名:例:test-template.json)" `
  -TemplateParameterFile "(パラメーターファイル名:例:test-template.parameter.json)" `
  -labName $labName

Bicepファイルの作成

ARM Templateのテンプレートファイルの変換

今回のテンプレートファイルも変換していきます。

bicep decompile this-template.json

やはりエラーが出ていてこのままでは使えないのですが、ひとまず出来ました。

Bicepファイル(クリックで展開

@description('Number of VM Sets to deploy')
@minValue(1)
@maxValue(20)
param numberOfInstances int = 1

@description('Password for the Virtual Machine.')
@secure()
param adminPassword string

@description('Unique Prefix. ex) ad0717')
param labName string
param scriptURI string

var adminUsername = '(管理者のID)'
var addressPrefix = '10.0.0.0/16'
var subnet1Name = 'Subnet-1'
var subnet1Prefix = '10.0.0.0/24'
var imageReference = {
  id: '(Azure Compute ImageのイメージID)'
}
var location = 'westus2'
var privateIP_vm1 = '10.0.0.5'

resource labName_PIP_VM1 'Microsoft.Network/publicIPAddresses@2018-11-01' = [for i in range(0, numberOfInstances): {
  name: '${labName}-PIP-VM1-${i}'
  location: location
  properties: {
    publicIPAllocationMethod: 'Dynamic'
    dnsSettings: {
      domainNameLabel: '${labName}-vm1-${i}'
    }
  }
}]

resource labName_NSG 'Microsoft.Network/networkSecurityGroups@2019-08-01' = [for i in range(0, numberOfInstances): {
  name: '${labName}-NSG-${i}'
  location: location
  properties: {
    securityRules: [
      {
        name: 'default-allow-RDP'
        properties: {
          priority: 1000
          access: 'Allow'
          direction: 'Inbound'
          destinationPortRange: '3389'
          protocol: 'Tcp'
          sourceAddressPrefix: '*'
          sourcePortRange: '*'
          destinationAddressPrefix: '*'
        }
      }
    ]
  }
}]

resource labName_VNET 'Microsoft.Network/virtualNetworks@2016-03-30' = [for i in range(0, numberOfInstances): {
  name: '${labName}-VNET-${i}'
  location: location
  properties: {
    addressSpace: {
      addressPrefixes: [
        addressPrefix
      ]
    }
    subnets: [
      {
        name: subnet1Name
        properties: {
          addressPrefix: subnet1Prefix
          networkSecurityGroup: {
            id: resourceId('Microsoft.Network/networkSecurityGroups', '${labName}-NSG-${i}')
          }
        }
      }
    ]
  }
  dependsOn: [
    resourceId('Microsoft.Network/networkSecurityGroups', '${labName}-NSG-${i}')
  ]
}]

resource labName_NIC_VM1 'Microsoft.Network/networkInterfaces@2016-03-30' = [for i in range(0, numberOfInstances): {
  name: '${labName}-NIC-VM1-${i}'
  location: location
  properties: {
    ipConfigurations: [
      {
        name: 'ipconfig1'
        properties: {
          privateIPAllocationMethod: 'Static'
          privateIPAddress: privateIP_vm1
          publicIPAddress: {
            id: resourceId('Microsoft.Network/publicIPAddresses', '${labName}-PIP-VM1-${i}')
          }
          subnet: {
            id: resourceId('Microsoft.Network/virtualNetworks/subnets', '${labName}-VNET-${i}', subnet1Name)
          }
        }
      }
    ]
  }
  dependsOn: [
    '${labName}-VNET-${i}'
    labName_PIP_VM1
  ]
}]

resource labName_VM1 'Microsoft.Compute/virtualMachines@2022-03-01' = [for i in range(0, numberOfInstances): {
  name: '${labName}-VM1-${i}'
  location: location
  properties: {
    hardwareProfile: {
      vmSize: 'Standard_B2s'
    }
    osProfile: {
      computerName: '${labName}-VM1-${i}'
      adminUsername: adminUsername
      adminPassword: adminPassword
    }
    storageProfile: {
      imageReference: imageReference
      osDisk: {
        createOption: 'FromImage'
        managedDisk: {
          storageAccountType: 'Standard_LRS'
        }
      }
    }
    networkProfile: {
      networkInterfaces: [
        {
          id: resourceId('Microsoft.Network/networkInterfaces', '${labName}-NIC-VM1-${i}')
        }
      ]
    }
  }
  dependsOn: [
    labName_NIC_VM1
  ]
}]

resource labName_VM1_Install_ADDS 'Microsoft.Compute/virtualMachines/extensions@2019-12-01' = [for i in range(0, numberOfInstances): {
  name: '${labName}-VM1-${i}/Install-ADDS'
  location: location
  properties: {
    publisher: 'Microsoft.Compute'
    type: 'CustomScriptExtension'
    typeHandlerVersion: '1.7'
    autoUpgradeMinorVersion: true
    settings: {
      fileUris: [
        scriptURI
      ]
      commandToExecute: 'powershell.exe -ExecutionPolicy Unrestricted -File adds.ps1'
    }
  }
  dependsOn: [
    'Microsoft.Compute/virtualMachines/${labName}-VM1-${i}'
  ]
}]

生成されたBicepファイルの修正

今回も、エラー発生個所は、各リソースのの依存関係に関する部分でした。一例です。

  dependsOn: [
    'Microsoft.Compute/virtualMachines/${labName}-VM1-${i}'
  ]

前回は、依存関係の表記を変えるだけだったのですが、そもそもBicepの場合、他のリソースを指定している場合は、暗黙的に依存関係が成り立つようです。

learn.microsoft.com

という事で、今回はdependsOnを削って、暗黙的な依存関係を使って修正していこうと思います。

例えば、仮想ネットワークはNSGに依存しているのですが

resource labName_VNET 'Microsoft.Network/virtualNetworks@2016-03-30' = [for i in range(0, numberOfInstances): {
  name: '${labName}-VNET-${i}'
  location: location
  properties: {
    addressSpace: {
      addressPrefixes: [
        addressPrefix
      ]
    }
    subnets: [
      {
        name: subnet1Name
        properties: {
          addressPrefix: subnet1Prefix
          networkSecurityGroup: {
            id: resourceId('Microsoft.Network/networkSecurityGroups', '${labName}-NSG-${i}')
          }
        }
      }
    ]
  }
  dependsOn: [
    resourceId('Microsoft.Network/networkSecurityGroups', '${labName}-NSG-${i}')
  ]
}]

dependsOnを削って、subnet > properties > networkSecurityGroupのidの指定方法をresource名を使った方法に変更します。ループを使っているので、それを考慮してid: labName_NSG[i].idのように書くのがポイントです。

resource labName_VNET 'Microsoft.Network/virtualNetworks@2016-03-30' = [for i in range(0, numberOfInstances): {
  name: '${labName}-VNET-${i}'
  location: location
  properties: {
    addressSpace: {
      addressPrefixes: [
        addressPrefix
      ]
    }
    subnets: [
      {
        name: subnet1Name
        properties: {
          addressPrefix: subnet1Prefix
          networkSecurityGroup: {
            id: labName_NSG[i].id
          }
        }
      }
    ]
  }
}]

同じように全体的に修正していきますが、ネットワークインターフェースでサブネットを指定するところだけ上記の書き方だと上手くいかず、次のように書きました。ループ使ってプロパティの深いところを指定する方法がちょっと難しかったです。

          subnet: {
            id: resourceId('Microsoft.Network/virtualNetworks/subnets', labName_VNET[i].name, subnet1Name)
          }

また、最後に実施するカスタムスクリプトの実行部分だけは、暗黙的な依存関係が無かったので、dependsOnを使って明示的に設定しました。

修正後のBicepファイルはこうなりました。

修正後のBicep(クリックで展開)

@description('Number of VM Sets to deploy')
@minValue(1)
@maxValue(20)
param numberOfInstances int = 1

@description('Password for the Virtual Machine.')
@secure()
param adminPassword string

@description('Unique Prefix. ex) ad0717')
param labName string
param scriptURI string

var adminUsername = '(管理者のID)'
var addressPrefix = '10.0.0.0/16'
var subnet1Name = 'Subnet-1'
var subnet1Prefix = '10.0.0.0/24'
var imageReference = {
  id: '(Azure Compute ImageのイメージID)'
}
var location = 'westus2'
var privateIP_vm1 = '10.0.0.5'

resource labName_PIP_VM1 'Microsoft.Network/publicIPAddresses@2018-11-01' = [for i in range(0, numberOfInstances): {
  name: '${labName}-PIP-VM1-${i}'
  location: location
  properties: {
    publicIPAllocationMethod: 'Dynamic'
    dnsSettings: {
      domainNameLabel: '${labName}-vm1-${i}'
    }
  }
}]

resource labName_NSG 'Microsoft.Network/networkSecurityGroups@2019-08-01' = [for i in range(0, numberOfInstances): {
  name: '${labName}-NSG-${i}'
  location: location
  properties: {
    securityRules: [
      {
        name: 'default-allow-RDP'
        properties: {
          priority: 1000
          access: 'Allow'
          direction: 'Inbound'
          destinationPortRange: '3389'
          protocol: 'Tcp'
          sourceAddressPrefix: '*'
          sourcePortRange: '*'
          destinationAddressPrefix: '*'
        }
      }
    ]
  }
}]

resource labName_VNET 'Microsoft.Network/virtualNetworks@2016-03-30' = [for i in range(0, numberOfInstances): {
  name: '${labName}-VNET-${i}'
  location: location
  properties: {
    addressSpace: {
      addressPrefixes: [
        addressPrefix
      ]
    }
    subnets: [
      {
        name: subnet1Name
        properties: {
          addressPrefix: subnet1Prefix
          networkSecurityGroup: {
            id: labName_NSG[i].id
          }
        }
      }
    ]
  }
}]

resource labName_NIC_VM1 'Microsoft.Network/networkInterfaces@2016-03-30' = [for i in range(0, numberOfInstances): {
  name: '${labName}-NIC-VM1-${i}'
  location: location
  properties: {
    ipConfigurations: [
      {
        name: 'ipconfig1'
        properties: {
          privateIPAllocationMethod: 'Static'
          privateIPAddress: privateIP_vm1
          publicIPAddress: {
            id: labName_PIP_VM1[i].id
          }
          subnet: {
            id: resourceId('Microsoft.Network/virtualNetworks/subnets', labName_VNET[i].name, subnet1Name)
          }
        }
      }
    ]
  }
}]

resource labName_VM1 'Microsoft.Compute/virtualMachines@2022-03-01' = [for i in range(0, numberOfInstances): {
  name: '${labName}-VM1-${i}'
  location: location
  properties: {
    hardwareProfile: {
      vmSize: 'Standard_B2s'
    }
    osProfile: {
      computerName: '${labName}-VM1-${i}'
      adminUsername: adminUsername
      adminPassword: adminPassword
    }
    storageProfile: {
      imageReference: imageReference
      osDisk: {
        createOption: 'FromImage'
        managedDisk: {
          storageAccountType: 'Standard_LRS'
        }
      }
    }
    networkProfile: {
      networkInterfaces: [
        {
          id: labName_NIC_VM1[i].id
        }
      ]
    }
  }
}]

resource labName_VM1_Install_ADDS 'Microsoft.Compute/virtualMachines/extensions@2019-12-01' = [for i in range(0, numberOfInstances): {
  name: '${labName}-VM1-${i}/Install-ADDS'
  location: location
  properties: {
    publisher: 'Microsoft.Compute'
    type: 'CustomScriptExtension'
    typeHandlerVersion: '1.7'
    autoUpgradeMinorVersion: true
    settings: {
      fileUris: [
        scriptURI
      ]
      commandToExecute: 'powershell.exe -ExecutionPolicy Unrestricted -File adds.ps1'
    }
  }
  dependsOn: [
    labName_VM1[i]
  ]
}]

実行結果

問題なくリソースを作成する事が出来ました。インストールスクリプトも問題なく動きました。

おわりに

今回は依存関係部分を暗黙的に変える対応もしたので少し苦戦しましたが、それでも比較的すんなり移行が出来ました。

ざっと触った感じのメリットです。

  • 変数周りの扱いが楽になった
    • 特に、パラメーターから受け取る値とテンプレート内の変数で扱いを分けなくていいのが楽
      • ここ変数にしてたけどパラメーターに変えたいな、という時が特に楽
    • 文字列結合周りの記述がシンプルになった
  • 文字数が減って可読性が上がった
    • 元のjsonファイルが214行(8368文字)で、Bicepファイルが152行(3904文字)なので文字数的には半分くらいになった、というところでしょうか
    • 暗黙的な依存関係を使う事でシンプルになり、依存関係の考慮で悩む必要が減る

今後新しく作る場合や、既存のテンプレートを修正するときは、Bicepを積極的に使っていきたいと思います。

執筆担当者プロフィール
舟越 匠

舟越 匠(日本ビジネスシステムズ株式会社)

人材開発部に所属。社内向けの技術研修をメインにしつつ、JBS Tech BlogやMS認定資格取得の推進役もやっています。資格としてはAzure Solutions Architect Expertを所持。好きなサービスはPower Automate / Logic Apps。好きなアーティストはZABADAK。

担当記事一覧