LoginSignup
9
3

ARM テンプレートと Bicep の相互変換を試してみた

Last updated at Posted at 2023-12-05

この記事は NTTコムウェア AdventCalendar の 6 日目です。

はじめに

NTT コムウェア コーポレート革新本部の相崎です。普段は Azure や AWS に関する社内からの問い合わせ対応や技術検証などを行っています。
Azure 純正の IaC ツールである ARM テンプレートを Bicep に変換するということを試してみたので、紹介します。

ARM テンプレートと Bicep

どちらも、Azure において 「Infrastructure as Code (IaC)」を実現するためのテンプレートです。
ARM テンプレートは JSON 形式で記述されますが、複雑であることやコメントが記載できず読みづらいこと、リソースの依存関係を記載する必要があることなどのデメリットもあります。

Bicep は、そういった ARM テンプレートのデメリットを解消するために生まれたツールで、 ARM テンプレートを生成するための言語といったイメージです。そのため、以下のように互換性があります。
Bicep_ARM.png

ただし、ARM から Bicep へのデコンパイルは保証されているわけではなく、ベストエフォートとなります。どのくらい手直しが必要なのかという観点でも見ていきたいと思います。

事前準備

VS Code を使用してデコンパイル~修正~デプロイまで行います。

以下の拡張機能を入れておきます。

変換する ARM テンプレートについて

今回変換を試す ARM テンプレートは、仮想マシン2台を作成し、 IIS をインストールして HTTP 接続した際にマシン名を出力するというカスタムスクリプト拡張機能を入れるというものです。
LB などの負荷分散サービスの動作確認を行うために使用したものです。
作成するリソースは、以下のようになります。

  • 仮想ネットワーク
  • サブネット
  • NIC(2つ)
  • NSG
  • 仮想マシン(2つ)
  • パブリックIPアドレス(2つ)
  • カスタムスクリプト拡張機能(2つ)
createWindowsVM.json
{
    "$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名、ユーザ名、パスワードをパラメータとして定義しています。

createWindowsVM.parameters.json
{
  "$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を選択します。
decompile.png

Azure CLI から行う場合は次のコマンドを実行します。

az bicep decompile –file createWindowsVM.json

ARM テンプレートと同じフォルダに Bicep ファイルが作成されます。
パラメータファイルは ARM と Bicep で同じものを使えるため、そのままとなります。
createBicep.PNG

createWindowsVM.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 ファイルを開きます。

bicepError.PNG

いくつかエラーや推奨事項の警告が出ているので修正していきます。23個エラー・警告が出ていますが以下の4つの内容でした。

  1. 依存関係の記述方法のエラー
  2. パスワードのパラメータadminPasswordがセキュアな記述でない
  3. 文字列の結合方法の推奨事項
  4. リソースグループの指定方法エラー

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 ファイルは以下のようになりました。

createWindowsVM.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 拡張機能では赤枠のボタンを押下すると、リソースの依存関係を視覚化することができます。
tempsnip-1.png

このようになりました。ここで依存関係が意図した通りになっていなければエラーになる可能性が高いので、その場合は見直します。

vscode_resource.png

それではデプロイができるか確認していきます。
コマンドパレットからDeploy Bicep Fileを選択します。

Azure アカウントへのサインインやリソースグループの選択などが表示されるので、
画面に従って進めていきます。

デプロイが成功しました。
deploy_success.png

仮想マシンカスタムスクリプト拡張機能(IIS インストール&VM 名の出力)の動作確認も OK でした。

キャプチャ.png

やってみての感想

ARM → Bicep への変換はベストエフォートということもあり、それなりに手直しが必要でした。
ただ、一から Bicep ファイルを作成するのと比べるとかなり楽なので、役立つ機能だと思いました。
ARM に比べてかなり見やすく、これなら他の人が書いたファイルをレビューすることもできるかなという印象です。

記載されている会社名、製品名、サービス名は、各社の商標または登録商標です。

9
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
3