【Azure】閉域環境内でAzure DNS Private Resolverを用いたオンプレミスからのAzure PaaSの名前解決をTerraformで実装【Terraform】

:::note warn
本記事で記載している月額料金は2025年9月26日時点のものです。
:::

私の記事を参照して閉域環境内でAzureのPaaSリソースに対してプライベートIPアドレスを返す、という記事が書かれていました。
参照してもらった私の記事は[Azure FirewallのDNSプロキシ機能を試してみた(前編 ～Azure Private EndpointとAzure DNS Private Zone～)](https://qiita.com/hidekko/items/c8251d4605c48bab429b)という記事です。
大変ありがたいですね。
Azure Firewallが既にある環境であれば良いのですが、わざわざ名前解決のためだけにAzure Firewallを構築するのは勿体ないですね。
**Azure FirewallにもSKU(レベル)がありますが、最も安価なBasicを選択して東日本リージョンに構築したとしても<font color="red">月額41,670円</font>かかります。**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643392/d139e440-c1c5-7bc8-2ab1-a4f8fca72b77.png)
Azure Firewallは暗黙的に冗長化されていますのでDNSサーバとして利用するにあたり文句は無いのです。
ただ価格がネックですね。
ではAzure Firewallのように**冗長化したDNSサーバをLinuxで構築した場合どうなるか？と考えて見積もってみました。**
東日本リージョン、SKUは現時点で最新のDv5の最小サイズD2v5(2コア、8GiB)、OSライセンス料金のかからないLinux(Ubuntu)を選択、OSディスクはStandard SSDでAzure VMのLinux OSのデフォルトサイズ32GiB、DNSレコードの領域としてOSディスクとは別に追加ディスクをStandard SSDで512GiBをそれぞれ追加する構成にすると、合計で<font color="red">**月額37,984円**</font>です。(画像は思いっきり小さくしましたが合計金額見切れてます。)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643392/f8d5f656-7ced-20b7-9fdc-6550e41307b3.png)
月額料金が少し安くなりましたね。
しかしVMで構築するとなるとOS以上のレイヤーでのメンテナンス、今回だとUbuntuなので```apt update```や```apt upgrade```、```do-release-upgrade```など管理面での煩雑さが発生してしまいます。
他にもDNSレコードの量にもよりますが、追加ディスクの拡張や追加、ディスク拡張や追加によるLVMの変更なども考えられます。
面倒ですね。
こうなるとやはりIaaSではなくPaaSを使いたいなとなってきますので、今回も前振りが長かったですが本題のAzure DNS Private Zone、Azure DNS Private Resolverの登場です。
Azure DNS Private Resolverだと料金が<font color="red">**月額26,012円**</font>なので、最も安価です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643392/d48f1057-0280-0cd9-ff37-ee6687bf6d98.png)
Azure DNS単体だとめちゃくちゃ安価ですが、お金はかかるので一応掲載しておくと<font color="red">**月額130円**</font>です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643392/afb80f87-db51-7838-ec10-e5b85f97b33c.png)
**Azure DNS Private ZoneとAzure DNS Private Resolverの合算で<font color="red">月額26,142円</font>ですね。**

# 構成図
はい。
構成図です。
毎回四の五の言わずにすぐに構成図載せて説明したいのですが、どうしても背景というか流れの分かる前段部分を記載しないと気が済まないんですよね。
生来おしゃべりなんで仕方ないですね。

本記事のタイトルでは**閉域環境内**と記載していますが、検証なのでインターネットVPN環境で構築しています。
インターネットVPN環境で構築しているので構成図は以下のようになります。

名前解決の通信が以下の図のようになります。
<font color="red">赤い線</font>が名前解決の通信になります。
自宅PC01が自宅内にあるDomain Controller兼DNSサーバを向いており、このDNSサーバ内で条件付きフォワーダーを設定してAzure DNS Private Resolver(Azure DNS Private Zoneも含めて記載)に特定のゾーンの名前解決の通信を転送します。
![構成図_名前解決_02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643392/b8ab8533-eb60-99cf-b9dc-86a1255f780a.png)


名前解決後の実際のデータ通信が以下の図のようになります。
<font color="red">赤い線</font>が実通信、データ通信になります。
(名前解決と同じ赤線だとわかりづらいですかね・・・
 構成図書くたびに迷っているのでアドバイスあればお手数ですがコメントください。)
![構成図_実通信_02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643392/c24a9c56-7bc5-fc94-47fe-90bc22f6fad1.png)


**名前解決も実際のデータもどちらの通信もインターネットVPNを経由するので、<font color="red">閉域環境での通信と同じで通信パケットはインターネットには出ません。</font>**

# Terraform
はい。
では構成図の紹介と説明も済みましたのでコードのご紹介です。

## providers.tf
```providers.tf
#----------------------------------------
# Define Provider
#----------------------------------------

terraform {
  required_providers {
        azapi = {
      source  = "azure/azapi"
      version = "~>1.5"
    }
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~>3.0"
    }
    random = {
      source  = "hashicorp/random"
      version = "~>3.0"
    }
  }
}

provider "azapi" {
}

provider "azurerm" {
  features {}
  subscription_id   = "${var.ARM_SUBSCRIPTION_ID}"
  tenant_id         = "${var.ARM_TENANT_ID}"
  client_id         = "${var.ARM_CLIENT_ID}"
  client_secret     = "${var.ARM_CLIENT_SECRET}"
}
```

はい。
```providers.tf```です。
何の変哲もないTerraformでAzure構築する際の```providers.tf```ですね。

## varriables.tf(変数設定)
```varriables.tf
# --------------------------------------
# Define Varriables
# --------------------------------------

# Import Subscription ID variable from OS's Enviroment variable
variable "ARM_SUBSCRIPTION_ID" {
  type        = string
#  default     = "${env.ARM_SUBSCRIPTION_ID}"
}

# Import Tenant ID variable from OS's Enviroment variable
variable "ARM_TENANT_ID" {
  type        = string
#  default     = "${env.ARM_TENANT_ID}"
}

# Import Client ID variable from OS's Enviroment variable
variable "ARM_CLIENT_ID" {
  type        = string
#  default     = "${env.ARM_CLIENT_ID}"
}

# Import Client Secret variable from OS's Enviroment variable
variable "ARM_CLIENT_SECRET" {
  type        = string
#  default     = "${env.ARM_CLIENT_SECRET}"
}

# Generate random text for a unique storage account name
resource "random_id" "random_id" {
  keepers = {
    # Generate a new ID only when a new resource group is defined
    resource_group = azurerm_resource_group.rg.name
  }
  byte_length = 8
}

# system name
variable "system_name" {
  type        = string
  default     = "azuredns-pr01"
  description = "pr=privateresolver"
}

# region
variable "region" {
  type        = string
  default     = "southeastasia"
}

# resource group (rg) name prefix
variable "rg_name_pre" {
  type        = string
  default     = "rg-"
}

# virtual network (vnet) name prefix
variable "vnet_name_pre" {
  type        = string
  default     = "vnet-"
}

# vnet ip address prefix01
variable "vnet_addr_pre01" {
  type        = string
  default     = "192.168."
}

# vnet ip address suffix
variable "vnet_addr_suf01" {
  type        = string
  default     = "200.0/23"
}

# subnet (sn) name prefix
variable "sn_name_pre" {
  type        = string
  default     = "sn-"
}

# sn for client name suffix for Private Resolver (pr)
variable "sn_name_suf02" {
  type        = string
  default     = "pr01"
}

# sn for client name suffix for Private Endpoint (pe)
variable "sn_name_suf03" {
  type        = string
  default     = "pe01"
}

# sn for clinet ip address suffix for Virtual Network Gateway
variable "sn_addr_suf01" {
  type        = string
  default     = "200.0/25"
}

# sn for clinet ip address suffix for Private Resolver
variable "sn_addr_suf02" {
  type        = string
  default     = "200.128/25"
}

# sn for clinet ip address suffix for Private Endpoint
variable "sn_addr_suf03" {
  type        = string
  default     = "201.0/24"
}

# Local Gateway name prefix
variable "Lg_name_pre" {
  type        = string
  default     = "lg-"
}

# Local Gateway name suffix
variable "Lg_name_suf" {
  type        = string
  default     = "RTX1300"
}

# Local Gateway DDNS FQDN
variable "Lg_DDNS" {
  type        = string
  default     = "RTX1300のDDNSで指定しているFQDN"
}

# Local Gateway Local Subnet01 (General Subnet)
variable "Lg_local_sn01" {
  type        = string
  default     = "192.168.11.0/24"
}

# Local Gateway Local Subnet02 (Server Subnet)
variable "Lg_local_sn02" {
  type        = string
  default     = "172.16.0.0/24"
}

# VPN connection name prefix
variable "connection_name_pre" {
  type        = string
  default     = "con-"
}

# Private Endpoint (pe) name prefix
variable "pe_name_pre" {
  type        = string
  default     = "pe-"
}

# Azure DNS Private Zone Name
variable "dns_zone_name" {
  type        = string
  default     = "file.core.windows.net"
  description = "Name of the DNS zone. This value is dns's suffix"
}

# Azure DNS Private Resolver name prefix
variable "azuredns_pr_name_pre01" {
  type        = string
  default     = "pr-"
}

# Azure DNS Private Resolver name suffix
variable "azuredns_pr_name_suf01" {
  type        = string
  default     = "azuredns01"
}

# Azure DNS Private Resolver inbound endpoint name prefix
variable "azuredns_pr_in_name_pre01" {
  type        = string
  default     = "in-"
}
```

```varriables.tf```の中で特筆すべき点は```variable "dns_zone_name"```のブロックです。
**Azure DNS Private Zoneは、<font color="red">Azure DNS Private Zoneのリソース名がDNSで定義したいゾーン名(apexレコード)になります。</font>**
ですのでこの```variable "dns_zone_name"```ではオンプレミスから名前解決させたいゾーン名を指定しなければなりません。
**<font color="red">これはAzure DNS Private Zoneの仕様です。</font>**

## resource_group.tf
```resource_group.tf
# --------------------------------------
# Create Resource Group
# --------------------------------------

resource "azurerm_resource_group" "rg" {
  location = var.region
  name     = "${var.rg_name_pre}${var.system_name}"
}
```
こちらも何の変哲もないリソースグループ作成用の.tfファイルです。

## vnet_subnet.tf
```vnet_subnet.tf
#----------------------------------------
# Create vnet and Subnet
#----------------------------------------

resource "azurerm_virtual_network" "vnet" {
  resource_group_name = azurerm_resource_group.rg.name
  location = azurerm_resource_group.rg.location
  name     = "${var.vnet_name_pre}${var.system_name}"
  address_space = [ "${var.vnet_addr_pre01}${var.vnet_addr_suf01}" ]
}

# sn-pr01 Create (Prvate Resolver Subnet)
resource "azurerm_subnet" "sn-pr01" {
  resource_group_name = azurerm_resource_group.rg.name
  name = "${var.sn_name_pre}${var.sn_name_suf02}"
  virtual_network_name = azurerm_virtual_network.vnet.name
  address_prefixes = [ "${var.vnet_addr_pre01}${var.sn_addr_suf02}" ]
  delegation {
    name = "azuredns"

    service_delegation {
      name = "Microsoft.Network/dnsResolvers"
      actions = [
        "Microsoft.Network/virtualNetworks/subnets/join/action"
      ]
    }
  }
}

# sn-pe01 Create (Private Endpoint Subnet)
resource "azurerm_subnet" "sn-pe01" {
  resource_group_name = azurerm_resource_group.rg.name
  name = "${var.sn_name_pre}${var.sn_name_suf03}"
  virtual_network_name = azurerm_virtual_network.vnet.name
  address_prefixes = [ "${var.vnet_addr_pre01}${var.sn_addr_suf03}" ]
}
```

ここで特筆すべきは13行目にある```resource "azurerm_subnet" "sn-pr01"```のブロックです。
**Azure DNS Private Resolverは仕様としてPrivate ResolverのプライベートIPアドレスを保持するサブネットに対して、<font color="red">サブネット内にNICを構築する権限(/join/action)を委任(delegation)を行わなければなりません。</font>**
ですのでこの委任を13行目にある```resource "azurerm_subnet" "sn-pr01"```のブロック内の```service_delegation```という部分で行っています。
<font color="red">この委任を行うことにより委任されたサブネットに対するAzure DNS Private Resolverによる仮想NICの作成が許可され、仮想NICが作成されるとDHCP(今回はDHCPで若番からプライベートIPアドレスを払い出しましたが固定IPアドレス設定ももちろん可能です)でプライベートIPアドレスが払い出されますので、結果Azure DNS Private ResolverがプライベートIPアドレスを持つことになります。</font>

深いですねー

## storage.tf
```storage.tf
# --------------------------------------
# Create Storage Account
# --------------------------------------

# Create storage account for azure files
resource "azurerm_storage_account" "storage_account" {
  resource_group_name      = azurerm_resource_group.rg.name
  location                 = azurerm_resource_group.rg.location
  name                     = "st${random_id.random_id.hex}"
  account_tier             = "Standard"
  account_replication_type = "LRS"
}

resource "azurerm_storage_account_network_rules" "for_storage_account_multiple_subnet_id" {
  storage_account_id = azurerm_storage_account.storage_account.id
  default_action = "Deny"
  ip_rules = ["構築元のグローバルIPアドレス"]
}

resource "azurerm_storage_share" "azure_files" {
  name                 = "azurefileshare01"
  storage_account_name = azurerm_storage_account.storage_account.name
  quota                = 5120 #GiB
}
```
いつもVMのdiagnosticsの出力先としての利用用途しかないのですが、今回はAzure FilesにアクセスさせたいのでAzure Filesの構築まで行っています。
今回の構成ではVMもないので、名称もdiagというプレフィックスは追記していないです。

## private_endpoint.tf
```private_endpoint.tf
#----------------------------------------
# Create Private Endpoint
#----------------------------------------

# Create private endpoint for Storage Account
resource "azurerm_private_endpoint" "pe01" {
#  name                = azurerm_storage_account.storage_account.name
  name                = "${var.pe_name_pre}st${random_id.random_id.hex}"
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name
  subnet_id           = azurerm_subnet.sn-pe01.id

  private_service_connection {
    name                           = "private-serviceconnection"
    private_connection_resource_id = azurerm_storage_account.storage_account.id
    subresource_names              = ["file"]
    is_manual_connection           = false
  }
    private_dns_zone_group {
    name                 = "dns-zone-group"
    private_dns_zone_ids = [azurerm_private_dns_zone.privatelink_database_windows_net.id]
  }
}
```
プライベートエンドポイントですね。
ここで特筆すべきは```private_service_connection```のブロック内にある```subresource_names = ["file"]```です。
これで構築したAzure Storage Account内に存在するAzure Filesにプライベートエンドポイントを割り当てることが可能です。

## azure_dns_pz.tf
```azure_dns_pz.tf
#----------------------------------------
# Create Azure DNS Private Zone (AzureDNSpz)
#----------------------------------------

# Create private DNS zone
resource "azurerm_private_dns_zone" "privatelink_database_windows_net" {
  name                = "${var.dns_zone_name}"
  resource_group_name = azurerm_resource_group.rg.name
}

# Create private DNS record
resource "azurerm_private_dns_a_record" "azurefiles_dns_record" {
  name                = "st${random_id.random_id.hex}"
  zone_name           = azurerm_private_dns_zone.privatelink_database_windows_net.name
  resource_group_name = azurerm_resource_group.rg.name
  ttl                 = 300
  records             = ["192.168.201.4"]
}

# Create virtual network link
resource "azurerm_private_dns_zone_virtual_network_link" "azuredns_pz_vnet_link" {
  name                  = "vnet-link"
  resource_group_name   = azurerm_resource_group.rg.name
  private_dns_zone_name = azurerm_private_dns_zone.privatelink_database_windows_net.name
  virtual_network_id    = azurerm_virtual_network.vnet.id
}
```
はい。
Azure DNS Private Zoneですね。
特筆すべき一点目は```varriables.tf```で記載済みです。
このコードだと7行目の```name = "{var.dns_zone_name}"```ですね。
もう一か所特筆すべき点があり、レコード作成部分、```resource "azurerm_private_dns_a_record" "azurefiles_dns_record"```のブロック内にある```records = ["192.168.201.4"]```です。
**プライベートエンドポイントは仕様上DHCPのみです。
固定IPアドレスを予め指定することはできません。
ですので<font color="red">プライベートエンドポイントのサブネットで払い出される最も若い番号を手動で指定してレコードを作成しています。</font>**

## azure_dns_pr.tf
```azure_dns_pr.tf
#----------------------------------------
# Create Azure DNS Private Resolver (AzureDNSpr)
#----------------------------------------

resource "azurerm_private_dns_resolver" "pr-azuredns01" {
  name                = "${var.azuredns_pr_name_pre01}${var.azuredns_pr_name_suf01}"
  resource_group_name = azurerm_resource_group.rg.name
  location = azurerm_resource_group.rg.location
  virtual_network_id  = azurerm_virtual_network.vnet.id
}

resource "azurerm_private_dns_resolver_inbound_endpoint" "azuredns-pr-in01" {
  name                    = "${var.azuredns_pr_in_name_pre01}${var.azuredns_pr_name_pre01}${var.azuredns_pr_name_suf01}"
  private_dns_resolver_id = azurerm_private_dns_resolver.pr-azuredns01.id
  location                = azurerm_private_dns_resolver.pr-azuredns01.location
  ip_configurations {
    subnet_id           = azurerm_subnet.sn-pr01.id
    private_ip_allocation_method = "Dynamic"
  }
}
```
はい。
やっとお出まし。
Azure DNS Private Resolverです。
でもなんてことはありません。
**特筆すべき点はこのAzure DNS Private Resolver自身ではなく、サブネットやDNS Zoneで説明済みです。
このリソースがきちんと動作するようにこれまでのコードで準備済みです。**

## vnetgateway.tf
```vnetgateway.tf
# ------------------------------------------
# Create Virtual Network Gateway Subnet
# ------------------------------------------

resource "azurerm_subnet" "GatewaySubnet" {
  resource_group_name = azurerm_resource_group.rg.name
  name = "GatewaySubnet"
  virtual_network_name = azurerm_virtual_network.vnet.name
  address_prefixes = [ "${var.vnet_addr_pre01}${var.sn_addr_suf01}" ]
}

# ------------------------------------------
# Create Virtual Network Gateway
# ------------------------------------------

resource "azurerm_public_ip" "pip-vgw" {
  name                = "pip-vgw01"
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name

  allocation_method = "Dynamic"
}

resource "azurerm_virtual_network_gateway" "vgw01" {
  name                = "vgw01"
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name

  type     = "Vpn"
  vpn_type = "RouteBased"

  active_active = false
  enable_bgp    = false
  sku           = "Basic"

  ip_configuration {
    name                          = "vnetGatewayConfig"
    public_ip_address_id          = azurerm_public_ip.pip-vgw.id
    private_ip_address_allocation = "Dynamic"
    subnet_id                     = azurerm_subnet.GatewaySubnet.id
  }
}
```
VPNを張るのに必要なAzure仮想ネットワークゲートウェイですね。
ここは本題とは無関係なので流しちゃいましょう。
以降に続くLocal GatewayやVPN Connectionも特筆すべき点はありません。

## localgateway.tf
```localgateway.tf
# --------------------------------------
# Create Local Network Gateway
# --------------------------------------

resource "azurerm_local_network_gateway" "lgw" {
  name                = "${var.Lg_name_pre}${var.Lg_name_suf}"
  resource_group_name = azurerm_resource_group.rg.name
  location            = azurerm_resource_group.rg.location
  gateway_fqdn     = "${var.Lg_DDNS}"
  address_space       = ["${var.Lg_local_sn01}","${var.Lg_local_sn02}"]
}
```

## vpn_connection.tf
```vpn_connection.tf
# --------------------------------------
# Create VPN Connection
# --------------------------------------

resource "azurerm_virtual_network_gateway_connection" "vpn-connection" {
    name                       = "${var.connection_name_pre}${var.vnet_name_pre}${var.system_name}-${var.Lg_name_pre}${var.Lg_name_suf}"
    location                   = azurerm_resource_group.rg.location
    resource_group_name        = azurerm_resource_group.rg.name
    type                       = "IPsec"
    virtual_network_gateway_id = azurerm_virtual_network_gateway.vgw01.id
    local_network_gateway_id   = azurerm_local_network_gateway.lgw.id
    shared_key                 = "いつもの超強力なPSK"
}
```

# 動作確認
はい。
やっと動作確認ですね。
VPN張ったりAzureの各種リソースが設計通り、コード通りに動作しているか確認しますが、今回はブラックボックステスト風にいきなりオンプレミスのDNSサーバ(実際はDomain Controller)にゾーン転送設定を行い、通信できるか確認します。

## オンプレミスDNSサーバにゾーン転送設定追加
厳密には動作確認ではなく事前設定扱いですが、Terraformのコードに含まれない部分であるということと、Azure PortalでAzure DNS Private ResolverのIPアドレスを未確認ですので、便宜上動作確認としています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643392/8afdfadc-49e6-5983-50c3-2af4df92b99f.png)
はい。
何の変哲もない条件付きフォワーダーの設定です。

## 名前解決の通信
![名前解決の結果.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643392/d4cafb55-c206-03f9-b043-f93b52a7a4a6.png)
<font color="red">**キタ！**</font>

![名前解決の結果_集中線.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643392/6ff8226f-526c-f96e-2c6d-c817e21deae2.png)
<font color="red">**キタ！キタ！**</font>
デスクトップの背景が青でDoS窓の背景が黒なので黄色で集中線付けました。
めっちゃ変な感じですね。
ですがまぁ設計通りの動作です。

## 実通信の確認
Azure Filesなのでネットワークドライブとして設定することが可能です。
プライベートIPアドレスで指定できるかやってみましょう。
![ネットワークドライブマウント.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643392/afe06108-4136-86ae-7c8d-e645bb31088c.png)
<font color="red">**キター！**</font>

![ネットワークドライブマウント_集中線.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643392/1cd65e95-1238-23db-374b-47885ee02597.png)
<font color="red">**完璧ですね！**</font>

はい。
<font color="red">**Azure DNS Private Resolverを使えばDNSサーバが暗黙的に冗長化されており、かつ閉域網内でも無事名前解決できる！**</font>

というこがわかりました！

本日はここまで！