この記事は NTTコムウェア AdventCalendar の 6 日目です。
はじめに
NTT コムウェア コーポレート革新本部の相崎です。普段は Azure や AWS に関する社内からの問い合わせ対応や技術検証などを行っています。
Azure 純正の IaC ツールである ARM テンプレートを Bicep に変換するということを試してみたので、紹介します。
ARM テンプレートと Bicep
どちらも、Azure において 「Infrastructure as Code (IaC)」を実現するためのテンプレートです。
ARM テンプレートは JSON 形式で記述されますが、複雑であることやコメントが記載できず読みづらいこと、リソースの依存関係を記載する必要があることなどのデメリットもあります。
Bicep は、そういった ARM テンプレートのデメリットを解消するために生まれたツールで、 ARM テンプレートを生成するための言語といったイメージです。そのため、以下のように互換性があります。
ただし、ARM から Bicep へのデコンパイルは保証されているわけではなく、ベストエフォートとなります。どのくらい手直しが必要なのかという観点でも見ていきたいと思います。
事前準備
VS Code を使用してデコンパイル~修正~デプロイまで行います。
以下の拡張機能を入れておきます。
- Bicep 拡張機能
https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-bicep - Azure Account : デプロイに必要
https://marketplace.visualstudio.com/items?itemName=ms-vscode.azure-account
変換する ARM テンプレートについて
今回変換を試す ARM テンプレートは、仮想マシン2台を作成し、 IIS をインストールして HTTP 接続した際にマシン名を出力するというカスタムスクリプト拡張機能を入れるというものです。
LB などの負荷分散サービスの動作確認を行うために使用したものです。
作成するリソースは、以下のようになります。
- 仮想ネットワーク
- サブネット
- NIC(2つ)
- NSG
- 仮想マシン(2つ)
- パブリックIPアドレス(2つ)
- カスタムスクリプト拡張機能(2つ)
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"VMName": {
"type": "string",
"metadata": {
"description": "VM name"
},
"minLength": 1,
"maxLength": 64
},
"adminUserName": {
"type": "string",
"metadata": {
"description": "admin user name"
},
"minLength": 1,
"maxLength": 20
},
"adminPassword": {
"type": "string",
"metadata": {
"description": "admin pass word"
}
},
"vmcount": {
"type": "int",
"defaultValue": 2,
"metadata": {
"description": "Number of VMs"
}
}
},
"functions": [],
"variables": {
"virtualNetworkNames": "testVnet",
"NSGnames": "[concat(parameters('VMName'), '-nsg')]",
"publicIpAdressNames": "[concat(parameters('VMName'), '-pip')]",
"nic": "testnic",
"vmExtensionName": "customScriptExtension"
},
"resources": [{
"name": "[concat(variables('publicIpAdressNames'),copyIndex())]",
"copy": {
"name": "pipCopy",
"count": "[parameters('vmcount')]"
},
"type": "Microsoft.Network/publicIPAddresses",
"apiVersion": "2020-11-01",
"location": "[resourceGroup().location]",
"tags": {
"displayName": "PublicIPAddress"
},
"properties": {
"publicIPAllocationMethod": "Static"
}
},
{
"name": "[variables('NSGnames')]",
"type": "Microsoft.Network/networkSecurityGroups",
"apiVersion": "2022-05-01",
"location": "[resourceGroup().location]",
"properties": {
"securityRules": [
{
"name": "allow-RDP",
"properties": {
"priority": 100,
"description": "description",
"protocol": "Tcp",
"sourcePortRange": "*",
"destinationPortRange": "3389",
"sourceAddressPrefix": "*",
"destinationAddressPrefix": "*",
"access": "Allow",
"direction": "Inbound"
}
},
{
"name": "default-allow-http",
"properties": {
"priority": 1100,
"sourceAddressPrefix": "*",
"protocol": "Tcp",
"destinationPortRange": "80",
"access": "Allow",
"direction": "Inbound",
"sourcePortRange": "*",
"destinationAddressPrefix": "*"
}
}
]
}
},
{
"name": "[variables('virtualNetworkNames')]",
"type": "Microsoft.Network/virtualNetworks",
"apiVersion": "2022-05-01",
"location": "[resourceGroup().location]",
"tags": {
"displayName": "testarmVM-VirtualNetwork"
},
"properties": {
"addressSpace": {
"addressPrefixes": [
"10.0.0.0/16"
]
},
"subnets": [
{
"name": "WindowsVM-VirtualNetwork-Subnet",
"properties": {
"addressPrefix": "10.0.0.0/24",
"networkSecurityGroup": {
"id": "[resourceId('Microsoft.Network/networkSecurityGroups', variables('NSGnames'))]"
}
}
}
]
}
},
{
"name": "[concat(variables('nic'),copyIndex())]",
"copy": {
"name": "nicCopy",
"count": "[parameters('vmcount')]"
},
"type": "Microsoft.Network/networkInterfaces",
"apiVersion": "2022-05-01",
"location": "[resourceGroup().location]",
"dependsOn": [
"[resourceId('Microsoft.Network/publicIPAddresses', concat(variables('publicIpAdressNames'), copyIndex()))]",
"[resourceId('Microsoft.Network/virtualNetworks', variables('virtualNetworkNames'))]"
],
"tags": {
"displayName": " Network Interface"
},
"properties": {
"ipConfigurations": [
{
"name": "ipConfig1",
"properties": {
"privateIPAllocationMethod": "Dynamic",
"publicIPAddress": {
"id": "[resourceId('Microsoft.Network/publicIPAddresses', concat(variables('publicIpAdressNames'), copyIndex()))]"
},
"subnet": {
"id": "[resourceId('Microsoft.Network/virtualNetworks/subnets', variables('virtualNetworkNames'), 'WindowsVM-VirtualNetwork-Subnet')]"
}
}
}
]
}
},
{
"name": "[concat(parameters('VMName'),copyIndex())]",
"copy": {
"name": "VMcopy",
"count": "[parameters('vmcount')]"
},
"type": "Microsoft.Compute/virtualMachines",
"apiVersion": "2022-03-01",
"location": "[resourceGroup().location]",
"dependsOn": [
"[resourceId('Microsoft.Network/networkInterfaces', concat(variables('nic'),copyIndex()))]"
],
"tags": {
"displayName": "[parameters('VMName')]"
},
"properties": {
"hardwareProfile": {
"vmSize": "Standard_D2s_v3"
},
"osProfile": {
"computerName": "[concat(parameters('VMName'),copyIndex())]",
"adminUsername": "[parameters('adminUserName')]",
"adminPassword": "[parameters('adminPassword')]"
},
"storageProfile": {
"imageReference": {
"publisher": "MicrosoftWindowsServer",
"offer": "WindowsServer",
"sku": "2022-datacenter-azure-edition",
"version": "latest"
},
"osDisk": {
"createOption": "FromImage"
}
},
"networkProfile": {
"networkInterfaces": [
{
"id": "[resourceId('Microsoft.Network/networkInterfaces', concat(variables('nic'),copyIndex()))]"
}
]
}
}
},
{
"name": "[concat(concat(parameters('vmName'),copyIndex()), '/', variables('vmExtensionName'))]",
"copy": {
"name": "AppInstall",
"count": "[parameters('vmcount')]"
},
"type": "Microsoft.Compute/virtualMachines/extensions",
"apiVersion": "2018-06-01",
"location": "[resourceGroup().location]",
"dependsOn": ["[resourceId('Microsoft.Compute/virtualMachines/', concat(parameters('vmName'),copyIndex()))]"
],
"properties": {
"publisher": "Microsoft.Compute",
"type": "CustomScriptExtension",
"typeHandlerVersion": "1.7",
"autoUpgradeMinorVersion": true,
"settings": {
"commandToExecute": "powershell.exe Install-WindowsFeature -name Web-Server -IncludeManagementTools && powershell.exe remove-item 'C:\\inetpub\\wwwroot\\iisstart.htm' && powershell.exe Add-Content -Path 'C:\\inetpub\\wwwroot\\iisstart.htm' -Value $('Hello World from ' + $env:computername)"
}
}
}
]
}
パラメータファイルは以下です。VM名、ユーザ名、パスワードをパラメータとして定義しています。
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"VMName": {
"value": "testVM"
},
"adminUserName": {
"value": "azureuser"
},
"adminPassword": {
"value": "XXXXXX"
}
}
}
Bicep の生成
それでは ARM テンプレートを Bicep へデコンパイルしていきます。
VS Code で ARM を開き、コマンドパレットからBicep
を入力すると Bicep コマンドが一覧表示されます。
Decompile into Bicep
を選択します。
Azure CLI から行う場合は次のコマンドを実行します。
az bicep decompile –file createWindowsVM.json
ARM テンプレートと同じフォルダに Bicep ファイルが作成されます。
パラメータファイルは ARM と Bicep で同じものを使えるため、そのままとなります。
@description('VM name')
@minLength(1)
@maxLength(64)
param VMName string
@description('admin user name')
@minLength(1)
@maxLength(20)
param adminUserName string
@description('admin pass word')
param adminPassword string
@description('Number of VMs')
param vmcount int = 2
var virtualNetworkNames_var = 'testVnet'
var NSGnames_var = '${VMName}-nsg'
var publicIpAdressNames_var = '${VMName}-pip'
var nic_var = 'testnic'
var vmExtensionName = 'customScriptExtension'
resource publicIpAdressNames 'Microsoft.Network/publicIPAddresses@2020-11-01' = [for i in range(0, vmcount): {
name: concat(publicIpAdressNames_var, i)
location: resourceGroup().location
tags: {
displayName: 'PublicIPAddress'
}
properties: {
publicIPAllocationMethod: 'Static'
}
}]
resource NSGnames 'Microsoft.Network/networkSecurityGroups@2022-05-01' = {
name: NSGnames_var
location: resourceGroup().location
properties: {
securityRules: [
{
name: 'allow-RDP'
properties: {
priority: 100
description: 'description'
protocol: 'Tcp'
sourcePortRange: '*'
destinationPortRange: '3389'
sourceAddressPrefix: '*'
destinationAddressPrefix: '*'
access: 'Allow'
direction: 'Inbound'
}
}
{
name: 'default-allow-http'
properties: {
priority: 1100
sourceAddressPrefix: '*'
protocol: 'Tcp'
destinationPortRange: '80'
access: 'Allow'
direction: 'Inbound'
sourcePortRange: '*'
destinationAddressPrefix: '*'
}
}
]
}
}
resource virtualNetworkNames 'Microsoft.Network/virtualNetworks@2022-05-01' = {
name: virtualNetworkNames_var
location: resourceGroup().location
tags: {
displayName: 'testarmVM-VirtualNetwork'
}
properties: {
addressSpace: {
addressPrefixes: [
'10.0.0.0/16'
]
}
subnets: [
{
name: 'WindowsVM-VirtualNetwork-Subnet'
properties: {
addressPrefix: '10.0.0.0/24'
networkSecurityGroup: {
id: NSGnames.id
}
}
}
]
}
}
resource nic 'Microsoft.Network/networkInterfaces@2022-05-01' = [for i in range(0, vmcount): {
name: concat(nic_var, i)
location: resourceGroup().location
tags: {
displayName: ' Network Interface'
}
properties: {
ipConfigurations: [
{
name: 'ipConfig1'
properties: {
privateIPAllocationMethod: 'Dynamic'
publicIPAddress: {
id: resourceId('Microsoft.Network/publicIPAddresses', concat(publicIpAdressNames_var, i))
}
subnet: {
id: resourceId('Microsoft.Network/virtualNetworks/subnets', virtualNetworkNames_var, 'WindowsVM-VirtualNetwork-Subnet')
}
}
}
]
}
dependsOn: [
resourceId('Microsoft.Network/publicIPAddresses', concat(publicIpAdressNames_var, i))
virtualNetworkNames
]
}]
resource VM 'Microsoft.Compute/virtualMachines@2022-03-01' = [for i in range(0, vmcount): {
name: concat(VMName, i)
location: resourceGroup().location
tags: {
displayName: VMName
}
properties: {
hardwareProfile: {
vmSize: 'Standard_D2s_v3'
}
osProfile: {
computerName: concat(VMName, i)
adminUsername: adminUserName
adminPassword: adminPassword
}
storageProfile: {
imageReference: {
publisher: 'MicrosoftWindowsServer'
offer: 'WindowsServer'
sku: '2022-datacenter-azure-edition'
version: 'latest'
}
osDisk: {
createOption: 'FromImage'
}
}
networkProfile: {
networkInterfaces: [
{
id: resourceId('Microsoft.Network/networkInterfaces', concat(nic_var, i))
}
]
}
}
dependsOn: [
resourceId('Microsoft.Network/networkInterfaces', concat(nic_var, i))
]
}]
resource vmName_vmExtension 'Microsoft.Compute/virtualMachines/extensions@2018-06-01' = [for i in range(0, vmcount): {
name: '${VMName}${i}/${vmExtensionName}'
location: resourceGroup().location
properties: {
publisher: 'Microsoft.Compute'
type: 'CustomScriptExtension'
typeHandlerVersion: '1.7'
autoUpgradeMinorVersion: true
settings: {
commandToExecute: 'powershell.exe Install-WindowsFeature -name Web-Server -IncludeManagementTools && powershell.exe remove-item \'C:\\inetpub\\wwwroot\\iisstart.htm\' && powershell.exe Add-Content -Path \'C:\\inetpub\\wwwroot\\iisstart.htm\' -Value $(\'Hello World from \' + $env:computername)'
}
}
dependsOn: [
resourceId('Microsoft.Compute/virtualMachines/', concat(VMName, i))
]
}]
エラー内容の修正のため、作成された Bicep ファイルを開きます。
いくつかエラーや推奨事項の警告が出ているので修正していきます。23個エラー・警告が出ていますが以下の4つの内容でした。
- 依存関係の記述方法のエラー
- パスワードのパラメータ
adminPassword
がセキュアな記述でない - 文字列の結合方法の推奨事項
- リソースグループの指定方法エラー
1. 依存関係の記述方法
依存関係とは、リソースの作成や変更に必要な順序を示すものです。
例えば、仮想マシンを作成する前に NIC を作成する必要があります。この場合、仮想マシンのリソース定義には NIC のリソース ID を指定してあげる必要があります。
ARM テンプレートではdependsOn
要素で依存関係を指定します。今回準備した ARM テンプレートでは以下のように仮想マシン(VirtualMachines)に NIC のリソース ID を指定していました。
"dependsOn": [ "[resourceId('Microsoft.Network/networkInterfaces',concat(variables('nic'),copyIndex()))]" ]
変換された Bicep ファイルの該当箇所を確認します。
networkProfile: {
networkInterfaces: [
{
id: resourceId('Microsoft.Network/networkInterfaces', concat(nic_var, i))
}
]
}
}
dependsOn: [
resourceId('Microsoft.Network/networkInterfaces', concat(nic_var, i))
]
Bicep ではdependsOn
で明示的に依存関係を指定しなくとも、以下のように NIC の resource 名を指定すれば OK です。dependsOn
は削除します。(作成する個数分ループを使っているので、 nic[i]
のように記載します。)
補足:dependsOn
を使う依存関係の指定方法を「明示的な依存関係」dependsOn
を使わず上記のように指定する方法を「暗黙的な依存関係」といいます。Bicep のベストプラクティスではなるべく暗黙的な依存関係を使うことが推奨されています。
networkProfile: {
networkInterfaces: [
{
id: nic[i].id
}
]
}
明示的な依存関係よりも暗黙的な依存関係を優先的に使用してください。
Bicep ファイルの開発時のベスト プラクティスを確認する| Microsoft Learn
他のリソースの箇所も同じように修正します。
仮想マシンのカスタムスクリプト拡張機能についてはうまく変換がされずにそのままになっていました。こちらの公式ドキュメントを参考に、仮想マシンへの紐づけはparent
プロパティで指定します。
ループの指定方法や変数も他のリソースに合わせて修正しました。
resource vmName_vmExtension 'Microsoft.Compute/virtualMachines/extensions@2018-06-01' = [for i in range(0, vmcount): {
name: '${vmExtensionName}${i}'
location: location
parent: VM[i]
properties: {
publisher: 'Microsoft.Compute'
type: 'CustomScriptExtension'
typeHandlerVersion: '1.7'
autoUpgradeMinorVersion: true
settings: {
commandToExecute: 'powershell.exe Install-WindowsFeature -name Web-Server -IncludeManagementTools && powershell.exe remove-item \'C:\\inetpub\\wwwroot\\iisstart.htm\' && powershell.exe Add-Content -Path \'C:\\inetpub\\wwwroot\\iisstart.htm\' -Value $(\'Hello World from \' + $env:computername)'
}
}
}
2. パスワードのパラメータ
パスワードのパラメータには@secure()
を追加します。これにより、パラメーターの値はデプロイ履歴に保存されず、ログにも記録されないようになります。
@description('admin pass word')
@secure()
param adminPassword string
3. 文字列の結合方法
たとえば、NSG のリソース名を'VM 名' + '-nsg'
としたい場合には変数名に文字列を結合する指定をします。ARM テンプレートでは以下のように concat 関数を使用していました。
"NSGnames": "[concat(parameters('VMName'), '-nsg')]"
Bicep では以下のように指定が可能です。concat 関数も使えますが、ネストが深くなるとかなり複雑になるので、こちらの方が見やすいです。
var NSGnames = '${VMName}-nsg'
他の箇所も同様に修正していきます。
4. リソースグループの指定方法
リソースのlocation の指定をresourceGroup().location
のように記載しますが、Bicep ではパラメータのみこの記載ができるので、以下のパラメータを追加します。
param location string = resourceGroup().location
各リソースは上記パラメータを参照する形にします。
resource virtualNetwork 'Microsoft.Network/virtualNetworks@2022-05-01' = {
name: virtualNetworkNames
location: location
修正後の Bicep ファイル
修正した Bicep ファイルは以下のようになりました。
@description('VM name')
@minLength(1)
@maxLength(64)
param VMName string
@description('admin user name')
@minLength(1)
@maxLength(20)
param adminUserName string
@description('admin pass word')
@secure()
param adminPassword string
@description('Number of VMs')
param vmcount int = 2
@description('deployment location')
param location string = resourceGroup().location
var virtualNetworkNames = 'testVnet'
var NSGnames = '${VMName}-nsg'
var publicIpAdressNames = '${VMName}-pip'
var nicName = 'testnic'
var vmExtensionName = 'customScriptExtension'
var subnetName = 'WindowsVM-VirtualNetwork-Subnet'
resource publicIpAdress 'Microsoft.Network/publicIPAddresses@2020-11-01' = [for i in range(0, vmcount): {
name: '${publicIpAdressNames}${i}'
location: location
tags: {
displayName: 'PublicIPAddress'
}
properties: {
publicIPAllocationMethod: 'Static'
}
}]
resource NSG 'Microsoft.Network/networkSecurityGroups@2022-05-01' = {
name: NSGnames
location: location
properties: {
securityRules: [
{
name: 'allow-RDP'
properties: {
priority: 100
description: 'description'
protocol: 'Tcp'
sourcePortRange: '*'
destinationPortRange: '3389'
sourceAddressPrefix: '*'
destinationAddressPrefix: '*'
access: 'Allow'
direction: 'Inbound'
}
}
{
name: 'default-allow-http'
properties: {
priority: 1100
sourceAddressPrefix: '*'
protocol: 'Tcp'
destinationPortRange: '80'
access: 'Allow'
direction: 'Inbound'
sourcePortRange: '*'
destinationAddressPrefix: '*'
}
}
]
}
}
resource virtualNetwork 'Microsoft.Network/virtualNetworks@2022-05-01' = {
name: virtualNetworkNames
location: location
tags: {
displayName: 'testarmVM-VirtualNetwork'
}
properties: {
addressSpace: {
addressPrefixes: [
'10.0.0.0/16'
]
}
subnets: [
{
name: subnetName
properties: {
addressPrefix: '10.0.0.0/24'
networkSecurityGroup: {
id: NSG.id
}
}
}
]
}
}
resource nic 'Microsoft.Network/networkInterfaces@2022-05-01' = [for i in range(0, vmcount): {
name: '${nicName}${i}'
location: location
tags: {
displayName: ' Network Interface'
}
properties: {
ipConfigurations: [
{
name: 'ipConfig1'
properties: {
privateIPAllocationMethod: 'Dynamic'
publicIPAddress: {
id: publicIpAdress[i].id
}
subnet: {
id: resourceId('Microsoft.Network/virtualNetworks/subnets', virtualNetwork.name, subnetName)
}
}
}
]
}
}]
resource VM 'Microsoft.Compute/virtualMachines@2022-03-01' = [for i in range(0, vmcount): {
name: '${VMName}${i}'
location: location
tags: {
displayName: VMName
}
properties: {
hardwareProfile: {
vmSize: 'Standard_D2s_v3'
}
osProfile: {
computerName: '${VMName}${i}'
adminUsername: adminUserName
adminPassword: adminPassword
}
storageProfile: {
imageReference: {
publisher: 'MicrosoftWindowsServer'
offer: 'WindowsServer'
sku: '2022-datacenter-azure-edition'
version: 'latest'
}
osDisk: {
createOption: 'FromImage'
}
}
networkProfile: {
networkInterfaces: [
{
id: nic[i].id
}
]
}
}
}]
resource vmName_vmExtension 'Microsoft.Compute/virtualMachines/extensions@2018-06-01' = [for i in range(0, vmcount): {
name: '${VMName}${i}/${vmExtensionName}'
location: location
properties: {
publisher: 'Microsoft.Compute'
type: 'CustomScriptExtension'
typeHandlerVersion: '1.7'
autoUpgradeMinorVersion: true
settings: {
commandToExecute: 'powershell.exe Install-WindowsFeature -name Web-Server -IncludeManagementTools && powershell.exe remove-item \'C:\\inetpub\\wwwroot\\iisstart.htm\' && powershell.exe Add-Content -Path \'C:\\inetpub\\wwwroot\\iisstart.htm\' -Value $(\'Hello World from \' + $env:computername)'
}
}
dependsOn: [
VM[i]
]
}]
また、VS Code の Bicep 拡張機能では赤枠のボタンを押下すると、リソースの依存関係を視覚化することができます。
このようになりました。ここで依存関係が意図した通りになっていなければエラーになる可能性が高いので、その場合は見直します。
それではデプロイができるか確認していきます。
コマンドパレットからDeploy Bicep File
を選択します。
Azure アカウントへのサインインやリソースグループの選択などが表示されるので、
画面に従って進めていきます。
仮想マシンカスタムスクリプト拡張機能(IIS インストール&VM 名の出力)の動作確認も OK でした。
やってみての感想
ARM → Bicep への変換はベストエフォートということもあり、それなりに手直しが必要でした。
ただ、一から Bicep ファイルを作成するのと比べるとかなり楽なので、役立つ機能だと思いました。
ARM に比べてかなり見やすく、これなら他の人が書いたファイルをレビューすることもできるかなという印象です。
記載されている会社名、製品名、サービス名は、各社の商標または登録商標です。