#010: NSGをBicepで定義し、AzureBastionSubnetに紐づける

※本記事は、個人の意見および個人的活動を記したものであり、会社を代表するものではありません

NSG自体は単純な定義で作成できたのだが、いくつかのエラーに遭遇し調査しながら解決した。結構重要な知識を得ることができたので、自分の中では特に学びの多い活動となった。

# 1. NSGのBicep定義

## 1-1. BastionSubnet用NSGの設定例

今一度、参考情報サイトのリンクを掲載。

このサイトで紹介されている設定に従って、今度はBicepでNSGを定義する。

https://jpaztech.github.io/blog/network/bastion-nsg/

この情報、後からMS公式ドキュメントに掲載されていることが分かった。。この記事ではその事に触れられていないので後から追記されたのかな。

https://learn.microsoft.com/ja-jp/azure/bastion/bastion-nsg

## 1-2. NSGの複数ルールの定義

NSGでは複数のルールを定義する必要があるが、１つ１つのルールをBicepでベタに定義するのは非効率なので、パラメータでルール配列を指定し、それをループで処理するBicepを定義する。

https://learn.microsoft.com/ja-jp/azure/templates/microsoft.network/networksecuritygroups?pivots=deployment-language-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リポジトリを参照。

https://github.com/masatoshisato/RagSystem/tree/v1.0.5

# 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ルールを定義しているパラメータファイルの抜粋で、それぞれを配列としてルールを定義している。

::: note
１つの配列でも良いのかもしれないが、自分的には、InboundとOutboundを分けて定義することで少し読みやすくなると思ったので分けてみた。
:::

```bash
// 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関数で複数の配列を１つの配列に統合できる。

以下では、モジュール定義の直前でOutboundとInboundをまとめてsecurityRulesとして１つの配列にし、モジュールのパラメータとして指定している。

```bash
@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の抜粋。

パラメータとして指定された配列を指定するだけ。ちょー簡単だった。

```bash
// 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の定義にエラーがあったため発生）

```bash
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の２つ。

調べたら、どちらのSubnetも「dependsOn」プロパティ（依存関係の定義）にNAT Gatewayを指定していなかったため、Subnet作成時にNAT Gatewayの作成が完了していなかったのが原因。

dependsOnプロパティにより、リソースの依存関係を指定しており、今回の例ではNAT Gatewayの作成後にSubnetを作成するように定義し、Subnet内でNAT Gatewayとの紐付けを行う時には確実にNAT GatewayのIDが取得できるようにする必要があった。

```tsx
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」というプロパティは本来は存在しているはずが、存在しないと言われている。

これは明確に原因を特定したというよりも、おそらくこれだろうという推測の域をでない状態。その推測に達するまでにかなり苦労した。

```bash
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で依存関係も定義している。

```bash
// 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に指定して、モジュール呼び出し元で参照できるようにしている。

```bash
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プロパティを直接返すように変更してみた。

```bash
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で実行される時に、１つのリソースの生成処理において以下２つの処理はシーケンシャルではあるものの、非同期で行われている可能性がありそうだということ。

- IPアドレスのリソース自体が生成されたタイミングでオブジェクトとして参照可能になる
- リソース内のID発行処理は継続して実行され、完了したらIPアドレスのリソース内でIDが参照可能になる

もし、IPアドレスのリソース生成が完了したと判断されるタイミングで、まだIDが発行されておらずリソースオブジェクトから参照できない場合、今回発生したエラーになると考えると納得できる。さらに、outputとして必要なプロパティ（今回はID）を指定することで、そのプロパティが生成されてからリソースの生成処理が完了する。

### 2-5-5. 結論：モジュールのoutputにはリソースオブジェクトではなく、必要な個々のプロパティを指定するのが確実

ここまでの推測により、outputで指定したプロパティが参照可能になったタイミングでリソース生成完了と判断されると思われるが、リソース自体の生成完了のタイミングと、そのリソースが持っている各プロパティの生成完了のタイミングは異なるということ。

生成したリソースのプロパティを参照したいのであれば、outputに目的のプロパティを明示的に指定するのが確実だ。

ここまで定義してきたすべてのbicepファイルで定義しているリソースのoutputに必ず生成したリソースのidを含めることとした。

いやー、この調査はなかなか大きな学びだった。

## 2-6. [tips]他のリソース作成処理が進行中のため処理が継続できないというエラー

まず、エラーは以下の内容で出力される。要するに、他のプロセスが進行中なので継続できないというエラー。

### 2-6-1. 依存関係と参照関係は別物

このエラー、先ほどのid参照エラーと関係なさそうだが、関係大有りだった。

```bash
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で依存関係を指定している。

```bash

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で参照権限を定義するために既存リソースを複数回取得している場合にこのような表現となるようだ。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466853/b192b023-41fa-dac1-280b-496dd67882de.png)

この図を見て２点気がついた。

- 「AdminSubnet」と「BastionSubnet」の定義が複雑になっている（改善案１：Subnetテンプレートの見直し）
- 「natGwPublicIp」はモジュールで定義されているのに「bastionIp」はBastionモジュール内で定義されている。（改善案２：IPアドレスのモジュール利用）

俯瞰してみると、コードのままでは気が付くのが難しいことでも、容易に気がつくことができる。

上記２点について、改善案を検討してみる。

## 3-2. 改善案１：Subnetのテンプレートの見直し

subnet.bicepでは２つのsubnetリソースの生成テンプレートを定義している。

１つは、NAT Gatewayと紐づける場合のテンプレート、もう一つはNAT Gateway不要の場合のテンプレート。

NAT Gateway情報が指定された場合、ifステートメントによりsubnet_relatedToNatGwが実行され、それ以外はsubnetが実行されるようになっている。

```tsx
// 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の定義自体を２つに分けてifステートメントで切り分けるという方法にたどり着いた。

### 3-2-1. 「条件によってプロパティを指定するロジック」のエラーが解消できない

以前にチャレンジして断念した「１つのsubnet定義の中で、条件によってNAT Gatewayを指定する」という実装のほうがbicepテンプレートとしてはスッキリするので、もう一度この方法にチャレンジしてみる。

以下、その時のロジック。

これはBicep構文としては正しいのだが、実際に実行するとidプロパティにはnullもしくは空文字は指定できないという実行時エラーが発生する。

以下はnatGw_nameが指定されていない場合にnullを指定しており、これはVSCode上ではエラーが検出されないが、実行時エラー（idプロパティにnullは指定できない）になる。

```tsx
resource subnet 'Microsoft.Network/virtualNetworks/subnets@2023-11-01' = {
  ... 省略
  properties: {
    ... 省略
    natGateway: {
      id: ((!empty(natGw_name) ? natGw.id : null)
    }
  }
}
```

```bash
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プロパティには空文字は指定できない）となる。

```bash
resource subnet 'Microsoft.Network/virtualNetworks/subnets@2023-11-01' = {
  ... 省略
  properties: {
    ... 省略
    natGateway: {
      id: ((!empty(natGw_name) ? natGw.id : '')
    }
  }
}
```

```bash
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` プロパティを設定する必要があります。以下のように修正します。
> 
> 
> ### **修正後のコード**
> 
> ```bash
> resource subnet 'Microsoft.Network/virtualNetworks/subnets@2023-11-01' = {
>   .. 省略
>   natGateway: empty(natGw_name) ? null : {
>     id: natGw.id
>   }
> }
> ```
> 

そして、このコードはうまく機能した。

今までもGithub Copilotを使ってコーディングしてきたが、結構間違った提案をしてくれるんだよねー。でも諦めずに深掘りして聞いていくと、結構いい提案をしてくれる。

間違った提案を試して、エラーが出たり、期待と異なる結果になることを経験することで、実はとても良い経験になっていたりする。
    
Github Copilotは、そのあたりが結構人間味のある相棒だ。

## 3-3. 改善案２：IPアドレスの外だし

せっかくipアドレスようにbicepを定義して外だししたのに、bastion.bicep内で直接IPアドレスのリソースを作成するような定義になっていた。

かなり特殊な定義で、他とは相容れないのであればそれでも許容できると思うが、他のIPアドレスと同じプロパティ定義で良いのであれば、同じ定義をいくつも定義するのではなく、定義はモジュールに局所化して再利用すべき。これはアプリ開発と同じセオリー。

ということで、bastion.bicepにてIPアドレスのリソース定義をモジュール呼び出しに変更。

## 3-4. 改善後の外観

なんとなくいい感じ。

いまひとつ違和感があるのは、bicepモジュール内で参照するために取得した生成済みリソースまで表現されていること。

例えばBastionSubnetは、MainVNetおよびNatGwとモジュール間の矢印線で依存関係があることがわかるのだが、BastionSubnet内にもvnetとnatGwが登場しsubnetとの参照関係が明示されている。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466853/c8f74dcf-d1eb-9845-53f2-3129265f579c.png)

このモジュール感の矢印線と、モジュール内のリソース間の矢印線について、少し考察してみた。

### 3-4-1. モジュール間の矢印線＝モジュール間の依存関係

Bicepテンプレートを確認したところ、これはdependsOnプロパティで依存関係を定義した場合に表示される矢印線だと思われる。

依存関係が必要となるケースは、例えば、MainVNetを作成するためにはRgが存在していなければ作成できないといった場合に定義する。

現時点で定義しているモジュールはRgがないと作成できないので、すべてのモジュールとRgとの依存関係は必須なのだが、この図を見るとすべてのモジュールは階層構造で定義されており、最終的にはRgに帰着するような構造になっている。このため、依存関係としては正しいと思う。

### 3-4-2. モジュール内リソース間の矢印線＝関連付け

これは、リソースを定義する際に、リソースの機能を満たすために明示的に他のリソースを指定している場合に表示される矢印線だと思われる。

例えば、AdminSubnetでは、SubnetをMainVNet内に作成する必要があるため、subnetに対してMainVNetのリソースIDを設定している。また、subnet内からインターネットにアクセスするためのネットワーク設定としてnatGwと紐づける必要があるため、natGwのリソースIDを設定している。

このように、リソース間で直接参照関係を定義した場合、リソース間での矢印線として表現されるのだと思われる。

::: note
publicIpのリソースだけは、この矢印線が表現されていない。これが仕様なのかバグなのかは不明。
:::
    
## 3-5. 意外にもVisualizerで不具合を発見

ここで、外観を眺めながら気がついたことがある。BastionNsgとBastionSubnetの関連はモジュール間であり、リソース間の関連が定義されていない。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466853/d8500c5f-0edf-aa1d-d357-54f01d089f84.png)

Bicepを確認すると、確かにモジュール間の依存関係は定義されているものの、肝心なsubnetとnsgの参照関係が設定されていない。。。

Visualizerで不具合が発見できるとは。

でも、この図を見て、たまたま気がついただけで、毎回これを確認するのはしんどい。。

今後、bicepでIoCする時に自動で確認できる仕組みも少し検討してみる。

修正後のsubnet.bicepがこちら。

```tsx
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の出力結果がこちら。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466853/fc59aeb3-87c7-321a-f141-a3735afa74a3.png)

一応subnetとnsgが参照関係になったが、条件によって生成する・しないが分かれる箇所が表現できていないのが残念。

現状、条件によって期待した通りにVisualizeしたいのであれば、条件ごとにbicepを分けるしかなさそうだが、そこまでしてこのイメージにこだわるのではなく、これはあくまでも参考程度として、効率的なbicepを作成する方が良いだろう。