ARM Templateで「ネストしたリソースでcopyを使いたいけど使えなくて困った」というときの対応

tl;dr
---------------

https://aka.ms/arm-copy/#looping-on-a-nested-resource



例
------------------

※コードは例であり、実際の動作確認はしていません

例として、SQL Database for SQL Serverを作り、そこに複数のIP制限をかけたい場合を考える。

ベースはこれ。

``` json
{
    "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
    },
    "variables": {
        "sqlServerName": "mysqlserver",
        "sqlServerAdminName": "superadmin",
        "sqlServerAdminPassword": "5ecRet!"
    },
    "resources": [
        {
            "comments": "SQL Database for SQL Server",
            "type": "Microsoft.Sql/servers",
            "name": "[variables('sqlServerName')]",
            "apiVersion": "2015-05-01-preview",
            "location": "[resourceGroup().location]",
            "properties": {
                "administratorLogin": "[variables('sqlServerAdminName')]",
                "administratorLoginPassword": "[variables('sqlServerAdminPassword')]",
            },
            "resources": [
                {
                    "type": "firewallRules",
                    "apiVersion": "2014-04-01-preview",
                    "dependsOn": [
                        "[concat('Microsoft.Sql/servers/', variables('sqlServerName'))]"
                    ],
                    "name": "AllowAllWindowsAzureIps",
                    "properties": {
                        "endIpAddress": "0.0.0.0",
                        "startIpAddress": "0.0.0.0"
                    }
                }
            ]
        }
    ]
}
```


ここで、複数のグローバルIPを与えて制限を行いたいと考え、以下のような配列を用意してVariablesを設定する。

```
"sqlServerAllowIps": ["1.1.1.1", "2.2.2.2"]
```


この時、書き方としては以下のように「配列の数だけ繰り返してリソースを用意すればよい」と思いついた。が、これはうまくいかない。


``` diff
    "resources": [
        {
            "comments": "SQL Database for SQL Server",
            "type": "Microsoft.Sql/servers",
            "name": "[variables('sqlServerName')]",
            "apiVersion": "2015-05-01-preview",
            "location": "[resourceGroup().location]",
            "properties": {
                "administratorLogin": "[variables('sqlServerAdminName')]",
                "administratorLoginPassword": "[variables('sqlServerAdminPassword')]",
            },
            "resources": [
                {
                    "type": "firewallRules",
                    "apiVersion": "2014-04-01-preview",
                    "dependsOn": [
                        "[concat('Microsoft.Sql/servers/', variables('sqlServerName'))]"
                    ],
                    "name": "AllowAllWindowsAzureIps",
                    "properties": {
                        "startIpAddress": "0.0.0.0",
                        "endIpAddress": "0.0.0.0"
                    }
-                }
+                },
+                {
+                    "type": "firewallRules",
+                    "apiVersion": "2014-04-01-preview",
+                    "dependsOn": [
+                        "[concat('Microsoft.Sql/servers/', variables('sqlServerName'))]"
+                    ],
+                    "name": "[concat('AllowIp-', copyIndex())]",
+                    "properties": {
+                        "startIpAddress": "[variables('sqlServerAllowIps')[copyIndex()]]",
+                        "endIpAddress": "[variables('sqlServerAllowIps')[copyIndex()]]"
+                    },
+                    "copy": {
+                        "name": "dummy",
+                        "count": "[variables('sqlServerAllowIps')]"
+                    }
+                }
            ]
        }
    ]
```

これを実行すると以下のようなエラーになる。

> Deployment template validation failed: 'The template resource '[concat('AllowIp-', copyIndex())]' at line '1' column '123' is not valid. Copying nested resources is not supported. Please see https://aka.ms/arm-copy/#looping-on-a-nested-resource for usage details.'.

ざっくりいえば、ネストしたリソースに対してcopyを使うことはできない、という意味。

エラー文に含まれているURLを見ろ、ということだが、これは少しわかりにくいと感じたので今回補足する。


先に成功例を貼ると、以下のように修正する。ネストさせない形で書けば良い。

``` diff
    "resources": [
        {
            "comments": "SQL Database for SQL Server",
            "type": "Microsoft.Sql/servers",
            "name": "[variables('sqlServerName')]",
            "apiVersion": "2015-05-01-preview",
            "location": "[resourceGroup().location]",
            "properties": {
                "administratorLogin": "[variables('sqlServerAdminName')]",
                "administratorLoginPassword": "[variables('sqlServerAdminPassword')]",
            },
            "resources": [
                {
                    "type": "firewallRules",
                    "apiVersion": "2014-04-01-preview",
                    "dependsOn": [
                        "[concat('Microsoft.Sql/servers/', variables('sqlServerName'))]"
                    ],
                    "name": "AllowAllWindowsAzureIps",
                    "properties": {
                        "startIpAddress": "0.0.0.0",
                        "endIpAddress": "0.0.0.0"
                    }
                }
            ]
-        }
+        },
+        {
+            "type": "Microsoft.Sql/servers/firewallRules",
+            "apiVersion": "2014-04-01-preview",
+            "dependsOn": [
+                "[concat('Microsoft.Sql/servers/', variables('sqlServerName'))]"
+            ],
+            "name": "[concat(variables('sqlServerName'), '/', 'AllowIp', '-', copyIndex())]",
+            "properties": {
+                "startIpAddress": "[variables('sqlServerAllowIps')[copyIndex()]]",
+                "endIpAddress": "[variables('sqlServerAllowIps')[copyIndex()]]"
+            },
+            "copy": {
+                 "name": "dummy",
+                 "count": "[variables('sqlServerAllowIps')]"
+            }
+        }
+    ]
```

要点は以下

- ネストにならないように変更する
- `type` を変更する指定する（ネストではなくなったので。）
- `name` は`"引数1/引数2"`というイメージで設定する必要がある。
  - firewallRulesの例でいうと、`name`には`"{serverName}/{firewallRuleName}"` となるようにする必要がある。
  - `{serverName}`は実際に存在するSQL Serverのリソースの名前を指定する。 `variables('sqlServerName')` になる。
  - `{firewallRuleName}` はユニークな値を選ぶ必要がある。（ここはユニークになれば自由な名前でよい）


分かりにくい解説
------


少し話はズレるが、Rest APIのURLを示すと以下のような形となっている。


```
https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Sql/servers/{serverName}/firewallRules/{firewallRuleName}?api-version=2014-04-01
```

(引用元: https://docs.microsoft.com/en-us/rest/api/sql/firewallrules/createorupdate)


この時、`providers`以降の文字列を見てほしい。

```
Microsoft.Sql/servers/{serverName}/firewallRules/{firewallRuleName}
```

この形はリソーステンプレートの`type`の部分に似ている。しかし、間に`{serverName}`を書く形となっており、少し違う。`type`にはこのようには書けない。

ARMTemplateでは**`name`に必要なパラメータを書く決まり**となっているようだ。なので、自由な名前を付けるだけではなく、このRestAPIの可変値の部分を満たすように設定する必要がある。
書き方も決まっており、可変値を順番に`/`で区切りながら設定しなければならない。今回の場合は`"{serverName}/{firewallRuleName}"`となる。

なので`"[concat(variables('sqlServerName'), '/', 'AllowIp-', copyIndex())]"`のように指定し、作成した際のserverNameを与えつつ、ループ毎にユニークなfirewallRuleNameを与える形にする必要があった。



~~ARM Template辛い。~~