このページの本文へ

Azure PrivateLinkをTerraformで構築したら理解が深まった話

2020年06月17日 11時00分更新

文● mochizuki.toshihiro@fixer.co.jp

  • この記事をはてなブックマークに追加
  • 本文印刷

 本記事はFIXERが提供する「cloud.config Tech Blog」に掲載された「PrivateLinkをTerraformで構築したら理解が深まった話 #Azureリレー」を再編集したものです。

 名古屋事業所の望月です。

 今回は最近アップデートが活発なAzure PrivateLinkについてナレッジを共有します。

本エントリーの概要/構造

 Azure Private LinkをTerraformスクリプトを用いて構築することにより、構成するコンポーネントや接続の仕組みについて理解を深めます

・Azure PrivateLink+SQL Server構成をTerraformを用いて構築する方法を紹介します
・Terraformスクリプトの実行、および動作確認用VMからSQLServerへの接続確認手順を記載します
・Terraformスクリプトの内容についてAzure Private Linkを構成する箇所を中心に解説します

学習成果

・Azure Private LinkをTerraformスクリプトを用いて構築できるようになります
・Terraformスクリプトで作成するAzureリソースの役割を理解できます

対象者

・Azure Private LinkをTerraformスクリプトで構築したい人
・AzurePortalで構築した際に自動作成されるAzureリソースについて理解したい人

前提条件

・Azureリソースを作成できる権限を持ったAzure サブスクリプションが準備できている
・Terraform 実行およびAzureVirtualMachineへSSH 接続できる環境が準備できている
・普段からWSLを使っている方はWSLを。それ以外の方はAzurePortalで利用できるCloudShellをオススメします

はじめに

 AzurePrivateLinkとは、SQLサーバー等のAzureサービスについて、インターネット向けの接続ポイントだけでなく、PrivateEndpointという仕組みを用いてVNETにも接続ポイントを作成することで、インターネットを介さずAzureサービスに接続でき、よりセキュアな通信を実現できるサービスです。

 そんなPrivateLinkですが、2019年9月17日にプレビューリリースされ翌年の2020年2月18日に一般提供が開始されました。一般提供の開始時にはまだ一部のAzureサービスがプレビュー扱いでしたが、3月17日にはAzureStorageをはじめとする各種Azureサービスに対しても一般提供が開始されました。

 今回は3月17日に一般提供されたサービスのうちSQLサーバーを対象に、PrivateLinkを用いてVNETに接続する環境を構築します。

 AzurePortalで構築する場合はウィザードにしがって進めれば構築できるのですが、Terraformを用いて構築する場合は注意点がありました。

 本エントリーでは完成版のTerraformスクリプトを公開すると共に、スクリプトの適用と動作確認までの手順を紹介し、Terraformスクリプトを作成する上でのナレッジを共有します。

 エントリー後半のスクリプト説明まで読んでいただくとインターネットを経由しないVNET内の通信経路について理解いただける内容を目指しています。

目次

1. はじめに
2. 構成
3. 構築手順
 a. 準備
 b. 構築
 c. 接続確認
 d. リソースの確認

4. スクリプト説明
 a. プライベートDNS作る&VNETと紐づける
 b. DNSレコード作る

5. まとめ
6. スクリプト全体

構成

PrivateLink構成図

構築手順

準備

1. 文末あるスクリプト全体をコピーし「main.tf」という名前で保存します。
2. azcliコマンドでAzureにログインします


$ az login

構築

(1) Terraform実行

 Terraformを実行します

 main.tfを保存したディレクトリで次のコマンドを実行します。


$ terraform init
$ terraform paln
$ terrafrom apply

(2) アウトプット確認

 以下の通り Terraformからアウトプットが出ることを確認します。

 ここで出力された内容は後のデータベース接続に用います。

 アウトプット例は次の通りです。

 データベース接続先やユーザ名、パスワード等が出力されます。


Outputs:
SQL_1_Server = al3gyr-sqlserver.database.windows.net
SQL_2_Database = al3gyr-sqldatabase
SQL_3_UserName = mradministrator
SQL_4_Password = t7D1QzNX2g6i
VM_1_PublicIpAddress = 104.215.43.12
VM_2_UserName = adminuser
VM_3_Password = t7D1QzNX2g6i
VM_4_sshcommand = ssh adminuser@104.215.43.12

接続確認

 仮想マシンにSSH接続し、簡単なPythonスクリプトを用いてSQLサーバーへの接続確認を行ないます。

(1) ログイン

 アウトプット確認で表示されたVM_4_sshcommandの内容で仮想マシンにSSH接続します。

 パスワードについてもアウトプット確認で表示された値を入力します。


$ ssh adminuser@104.215.43.12

(2) パッケージインストール

 下記コマンドを実行して動作確認に必要なパッケージをインストールします。


$ sudo su
$ curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add -
$ curl https://packages.microsoft.com/config/ubuntu/16.04/prod.list > /etc/apt/sources.list.d/mssql-release.list
$ exit
$ sudo apt-get update
$ sudo ACCEPT_EULA=Y apt-get -y install msodbcsql17
$ sudo apt install -y python-pyodbc

(3) 接続確認スクリプト実行

 Terraformアウトプットの内容を元に下記スクリプトを修正。

 pythonコマンドを実行してインタラクティブシェルを起動し、下記スクリプトを貼り付けます。


import pyodbc
server = 'tcp:<Terraform アウトプット SQL_1_Server の値を入力>,1433'
database = '<Terraform アウトプット SQL_2_Database の値を入力>'
username = 'mradministrator'
password = '<Terraform アウトプットSQL_4_Passwordの値を入力>'
cnxn = pyodbc.connect('DRIVER={ODBC Driver 17 for SQL Server};SERVER='+server+';DATABASE='+database+';UID='+username+';PWD='+ password)
cursor = cnxn.cursor()
cursor.execute("SELECT @@version;")
row = cursor.fetchone()
while row:
	print(row[0])
	row = cursor.fetchone()

(4) 結果確認

 以下の通りバージョン情報が出れば 正常に接続できています。

 Ctrl-Dキーを押してインタラクティブシェルを終了します。


> > > Microsoft SQL Azure (RTM) - 12.0.2000.8
        Apr  9 2020 16:39:55
       Copyright (C) 2019 Microsoft Corporation

リソース確認

 すでに上記接続確認でリソースが正しく構築されていることは確認済ですが、一応どんなリソースが作成されているか確認してみましょう。

 リソースグループ画面で以下の通り確認できます。

Azureリソース一覧

 確認手順では意識することは無かったですが、PrivateLinkのネットワークインターフェース(上記画像のfiuht4-endpoint.nic.)が作成されています。

 ネットワークインターフェースの詳細を参照することでVNETのプライベートIPアドレスが付与され、仮想ネットワーク/サブネットに接続されていることが確認できます。

ネットワークインターフェース詳細

 「あれ?普通に***.database.windows.netのFQDNに接続したのに、このサブネットのIPアドレスに接続しているの?」という疑問については、下記スクリプトの説明を確認いただくと理解いただけると思います。

 このプライベートIPアドレスはスクリプト説明の中でも出てきますので、存在だけ覚えておいてください。

スクリプトの説明

 ここからは「プライベートDNSも併せて構築し、かつPrivateLinkに必要なDNSレコードの設定」部分について、スクリプトを元に解説していきます。

・PrivateLinkをTerraformスクリプトで構築するにあたり、つまづくのは「プライベートDNSサーバーを自分で作る」という点だけだと思います。
・この点以外はTerraform公式ドキュメントにあるサンプルを切り貼りしてつなぎ合わせれば完成します
・冒頭に記載した「AzurePortalで構築する場合はウィザードにしがって進めれば構築できる」というのは、AzurePortalでPrivateLinkを構築した場合はプライベートDNSサーバーが自動で作成されるためです
・今回のようにTerraformスクリプトを用いて構築する場合はTerraformスクリプトの中でプライベートDNSも併せて構築し、かつPrivateLinkに必要なDNSレコードの設定が必要です
 ちゃんとMSのドキュメントを読んでから構築すれば躓き無くできるのですが、いつものノリでTerraform公式ドキュメントをサンプルを見ながら作るとハマりポイントでした。MSドキュメント大事です。

 工程は「プライベートDNS作る&VNETと紐づける」と「DNSレコード作る」です。

プライベートDNS作る&VNETと紐づける

 まずはプライベートDNS作成部分です。

 azurerm_private_dns_zoneを用いて構成し、ゾーン名に該当するnameにはAzureサービスDNSゾーンの構成で指定されている”privatelink.database.windows.net”を設定します。この設定値はPrivateLinkで接続するAzureサービス毎に異なる値を設定しますのでSQLサーバー以外を接続する場合はMSドキュメントを参照して該当する値を設定します。

 また、まれにPrivateLinkの構成に失敗した場合にプライベートDNSだけ作成されてしまう事を防ぐため、depends_onで依存関係を設定しています。ここは必須ではなく念のためです。


	resource "azurerm_private_dns_zone" "example" {
	  name                = "privatelink.database.windows.net"
	  resource_group_name = azurerm_resource_group.example.name
	  depends_on = [azurerm_private_endpoint.example]
}

 次にプライベートDNSとVNETの紐づけです。

 azurerm_private_dns_zone_virtual_network_linkを用いて構成します。

 ここは必要とされるパラメーターに作成済のリソース名を割り当てるだけですね。これを設定しておくと、virtual_network_idに指定したVNETに作成したVMは自動的にプライベートDNSを参照するように構成されます。


	resource "azurerm_private_dns_zone_virtual_network_link" "example" {
	  name                  = "test"
	  resource_group_name   = azurerm_resource_group.example.name
	  private_dns_zone_name = azurerm_private_dns_zone.example.name
	  virtual_network_id    = azurerm_virtual_network.example.id
}

DNSレコード作る

 SQLサーバーに該当するDNSレコードを作成します。

 ここでは接続確認で設定、接続した***.database.windows.netのFQDNと、リソース確認の中で確認したPrivateLinkのnicのIPアドレスの紐づけを行ないます。

 まず、azurerm_private_endpoint_connectionを用いてPrivateEndpointに割り当てられたIPアドレスを取得します。

※すでに作成済のリソースの情報を得るだけなので、頭についているのがresourceではなくdataとなっています

 次に、azurerm_private_dns_a_recordを用いてDNSレコード(Aレコード)を作成します。

 nameにSQLサーバーのホスト名、recordsにホスト名に対応するIPアドレスとして前の手順で準備したPrivateEndpointに割り当てられたIPアドレスを設定します。

※PrivateEndpointは複数のVNETに接続しそれぞれでIPアドレスを持つMulti~~という機能があります。そのため、.example.private_service_connectionとprivate_ip_addressの間に1個目を表す0(配列は0からはじまるので)を設定しています。


data "azurerm_private_endpoint_connection" "example" {
  name                = azurerm_private_endpoint.example.name
  resource_group_name = azurerm_resource_group.example.name
}
	~中略~
resource azurerm_private_dns_a_record sql_server_dns_record {
  name                = azurerm_sql_server.example.name
  zone_name           = azurerm_private_dns_zone.example.name
  resource_group_name = azurerm_resource_group.example.name
  ttl                 = 300
  records             = [data.azurerm_private_endpoint_connection.example.private_service_connection.0.private_ip_address]
}

 ここまで記載した「プライベートDNS作る&VNETと紐づける」と「DNSレコード作る」が行なわれることで、下記の流れでSQLサーバーに接続する事になります。

1. プライベートDNSZoneが作成され、VNETと紐づけされる
2. 1で紐づけしたVNET上に存在するVMはプライベートDNSを参照する
3. ***.database.windows.netの名前解決を行なうと、レコード追加したPrivateLinkのIPアドレスが回答される
4. VMはPrivateLinkを経由してSQLサーバーに接続する

 実際の名前解決を含めた具体的な通信については「カスタムDNSサーバーのない仮想ネットワークのワークロード」を参照ください。

プライベートDNSの必要性

 リソース確認の章でPrivateEndpointのインターフェースに設定されたプライベートIPを確認しました。

 このタイミングでプライベートIPに直接接続したら良いのでは? と思われたかと思いますが、結論としてはNGでした。

 SQLサーバーの手前にゲートウェイが存在し、SQLサーバーとゲートウェイは多対多の構成になっているとドキュメントに記載があります。

 ここからは想像ですがゲートウェイがどのSQLサーバーにトラフィックを転送するかの判断情報として接続してきたFQDNを用いているためではないかと考えています。

 上記より、IPアドレスではなくFQDNでアクセスすれば良いのでテスト用途であればhostsファイルによる名前解決でも問題無いようです。

まとめ

・Terraformスクリプト自体はTerraform公式サイトにあるサンプルを切り貼りしているだけなのですが、その過程の「なんでこうしてるの?」を解説してみると思ったより色々ありました。
・DNS作ったりレコード追加したり、ポータルで作れば自動作成なのにちょっとめんどくさいなと最初は思いましたが、このおかげでMSドキュメントをちゃんと読むきっかけになったし、接続の流れについても知ることができ、何も知らずに使うよりはよかったなと思います。
・ひと手間あったDNS部分ですが、この仕組みにより、インターネット経由で構築済の既存アプリ+SQLサーバーに対してもアプリの改修を行なわずにPrivateLinkが導入できるというメリットを生み出していると思います。もしかしたらダウンタイム無しで切替できるかも?
 これ、ちょっといいですよね? ひと手間もゆるせる気持ちになりました。
・今回の教訓は”急がば回れ”、「ささっと作り終えたいと思ってもTerraform公式サイトのサンプルスクリプトよりも前にMSドキュメントをちゃんと読みましょう」です。

スクリプト全文


provider "azurerm" {
  version = "=2.8.0"
  features {}
}

resource "azurerm_resource_group" "example" {
  name     = "example-resources"
  location = "japanwest"
}

# VNET & Subnet
resource "azurerm_virtual_network" "example" {
  name                = "example-network"
  address_space       = ["10.0.0.0/16"]
  resource_group_name = azurerm_resource_group.example.name
  location            = azurerm_resource_group.example.location
}

resource "azurerm_subnet" "example" {
  name                 = "internal"
  resource_group_name  = azurerm_resource_group.example.name
  virtual_network_name = azurerm_virtual_network.example.name
  address_prefixes     = ["10.0.2.0/24"]

  enforce_private_link_endpoint_network_policies = true
}

resource "random_string" "password" {
  length  = 12
  special = false
  upper   = true
  number  = true
}

# SQL Server & Database
resource "azurerm_sql_server" "example" {
  name                         = "${random_string.random.result}-sqlserver"
  resource_group_name          = azurerm_resource_group.example.name
  location                     = azurerm_resource_group.example.location
  version                      = "12.0"
  administrator_login          = "mradministrator"
  administrator_login_password = random_string.password.result
}

resource "azurerm_sql_database" "example" {
  name                = "${random_string.random.result}-sqldatabase"
  resource_group_name = azurerm_resource_group.example.name
  location            = azurerm_resource_group.example.location
  server_name         = azurerm_sql_server.example.name
}

# PrivateEndpoint
resource "random_string" "random" {
  length  = 6
  special = false
  upper   = false
}

resource "azurerm_private_endpoint" "example" {
  name                = "${random_string.random.result}-endpoint"
  resource_group_name = azurerm_resource_group.example.name
  location            = azurerm_resource_group.example.location
  subnet_id           = azurerm_subnet.example.id

  depends_on = [azurerm_sql_database.example]

  private_service_connection {
    name                           = "${random_string.random.result}-privateserviceconnection"
    private_connection_resource_id = azurerm_sql_server.example.id
    subresource_names              = ["sqlServer"]
    is_manual_connection           = false
  }
}

data "azurerm_private_endpoint_connection" "example" {
  name                = azurerm_private_endpoint.example.name
  resource_group_name = azurerm_resource_group.example.name
}

# Private DNS Zone
resource "azurerm_private_dns_zone" "example" {
  name                = "privatelink.database.windows.net"
  resource_group_name = azurerm_resource_group.example.name

  depends_on = [azurerm_private_endpoint.example]
}

resource "azurerm_private_dns_zone_virtual_network_link" "example" {
  name                  = "test"
  resource_group_name   = azurerm_resource_group.example.name
  private_dns_zone_name = azurerm_private_dns_zone.example.name
  virtual_network_id    = azurerm_virtual_network.example.id
}

resource azurerm_private_dns_a_record sql_server_dns_record {
  name                = azurerm_sql_server.example.name
  zone_name           = azurerm_private_dns_zone.example.name
  resource_group_name = azurerm_resource_group.example.name
  ttl                 = 300
  records             = [data.azurerm_private_endpoint_connection.example.private_service_connection.0.private_ip_address]
}


# Virtual Machine

resource "azurerm_public_ip" "example" {
  name                = "acceptanceTestPublicIp1"
  resource_group_name = azurerm_resource_group.example.name
  location            = azurerm_resource_group.example.location
  allocation_method   = "Dynamic"
}

resource "azurerm_network_interface" "example" {
  name                = "example-nic"
  resource_group_name = azurerm_resource_group.example.name
  location            = azurerm_resource_group.example.location

  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.example.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.example.id
  }
}

resource "azurerm_linux_virtual_machine" "example" {
  name                = "example-machine"
  resource_group_name = azurerm_resource_group.example.name
  location            = azurerm_resource_group.example.location

  disable_password_authentication = false
  admin_username                  = "adminuser"
  admin_password                  = random_string.password.result

  size                  = "Standard_F2"
  network_interface_ids = [azurerm_network_interface.example.id]
  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }
  source_image_reference {
    publisher = "Canonical"
    offer     = "UbuntuServer"
    sku       = "16.04-LTS"
    version   = "latest"
  }
}

output "SQL_1_Server" {
  value = "${azurerm_sql_server.example.name}.database.windows.net"
}
output "SQL_2_Database" {
  value = azurerm_sql_database.example.name
}
output "SQL_3_UserName" {
  value = azurerm_sql_server.example.administrator_login
}
output "SQL_4_Password" {
  value = azurerm_sql_server.example.administrator_login_password
}

output "VM_1_PublicIpAddress" {
  value = azurerm_public_ip.example.ip_address
}
output "VM_2_UserName" {
  value = azurerm_linux_virtual_machine.example.admin_username
}
output "VM_3_Password" {
  value = azurerm_linux_virtual_machine.example.admin_password
}

output "VM_4_sshcommand" {
  value = "ssh ${azurerm_linux_virtual_machine.example.admin_username}@${azurerm_public_ip.example.ip_address}"
}

mochizuki.toshihiro@fixer.co.jp

クラウド環境含めたインフラ構築のスペシャリスト。
アプリ開発の分野におけるチームのあり方に対する興味と情熱の強い人。

[転載元]
 PrivateLinkをTerraformで構築したら理解が深まった話 #Azureリレー

カテゴリートップへ