ARM Template の書き方 その3 依存関係,ResourceID()
過去記事
ARM Template について3本目の記事となります。今回は、リソースの依存関係と関数 ResourceID を紹介していきたいと思います。過去記事のリンクは以下に掲載します。
これまでに、テンプレートの基本的な書き方を覚えて、VirtualNetwork/Subnet を作りながら、変数や、パラメータファイルの基本的な使い方について確認しました。
やりたいこと
構成
後々 VirtualMachine を構築することを視野に入れて、下記のようなネットワーク構成を作ってみたいと思います。これらのリソースを作成するにあたって依存関係の解決をする必要があり、その過程で関数 ResourceID を使います。
コンソール画面
最終的には、以下のようにリソースを作成したいと思います。コンソールでみるとこんな感じです。
依存関係
リソースの依存関係を理解するためには以下の図が分かりやすいかと思います。こちらの内容を参考とさせていただきました。
上記のような順番でリソースを作成するように依存関係をテンプレートないで指定しなければいけません。各リソースに dependsOn という項目を作成して依存するリソースのリソースIDを指定します。
他のリソースIDの取得方法は関数 resouceID が便利です。下記のような文法で利用できます。
リソースのタイプ名と、リソース名を指定することでリソースIDを参照可能です。
ARM Template を用意
ファイル構成
使用するファイルは以下の4つです。
ポイント ネーミングルール
ARM Template の運用においてネーミングルールは最重要事項です。テンプレートの書き方に大きな影響を与えます。今回作成した私のテンプレートでは以下のようなルールを敷いています。
- 3つのタグ [Owner] , [Service] , [Env] を全てのリソースに指定する。
- リソースグループ名 = "[Owner].[Service].[Env]" とする。
- 各リソース名 = "[Service]-[Env]-[任意の文字列]" とする。
こうすることで、parameters に記述するのはわずかな定義のみで、後は決められたルールに従って全てのリソースを作成することができます。難点は variables の記述量が増えてしまうことですが、テンプレートを他で応用するにあたっては、この方が便利です。
ARM Template とデプロイコマンド実行スクリプトの中身
deployment.ps1
スクリプトの内容は簡単な内容となっています。自分の検証作業用の環境を作るためのスクリプトですので最低限のエラーハンドリングとなっています。
ポイントは、ディレクトリ名、ファイル名、とネーミングルールを合わせることです。新たな ARM テンプレートを格納するディレクトリを増やすことで、複数環境のデプロイにも対応できます。
# version 1.2.3
<#
.SYNOPSIS
ARM Template を Deploy するにあたって、本スクリプトを実行します。
.DESCRIPTION
以下の順で処理が実行されます。
1. リソースグループの作成
2. $templateListに記述したテンプレートに対して順次実施(スキップ可)
2-1. テンプレートのテスト
2-2. テンプレートのデプロイ
3. デプロイ結果をログに保存
.OPERATION
以下の方針で運用します。
1. 1環境:1テンプレートファイル
2. リソースグループ名は $ownerName.$serviceName.$environmentName
3. タグを付与 Owner=$ownerName,Service=$serviceName,Env=$environmentName
4. デプロイモードは Complete
#>
# Environments
$location = "eastus"
$ownerName = "atsushi.koizumi"
$serviceName = Read-Host "dba/cia" # 選択させたいサービス識別子を書く
$environmentName = Read-Host "dev/stg/prd" # 選択させたい環境識別子を書く
$templateFile = "$PSScriptRoot\$serviceName\azuredeploy.json"
$prametersFile = "$PSScriptRoot\$serviceName\$environmentName.parameters.json"
$logfile = "deployment.log"
$resourceGroupName = "$ownerName.$serviceName.$environmentName"
################
# Script Start #
################
# error handling
$ErrorActionPreference = "Stop"
# get datetime
$Datetime = Get-date -format "yyyyMMddHHmmss"
# check $templateFile
Write-Host ""
if (Test-Path -Path $templateFile ) {
Write-Host "Template File: $templateFile"
} else {
Write-Host """Template File: $templateFile"" does not exist."
exit
}
# check $prametersFile
if (Test-Path -Path $prametersFile ) {
Write-Host "Parameter File: $prametersFile"
} else {
Write-Host """Parameter File: $prametersFile"" does not exist."
exit
}
Write-Host ""
# deploy start
# create resource group
try {
$rgstate = Get-AzResourceGroup -Name "$resourceGroupName"
if ($rgstate.ProvisioningState -eq "Succeeded") {
} else {
Write-Host "Resource Group Exists. But State is not Succeeded."
exit
}
}
catch {
New-AzResourceGroup `
-Name "$resourceGroupName" `
-location $location `
-Tag @{Owner=$ownerName; Service=$serviceName; Env=$environmentName} `
| Out-File -Append $logfile
}
# gain permission to test
Write-Host "Test the template ""$templateFile"" ?"
$YesNo = Read-Host "yes or no "
while (($YesNo -ne "yes") -And ($YesNo -ne "no")) {
$YesNo = Read-Host "yes or no "
}
Write-Host ""
# test template
if ($YesNo -eq "yes") {
New-AzResourceGroupDeployment `
-Name "$serviceName-$environmentName-$Datetime" `
-ResourceGroupName "$resourceGroupName" `
-WhatIf `
-TemplateFile $templateFile `
-TemplateParameterFile $prametersFile
} elseif ($YesNo -eq "no") {
Write-Host "Skip test the template ""$templateFile""."
Write-Host "End."
Write-Host ""
exit
}
# gain permission to deploy
Write-Host "Deploy the template ""$templateFile"" ?"
$YesNo = Read-Host "yes or no "
while (($YesNo -ne "yes") -And ($YesNo -ne "no")) {
$YesNo = Read-Host "yes or no "
}
Write-Host ""
# deeploy start
if ($YesNo -eq "yes") {
New-AzResourceGroupDeployment `
-Name "$serviceName-$environmentName-$Datetime" `
-ResourceGroupName "$resourceGroupName" `
-Mode Complete `
-Force `
-TemplateFile $templateFile `
-TemplateParameterFile $prametersFile `
| Out-File -Append $logfile
Write-Host ""
} elseif ($YesNo -eq "no") {
Write-Host "Skip deploy the template ""$templateFile""."
}
azuredeploy.json
関数 resourceID を使って他のリソースとの依存関係を解決しています。
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.2.1.0",
"parameters": {
"ownerName": {
"type": "string",
"metadata": {
"description": "Define Owner Name"
}
},
"serviceName": {
"type": "string",
"metadata": {
"description": "Define Service Name"
}
},
"environmentName": {
"type": "string",
"metadata": {
"description": "Define Environment Name"
}
},
"myhomeIPaddress": {
"type": "string",
"metadata": {
"description": "Define My Home IPaddress"
}
}
},
"variables": {
// 全てのリソースで共有する変数
"location": "[resourceGroup().location]",
"tags": {
"Owner": "[parameters('ownerName')]",
"Service": "[parameters('serviceName')]",
"Env": "[parameters('environmentName')]"
},
// "resources" の "type" 毎に変数を block 形式で書く。
// リソースネーム = [serviceName]-[environmentName]-*
"virtualNetworks": {
"vnet01": {
"name": "[concat(parameters('serviceName'),'-',parameters('environmentName'),'-','vnet01')]",
"prefix": "10.1.0.0/16",
"subnet01": {
"name": "subnet01",
"prefix": "10.1.1.0/24"
}
}
},
"networkSecurityGroups": {
"sg01": {
"name": "[concat(parameters('serviceName'),'-',parameters('environmentName'),'-sg01')]"
}
},
"publicIPAddresses": {
"pubip01": {
"name": "[concat(parameters('serviceName'),'-',parameters('environmentName'),'-public01')]",
"dnsName": "[concat(parameters('serviceName'),'-',parameters('environmentName'),'-vm01-',uniqueString(resourceGroup().name))]"
}
},
"applicationSecurityGroups": {
"asg01": {
"name": "[concat(parameters('serviceName'),'-',parameters('environmentName'),'-asg01')]"
}
},
"networkInterfaces": {
"nic01": {
"name": "[concat(parameters('serviceName'),'-',parameters('environmentName'),'-nic01')]",
"ipconf01name": "[concat(parameters('serviceName'),'-',parameters('environmentName'),'-nic01-ipconf01')]"
}
},
// resourceID は以下に全て記述する。
// outputs で resourceID をそのまま object 形式で出力する。
// 各リソースで参照する場合は必ず dependsOn にも記述する。
"resourceID": {
"vnet01": "[resourceId('Microsoft.Network/virtualNetworks',variables('virtualNetworks').vnet01.name)]",
"subnet01": "[resourceId('Microsoft.Network/virtualNetworks/subnets',variables('virtualNetworks').vnet01.name,variables('virtualNetworks').vnet01.subnet01.name)]",
"pubip01": "[resourceId('Microsoft.Network/publicIPAddresses',variables('publicIPAddresses').pubip01.name)]",
"asg01": "[resourceId('Microsoft.Network/applicationSecurityGroups',variables('applicationSecurityGroups').asg01.name)]",
"nsg01": "[resourceId('Microsoft.Network/networkSecurityGroups',variables('networkSecurityGroups').sg01.name)]",
"nic01": "[resourceId('Microsoft.Network/networkInterfaces',variables('networkInterfaces').nic01.name)]"
}
},
"resources": [
{
"comments": "Virtual Network.",
"type": "Microsoft.Network/virtualNetworks",
"name": "[variables('virtualNetworks').vnet01.name]",
"apiVersion": "2020-05-01",
"location": "[variables('location')]",
"tags": "[variables('tags')]",
"properties": {
"addressSpace": {
"addressPrefixes": [
"[variables('virtualNetworks').vnet01.prefix]"
]
},
"subnets": [
{
"name": "[variables('virtualNetworks').vnet01.subnet01.name]",
"properties": {
"addressPrefix": "[variables('virtualNetworks').vnet01.subnet01.prefix]",
"networkSecurityGroup": {
"id": "[variables('resourceID').nsg01]"
},
"privateEndpointNetworkPolicies": "Enabled",
"privateLinkServiceNetworkPolicies": "Enabled"
}
}
]
},
"dependsOn": [
"[variables('resourceID').nsg01]"
]
},
{
"comments": "Public IP",
"name": "[variables('publicIPAddresses').pubip01.name]",
"type": "Microsoft.Network/publicIPAddresses",
"apiVersion": "2020-05-01",
"location": "[variables('location')]",
"properties": {
"dnsSettings": {
"domainNameLabel": "[variables('publicIPAddresses').pubip01.dnsName]"
},
"publicIPAllocationMethod": "Dynamic"
},
"tags": "[variables('tags')]"
},
{
"comments": "Application Security Group",
"name": "[variables('applicationSecurityGroups').asg01.name]",
"type": "Microsoft.Network/applicationSecurityGroups",
"apiVersion": "2020-05-01",
"location": "[variables('location')]",
"tags": "[variables('tags')]",
"properties": {}
},
{
"comments": "Network Security Group",
"name": "[variables('networkSecurityGroups').sg01.name]",
"type": "Microsoft.Network/networkSecurityGroups",
"apiVersion": "2020-05-01",
"location": "[variables('location')]",
"tags": "[variables('tags')]",
"dependsOn": [
"[variables('resourceID').asg01]"
],
"properties": {
"securityRules": [
{
"name": "myhome",
"properties": {
"description": "Allow RDP from my home ip address",
"direction": "Inbound",
"access": "Allow",
"priority": 1001,
"protocol": "Tcp",
"sourcePortRange": "*",
"sourceAddressPrefix": "[parameters('myhomeIPaddress')]",
"destinationPortRange": "3389",
"destinationApplicationSecurityGroups": [
{
"id": "[variables('resourceID').asg01]"
}
]
}
}
]
}
},
{
"comments": "Network Inerface",
"name": "[variables('networkInterfaces').nic01.name]",
"type": "Microsoft.Network/networkInterfaces",
"apiVersion": "2020-05-01",
"location": "[variables('location')]",
"tags": "[variables('tags')]",
"dependsOn": [
"[variables('resourceID').pubip01]",
"[variables('resourceID').vnet01]",
"[variables('resourceID').asg01]"
],
"properties": {
"ipConfigurations": [
{
"name": "[variables('networkInterfaces').nic01.ipconf01name]",
"properties": {
"primary": true,
"subnet": {
"id": "[variables('resourceID').subnet01]"
},
"privateIPAddressVersion": "IPv4",
"privateIPAllocationMethod": "Dynamic",
"publicIpAddress": {
"id": "[variables('resourceID').pubip01]"
},
"applicationSecurityGroups": [
{
"id": "[variables('resourceID').asg01]"
}
]
}
}
]
}
}
],
"outputs": {
"resourceID": {
"type": "object",
"value": "[variables('resourceID')]"
},
"dnsName": {
"type": "object",
"value": {
"vm01": "[reference(variables('resourceID').pubip01).dnsSettings.fqdn]"
}
}
}
}
dev.parameters.json
パラメータは下記の4つのみです。
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"ownerName": {
"value": "atsushi.koizumi"
},
"serviceName": {
"value": "dba"
},
"environmentName": {
"value": "dev"
},
"myhomeIPaddress": {
"value": "XXX.XXX.XXX.XXX/32"
}
}
}
実行してみる
それではデプロイ用のスクリプト(deployment.ps1)を実行します。
deployment.log
実行結果はログファイルに出力されます。
DeploymentName : dba-dev-20210117225040
ResourceGroupName : atsushi.koizumi.dba.dev
ProvisioningState : Succeeded
Timestamp : 1/17/2021 1:51:29 PM
Mode : Complete
TemplateLink :
Parameters :
Name Type Value
================= ========================= ==========
ownerName String atsushi.koizumi
serviceName String dba
environmentName String dev
myhomeIPaddress String XXX.XXX.XXX.XXX/32
Outputs :
Name Type Value
=============== ========================= ==========
resourceID Object {
"vnet01": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/atsushi.koizumi.dba.dev/providers/Microsoft.Network/virtualNetworks/dba-dev-vnet01",
"subnet01": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/atsushi.koizumi.dba.dev/providers/Microsoft.Network/virtualNetworks/dba-dev-vnet01/subnets/subnet01",
"pubip01": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/atsushi.koizumi.dba.dev/providers/Microsoft.Network/publicIPAddresses/dba-dev-public01",
"asg01": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/atsushi.koizumi.dba.dev/providers/Microsoft.Network/applicationSecurityGroups/dba-dev-asg01",
"nsg01": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/atsushi.koizumi.dba.dev/providers/Microsoft.Network/networkSecurityGroups/dba-dev-sg01",
"nic01": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/atsushi.koizumi.dba.dev/providers/Microsoft.Network/networkInterfaces/dba-dev-nic01"
}
dnsName Object {
"vm01": "dba-dev-vm01-xxxxxxxxxxx.eastus.cloudapp.azure.com"
}
DeploymentDebugLogLevel :
上記の結果は成功した時のものです。
遭遇したエラー
上記に記載したテンプレートファイルでは下記のエラーは解決済みです。
Code:InvalidTemplate
関数 "resourceId" の使い方を間違えたことによるエラーを発生させてしまいました。
関数 "resourceId" は他のリソースの ID を参照する場合に使用するのですが、相手のリソースによって指定方法が異なります。VirtualNetwork と Subnet の違いは以下です。
{
"resourceID": {
"vnet01": "[resourceId('Microsoft.Network/virtualNetworks',variables('virtualNetworks').vnet01.name)]",
"subnet01": "[resourceId('Microsoft.Network/virtualNetworks/subnets',variables('virtualNetworks').vnet01.name,variables('virtualNetworks').vnet01.subnet01.name)]"
}
}
この違いは、コンソールでみると分かりやすいです。
上記のように VirtualNetwork と Subnet は親子関係になっているため、リソースの指定方法が異なるのです。この辺のリソースの特徴を理解していないとテンプレートは書けません。
Code:InvalidResourceReference
リソース間の依存関係を定義しなければ、デプロイに失敗してしまいます。
解決方法は2種類ありまして
- 他のリソースを参照するときは常に reference 関数を使う。
- dependsOn に参照元の resourceID を明記する。
reference 関数を使うと ARM 側で自動的に依存関係を解決してくれるそうです。
とりあえず、先人の知恵を借りようと、Microsoft のクイックスタートのテンプレートを眺めてみたところ、dependsOn に resourceID を記載していることが多いことが分かりました。チュートリアルでもよく出てくる 101-vm-simple-windows でも、dependsOn を使っています。
私もこれにならって、とりあえずは dependsOn で進めていこうと思います。
Complete と Incremental
デプロイモードの選択
デプロイコマンドには、Complete と Incremental という2つのデプロイモードが存在します。
Complete はテンプレートでリソースを完全管理、余計なリソースは勝手に消します。Incremental は増分管理ですので、余計なリソースは消しません。
私は、余計なリソースを増やさないために、デプロイモードを Complete で運用したいと考えていますが、Complete の場合、テンプレート1つでリソースグループ1つという縛りが発生します。
リンクテンプレートといって複数のテンプレートを使ってリソースを作成することも可能ですが、その場合、Incremental を指定しなければなりません。
詳細は、リンク済みテンプレートを参照ください。
個人で使う分には、Complete でも支障ないですが、大きなプロジェクトで使う場合は、リンクテンプレートを使った Incremental になると思われます。
最後に
今回やってみて感じたことは、デプロイの方針、今後の拡張性、等々を考慮した場合、ネーミングルールを決めるのがとても難しいと言うことです。
何が正解かは、色々なリソースを作っては消してを繰り返して、運用しながら見つけていくしかないのだと思います。
それでは、今回は以上となります。次回は VirtualMachine を作成します。