はじめに
AzureをTerraformで書いていて、各リソースに対してIPアドレスアクセス制限をする際に、
求められるIP形式にばらつきがありすぎてキレそうになった経験は無いでしょうか。
私はあります。
例えば、下記のようなIPアドレス帯を登録したいとしましょう。
10.0.0.0/22
192.168.0.5/32
この2つを登録するために、各リソースでは色々な形式でIPアドレスを求めてきます。
Storage Accountの場合
- アドレス範囲ではない(/32)の場合は/32記述してしまうとエラーになるため、削除が必要
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アドレス範囲で書かないといけない
- 1つのIP帯を指定するためにリソースを1つ書かないといけないため、冗長なコードになる
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を書いてもいい(統一せい)
- アドレス帯を追加する度にブロックを増やさないと行けない上に書いてある内容が冗長になる
resource "azurerm_linux_web_app" "test" {
(中略)
//こちらは/32を書いても良いものの、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を埋め込まないといけない
- 単一アドレスの場合とアドレス範囲で指定する場合で書き方が違う
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")
}
#こちらはプレフィックスで書けない上に単一アドレスとアドレス範囲の場合で書き方が違う。また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アドレスの管理なんてプロジェクトによっては日々変わったりするものなので、簡単に管理がしたいはずです。
今回例では2つのIPアドレス帯しか書いてませんが、2つだけでもこれだけのパターンで書き換えるのはなかなか骨が折れる作業ですよね。
ましてや、IPアドレスが十数個になったときにはおそらくパソコンを投げていると思います。
こんな悩みから、いい感じIPアドレス制限を設定する方法を模索してみました。
いい感じの書き方
IPアドレスは一つの変数にまとめる
あちこちに散らばったIPアドレスをメンテしたくないですよね。
IPアドレスは一つの変数に纏めましょう
locals {
ip_ranges = [
10.0.0.0/22,
192.168.0.5/32
]
}
すべてのリソースで、この変数の中身さえ書き換えればOKなようにしていきます。
Storage Accountの場合
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で要素1つ1つにtrimsuffix関数を使用する
- これにより、要素の最後に
/32
がついている場合のみ消すことができる
(参考)forの戻り値
> [for ip_range in ip_ranges : trimsuffix(ip_range, "/32")]
[
"10.0.0.0/22",
"192.168.0.5"
]
SQLServerの場合
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を使用して、冗長にリソースを書かないといけない問題を解消する
-
forを使用して宣言していた変数を
{index = value}
の形に変換したものを引数にする - indexを含めることにより、リソース名の重複を防止する
-
forを使用して宣言していた変数を
-
cidrhostを使用して、IPアドレス範囲の形に変換あうえう
- この関数は、第一引数をプレフィックス形式のIP、第二引数をn番目とすることでIP範囲の中でn番目のIPアドレスを返却してくれる
- そのため、
0
でネットワークアドレス、-1
にすることでブロードキャストアドレスを求めることができる
(参考)forの戻り値
> { for index, value in local.ip_ranges : index + 1 => value }
{
"1" = "10.0.0.0/22"
"2" = "192.168.0.5/32"
}
(参考)cidrhostの戻り値
> 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の場合
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で動的にブロックを複数作成するような記述にすることで冗長を解消する
- dynamicのfor_eachの引数は先程のSQLで使用したものと同様の内容
API Managementの場合
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
})
}
<policies>
<inbound>
<base />
<ip-filter action="allow">
${ip_ranges_xml}
</ip-filter>
</inbound>
</policies>
-
strings directiveをうまく活用し、文字列の中にfor文・if文を混ぜ込む
- これで
ip_ranges
の要素分
if strcontains(ip,"/32)
(IPアドレスに/32が含まれているか)が実行されて、
含まれていれば<address>
に格納、
含まれていなかったら先ほどのcidrhostを使用して<address-range>
に格納という具合です
- これで
- templatefileを使用することでファイル内に仕込んだ変数を置換します
string directiveはこんな感じになります
> <<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アドレスのフォーマット整えてくれないかな・・・。