ARM template の `reference` 関数は入れ子にできない
TL;DR
- Bicep で 2 つの VPN Gateway を接続したいのに、どうもエラーになるのでどうにかし隊
-
reference
関数が入れ子になるとエラーになるので、別の deployment に分けることで回避した - Bicep 一発で VPN 環境が作れるようになったのでうれしい
はじめに
ARM template には reference 関数というのがあって、Resource ID を指定するとそのプロパティがとれるようになります。
今回やりたかったシナリオでは、シンプルに実装すると reference
関数が入れ子になることによりエラーとなりデプロイできないため、それをどうにか回避した、という話です。
やりたかったこと
Bicep を最近また書いています。
検証環境として、VNet を 2 つ用意して、それらにそれぞれ VPN Gateway を用意して、それらを Internet VPN で接続することで片方の VNet をオンプレミスに見立てる、というのをよくやります。
その上で、(1) static route で設定するか、(2) BGP を有効化するか、の選択肢がありますが、今回は (1) でやります。
VPN Gateway #1 の対向を示す Local Network Gateway #1 は VPN Gateway #2 の Public IP アドレスを指定すればよいわけで、結局は VPN Gateway #1 と #2 を指定すればあとはその間を VPN でつないでくれ、という気持ちになります。
それを Bicep の module
として一番シンプルに表現するとこうなるわけです。
VPN Gateway #1 と #2 の名前と、あとは一応 psk (preshared key) を与えます。
module connection '../lib/connection-vpngw-static-route.bicep' = {
name: 'connection-${vpngw01Name}-${vpngw02Name}'
params: {
vpnGateway01Name: vpngw01.name
vpnGateway02Name: vpngw02.name
psk: psk
}
}
ただ、これをそのまま実装したところ、内部的 (Bicep から ARM template への暗黙的な transpile の結果として) に reference
関数が入れ子になってしまい、InvalidTemplate
エラーが出ました。
どうすれば回避できるか
いろいろ調べてみたところ、この reference
関数の制限は、別の deployment に分ければ回避できそうな雰囲気がわかりました。
やや面倒ですが、Bicep ファイルを 2 つに分割し、さらに入れ子になった Bicep ファイル側で実際の connection リソース (VPN Gateway と Local Network Gateway を関連付けるリソース) の作成を行います。
まずは 1 つめの入れ子の Bicep ファイルですが、Bicep の便利な機能である existing
キーワードを使いますが、これを使って vnet02.name
などと properties にアクセスする際に、内部的には reference
関数が使われています。
connection-vpngw-static-route.bicep
param vpnGateway01Name string = 'vpngw01'
param vpnGateway02Name string = 'vpngw02'
param connection01Name string = ''
param connection02Name string = ''
@secure()
param psk string
var _connection01Name = !empty(connection01Name) ? connection01Name : 'conn-${vpnGateway01Name}'
var _connection02Name = !empty(connection02Name) ? connection02Name : 'conn-${vpnGateway02Name}'
resource vnet01 'Microsoft.Network/virtualNetworks@2022-01-01' existing = {
name: split(vpngw01.properties.ipConfigurations[0].properties.subnet.id, '/')[8]
}
resource pip_vpngw01 'Microsoft.Network/publicIPAddresses@2022-01-01' existing = {
name: split(vpngw01.properties.ipConfigurations[0].properties.publicIPAddress.id, '/')[8]
}
resource vpngw01 'Microsoft.Network/virtualNetworkGateways@2022-01-01' existing = {
name: vpnGateway01Name
}
resource vnet02 'Microsoft.Network/virtualNetworks@2022-01-01' existing = {
name: split(vpngw02.properties.ipConfigurations[0].properties.subnet.id, '/')[8]
}
resource pip_vpngw02 'Microsoft.Network/publicIPAddresses@2022-01-01' existing = {
name: split(vpngw02.properties.ipConfigurations[0].properties.publicIPAddress.id, '/')[8]
}
resource vpngw02 'Microsoft.Network/virtualNetworkGateways@2022-01-01' existing = {
name: vpnGateway02Name
}
module connection01 'connection-vpngw-static-route-helper.bicep' = {
name: _connection01Name
params: {
location: vpngw01.location
connection01Name: _connection01Name
vnet02Name: vnet02.name
pipGateway02Name: pip_vpngw02.name
vpnGateway01Name: vpnGateway01Name
vpnGateway02Name: vpnGateway02Name
psk: psk
}
}
module connection02 'connection-vpngw-static-route-helper.bicep' = {
name: _connection02Name
params: {
location: vpngw02.location
connection01Name: _connection02Name
vnet02Name: vnet01.name
pipGateway02Name: pip_vpngw01.name
vpnGateway01Name: vpnGateway02Name
vpnGateway02Name: vpnGateway01Name
psk: psk
}
}
ここから呼び出される helper 側は以下のとおりです。
connection-vpngw-static-route-helper.bicep
param location string = 'eastasia'
param vnet02Name string = 'vnet02'
param pipGateway02Name string = 'pip-vpngw02'
param vpnGateway01Name string = 'vpngw01'
param vpnGateway02Name string = 'vpngw02'
param connection01Name string = 'conn-vpngw01'
@secure()
param psk string
resource vnet02 'Microsoft.Network/virtualNetworks@2022-01-01' existing = {
name: vnet02Name
}
resource vpngw01 'Microsoft.Network/virtualNetworkGateways@2022-01-01' existing = {
name: vpnGateway01Name
}
resource pip_vpngw02 'Microsoft.Network/publicIPAddresses@2022-01-01' existing = {
name: pipGateway02Name
}
resource lng_vpngw02 'Microsoft.Network/localNetworkGateways@2022-01-01' = {
name: 'lng-${vpnGateway02Name}'
location: location
properties: {
gatewayIpAddress: pip_vpngw02.properties.ipAddress
localNetworkAddressSpace: {
addressPrefixes: vnet02.properties.addressSpace.addressPrefixes
}
}
}
resource connection01 'Microsoft.Network/connections@2022-01-01' = {
name: connection01Name
location: location
properties: {
connectionType: 'IPsec'
virtualNetworkGateway1: {
id: vpngw01.id
}
connectionProtocol: 'IKEv2'
localNetworkGateway2: {
id: lng_vpngw02.id
}
sharedKey: psk
}
}
まとめ
なんとなくまぁ reference
したものの reference
という意味で 2 回評価しなきゃいけないのは仕様上の制限としてわからなくはないです。
最初は 1 つ目の入れ子の module
のみで解決しようと思いましたが、そうすると main.bicep
からの呼び出しの際にすら VNet の名前やら Public IP の名前やらを与えなければならず、だいぶ面倒になってしまいました。
ベッドでうんうん考えていたらこの方法を思いつき、入れ子になってしまったため deployment の履歴としてはやや複雑にはなりますが、main.bicep
からの呼び出しの際の引数を最小限に抑えることができたため、満足度は非常に高いです。
プログラミングの方法論でよく言われる、インターフェースを単純化して水面下で頑張るか、そうじゃないか、みたいな話かとは思います。
詳細な解説 (Public IP 編)
ここから詳細な説明に入っていきますが、私自身 2 週間後に読んで理解できる自信がないほど、やや複雑です。
まずは VPN Gateway 側の JSON を示します。
少しだけ要素は消しましたが、大体こんな感じです。
{
"name": "vpngw01",
"id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/s2svpn/providers/Microsoft.Network/virtualNetworkGateways/vpngw01",
"type": "Microsoft.Network/virtualNetworkGateways",
"location": "eastasia",
"properties": {
"ipConfigurations": [
{
"name": "ipconfig1",
"id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/s2svpn/providers/Microsoft.Network/virtualNetworkGateways/vpngw01/ipConfigurations/ipconfig1",
"type": "Microsoft.Network/virtualNetworkGateways/ipConfigurations",
"properties": {
"publicIPAddress": {
"id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/s2svpn/providers/Microsoft.Network/publicIPAddresses/pip-vpngw01"
},
"subnet": {
"id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/s2svpn/providers/Microsoft.Network/virtualNetworks/vnet-ea01/subnets/GatewaySubnet"
}
}
}
],
「VPN Gateway #1 の対向を示す Local Network Gateway #1 は VPN Gateway #2 の Public IP アドレスを指定すればよい」と書きましたが、VPN Gateway #2 のリソースにおいて、properties.ipConfigurations[0].properties.publicIPAddress.id
を見れば、紐づいている Public IP アドレスリソースがわかります。
次に Public IP アドレスのリソースの JSON を示します。
{
"name": "pip-vpngw02",
"id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/s2svpn/providers/Microsoft.Network/publicIPAddresses/pip-vpngw02",
"location": "southeastasia",
"properties": {
"ipAddress": "x.x.x.x",
ということで、この Public IP アドレスリソースの properties.ipAddress
を見ればいいわけです。
ただ、これを ARM template で実装すると、
"[reference(resourceId('Microsoft.Network/publicIPAddresses', split(reference(resourceId('Microsoft.Network/virtualNetworkGateways', parameters('vpnGateway02Name')), '2022-01-01').ipConfigurations[0].properties.publicIPAddress.id, '/')[8]), '2022-01-01').ipAddress]"
という感じになり、reference
関数が 2 回登場してしまっています。
順を追って説明すると、以下のとおりです。
- VPN Gateway のリソース名が、
module
の呼び出し元側からparameters('pipGateway02Name')
として渡されます -
resourceId('Microsoft.Network/virtualNetworkGateways', parameters('vpnGateway02Name'))
で VPN Gateway #2 の Resource ID (/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/s2svpn/providers/Microsoft.Network/virtualNetworkGateways/vpngw01
) を取得 (これを_1_
とします) -
reference(_1_, '2022-01-01')
でそのリソースを参照 (1 回目のreference
関数) -
reference(_1_, '2022-01-01').ipConfigurations[0].properties.publicIPAddress.id
で Public IP の Resouce ID (/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/s2svpn/providers/Microsoft.Network/publicIPAddresses/pip-vpngw01
) がとれる (これを_2_
とします) -
split(_2_, '/')[8]
で Public IP のリソース名 (pip-vpngw01
) を取得 (これを_3_
とします) -
resourceId('Microsoft.Network/publicIPAddresses', _3_)
で Public IP の Resource ID がとれる (これを_4_
とします。これは Bicep から変換した都合上であり、省略は可能です) -
reference(_4_, '2022-01-01')
で Public IP のリソースを参照 (2 回目のreference
関数、ここがエラーになるっぽい、たぶん) -
reference(_4_, '2022-01-01').ipAddress
でその Public IP の実際の IP アドレスがとれる (はずだった)
deployment を分ければいい、の方針のもと、Bicep ファイルも 2 つに分けていきます。
その結果、上記の Bicep ファイルのように helper
としての Bicep ファイルを用意して、reference
関数が 1 つの deployment 上では入れ子にならないようにしています。
実際、deploy されている ARM template 側を見るとうまく回避できています。
{
"parameters": {
"location01": {
"defaultValue": "eastasia",
"type": "String"
},
"location02": {
"defaultValue": "southeastasia",
"type": "String"
},
"psk": {
"type": "SecureString"
}
},
"resources": [
{
"type": "Microsoft.Resources/deployments",
"name": "[format('connection-{0}-{1}', variables('vpngw01Name'), variables('vpngw02Name'))]",
"properties": {
"parameters": {
"vpnGateway01Name": {
"value": "[variables('vpngw01Name')]"
},
"vpnGateway02Name": {
"value": "[variables('vpngw02Name')]"
},
"psk": {
"value": "[parameters('psk')]"
}
},
"template": {
"resources": [
{
"type": "Microsoft.Resources/deployments",
"name": "[variables('_connection01Name')]",
"properties": {
"parameters": {
"vnet02Name": {
"value": "[split(reference(resourceId('Microsoft.Network/virtualNetworkGateways', parameters('vpnGateway02Name')), '2022-01-01').ipConfigurations[0].properties.subnet.id, '/')[8]]"
},
"pipGateway02Name": {
"value": "[split(reference(resourceId('Microsoft.Network/virtualNetworkGateways', parameters('vpnGateway02Name')), '2022-01-01').ipConfigurations[0].properties.publicIPAddress.id, '/')[8]]"
},
"vpnGateway02Name": {
"value": "[parameters('vpnGateway02Name')]"
}
},
"template": {
"resources": [
{
"type": "Microsoft.Network/localNetworkGateways",
"name": "[format('lng-{0}', parameters('vpnGateway02Name'))]",
"properties": {
"gatewayIpAddress": "[reference(resourceId('Microsoft.Network/publicIPAddresses', parameters('pipGateway02Name')), '2022-01-01').ipAddress]",
"localNetworkAddressSpace": {
"addressPrefixes": "[reference(resourceId('Microsoft.Network/virtualNetworks', parameters('vnet02Name')), '2022-01-01').addressSpace.addressPrefixes]"
}
}
}
]
}
}
},
Microsoft.Resources/deployments
が入れ子になっていますが、2 つめの入れ子に入る際に、parameter
として [split(reference(resourceId('Microsoft.Network/virtualNetworkGateways', parameters('vpnGateway02Name')), '2022-01-01').ipConfigurations[0].properties.publicIPAddress.id, '/')[8]]
が入っています。
2 つめの入れ子側で [reference(resourceId('Microsoft.Network/publicIPAddresses', parameters('pipGateway02Name')), '2022-01-01').ipAddress]
として reference
関数に渡されており、parameters('pipGateway02Name')
自体が呼び出し元側で reference
関数が利用されているため reference
関数の入れ子になってしまうはずですが、deployment が分かれているせいか回避できています。
詳細な解説 (VNet 編)
同様の動作を VNet に対しても行っていきます。
VPN Gateway の対向を示す Local Network Gateway について、BGP を使わない場合には properties.localNetworkAddressSpace.addressPrefixes
を指定しないと VPN 接続が開始されません。
properties.localNetworkAddressSpace.addressPrefixes
は VPN 接続における static route の意味であり、対向側にどのようなネットワークアドレスがあるかを明示的に指定するものです。
これも Public IP 同様、自動的に設定するために以下の内容を考えていきます。
- VPN Gateway #1 の対向は VPN Gateway #2
- VPN Gateway #2 は VNet #2 の
GatewaySubnet
に所属している - 所属している subnet の情報から VNet #2 は参照可能
- 参照した VNet の
properties.addressSpace.addressPrefixes
にその IP アドレス空間が記載されているため、これを Local Network Gateway のproperties.localNetworkAddressSpace.addressPrefixes
とすればよい
実装した結果は上の Bicep ファイルにそのまま記載されていますが、reference
関数が 2 回必要になるのは Public IP アドレスの時と同様のため、同じ入れ子構造の中でこの問題が解決できています。
Update log
- 全体的に
# (見出し1)
から## (見出し2)
に変更 - 2024/07/14 - TL;DR とかの追加 - 2024/07/14
- tag の修正 - 2024/07/14
Discussion