ARM Template の書き方 その3 依存関係,ResourceID()

  • VNet
  • SecurityGroup
  • AzureResourceManager
  • AzurePowerShell
  • 過去記事

    ARM Template について3本目の記事となります。今回は、リソースの依存関係と関数 ResourceID を紹介していきたいと思います。過去記事のリンクは以下に掲載します。

    これまでに、テンプレートの基本的な書き方を覚えて、VirtualNetwork/Subnet を作りながら、変数や、パラメータファイルの基本的な使い方について確認しました。

    やりたいこと

    構成

    後々 VirtualMachine を構築することを視野に入れて、下記のようなネットワーク構成を作ってみたいと思います。これらのリソースを作成するにあたって依存関係の解決をする必要があり、その過程で関数 ResourceID を使います。

    arm-template-writing-3-1

    コンソール画面

    最終的には、以下のようにリソースを作成したいと思います。コンソールでみるとこんな感じです。

    title

    依存関係

    リソースの依存関係を理解するためには以下の図が分かりやすいかと思います。こちらの内容を参考とさせていただきました。

    title

    上記のような順番でリソースを作成するように依存関係をテンプレートないで指定しなければいけません。各リソースに dependsOn という項目を作成して依存するリソースのリソースIDを指定します。

    他のリソースIDの取得方法は関数 resouceID が便利です。下記のような文法で利用できます。

    "[resourceId('Microsoft.ServiceBus/namespaces', 'namespace1')]"

    リソースのタイプ名と、リソース名を指定することでリソースIDを参照可能です。

    ARM Template を用意

    ファイル構成

    使用するファイルは以下の4つです。

    $PSScriptRoot |-- deployment.ps1 # デプロイコマンド実行スクリプト |-- dba\ |-- azuredeploy.json # テンプレートファイル |-- dev.parameters.json # パラメータ管理

    ポイント ネーミングルール

    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)を実行します。

    PS /Users/atsushi/github/azure_arm_v1> ./deployment.ps1 dba/cia: dba dev/stg/prd: dev Template File: /Users/atsushi/github/azure_arm_v1\dba\azuredeploy.json Parameter File: /Users/atsushi/github/azure_arm_v1\dba\dev.parameters.json Test the template "/Users/atsushi/github/azure_arm_v1\dba\azuredeploy.json" ? yes or no : yes Note: The result may contain false positive predictions (noise). You can help us improve the accuracy of the result by opening an issue here: https://aka.ms/WhatIfIssues. Resource and property changes are indicated with this symbol: + Create The deployment will update the following scope: Scope: /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/atsushi.koizumi.dba.dev + Microsoft.Network/applicationSecurityGroups/dba-dev-asg01 [2020-05-01] apiVersion: "2020-05-01" id: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/atsushi.koizumi.dba.dev/providers/Microsoft.Network/applicationSecurityGroups/dba-dev-asg01" location: "eastus" name: "dba-dev-asg01" tags.Env: "dev" tags.Owner: "atsushi.koizumi" tags.Service: "dba" type: "Microsoft.Network/applicationSecurityGroups" ... Resource changes: 5 to create. Deploy the template "/Users/atsushi/github/azure_arm_v1\dba\azuredeploy.json" ? yes or no : yes

    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" の使い方を間違えたことによるエラーを発生させてしまいました。

    New-AzResourceGroupDeployment: /Users/atsushi/github/azure_arm_v1/deployment.ps1:96:13 Line | 96 | New-AzResourceGroupDeployment ` | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | 10:56:58 PM - The deployment 'network-20210115225316' failed with error(s). Showing 1 out of 1 error(s). Status Message: Unable to process template | language expressions for resource | '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/atsushi.koizumi.arm/providers/Microsoft.Network/networkInterfaces/nsg01' at line | '156' and column '9'. 'Unable to evaluate template language function 'resourceId': the type 'Microsoft.Network/virtualNetworks/subnets' requires '2' | resource name argument(s). Please see https://aka.ms/arm-template-expressions/#resourceid for usage details.' (Code:InvalidTemplate) CorrelationId: | 00000000-0000-0000-0000-000000000000

    関数 "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)]"
        }
    }

    この違いは、コンソールでみると分かりやすいです。

    title

    上記のように VirtualNetwork と Subnet は親子関係になっているため、リソースの指定方法が異なるのです。この辺のリソースの特徴を理解していないとテンプレートは書けません。

    Code:InvalidResourceReference

    リソース間の依存関係を定義しなければ、デプロイに失敗してしまいます。

    New-AzResourceGroupDeployment: /Users/atsushi/github/azure_arm_v1/deployment.ps1:96:13 Line | 96 | New-AzResourceGroupDeployment ` | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | 8:10:52 AM - The deployment 'network-20210116080911' failed with error(s). Showing 3 out of 4 error(s). Status Message: Resource | /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/atsushi.koizumi.arm/providers/Microsoft.Network/networkSecurityGroups/sg01 | referenced by resource | /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/atsushi.koizumi.arm/providers/Microsoft.Network/networkInterfaces/nic01 was not | found. Please make sure that the referenced resource exists, and that both resources are in the same region. (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 を作成します。

  • VNet
  • SecurityGroup
  • AzureResourceManager
  • AzurePowerShell