※本記事は、個人の意見および個人的活動を記したものであり、会社を代表するものではありません
NSG自体は単純な定義で作成できたのだが、いくつかのエラーに遭遇し調査しながら解決した。結構重要な知識を得ることができたので、自分の中では特に学びの多い活動となった。
1. NSGのBicep定義
1-1. BastionSubnet用NSGの設定例
今一度、参考情報サイトのリンクを掲載。
このサイトで紹介されている設定に従って、今度はBicepでNSGを定義する。
この情報、後からMS公式ドキュメントに掲載されていることが分かった。。この記事ではその事に触れられていないので後から追記されたのかな。
1-2. NSGの複数ルールの定義
NSGでは複数のルールを定義する必要があるが、1つ1つのルールをBicepでベタに定義するのは非効率なので、パラメータでルール配列を指定し、それをループで処理するBicepを定義する。
1-2-1. NSGのパラメータの定義
以下、かなり冗長だが、今回設定するNSGのパラメータを定義。これは前回Azure PortalでNSGを定義した時のjson定義から抜粋し、不要なパラメータを削除したもの。これを元にBicepをNSG本体とNSGのセキュリティルールを定義する。
- name(必須): “RagSystem-AzureBastionSubnetNSG-dev”
- subnets: [0]
- id: AzureBastionSubnetのid
- properties.securityRules[0]
- name: “AllowSshSshRdp”
- properties.protocol: “TCP”
- properties.sourcePortRange: “*”
- properties.sourceAddressPrefix: “*”
- properties.destinationAddressPrefix: “VirtualNetwork”
- properties.access: “Allow”
- properties.priority: 110
- properties.direction: “Outbound”
- properties.destinationPortRanges: [”22”, ”3389”]
- properties.securityRules[1]
- name: “AllowAzureCloudOut”
- properties.protocol: “TCP”
- properties.sourcePortRange: “*”
- properties.destinationPortRange: “443”
- properties.sourceAddressPrefix: “*”
- properties.destinationAddressPrefix: “AzureCloud”
- properties.access: “Allow”
- properties.priority: 120
- properties.direction: “Outbound”
- properties.securityRules[2]
- name: “AllowSessionInformation”
- properties.protocol: “TCP”
- properties.sourcePortRange: “*”
- properties.destinationPortRange: “80”
- properties.sourceAddressPrefix: “*”
- properties.destinationAddressPrefix: “Internet”
- properties.access: “Allow”
- properties.priority: 130
- properties.direction: “Outbound”
- properties.securityRules[3]
- name: “AllowBastionCommunicationOut”
- properties.protocol: “*”
- properties.sourcePortRange: “*”
- properties.sourceAddressPrefix: “VirtualNetwork”
- properties.destinationAddressPrefix: “VirtualNetwork”
- properties.access: “Allow”
- properties.priority: 140
- properties.direction: “Outbound”
- properties.destinationPortRanges: [”8080”, “5701”]
- properties.securityRules[4]
- name: “DenyAllOutbound”
- properties.protocol: “*”
- properties.sourcePortRange: “*”
- properties.destinationPortRange: “*”
- properties.sourceAddressPrefix: “*”
- properties.destinationAddressPrefix: “*”
- properties.access: “Deny”
- properties.priority: 4096
- properties.direction: “Outbound”
- properties.securityRules[5]
- name: “AllowGatewayManager”
- properties.protocol: “TCP”
- properties.sourcePortRange: “*”
- properties.destinationPortRange: “443”
- properties.sourceAddressPrefix: “GatewayManager”
- properties.destinationAddressPrefix: “*”
- properties.access: “Allow”
- properties.priority: 110
- properties.direction: “Inbound”
- properties.securityRules[6]
- name: “AllowAzureLoadBalancer”
- properties.protocol: “TCP”
- properties.sourcePortRange: “*”
- properties.destinationPortRange: “443”
- properties.sourceAddressPrefix: “AzureLoadBalancer”
- properties.destinationAddressPrefix: “*”
- properties.access: “Allow”
- properties.priority: 120
- properties.direction: “Inbound”
- properties.securityRules[7]
- name: “AllowBastionHostCommunications”
- properties.protocol: “*”
- properties.sourcePortRange: “*”
- properties.sourceAddressPrefix: “VirtualNetwork”
- properties.destinationAddressPrefix: “VirtualNetwork”
- properties.access: “Allow”
- properties.priority: 130
- properties.direction: “Inbound”
- properties.destinationPortRanges: [”8080”, “5701”]
- properties.securityRules[8]
- name: “AllowClientToAccssToBastion”
- properties.protocol: “TCP”
- properties.sourcePortRange: “*”
- properties.sourceAddressPrefix: “<自分のクライアントIP>”
- properties.destinationAddressPrefix: “*”
- properties.access: “Allow”
- properties.priority: 140
- properties.direction: “Inbound”
- properties.destinationPortRanges: “443”
- properties.securityRules[8]
- name: “DenyAllInbound”
- properties.protocol: “*”
- properties.sourcePortRange: “*”
- properties.destinationPortRange: “*”
- properties.sourceAddressPrefix: “*”
- properties.destinationAddressPrefix: “*”
- properties.access: “Deny”
- properties.priority: 4096
- properties.direction: “Inbound”
詳細は以下Githubリポジトリを参照。
2. NSGを定義する際のTips
2-1. [tips]デフォルトルールの定義は不要
NSGを作成する時、以下の設定はBicepで指定しなくてもデフォルトルールとして常に作成される。
- 65000: AllowVnetInBound (from vnet)
- 65001: AllowAzureLoadBalancerInBound (from AzureLoadBalander)
- 65500: DenyAllInBound (from any)
- 65000: AllowVnetOutBound (to vnet)
- 65001: AllowInternetOutBound (to internet)
- 65500: DenyAllOutBound (to any)
2-2. [tips]複数ルールは配列型のパラメータで宣言する
最初は、ルールを定義した配列型のパラメータを指定し、Bicep内でループすれば良いと思っていたが、NSGのルール自体が配列型なので、そのままセットすれば良いだけだった。。
以下はNSGのOutboundルールとInboundルールを定義しているパラメータファイルの抜粋で、それぞれを配列としてルールを定義している。
1つの配列でも良いのかもしれないが、自分的には、InboundとOutboundを分けて定義することで少し読みやすくなると思ったので分けてみた。
// Definitions of Outbound traffic rules.
param bastionNsg_securityRules_outBound = [
{
name: 'AllowSshRdpToVNet'
properties: {
description: 'Allow SSH and RDP to the Virtual Network.'
protocol: 'TCP'
sourcePortRange: '*'
destinationPortRanges: [
'22'
'3389'
]
sourceAddressPrefix: '*'
destinationAddressPrefix: 'VirtualNetwork'
access: 'Allow'
priority: 110
direction: 'Outbound'
}
}
... 省略
]
// Definitions of Inbound traffic rules.
param bastionNsg_securityRules_inBound = [
{
name: 'AllowFromGatewayManager'
properties: {
description: 'Allow Gateway Manager.'
protocol: 'TCP'
sourcePortRange: '*'
destinationPortRange: '443'
sourceAddressPrefix: 'VirtualNetwork'
destinationAddressPrefix: 'VirtualNetwork'
access: 'Allow'
priority: 110
direction: 'Inbound'
}
}
上記パラメータをまとめて、nsg.bicepをモジュール呼び出ししている箇所の抜粋が以下。
concat関数で複数の配列を1つの配列に統合できる。
以下では、モジュール定義の直前でOutboundとInboundをまとめてsecurityRulesとして1つの配列にし、モジュールのパラメータとして指定している。
@description('The security rules of the Network Security Group for the Azure Bastion Subnet.')
var bastionNsg_securityRules = concat(bastionNsg_securityRules_outBound, bastionNsg_securityRules_inBound)
// Build the Network Security Group for the AzureBastionSubnet.
module BastionNsg './network/nsg.bicep' = {
scope: resourceGroup(rg_name)
name: bastionNsg_name
params: {
tags: tags
name: bastionNsg_name
securityRules: bastionNsg_securityRules <ーここね
}
dependsOn: [
Rg
]
}
そして、nsg.bicepの抜粋。
パラメータとして指定された配列を指定するだけ。ちょー簡単だった。
// The Network Security Group.
resource nsg 'Microsoft.Network/networkSecurityGroups@2024-01-01' = {
name: name
location: resourceGroup().location
tags: tags
properties: {
securityRules: securityRules
}
}
2-3. [tips]NSGはSubnet側もしくはNIC側で紐づける
NSGのBicepでは、AzureBastionSubnetと紐付けるパラメータが定義されておらず、AzureBastionSubnet側で紐づける必要がある。
Bicepは宣言型言語であるため、NSGの定義はBicep内のどこでもよく、AzureBastionSubnetのBicep定義にてNSGとの依存関係を定義すればよい。
2-4. [tips]エラーメッセージ「not found」の原因
bicepテンプレートを使用したデプロイでいつも面倒なのが、エラーが検出された時にどこにその原因があるのかを探すこと。
例えば以下のエラー、これだけでどこにエラーの原因があるのかを特定することはできないため推測するしかない。
(以下のエラーはNSGとは関係ないが、たまたまNAT Gatewayの定義にエラーがあったため発生)
sato@[12:14:37]:~/proj/RagSystem% azd up
... 省略
ERROR: error executing step command 'provision': deployment failed: error deploying infrastructure: deploying to subscription:
Deployment Error Details:
NotFound: Resource /subscriptions/xxx/resourceGroups/xxx/providers/Microsoft.Network/natGateways/RagSystem-NatGw-dev not found.
このエラーは最後の行で「NAT Gatewayが見つからない」と言っているので、NAT Gatewayを参照している箇所で発生していることはわかる。
今作っているBicepでNAT Gatewayを参照している箇所は、AdminSubnetとAzureBastionSubnetの2つ。
調べたら、どちらのSubnetも「dependsOn」プロパティ(依存関係の定義)にNAT Gatewayを指定していなかったため、Subnet作成時にNAT Gatewayの作成が完了していなかったのが原因。
dependsOnプロパティにより、リソースの依存関係を指定しており、今回の例ではNAT Gatewayの作成後にSubnetを作成するように定義し、Subnet内でNAT Gatewayとの紐付けを行う時には確実にNAT GatewayのIDが取得できるようにする必要があった。
module BastionSubnet './network/subnet.bicep' = {
scope: resourceGroup(rg_name)
name: bastionSubnet_name
params: {
subnet_name: bastionSubnet_name
subnet_addressPrefix: bastionSubnet_addressPrefix
subnet_defaultOutboundAccess: bastionSubnet_defaultOutboundAccess
subnet_privateEndpointNetworkPolicies: bastionSubnet_privateEndpointNetworkPolicies
vnet_name: mainVNet_name
natGw_name: natGw_name
}
dependsOn: [
MainVNet
NatGw <- これが抜けてた
BastionNsg
]
}
2-5. [tips] リソースオブジェクトのプロパティ参照エラー
2-5-1. 出力されたエラーメッセージ
以下のエラーは、生成したリソースの存在しないプロパティ(今回は「id」)を指定した場合に出力されるエラーメッセージ。「RagSystem-NatGw-dev」という名前のリソースで参照している「id」というプロパティは本来は存在しているはずが、存在しないと言われている。
これは明確に原因を特定したというよりも、おそらくこれだろうという推測の域をでない状態。その推測に達するまでにかなり苦労した。
sato@[12:42:38]:~/proj/RagSystem% azd up
... 省略
ERROR: error executing step command 'provision': deployment failed: error deploying infrastructure: deploying to subscription:
Deployment Error Details:
InvalidTemplate: Unable to process template language expressions for resource '/subscriptions/xxx/resourceGroups/xxx/providers/Microsoft.Network/natGateways/RagSystem-NatGw-dev' at line '1' and column '767'. 'The language expression property 'id' doesn't exist, available properties are 'apiVersion, location, sku, tags, properties, condition, isConditionTrue, subscriptionId, resourceGroupName, scope, resourceId, referenceApiVersion, existing, isTemplateResource, isAction, isExtensibleResourceReference, provisioningOperation'.'
TraceID: eb438c56f7fee6324adab19180e42cf2
2-5-2. エラー発生箇所のbicep定義
以下はnatGw.bicepというファイルの抜粋。NAT Gateway用のIPの生成はpublic-ip.bicepというファイルに定義したbicepをモジュールとして読み込み実行している。
dependsOnで依存関係も定義している。
// The public IP address for the NAT Gateway.
module natGwPublicIP './public-ip.bicep' = {
name: 'natGatewayPublicIP'
params: {
name: natGw_Ip_name
tags: tags
}
}
// The NAT Gateway.
resource natGw 'Microsoft.Network/natGateways@2024-01-01' = {
name: natGw_name
location: resourceGroup().location
tags: tags
sku: {
name: 'Standard'
}
properties: {
idleTimeoutInMinutes: 4
publicIpAddresses: [
{
id: natGwPublicIP.outputs.publicIP.id <- ここ
}
]
}
dependsOn [
natGwPublicIP
]
}
以下、public-ip.bicepの定義。
生成したpublicIpのオブジェクトをoutputに指定して、モジュール呼び出し元で参照できるようにしている。
resource publicIp 'Microsoft.Network/publicIPAddresses@2024-01-01' = {
name: name
location: resourceGroup().location
tags: tags
sku: {
name: 'Standard'
tier: 'Regional'
}
properties: {
publicIPAllocationMethod: 'Static'
publicIPAddressVersion: 'IPv4'
}
}
output publicIp object = publicIp
先にIPアドレスのリソースが作成され、そのオブジェクトを参照しているので、テンプレート上は一見問題なさそうだが、実はこれは「うまく動作しないことがある」ということが分かった。
2-5-3. オブジェクト参照ではなく明示的にオブジェクト内のプロパティを参照するよう変更
この処理がうまく動作しないのは全然納得できなかったのだが、必要な情報はIPアドレスのオブジェクトではなく、そのidであるため、publicIPのモジュールのoutputとしてリソースオブジェクトそのものではなく、idプロパティを直接返すように変更してみた。
resource publicIp 'Microsoft.Network/publicIPAddresses@2024-01-01' = {
name: name
location: resourceGroup().location
tags: tags
sku: {
name: 'Standard'
tier: 'Regional'
}
properties: {
publicIPAllocationMethod: 'Static'
publicIPAddressVersion: 'IPv4'
}
}
output publicIpId string = publicIp.id <- ここでidを返すように変更。
するとエラーは解消し、正常にリソースがデプロイできた。
この事象から推測できることは以下。
2-5-4. リソース自体の生成と各プロパティ生成のタイミングは非同期である可能性が高い
仕様として明記されている箇所を発見できなかったので、あくまでも私個人の憶測だが、BicepがARMに変換されAzure Resource Managerで実行される時に、1つのリソースの生成処理において以下2つの処理はシーケンシャルではあるものの、非同期で行われている可能性がありそうだということ。
- IPアドレスのリソース自体が生成されたタイミングでオブジェクトとして参照可能になる
- リソース内のID発行処理は継続して実行され、完了したらIPアドレスのリソース内でIDが参照可能になる
もし、IPアドレスのリソース生成が完了したと判断されるタイミングで、まだIDが発行されておらずリソースオブジェクトから参照できない場合、今回発生したエラーになると考えると納得できる。さらに、outputとして必要なプロパティ(今回はID)を指定することで、そのプロパティが生成されてからリソースの生成処理が完了する。
2-5-5. 結論:モジュールのoutputにはリソースオブジェクトではなく、必要な個々のプロパティを指定するのが確実
ここまでの推測により、outputで指定したプロパティが参照可能になったタイミングでリソース生成完了と判断されると思われるが、リソース自体の生成完了のタイミングと、そのリソースが持っている各プロパティの生成完了のタイミングは異なるということ。
生成したリソースのプロパティを参照したいのであれば、outputに目的のプロパティを明示的に指定するのが確実だ。
ここまで定義してきたすべてのbicepファイルで定義しているリソースのoutputに必ず生成したリソースのidを含めることとした。
いやー、この調査はなかなか大きな学びだった。
2-6. [tips]他のリソース作成処理が進行中のため処理が継続できないというエラー
まず、エラーは以下の内容で出力される。要するに、他のプロセスが進行中なので継続できないというエラー。
2-6-1. 依存関係と参照関係は別物
このエラー、先ほどのid参照エラーと関係なさそうだが、関係大有りだった。
sato@[22:23:16]:~/proj/RagSystem% azd up
... 省略
ERROR: error executing step command 'provision': deployment failed: error deploying infrastructure: deploying to subscription:
Deployment Error Details:
AnotherOperationInProgress: Another operation on this or dependent resource is in progress. To retrieve status of the operation use uri: https://management.azure.com/subscriptions/xxx/providers/Microsoft.Network/locations/eastus2/operations/13257a56-117b-4426-92a1-36e581eea70f?api-version=2023-11-01.
TraceID: c90397d334f1cd4e4b8ad1a29f89bb83
このエラーメッセージだけでは、どこで発生しているのか全くわからないのだが、Azure Portalのデプロイメントの履歴を確認すると、Subnetの生成で失敗していることが分かった。
Subnet生成時に、他のリソースが進行中ということは、Subnetと依存関係のあるVNet, NAT Gateway, NSG(BastionSubnetのみ)が関係していることがわかる。
だが、これらのリソースはすでにdependsOnで依存関係を指定している。
module AdminSubnet './network/subnet.bicep' = {
scope: resourceGroup(rg_name)
name: adminSubnet_name
params: {
subnet_name: adminSubnet_name
subnet_addressPrefix: adminSubnet_addressPrefix
subnet_defaultOutboundAccess: adminSubnet_defaultOutboundAccess
subnet_privateEndpointNetworkPolicies: adminSubnet_privateEndpointNetworkPolicies
vnet_name: mainVNet_name
natGw_name: natGw_name
}
dependsOn: [
MainVNet
NatGw
]
}
module BastionSubnet './network/subnet.bicep' = {
scope: resourceGroup(rg_name)
name: bastionSubnet_name
params: {
subnet_name: bastionSubnet_name
subnet_addressPrefix: bastionSubnet_addressPrefix
subnet_defaultOutboundAccess: bastionSubnet_defaultOutboundAccess
subnet_privateEndpointNetworkPolicies: bastionSubnet_privateEndpointNetworkPolicies
vnet_name: mainVNet_name
natGw_name: natGw_name
}
dependsOn: [
MainVNet
NatGw
BastionNsg
]
}
先ほどのidの話が少し気になり、各モジュールのoutputとして、生成したリソースのidをoutputとして指定するようにしてみたところ、このエラーは解消された。
2-6-2. 結論:生成したリソースを確実に参照させるために、outputでidを必ず指定する
依存関係のあるリソースを宣言することで、作成順番は指定した依存関係に準じて順番に作成処理が実行されるようだが、参照できるかどうかは別のようだ。
すべてのモジュールにoutputとして生成したリソースのidを返すように変更することで、このエラーは発生しなくなった。
でもみんな困ってないのかな。。
3. ここまでのリソース作成状況の確認
ここまでのリソース構成をVSCodeのAzure Developer CLIプラグインで確認。
3-1. VSCodeの「Bicep Visualizer」で外観を確認
黄色い四角はモジュール、その中でリソースが定義されている。各リソースの参照関係は矢印で表現されている。一部のリソースは重複して定義され、参照関係が表現されている。これはBicepで参照権限を定義するために既存リソースを複数回取得している場合にこのような表現となるようだ。
この図を見て2点気がついた。
- 「AdminSubnet」と「BastionSubnet」の定義が複雑になっている(改善案1:Subnetテンプレートの見直し)
- 「natGwPublicIp」はモジュールで定義されているのに「bastionIp」はBastionモジュール内で定義されている。(改善案2:IPアドレスのモジュール利用)
俯瞰してみると、コードのままでは気が付くのが難しいことでも、容易に気がつくことができる。
上記2点について、改善案を検討してみる。
3-2. 改善案1:Subnetのテンプレートの見直し
subnet.bicepでは2つのsubnetリソースの生成テンプレートを定義している。
1つは、NAT Gatewayと紐づける場合のテンプレート、もう一つはNAT Gateway不要の場合のテンプレート。
NAT Gateway情報が指定された場合、ifステートメントによりsubnet_relatedToNatGwが実行され、それ以外はsubnetが実行されるようになっている。
// Resource definition for the Subnet.
// Refer to: https://learn.microsoft.com/ja-jp/azure/templates/microsoft.network/virtualnetworks/subnets?pivots=deployment-language-bicep
resource subnet_relatedToNatGw 'Microsoft.Network/virtualNetworks/subnets@2023-11-01' = if (!empty(natGw_name)) {
// ** Required
name: subnet_name
...
}
resource subnet 'Microsoft.Network/virtualNetworks/subnets@2023-11-01' = if (empty(natGw_name)) {
// ** Required
name: subnet_name
...
}
このようにした理由は、以前試した時にはsubnet内に指定するnatGatewayプロパティに対して、条件によってNAT Gatewayの紐付けができなかったため。
本当はこんな冗長な定義はしたくなかったのだが、結局subnetの定義自体を2つに分けてifステートメントで切り分けるという方法にたどり着いた。
3-2-1. 「条件によってプロパティを指定するロジック」のエラーが解消できない
以前にチャレンジして断念した「1つのsubnet定義の中で、条件によってNAT Gatewayを指定する」という実装のほうがbicepテンプレートとしてはスッキリするので、もう一度この方法にチャレンジしてみる。
以下、その時のロジック。
これはBicep構文としては正しいのだが、実際に実行するとidプロパティにはnullもしくは空文字は指定できないという実行時エラーが発生する。
以下はnatGw_nameが指定されていない場合にnullを指定しており、これはVSCode上ではエラーが検出されないが、実行時エラー(idプロパティにnullは指定できない)になる。
resource subnet 'Microsoft.Network/virtualNetworks/subnets@2023-11-01' = {
... 省略
properties: {
... 省略
natGateway: {
id: ((!empty(natGw_name) ? natGw.id : null)
}
}
}
sato@[9:08:13]:~/proj/RagSystem% azd up
... 省略
ERROR: error executing step command 'provision': deployment failed: error deploying infrastructure: deploying to subscription:
Deployment Error Details:
InvalidRequestFormat: Cannot parse the request.
InvalidJsonPropertyType: Value for the id property is invalid. Expecting a string. Actual value is Null. Path properties.natGateway.
TraceID: 7efa34cf3b1141ee369e66683a22d57b
以下は、natGw_nameが指定されていない場合に空文字を指定している。こちらもVSCode上ではエラーにならず、実行時エラー(idプロパティには空文字は指定できない)となる。
resource subnet 'Microsoft.Network/virtualNetworks/subnets@2023-11-01' = {
... 省略
properties: {
... 省略
natGateway: {
id: ((!empty(natGw_name) ? natGw.id : '')
}
}
}
sato@[9:16:32]:~/proj/RagSystem% azd up
... 省略
ERROR: error executing step command 'provision': deployment failed: error deploying infrastructure: deploying to subscription:
Deployment Error Details:
LinkedInvalidPropertyId: Property id '' at path 'properties.natGateway.id' is invalid. Expect fully qualified resource Id that start with '/subscriptions/{subscriptionId}' or '/providers/{resourceProviderNamespace}/'.
TraceID: a6d83fc8d022bf8736305a88cbc0b761
3-2-2. Github Copilotくんは人間味のある相棒としてかなり優秀
このエラー、ググってもなかなか目的の対応案に辿り着けない。そして、自分がやりたいことに対するサンプルにもなかなかお目にかかれない。
自分で解決できず、諦めてGithub Copilotくんに聞いてみた。
subnet.bicepの今の定義では、以下のエラーが発生する。
ーーー
Deployment Error Details: LinkedInvalidPropertyId: Property id '' at path 'properties.natGateway.id' is invalid. Expect fully qualified resource Id that start with '/subscriptions/{subscriptionId}' or '/providers/{resourceProviderNamespace}/'.
ーーー
これを解消するためにはどのように実装すれば良いか? natGw_nameがemptyの場合、natGateway.idには値を指定したくない。
この質問に対するGCくんの提案は以下のコードへの修正だが、bicep構文的にはプロパティ行にはifステートメントは使えないので、この提案はNG。
natGw_name
が空の場合、natGateway
プロパティ自体を省略するように条件付きでリソース定義を変更する必要があります。これにより、natGateway.id
に無効な値が設定されることを防ぎます。以下のように、
natGateway
プロパティを条件付きで追加するように変更します。修正後のコード
resource subnet 'Microsoft.Network/virtualNetworks/subnets@2023-11-01' = { .. 省略 // Conditionally add natGateway property natGateway: if (!empty(natGw_name)) { id: natGw.id } }
案の定VSCodeで文法エラーが検出された。
これを元にさらにGCくんに聞いてみる。
提案のコードでは以下のエラーが発生する。
The function "if" is not supported. Use the "?:" (ternary conditional) operator instead, e.g. condition ? ValueIfTrue : ValueIfFalsebicep(BCP100)
これを解消し、natGw_nameが空文字だった場合にnatGateway.idを指定しないようにするためにはどのように実装すれば良いか?
その答えがこちら。
三項演算子の最後のステートメントにブランケットで囲ったコードをまるっと指定するという方法は知らなかった。。
if
関数の代わりに三項演算子?:
を使用して条件付きでnatGateway
プロパティを設定する必要があります。以下のように修正します。修正後のコード
resource subnet 'Microsoft.Network/virtualNetworks/subnets@2023-11-01' = { .. 省略 natGateway: empty(natGw_name) ? null : { id: natGw.id } }
そして、このコードはうまく機能した。
今までもGithub Copilotを使ってコーディングしてきたが、結構間違った提案をしてくれるんだよねー。でも諦めずに深掘りして聞いていくと、結構いい提案をしてくれる。
間違った提案を試して、エラーが出たり、期待と異なる結果になることを経験することで、実はとても良い経験になっていたりする。
Github Copilotは、そのあたりが結構人間味のある相棒だ。
3-3. 改善案2:IPアドレスの外だし
せっかくipアドレスようにbicepを定義して外だししたのに、bastion.bicep内で直接IPアドレスのリソースを作成するような定義になっていた。
かなり特殊な定義で、他とは相容れないのであればそれでも許容できると思うが、他のIPアドレスと同じプロパティ定義で良いのであれば、同じ定義をいくつも定義するのではなく、定義はモジュールに局所化して再利用すべき。これはアプリ開発と同じセオリー。
ということで、bastion.bicepにてIPアドレスのリソース定義をモジュール呼び出しに変更。
3-4. 改善後の外観
なんとなくいい感じ。
いまひとつ違和感があるのは、bicepモジュール内で参照するために取得した生成済みリソースまで表現されていること。
例えばBastionSubnetは、MainVNetおよびNatGwとモジュール間の矢印線で依存関係があることがわかるのだが、BastionSubnet内にもvnetとnatGwが登場しsubnetとの参照関係が明示されている。
このモジュール感の矢印線と、モジュール内のリソース間の矢印線について、少し考察してみた。
3-4-1. モジュール間の矢印線=モジュール間の依存関係
Bicepテンプレートを確認したところ、これはdependsOnプロパティで依存関係を定義した場合に表示される矢印線だと思われる。
依存関係が必要となるケースは、例えば、MainVNetを作成するためにはRgが存在していなければ作成できないといった場合に定義する。
現時点で定義しているモジュールはRgがないと作成できないので、すべてのモジュールとRgとの依存関係は必須なのだが、この図を見るとすべてのモジュールは階層構造で定義されており、最終的にはRgに帰着するような構造になっている。このため、依存関係としては正しいと思う。
3-4-2. モジュール内リソース間の矢印線=関連付け
これは、リソースを定義する際に、リソースの機能を満たすために明示的に他のリソースを指定している場合に表示される矢印線だと思われる。
例えば、AdminSubnetでは、SubnetをMainVNet内に作成する必要があるため、subnetに対してMainVNetのリソースIDを設定している。また、subnet内からインターネットにアクセスするためのネットワーク設定としてnatGwと紐づける必要があるため、natGwのリソースIDを設定している。
このように、リソース間で直接参照関係を定義した場合、リソース間での矢印線として表現されるのだと思われる。
publicIpのリソースだけは、この矢印線が表現されていない。これが仕様なのかバグなのかは不明。
3-5. 意外にもVisualizerで不具合を発見
ここで、外観を眺めながら気がついたことがある。BastionNsgとBastionSubnetの関連はモジュール間であり、リソース間の関連が定義されていない。
Bicepを確認すると、確かにモジュール間の依存関係は定義されているものの、肝心なsubnetとnsgの参照関係が設定されていない。。。
Visualizerで不具合が発見できるとは。
でも、この図を見て、たまたま気がついただけで、毎回これを確認するのはしんどい。。
今後、bicepでIoCする時に自動で確認できる仕組みも少し検討してみる。
修正後のsubnet.bicepがこちら。
resource subnet 'Microsoft.Network/virtualNetworks/subnets@2023-11-01' = {
// ** Required
name: subnet_name
parent: vnet
properties: {
addressPrefix: subnet_addressPrefix
// Private endpoint network policies.
// 'Disabled' / 'Enabled' / 'NetworkSecurityGroupEnabled' / 'RouteTableEnabled'
privateEndpointNetworkPolicies : subnet_privateEndpointNetworkPolicies
// Enable outbound access to the internet by default.
// 'false' / 'true'
defaultOutboundAccess: subnet_defaultOutboundAccess
// Network security group
natGateway: empty(natGw_name) ? null : {
id: natGw.id
}
// Network security group
networkSecurityGroup: empty(nsg_name) ? null : {
id: nsg.id
}
}
}
また、修正後のBicep Visualizerの出力結果がこちら。
一応subnetとnsgが参照関係になったが、条件によって生成する・しないが分かれる箇所が表現できていないのが残念。
現状、条件によって期待した通りにVisualizeしたいのであれば、条件ごとにbicepを分けるしかなさそうだが、そこまでしてこのイメージにこだわるのではなく、これはあくまでも参考程度として、効率的なbicepを作成する方が良いだろう。