[Azure]TerraformでStorageAccount、AppService、SQL、API ManagementのXMLポリシーなどのIP制限をいい感じに書きたい

# はじめに
AzureをTerraformで書いていて、各リソースに対してIPアドレスアクセス制限をする際に、
求められるIP形式にばらつきがありすぎてキレそうになった経験は無いでしょうか。

私はあります。

例えば、下記のようなIPアドレス帯を登録したいとしましょう。
```
10.0.0.0/22
192.168.0.5/32
```

この２つを登録するために、各リソースでは色々な形式でIPアドレスを求めてきます。
### Storage Accountの場合
- アドレス範囲ではない(/32)の場合は/32記述してしまうとエラーになるため、削除が必要
```terraform
resource "azurerm_storage_account_network_rules" "ip_restriction" {
  storage_account_id = azurerm_storage_account.test.id
  default_action     = "Deny"
  ip_rules           = [
    "10.0.0.0/22",
    "192.168.0.5" //アドレス範囲ではない(/32)の場合は/32記述してしまうとエラーになる
  ]
}
```
### SQLServerの場合
- ネットワークアドレス、ブロードキャストアドレスを算出してIPアドレス範囲で書かないといけない
- １つのIP帯を指定するためにリソースを１つ書かないといけないため、冗長なコードになる
```terraform
resource "azurerm_mssql_firewall_rule" "ip_restriction_1" {
  name             = "ip-restriction-1"
  server_id        = azurerm_mssql_server.test.id
  #プレフィックスが使えずネットワークアドレス・ブロードキャストアドレスを算出して範囲で渡す必要がある
  start_ip_address = "10.0.0.0"
  end_ip_address   = "10.0.3.255"
}
resource "azurerm_mssql_firewall_rule" "ip_restriction_2" {
  name             = "ip-restriction-2"
  server_id        = azurerm_mssql_server.test.id
  start_ip_address = "192.168.0.5"
  end_ip_address   = "192.168.0.5"
}
```
### AppService/AzureFunctionsの場合
- /32を書いてもいい（統一せい）
- アドレス帯を追加する度にブロックを増やさないと行けない上に書いてある内容が冗長になる
```terraform
resource "azurerm_linux_web_app" "test" {
 (中略) 
  //こちらは/３２を書いても良いものの、1つのアドレス範囲を指定するために書かないといけない行が冗長
  site_config {
    "ip_restriction" {
        name                      = "ip_restriction_1"
        ip_address                = "10.0.0.0/22"
        priority                  = 1
        action                    = "Allow"
    }
    "ip_restriction" {
        name                      = "ip_restriction_2"
        ip_address                = "192.168.0.5/32"
        priority                  = 2
        action                    = "Allow"
    }
  }
}
```

### API Managementの場合
- 一番厄介。XMLで定義したポリシーファイルの中にIPを埋め込まないといけない
- 単一アドレスの場合とアドレス範囲で指定する場合で書き方が違う
```main.tf
resource "azurerm_api_management_api_policy" "ip_restriction" {
  api_name            = azurerm_api_management_api.test.name
  api_management_name = azurerm_api_management_api.test.api_management_name
  resource_group_name = azurerm_api_management_api.test.resource_group_name
  xml_content = file("${path.module}/apim_policy.xml")
}
```
```apim_policy.xml
#こちらはプレフィックスで書けない上に単一アドレスとアドレス範囲の場合で書き方が違う。またXMLファイルの一部になる
<policies>
    <inbound>
        <base />
        <ip-filter action="allow">
            <address-range from="10.0.0.0" to="10.0.3.255" />
            <address>192.168.0.5</address>
        </ip-filter>
    </inbound>
</policies>

```

・・・厄介すぎんか？
IPアドレスの管理なんてプロジェクトによっては日々変わったりするものなので、簡単に管理がしたいはずです。
今回例では２つのIPアドレス帯しか書いてませんが、２つだけでもこれだけのパターンで書き換えるのはなかなか骨が折れる作業ですよね。
ましてや、IPアドレスが十数個になったときにはおそらくパソコンを投げていると思います。


こんな悩みから、いい感じIPアドレス制限を設定する方法を模索してみました。

# いい感じの書き方
### IPアドレスは一つの変数にまとめる
あちこちに散らばったIPアドレスをメンテしたくないですよね。
IPアドレスは一つの変数に纏めましょう
```terraform
locals {
  ip_ranges = [
    10.0.0.0/22,
    192.168.0.5/32
  ]
}
```
すべてのリソースで、この変数の中身さえ書き換えればOKなようにしていきます。
### Storage Accountの場合
```terraform
resource "azurerm_storage_account_network_rules" "ip_restriction" {
  storage_account_id = azurerm_storage_account.test.id
  default_action     = "Deny"
  ip_rules           = [for ip_range in ip_ranges : trimsuffix(ip_range, "/32")]
}
```
- [for](https://developer.hashicorp.com/terraform/language/expressions/for)で要素1つ1つに[trimsuffix](https://developer.hashicorp.com/terraform/language/functions/trimsuffix)関数を使用する
- これにより、要素の最後に`/32`がついている場合のみ消すことができる
#### (参考）forの戻り値
```terraform
> [for ip_range in ip_ranges : trimsuffix(ip_range, "/32")]
[
    "10.0.0.0/22",
    "192.168.0.5"
]
```
### SQLServerの場合
```terraform
resource "azurerm_mssql_firewall_rule" "ip_restriction" {
  for_each         = { for index, value in local.ip_ranges : index + 1 => value }
  name             = "ip-restriction-${each.key}"
  server_id        = azurerm_mssql_server.test.id
  start_ip_address = cidrhost(each.value, 0)
  end_ip_address   = cidrhost(each.value, -1)
}
```
- [for_each](https://developer.hashicorp.com/terraform/language/meta-arguments/for_each)を使用して、冗長にリソースを書かないといけない問題を解消する
    - [for](https://developer.hashicorp.com/terraform/language/expressions/for)を使用して宣言していた変数を`{index = value}`の形に変換したものを引数にする
    - indexを含めることにより、リソース名の重複を防止する
- [cidrhost](https://developer.hashicorp.com/terraform/language/functions/cidrhost)を使用して、IPアドレス範囲の形に変換あうえう
    - この関数は、第一引数をプレフィックス形式のIP、第二引数をn番目とすることでIP範囲の中でn番目のIPアドレスを返却してくれる
    - そのため、`0`でネットワークアドレス、`-1`にすることでブロードキャストアドレスを求めることができる
#### (参考)forの戻り値
```terraform
> { for index, value in local.ip_ranges : index + 1 => value }
{ 
  "1" = "10.0.0.0/22"
  "2" = "192.168.0.5/32"
}
```
#### (参考)cidrhostの戻り値
```terraform
> cidrhost("10.0.0.0/22", 0)
"10.0.0.0"
> cidrhost("10.0.0.0/22", 1)
"10.0.0.1"
> cidrhost("10.0.0.0/22", -1)
"10.0.3.255"
> cidrhost("192.168.0.5/32", 0)
"192.168.0.5"
> cidrhost("192.168.0.5/32", -1)
"192.168.0.5"
```
### AppService/AzureFunctionsの場合
```terraform
resource "azurerm_linux_web_app" "test" {
 (中略) 
  site_config {
   dynamic "ip_restriction" {
      for_each =  { for index, value in local.ip_ranges : index + 1 => value }
      content {
        name                      = "ip_restriction_${ip_restriction.key}"
        ip_address                = ip_restriction.value
        priority                  = ip_restriction.key
        action                    = "Allow"
      }
    }
}
```
- [dynamic](https://developer.hashicorp.com/terraform/language/expressions/dynamic-blocks)で動的にブロックを複数作成するような記述にすることで冗長を解消する
- dynamicのfor_eachの引数は先程のSQLで使用したものと同様の内容
### API Managementの場合
```main.tf
resource "azurerm_api_management_api_policy" "ip_restriction" {
  api_name            = azurerm_api_management_api.test.name
  api_management_name = azurerm_api_management_api.test.api_management_name
  resource_group_name = azurerm_api_management_api.test.resource_group_name
  xml_content = templatefile("${path.module}/apim_policy.xml", {
    ip_ranges_xml = <<EOT
    %{for ip in local.ip_ranges~}
    %{if strcontains(ip, "/32")}<address>${trimsuffix(ip, "/32")}</address>%{else}<address-range from="${cidrhost(ip, 0)}" to="${cidrhost(ip, -1)}" />%{endif}
    %{endfor~}
    EOT
  })
}
```
```apim_policy.xml
<policies>
    <inbound>
        <base />
        <ip-filter action="allow">
${ip_ranges_xml}
        </ip-filter>
    </inbound>
</policies>

```
- [strings directive](https://developer.hashicorp.com/terraform/language/expressions/strings#directives)をうまく活用し、文字列の中にfor文・if文を混ぜ込む
    - これで`ip_ranges`の要素分
    `if　strcontains(ip,"/32)`(IPアドレスに/32が含まれているか）が実行されて、
    含まれていれば`<address>`に格納、
    含まれていなかったら先ほどのcidrhostを使用して`<address-range>`に格納という具合です
- [templatefile](https://developer.hashicorp.com/terraform/language/functions/templatefile)を使用することでファイル内に仕込んだ変数を置換します
#### string directiveはこんな感じになります
```bash
>　　　　　　<<EOT
    %{for ip in local.ip_ranges~}
    %{if strcontains(ip, "/32")}<address>${trimsuffix(ip, "/32")}</address>%{else}<address-range from="${cidrhost(ip, 0)}" to="${cidrhost(ip, -1)}" />%{endif}
    %{endfor~}
    EOT
    
<address-range from="10.0.0.0" to="10.0.3.255" />
<address>192.168.0.5</address>
```

# 最後に
ざっくりと書きましたが、こんな感じでIPアドレスの管理がグッと楽になりました。
必要に応じて、localではなくvariableを使用して柔軟にIPアドレスを変更できるようにしても良いかもしれませんね。
また、indexが何度か登場しましたが、必要に応じて　`format("%03d", index + 1)`などと書き方を変えてあげると、3桁でゼロパディングもできたりします。宗教上の理由でプライオリティやリソース名の桁数を固定したい場合などは、この関数を併用しても良いでしょう。

Microsoftさん、IPアドレスのフォーマット整えてくれないかな・・・。