0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Azureのネットワークまわりがすべてわかる

0
Posted at

Azureのネットワークを理解するために最初に覚える用語

Azureのネットワークは、AWSのVPCに近い考え方ですが、Azure独自の用語が多くあります。

1. Virtual Network (VNet)

Azureの仮想ネットワークです。
AWSでいう

VPC

に相当します。

VNet
├ Subnet-A
├ Subnet-B
└ Subnet-C

特徴

  • IPアドレス範囲を持つ
  • Azureリソースを配置する土台
  • サブネットを作成できる

10.0.0.0/16

2. Address Space

VNet全体のIP範囲です。

10.0.0.0/16

この中から

10.0.1.0/24
10.0.2.0/24
10.0.3.0/24

などのSubnetを作ります。

3. Subnet

VNetを分割したネットワークです。

VNet
├ Public Subnet
├ App Subnet
└ DB Subnet

AWSとほぼ同じです。

4. Private IP

VNet内で使うIP

10.0.1.4
10.0.1.5

VM同士の通信に利用します。

5. Public IP

インターネット公開用IP

20.x.x.x

VM
Load Balancer
Application Gateway
などへ割り当てます。

6. CIDR

IP範囲の表記方法

10.0.0.0/16

意味

10.0.0.0
~
10.0.255.255

7. NIC (Network Interface)

仮想NICです。Azureリソースをネットワークへ接続するための仮想ネットワークアダプタです。物理サーバーでいうLANカード・ネットワークアダプタに相当します。
VM作成時に自動作成されます。

VM
↓
NIC
↓
Subnet

IPアドレスはNICに付きます。

8. Route

通信経路です。

0.0.0.0/0
↓
Internet

9. Route Table

ルーティングルール
AWSと同じです。

10.0.0.0/16 → Local

0.0.0.0/0 → Firewall

10. NSG (Network Security Group)

Azure版Security Group
最重要です。

Allow TCP 80
Allow TCP 443
Deny All

適用先

Subnet
または
NIC

11. ASG (Application Security Group)

IPではなくアプリ単位で通信制御できます。

Web-ASG
App-ASG
DB-ASG

NSGで

Web-ASG → App-ASG

のみ許可

のように設定できます。

12. Service Endpoint

Azureサービスへ直接接続

VM
 ↓
Storage Account

通信はAzureバックボーンを利用します。

13. Private Endpoint

超重要です。
PaaSへプライベート接続します。

Storage Account
SQL Database
Key Vault
OpenAI

アクセス

10.0.1.10

のようなPrivate IPになります。

14. Private DNS Zone

Private Endpoint用DNS

storageaccount.blob.core.windows.net
↓
10.0.1.5

へ変換

15. VNet Peering

VNet同士を接続

VNet-A
   │
Peering
   │
VNet-B

VPN不要
高速

16. VPN Gateway

オンプレ接続

会社
↓
VPN
↓
Azure

17. ExpressRoute

専用線接続

会社
↓
専用線
↓
Azure

VPNより高速

18. Hub and Spoke

Azureで非常によく使う構成

Hub VNet
├ Firewall
├ VPN Gateway
└ Bastion

Spoke-A
Spoke-B
Spoke-C

共通機能
↓
Hub

システムごとの環境
↓
Spoke

企業での標準構成です。

19.Bation

普通のVM
= アプリやDBを動かすサーバ

Azure Bastion
= Public IPなしでVMへ安全にログインするための管理サービス

管理者PC
↓
HTTPS(443)
↓
Azure Portal
↓
Azure Bastion
↓
VM

20. Azure Firewall

Azure管理型Firewall
構成

Internet
↓
Firewall
↓
VNet

特徴

  • L3/L4制御
  • FQDN制御
  • NAT

21. Load Balancer

L4ロードバランサ

TCP
UDP

負荷分散

22. Azure Front Door

グローバル負荷分散

日本
アメリカ
ヨーロッパ

へ振り分け
機能

CDN
WAF
SSL

23. NAT Gateway

プライベートSubnetの外向き通信
構成

VM
↓
NAT Gateway
↓
Internet

AWSとほぼ同じ

24. VPN Gateway

Site-to-Site VPN

Office
↓
VPN
↓
Azure

実践

VMを2個立て、LoadBalancerがそれぞれのVMに振り分け、各VMはMysql・Storageのデータ一覧を取得し、WEBページとして表示するシステムとする。ManegementVMは2個のVM・Mysql・Storageの調査管理用に1つ立てます。

構成図

azure_hub_spoke_large_spaced_corrected (1).png

構成方針

Internet入口
  → Application Gateway

VMへの内部分散
  → Internal Load Balancer

VMからMySQL / Storage
  → Private Endpoint

VMからInternet
  → Azure Firewall

管理者接続
  → Azure Bastion
  → Management VM

Private DNS
  → Hubで管理
  → Hub VNet / Spoke VNet の両方にリンク

NAT Gateway
  → 使用しない
  → 外向き通信は Azure Firewall に統一

ブラウザからVM1 / VM2 のWeb画面への通信経路

# 構成図
利用者ブラウザ
  │
  │ HTTPS : 443
  ▼
Application Gateway Public IP
  │
  ▼
Application Gateway
AppGW Subnet
  │
  ├─ WAF
  ├─ TLS終端
  ├─ HTTP/HTTPSルーティング
  └─ Backend Pool = Internal Load Balancer Private IP
  │
  │ ③ Spoke VNet 内部通信
  ▼
Internal Load Balancer
LB Subnet
  │
  ├─ L4 Load Balancing
  ├─ Health Probe
  └─ Backend Pool = VM1 / VM2
  │
  ├───────────────┐
  │               │
  ▼               ▼
VM1             VM2
App Subnet      App Subnet
Web App         Web App

# 通るサービス
Public IP
↓
Application Gateway
↓
WAF
↓
Internal Load Balancer
↓
VM1 / VM2

# ポイント
Application Gateway は L7
Internal Load Balancer は L4
Application Gateway でHTTPSを受け、
その後 Internal Load Balancer に流し、
VM1 / VM2 に分散します。

VM1 / VM2 → Storage Account/Mysql の通信経路

# 構成図
VM1 / VM2
App Subnet
  │
  │ ① Storage名を名前解決
  │
  │ 例:
  │ mystorage.blob.core.windows.net
  │
  │ CNAME
  │ ↓
  │ mystorage.privatelink.blob.core.windows.net
  │
  │ A Record
  │ ↓
  │ Storage Private Endpoint の Private IP
  │
  ▼
Private DNS Zone
privatelink.blob.core.windows.net
Hubで管理 / Spokeにもリンク
  │
  ▼
Storage Private Endpoint
Private Endpoint Subnet
  │
  ▼
Storage Account
Public Network Access Disabled

# 通るサービス
Azure DNS
↓
Private DNS Zone
↓
Storage Private Endpoint
↓
Storage Account

# ポイント
Storage通信は Azure Firewall を通しません。

理由:
StorageのFQDNが Private Endpoint のプライベートIPに解決されるため、
通信先がVNet内のPrivate IPになります。

そのため、
0.0.0.0/0 → Azure Firewall
のルートよりも、
Private Endpoint Subnet向けのVNet内部ルートが優先されます。

管理者PC → Management VM の通信経路

# 構成図
管理者PC
  │
  │ HTTPS : 443
  ▼
Azure Portal / Bastion接続画面
  │
  │ HTTPS : 443
  ▼
Azure Bastion Public IP
AzureBastionSubnet
  │
  │ RDP / SSH
  │ ※ VMにPublic IPは不要
  ▼
Management VM
Management Subnet

# 通るサービス
Azure Portal
↓
Azure Bastion
↓
Management VM

# ポイント
Management VMにPublic IPは付けません。

管理者は直接VMへRDP/SSHするのではなく、
Azure Bastion経由でManagement VMへ接続します。

Management VM → VM1 / VM2 の管理通信

# 構成図
Management VM
Management Subnet
Hub VNet
  │
  │ RDP / SSH / HTTP確認 / ログ確認など
  ▼
VNet Peering
Hub VNet ⇄ Spoke VNet
  │
  ▼
VM1 / VM2
App Subnet
Spoke VNet

# 通るサービス
VNet Peering
↓
NSG
↓
VM1 / VM2

# ポイント
Management VMは調査・管理専用です。

VM1 / VM2にPublic IPを付けず、
Hub VNetのManagement VMからVNet Peering経由で管理します。

Management VM → Storage / MySQL の接続確認

# 構成図
Management VM
Management Subnet
Hub VNet
  │
  │ ① Storage名を名前解決
  ▼
Private DNS Zone
privatelink.blob.core.windows.net
Hubにリンク済み
  │
  ▼
VNet Peering
Hub VNet ⇄ Spoke VNet
  │
  ▼
Storage Private Endpoint
Private Endpoint Subnet
Spoke VNet
  │
  ▼
Storage Account
Public Network Access Disabled

# 通るサービス
Azure DNS
↓
Private DNS Zone
↓
VNet Peering
↓
Private Endpoint
↓
Storage / MySQL

# ポイント
Management VMからStorage / MySQLに接続確認するため、
Private DNS ZoneはHub VNetにもリンクが必要です。

Spokeだけにリンクしていると、
Management VMから名前解決できない可能性があります。

VM1 / VM2 → Internet の外向き通信

# 構成図
VM1 / VM2
App Subnet
  │
  │ 宛先:
  │ Internet
  │ 例: apt update / 外部API / OS更新
  ▼
App Subnet Route Table
  │
  │ UDR
  │ 0.0.0.0/0 → Azure Firewall Private IP
  ▼
VNet Peering
Spoke VNet → Hub VNet
  │
  ▼
Azure Firewall
AzureFirewallSubnet
  │
  ├─ Network Rule
  ├─ Application Rule
  ├─ FQDN Filter
  └─ SNAT
  │
  ▼
Firewall Public IP
  │
  ▼
Internet

# 通るサービス
Route Table
↓
VNet Peering
↓
Azure Firewall
↓
Firewall Public IP
↓
Internet

# ポイント
NAT Gatewayは使いません。

VM1 / VM2の外向き通信はAzure Firewallに統一します。

Azure Firewallで送信先を制御できます。
例:
- OS更新のみ許可
- 特定外部APIのみ許可
- 不要な外向き通信を拒否

DNS名前解決の通信経路

# 構成図
VM1 / VM2
Management VM
  │
  │ 名前解決
  ▼
Azure DNS
168.63.129.16
  │
  ├─────────────────────────────────────┐
  │                                     │
  ▼                                     ▼
Private DNS Zone                    Public DNS
Hubで管理                            Internet向け名前解決
  │
  ├─ privatelink.blob.core.windows.net
  │      ↓
  │   Storage Private Endpoint IP
  │
  └─ privatelink.mysql.database.azure.com
         ↓
      MySQL Private Endpoint IP

# Private DNS Zone のリンク
Private DNS Zones
Hubで管理

├─ privatelink.blob.core.windows.net
│   ├─ Hub VNet Link
│   └─ Spoke VNet Link
│
└─ privatelink.mysql.database.azure.com
    ├─ Hub VNet Link
    └─ Spoke VNet Link

# 名前解決の流れ
mystorage.blob.core.windows.net
  ↓
mystorage.privatelink.blob.core.windows.net
  ↓
Storage Private Endpoint の Private IP

# ポイント
Private Endpoint構成ではDNSが非常に重要です。

DNSが正しくない場合、
VMはStorage / MySQLのPublic側IPへ接続しようとします。

ただし今回は、
Storage / MySQLの Public Network Access が Disabled なので、
Public側へ解決されると接続に失敗します。

Application Gateway / Load Balancer のヘルスチェック通信

# Application Gateway → Internal Load Balancer の正常性確認
Application Gateway
AppGW Subnet
  │
  │ Health Probe
  ▼
Internal Load Balancer
LB Subnet

# Internal Load Balancer → VM1 / VM2 の正常性確認
Internal Load Balancer
  │
  │ Health Probe
  ├───────────────┐
  │               │
  ▼               ▼
VM1             VM2
App Subnet      App Subnet

# 必要な許可
VM側NSGで許可

送信元:
AzureLoadBalancer Service Tag
または
Load Balancer Probe

宛先:
VM1 / VM2

ポート:
Health Probe用ポート
例:
80 / 443 / 8080

# ポイント
ヘルスチェックが通らないと、
VMが正常でもLoad BalancerやApplication Gatewayから
バックエンド異常と判断されます

最終的な通信経路まとめ

[入口通信]
利用者
→ Public DNS
→ Application Gateway Public IP
→ Application Gateway
→ Internal Load Balancer
→ VM1 / VM2


[VMからStorage]
VM1 / VM2
→ Azure DNS
→ Private DNS Zone
→ Storage Private Endpoint
→ Storage Account


[VMからMySQL]
VM1 / VM2
→ Azure DNS
→ Private DNS Zone
→ MySQL Private Endpoint
→ Azure Database for MySQL


[管理者からManagement VM]
管理者PC
→ Azure Bastion
→ Management VM


[Management VMからVM管理]
Management VM
→ VNet Peering
→ VM1 / VM2


[Management VMからStorage / MySQL確認]
Management VM
→ Private DNS Zone
→ VNet Peering
→ Private Endpoint
→ Storage / MySQL


[VMの外向き通信]
VM1 / VM2
→ App Subnet Route Table
→ VNet Peering
→ Azure Firewall
→ Firewall Public IP
→ Internet


[DNS]
Private DNS ZoneはHubで管理
Hub VNetとSpoke VNetの両方にリンク

コマンド

変数作成

LOCATION="southafricanorth"
RG="test-rg"

HUB_VNET="hub-vnet"
SPOKE_VNET="spoke-vnet"

ADMIN_USER="azureuser"
ADMIN_PASS='ChangeMe12345!'

STORAGE_NAME="teststorage$RANDOM$RANDOM"
MYSQL_NAME="test-mysql-$RANDOM"

MYSQL_ADMIN="mysqladmin"
MYSQL_PASS='ChangeMe12345!'

VM1="vm-app-01"
VM2="vm-app-02"
MGMT_VM="vm-management"

APPGW_NAME="appgw-web"
ILB_NAME="ilb-app"
FIREWALL_NAME="test-firewall"
BASTION_NAME="test-bation"

APP_PORT="8000"
ILB_PRIVATE_IP="10.1.2.10"

CERT_PASS='ChangeMe12345!'

ログインと設定

az login
az account list -o table
az account set --subscription "<SUBSCRIPTION_ID>"
# Azure CLI拡張機能の自動インストールを許可
az config set extension.use_dynamic_install=yes_without_prompt

Resource Group作成

az group create \
  --name $RG \
  --location $LOCATION

HUB NET作成

az network vnet create \
  --resource-group $RG \
  --name $HUB_VNET \
  --location $LOCATION \
  --address-prefix 10.0.0.0/16 \
  --subnet-name AzureFirewallSubnet \
  --subnet-prefix 10.0.1.0/26

下記を作っている

Resource Group
└─ Hub VNet: 10.0.0.0/16
   └─ AzureFirewallSubnet: 10.0.1.0/26

Azure Bastion Subnet作成

az network vnet subnet create \
  --resource-group $RG \
  --vnet-name $HUB_VNET \
  --name AzureBastionSubnet \
  --address-prefix 10.0.2.0/26

Management Subnet作成

az network vnet subnet create \
  --resource-group $RG \
  --vnet-name $HUB_VNET \
  --name ManagementSubnet \
  --address-prefix 10.0.10.0/24

Spoke VNet作成

az network vnet create \
  --resource-group $RG \
  --name $SPOKE_VNET \
  --location $LOCATION \
  --address-prefix 10.1.0.0/16 \
  --subnet-name AppGWSubnet \
  --subnet-prefix 10.1.1.0/24

下記を作っている

Resource Group
└─ Hub VNet: 10.1.0.0/16 
   └─ AzureFirewallSubnet: 10.1.1.0/24

Internal Load Balancer Subnet作成

az network vnet subnet create \
  --resource-group $RG \
  --vnet-name $SPOKE_VNET \
  --name LBSubnet \
  --address-prefix 10.1.2.0/24

App Subnet作成

az network vnet subnet create \
  --resource-group $RG \
  --vnet-name $SPOKE_VNET \
  --name AppSubnet \
  --address-prefix 10.1.10.0/24

Private Endpoint Subnet作成

az network vnet subnet create \
  --resource-group $RG \
  --vnet-name $SPOKE_VNET \
  --name PrivateEndpointSubnet \
  --address-prefix 10.1.20.0/24

Private Endpoint用Subnetポリシー無効化

Private Endpointを安定して配置するため、Private Endpoint Subnetのネットワークポリシーを無効化します。

az network vnet subnet update \
  --resource-group $RG \
  --vnet-name $SPOKE_VNET \
  --name PrivateEndpointSubnet \
  --disable-private-endpoint-network-policies true

PrivateEndpointSubnet 内の Private Endpoint に対して、NSGやUDRなどのネットワークポリシーを無効化する

なぜ無効化するのか

Private Endpointは、たとえばStorageやMySQLをVNet内のプライベートIPで使うための入口です。

VM1 / VM2
  │
  │ Storage名を名前解決
  ▼
Private DNS Zone
  │
  ▼
Storage Private Endpoint IP
例: 10.1.20.4
  │
  ▼
Storage Account

このPrivate EndpointのNICに対して、サブネットのNSGやRoute Tableが中途半端に効くと、

  • Private Endpointへの通信がNSGでブロックされる
  • UDRでFirewallへ強制転送される
  • 戻り通信が崩れる
  • 名前解決はできるのに接続できない
    というトラブルが起きやすくなります。そのため、シンプルな構成ではまず、Private Endpoint SubnetではPrivate Endpoint用ネットワークポリシーを無効化するという設計にします。

Hub-Spoke VNet Peering作成

VNet ID取得

HUB_ID=$(az network vnet show \
  --resource-group $RG \
  --name $HUB_VNET \
  --query id \
  -o tsv)

SPOKE_ID=$(az network vnet show \
  --resource-group $RG \
  --name $SPOKE_VNET \
  --query id \
  -o tsv)

Hub → Spoke Peering

az network vnet peering create \
  --resource-group $RG \
  --name hub-to-spoke \
  --vnet-name $HUB_VNET \
  --remote-vnet $SPOKE_ID \
  --allow-vnet-access

Hub VNetからSpoke VNetへ通信できるようにPeeringを作成します。

Spoke → Hub Peering

az network vnet peering create \
  --resource-group $RG \
  --name spoke-to-hub \
  --vnet-name $SPOKE_VNET \
  --remote-vnet $HUB_ID \
  --allow-vnet-access

Azure Firewall作成

Firewall Public IP作成

az network public-ip create \
  --resource-group $RG \
  --name pip-azure-firewall \
  --location $LOCATION \
  --sku Standard \
  --allocation-method Static

Azure Firewallの外向き通信に使うStatic Public IPを作成します。

Azure Firewall本体作成

az network firewall create \
  --resource-group $RG \
  --name $FIREWALL_NAME \
  --location $LOCATION

FirewallにIP構成を追加

az network firewall ip-config create \
  --resource-group $RG \
  --firewall-name $FIREWALL_NAME \
  --name fw-ipconfig \
  --public-ip-address pip-azure-firewall \
  --vnet-name $HUB_VNET
Hub VNet: 10.0.0.0/16
└─ AzureFirewallSubnet: 10.0.1.0/26
   └─ Azure Firewall
      ├─ Private IP: 10.0.1.4 など
      └─ Public IP: 20.x.x.x など

FirewallのPrivate IP取得

FW_PRIVATE_IP=$(az network firewall show \
  --resource-group $RG \
  --name $FIREWALL_NAME \
  --query "ipConfigurations[0].privateIPAddress" \
  -o tsv)

VM1 / VM2 / Management VMの外向き通信の際に使用する

Azure Firewallの許可ルール作成

az network firewall network-rule create \
  --resource-group $RG \
  --firewall-name $FIREWALL_NAME \
  --collection-name allow-vm-outbound \
  --name allow-http-https-rule \
  --priority 100 \
  --action Allow \
  --protocols TCP \
  --source-addresses 10.1.10.0/24 10.0.10.0/24 \
  --destination-addresses "*" \
  --destination-ports 80 443

App SubnetとManagement SubnetからInternet向けHTTP/HTTPS通信をAzure Firewallで許可

Route Table作成

App Subnet用Route Table作成

az network route-table create \
  --resource-group $RG \
  --name rt-app-to-firewall \
  --location $LOCATION

VM1 / VM2の外向き通信をAzure Firewallに向けるためのRoute Tableを作成

App Subnet用Default Route作成

az network route-table route create \
  --resource-group $RG \
  --route-table-name rt-app-to-firewall \
  --name default-to-azure-firewall \
  --address-prefix 0.0.0.0/0 \
  --next-hop-type VirtualAppliance \
  --next-hop-ip-address $FW_PRIVATE_IP

下記をしている

App Subnet の VM
  │
  │ インターネット宛通信
  ▼
Route Table
  │
  │ 0.0.0.0/0(すべての通信) は Firewall へ
  ▼
Azure Firewall
  │
  ▼
Internet / 外部API / Microsoft Update など

App SubnetにRoute Table関連付け

az network vnet subnet update \
  --resource-group $RG \
  --vnet-name $SPOKE_VNET \
  --name AppSubnet \
  --route-table rt-app-to-firewall

Management Subnet用Route Table作成

az network route-table create \
  --resource-group $RG \
  --name rt-management-to-firewall \
  --location $LOCATION

Management Subnet用Default Route作成

az network route-table route create \
  --resource-group $RG \
  --route-table-name rt-management-to-firewall \
  --name default-to-azure-firewall \
  --address-prefix 0.0.0.0/0 \
  --next-hop-type VirtualAppliance \
  --next-hop-ip-address $FW_PRIVATE_IP

Management SubnetにRoute Table関連付け

az network vnet subnet update \
  --resource-group $RG \
  --vnet-name $HUB_VNET \
  --name ManagementSubnet \
  --route-table rt-management-to-firewall

NSG作成

App Subnet用NSG作成

az network nsg create \
  --resource-group $RG \
  --name nsg-app-subnet \
  --location $LOCATION

Web通信許可ルール作成

az network nsg rule create \
  --resource-group $RG \
  --nsg-name nsg-app-subnet \
  --name allow-web-from-vnet \
  --priority 100 \
  --direction Inbound \
  --access Allow \
  --protocol Tcp \
  --source-address-prefixes VirtualNetwork \
  --source-port-ranges "*" \
  --destination-address-prefixes "*" \
  --destination-port-ranges $APP_PORT

Application Gateway / Internal Load BalancerからVMのWebアプリポート 8000 への通信を許可します。

SSH許可ルール作成

az network nsg rule create \
  --resource-group $RG \
  --nsg-name nsg-app-subnet \
  --name allow-ssh-from-vnet \
  --priority 110 \
  --direction Inbound \
  --access Allow \
  --protocol Tcp \
  --source-address-prefixes VirtualNetwork \
  --source-port-ranges "*" \
  --destination-address-prefixes "*" \
  --destination-port-ranges 22

App SubnetにNSG関連付け

az network vnet subnet update \
  --resource-group $RG \
  --vnet-name $SPOKE_VNET \
  --name AppSubnet \
  --network-security-group nsg-app-subnet

Management Subnet用NSG作成

az network nsg create \
  --resource-group $RG \
  --name nsg-management-subnet \
  --location $LOCATION

Management SubnetのSSH許可

az network nsg rule create \
  --resource-group $RG \
  --nsg-name nsg-management-subnet \
  --name allow-ssh-from-bastion-vnet \
  --priority 100 \
  --direction Inbound \
  --access Allow \
  --protocol Tcp \
  --source-address-prefixes VirtualNetwork \
  --source-port-ranges "*" \
  --destination-address-prefixes "*" \
  --destination-port-ranges 22

Azure Bastion経由でManagement VMへSSHできるようにします。

Management SubnetにNSG関連付け

az network vnet subnet update \
  --resource-group $RG \
  --vnet-name $HUB_VNET \
  --name ManagementSubnet \
  --network-security-group nsg-management-subnet

Private DNS Zone作成

Private Endpointでは、PaaSのFQDNをPrivate IPへ名前解決させるためにPrivate DNS Zoneが重要です。Azureの名前解決ではPrivate DNS ZoneやDNS Resolverを使ってVNet内の名前解決を構成できます。

Storage/MySQL用Private DNS Zone作成

az network private-dns zone create \
  --resource-group $RG \
  --name privatelink.blob.core.windows.net

az network private-dns zone create \
  --resource-group $RG \
  --name privatelink.mysql.database.azure.com

DNS ZoneをHub VNetにリンク

Management VMからStorage Private Endpointを名前解決できるようにHub VNetへDNS Zoneをリンク

az network private-dns link vnet create \
  --resource-group $RG \
  --zone-name privatelink.blob.core.windows.net \
  --name blob-link-hub \
  --virtual-network $HUB_VNET \
  --registration-enabled false

az network private-dns link vnet create \
  --resource-group $RG \
  --zone-name privatelink.mysql.database.azure.com \
  --name mysql-link-hub \
  --virtual-network $HUB_VNET \
  --registration-enabled false

DNS ZoneをSpoke VNetにリンク

VM1 / VM2からStorage Private Endpointを名前解決できるようにSpoke VNetへDNS Zoneをリンク

az network private-dns link vnet create \
  --resource-group $RG \
  --zone-name privatelink.blob.core.windows.net \
  --name blob-link-spoke \
  --virtual-network $SPOKE_VNET \
  --registration-enabled false

az network private-dns link vnet create \
  --resource-group $RG \
  --zone-name privatelink.mysql.database.azure.com \
  --name mysql-link-spoke \
  --virtual-network $SPOKE_VNET \
  --registration-enabled false

Private Endpoint作成時に使用

Storage

Storage Account作成

az storage account create \
  --resource-group $RG \
  --name $STORAGE_NAME \
  --location $LOCATION \
  --sku Standard_LRS \
  --kind StorageV2 \
  --public-network-access Disabled \
  --allow-blob-public-access false

Public Network Accessを無効化したStorage Accountを作成

Storage Account ID取得

STORAGE_ID=$(az storage account show \
  --resource-group $RG \
  --name $STORAGE_NAME \
  --query id \
  -o tsv)

Storage Private Endpoint作成

az network private-endpoint create \
  --resource-group $RG \
  --name pe-storage-blob \
  --location $LOCATION \
  --vnet-name $SPOKE_VNET \
  --subnet PrivateEndpointSubnet \
  --private-connection-resource-id $STORAGE_ID \
  --group-id blob \
  --connection-name pe-storage-blob-conn

Storage Private EndpointをDNS Zoneに紐付け

az network private-endpoint dns-zone-group create \
  --resource-group $RG \
  --endpoint-name pe-storage-blob \
  --name storage-zone-group \
  --private-dns-zone privatelink.blob.core.windows.net \
  --zone-name blob

Mysql

Azure Database for MySQL Flexible Server作成

Public Accessを無効化したAzure Database for MySQL Flexible Serverを作成

az mysql flexible-server create \
  --resource-group $RG \
  --name $MYSQL_NAME \
  --location $LOCATION \
  --admin-user $MYSQL_ADMIN \
  --admin-password "$MYSQL_PASS" \
  --sku-name Standard_B1ms \
  --tier Burstable \
  --version 8.0.21 \
  --storage-size 32 \
  --public-access Disabled

MySQL Resource ID取得

MYSQL_ID=$(az mysql flexible-server show \
  --resource-group $RG \
  --name $MYSQL_NAME \
  --query id \
  -o tsv)

MySQL Private Endpoint作成

az network private-endpoint create \
  --resource-group $RG \
  --name pe-mysql \
  --location $LOCATION \
  --vnet-name $SPOKE_VNET \
  --subnet PrivateEndpointSubnet \
  --private-connection-resource-id $MYSQL_ID \
  --group-id mysqlServer \
  --connection-name pe-mysql-conn

MySQL Private EndpointをDNS Zoneに紐付け

az network private-endpoint dns-zone-group create \
  --resource-group $RG \
  --endpoint-name pe-mysql \
  --name mysql-zone-group \
  --private-dns-zone privatelink.mysql.database.azure.com \
  --zone-name mysql

VM1 / VM2 / Management VM作成

VM1作成

quota(作成上限)と指定のreationで作成可能なVMがあるか確認してから実行

az vm create \
  --resource-group $RG \
  --name $VM1 \
  --location $LOCATION \
  --zone 2 \
  --image Ubuntu2204 \
  --vnet-name $SPOKE_VNET \
  --subnet AppSubnet \
  --admin-username $ADMIN_USER \
  --admin-password $ADMIN_PASS \
  --authentication-type password \
  --public-ip-address "" \
  --size Standard_DS1_v2

VM2作成

az vm create \
  --resource-group $RG \
  --name $VM2 \
  --location $LOCATION \
  --image Ubuntu2204 \
  --vnet-name $SPOKE_VNET \
  --subnet AppSubnet \
  --admin-username $ADMIN_USER \
  --admin-password $ADMIN_PASS \
  --authentication-type password \
  --public-ip-address "" \
  --size Standard_DS1_v2

Management VM作成

az vm create \
  --resource-group $RG \
  --name $MGMT_VM \
  --location $LOCATION \
  --image Ubuntu2204 \
  --vnet-name $HUB_VNET \
  --subnet ManagementSubnet \
  --admin-username $ADMIN_USER \
  --admin-password $ADMIN_PASS \
  --authentication-type password \
  --public-ip-address "" \
  --size Standard_DS1_v2

VMにManaged Identity付与

VMからStorageへパスワードなしでアクセスできるようにManaged Identityを有効化します。

az vm identity assign \
  --resource-group $RG \
  --name $VM1
az vm identity assign \
  --resource-group $RG \
  --name $VM2
az vm identity assign \
  --resource-group $RG \
  --name $MGMT_VM

Managed Identity Principal ID取得

VM1_PRINCIPAL_ID=$(az vm show \
  --resource-group $RG \
  --name $VM1 \
  --query identity.principalId \
  -o tsv)
VM2_PRINCIPAL_ID=$(az vm show \
  --resource-group $RG \
  --name $VM2 \
  --query identity.principalId \
  -o tsv)
MGMT_PRINCIPAL_ID=$(az vm show \
  --resource-group $RG \
  --name $MGMT_VM \
  --query identity.principalId \
  -o tsv)

VMへStorage読み取り権限付与

az role assignment create \
  --assignee-object-id $VM1_PRINCIPAL_ID \
  --assignee-principal-type ServicePrincipal \
  --role "Storage Blob Data Reader" \
  --scope $STORAGE_ID
az role assignment create \
  --assignee-object-id $VM2_PRINCIPAL_ID \
  --assignee-principal-type ServicePrincipal \
  --role "Storage Blob Data Reader" \
  --scope $STORAGE_ID
az role assignment create \
  --assignee-object-id $MGMT_PRINCIPAL_ID \
  --assignee-principal-type ServicePrincipal \
  --role "Storage Blob Data Contributor" \
  --scope $STORAGE_ID

Azure Bastion作成

Bastion Public IP作成

az network public-ip create \
  --resource-group $RG \
  --name pip-bastion \
  --location $LOCATION \
  --sku Standard \
  --allocation-method Static

Bastion作成

az network bastion create \
  --resource-group $RG \
  --name $BASTION_NAME \
  --location $LOCATION \
  --vnet-name $HUB_VNET \
  --public-ip-address pip-bastion

Internal Load Balancer作成

Internal Load Balancer本体作成

az network lb create \
  --resource-group $RG \
  --name $ILB_NAME \
  --location $LOCATION \
  --sku Standard \
  --vnet-name $SPOKE_VNET \
  --subnet LBSubnet \
  --frontend-ip-name ilb-frontend \
  --backend-pool-name ilb-backend-pool \
  --private-ip-address $ILB_PRIVATE_IP

ILB Health Probe作成

VM1 / VM2のWebアプリポート 8000 が応答するか確認するHealth Probeを作成

az network lb probe create \
  --resource-group $RG \
  --lb-name $ILB_NAME \
  --name http-probe-8000 \
  --protocol Tcp \
  --port $APP_PORT

ILB Rule作成

Internal Load Balancerの 8000 番ポートへ来た通信をVM1 / VM2へ分散

az network lb rule create \
  --resource-group $RG \
  --lb-name $ILB_NAME \
  --name http-rule-8000 \
  --protocol Tcp \
  --frontend-port $APP_PORT \
  --backend-port $APP_PORT \
  --frontend-ip-name ilb-frontend \
  --backend-pool-name ilb-backend-pool \
  --probe-name http-probe-8000

VMをILB Backend Poolへ追加

VMをInternal Load Balancerの分散先に追加します。

VM1_NIC=$(az vm show \
  --resource-group $RG \
  --name $VM1 \
  --query "networkProfile.networkInterfaces[0].id" \
  -o tsv | xargs basename)

VM2_NIC=$(az vm show \
  --resource-group $RG \
  --name $VM2 \
  --query "networkProfile.networkInterfaces[0].id" \
  -o tsv | xargs basename)

VM1_IPCONFIG=$(az network nic ip-config list \
  --resource-group $RG \
  --nic-name $VM1_NIC \
  --query "[0].name" \
  -o tsv)

VM2_IPCONFIG=$(az network nic ip-config list \
  --resource-group $RG \
  --nic-name $VM2_NIC \
  --query "[0].name" \
  -o tsv)

az network nic ip-config address-pool add \
  --resource-group $RG \
  --nic-name $VM1_NIC \
  --ip-config-name $VM1_IPCONFIG \
  --lb-name $ILB_NAME \
  --address-pool ilb-backend-pool

az network nic ip-config address-pool add \
  --resource-group $RG \
  --nic-name $VM2_NIC \
  --ip-config-name $VM2_IPCONFIG \
  --lb-name $ILB_NAME \
  --address-pool ilb-backend-pool

Application Gateway作成

自己署名証明書作成

今回は構成図に合わせて HTTPS入口 にします。
学習用なので自己署名証明書を使います。実運用では独自ドメイン用の正式証明書をKey Vaultなどで管理してください。

openssl req -x509 -nodes -days 365 \
  -newkey rsa:2048 \
  -keyout appgw.key \
  -out appgw.crt \
  -subj "/CN=demo.local"
# PFX形式へ変換
openssl pkcs12 -export \
  -out appgw.pfx \
  -inkey appgw.key \
  -in appgw.crt \
  -password pass:$CERT_PASS

Application Gateway用Public IP作成

az network public-ip create \
  --resource-group $RG \
  --name pip-appgw \
  --location $LOCATION \
  --sku Standard \
  --allocation-method Static

Application Gateway作成

# WAFpolicy作成
WAF_POLICY_NAME="appgw-waf-policy"

az network application-gateway waf-policy create \
  --resource-group "$RG" \
  --name "$WAF_POLICY_NAME" \
  --location "$LOCATION"

WAF_POLICY_ID=$(az network application-gateway waf-policy show \
  --resource-group "$RG" \
  --name "$WAF_POLICY_NAME" \
  --query id \
  -o tsv)

echo "$WAF_POLICY_ID"

# 作成
az network application-gateway create \
  --resource-group $RG \
  --name $APPGW_NAME \
  --location $LOCATION \
  --sku WAF_v2 \
  --capacity 1 \
  --vnet-name $SPOKE_VNET \
  --subnet AppGWSubnet \
  --public-ip-address pip-appgw \
  --frontend-port 443 \
  --http-settings-port $APP_PORT \
  --http-settings-protocol Http \
  --servers $ILB_PRIVATE_IP \
  --cert-file appgw.pfx \
  --cert-password $CERT_PASS \
  --priority 100\
  --waf-policy "$WAF_POLICY_ID"

Python Webアプリ作成

機能

  • どちらのVMで応答しているか
  • Storage Blob一覧
  • MySQLのitemsテーブル一覧

app.py作成

cat > app.py << 'PY'
import os
import socket

import pymysql
from azure.identity import ManagedIdentityCredential
from azure.storage.blob import BlobServiceClient
from flask import Flask, render_template_string

app = Flask(__name__)

STORAGE_ACCOUNT = os.environ["STORAGE_ACCOUNT"]
MYSQL_HOST = os.environ["MYSQL_HOST"]
MYSQL_USER = os.environ["MYSQL_USER"]
MYSQL_PASSWORD = os.environ["MYSQL_PASSWORD"]
MYSQL_DATABASE = os.environ["MYSQL_DATABASE"]

HTML = """
<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>VM Web App</title>
  <style>
    body {
      font-family: Arial, sans-serif;
      margin: 40px;
      background: #f7f9fb;
    }
    h1 {
      color: #1f4e79;
    }
    .box {
      background: white;
      border: 1px solid #d0d7de;
      padding: 16px;
      margin-bottom: 20px;
      border-radius: 8px;
    }
    code {
      background: #eee;
      padding: 2px 4px;
    }
    li {
      margin: 4px 0;
    }
  </style>
</head>
<body>
  <h1>VM Web App</h1>

  <div class="box">
    <h2>Server</h2>
    <p>Hostname: <code>{{ hostname }}</code></p>
  </div>

  <div class="box">
    <h2>Storage Blob List</h2>
    <ul>
      {% for blob in blobs %}
        <li>{{ blob }}</li>
      {% endfor %}
    </ul>
  </div>

  <div class="box">
    <h2>MySQL Items</h2>
    <ul>
      {% for item in mysql_items %}
        <li>{{ item }}</li>
      {% endfor %}
    </ul>
  </div>
</body>
</html>
"""


def list_blobs():
    account_url = f"https://{STORAGE_ACCOUNT}.blob.core.windows.net"
    credential = ManagedIdentityCredential()
    service = BlobServiceClient(account_url=account_url, credential=credential)

    results = []

    for container in service.list_containers():
        container_client = service.get_container_client(container.name)

        for blob in container_client.list_blobs():
            results.append(f"{container.name}/{blob.name}")

    return results or ["No blobs found"]


def list_mysql_items():
    conn = pymysql.connect(
        host=MYSQL_HOST,
        user=MYSQL_USER,
        password=MYSQL_PASSWORD,
        database=MYSQL_DATABASE,
        ssl={"ssl": {}},
        connect_timeout=5,
    )

    try:
        with conn.cursor() as cur:
            cur.execute("SELECT name FROM items ORDER BY id")
            rows = cur.fetchall()
            return [row[0] for row in rows] or ["No MySQL rows found"]
    finally:
        conn.close()


@app.route("/")
def index():
    try:
        blobs = list_blobs()
    except Exception as exc:
        blobs = [f"Storage error: {exc}"]

    try:
        mysql_items = list_mysql_items()
    except Exception as exc:
        mysql_items = [f"MySQL error: {exc}"]

    return render_template_string(
        HTML,
        hostname=socket.gethostname(),
        blobs=blobs,
        mysql_items=mysql_items,
    )


@app.route("/health")
def health():
    return "OK", 200


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8000)
PY

VM1 / VM2へPythonアプリを配置

VMに必要パッケージをインストール

for VM in $VM1 $VM2; do
  az vm run-command invoke \
    --resource-group $RG \
    --name $VM \
    --command-id RunShellScript \
    --scripts "
sudo apt-get update
sudo apt-get install -y python3-pip
sudo pip3 install flask azure-identity azure-storage-blob pymysql gunicorn
sudo mkdir -p /opt/vmweb
"
done

app.pyをBase64化

APP_CONTENT=$(base64 -w 0 app.py)

VM1 / VM2へapp.pyとsystemd設定を配置

for VM in $VM1 $VM2; do
  az vm run-command invoke \
    --resource-group $RG \
    --name $VM \
    --command-id RunShellScript \
    --scripts "
echo $APP_CONTENT | base64 -d | sudo tee /opt/vmweb/app.py > /dev/null

sudo tee /etc/systemd/system/vmweb.service > /dev/null << EOF
[Unit]
Description=VM Web App
After=network.target

[Service]
WorkingDirectory=/opt/vmweb
Environment=STORAGE_ACCOUNT=$STORAGE_NAME
Environment=MYSQL_HOST=$MYSQL_NAME.mysql.database.azure.com
Environment=MYSQL_USER=$MYSQL_ADMIN
Environment=MYSQL_PASSWORD=$MYSQL_PASS
Environment=MYSQL_DATABASE=appdb
ExecStart=/usr/local/bin/gunicorn -w 2 -b 0.0.0.0:$APP_PORT app:app
Restart=always

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable vmweb
sudo systemctl restart vmweb
"
done

状態確認や初期化(主にmanegement vm)

Webアプリ起動状態確認

for VM in $VM1 $VM2; do
  az vm run-command invoke \
    --resource-group $RG \
    --name $VM \
    --command-id RunShellScript \
    --scripts "systemctl status vmweb --no-pager"
done

Management VMにツールを入れる

az vm run-command invoke \
  --resource-group $RG \
  --name $MGMT_VM \
  --command-id RunShellScript \
  --scripts "
sudo apt-get update
sudo apt-get install -y mysql-client curl ca-certificates lsb-release gnupg
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
"

Storageテストデータ作成

az vm run-command invoke \
  --resource-group $RG \
  --name $MGMT_VM \
  --command-id RunShellScript \
  --scripts "
az login --identity
echo 'hello from private storage' > /tmp/sample.txt

az storage container create \
  --account-name $STORAGE_NAME \
  --name data \
  --auth-mode login

az storage blob upload \
  --account-name $STORAGE_NAME \
  --container-name data \
  --name sample.txt \
  --file /tmp/sample.txt \
  --auth-mode login \
  --overwrite true
"

MySQL初期データ作成

az vm run-command invoke \
  --resource-group $RG \
  --name $MGMT_VM \
  --command-id RunShellScript \
  --scripts "
mysql -h $MYSQL_NAME.mysql.database.azure.com \
  -u $MYSQL_ADMIN \
  -p'$MYSQL_PASS' \
  --ssl-mode=REQUIRED \
  -e \"
CREATE DATABASE IF NOT EXISTS appdb;
USE appdb;
CREATE TABLE IF NOT EXISTS items (
  id INT AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(255) NOT NULL
);
INSERT INTO items (name) VALUES
  ('mysql-item-001'),
  ('mysql-item-002'),
  ('mysql-item-003');
\"
"

DNS確認

Storage/Mysqlの名前解決確認

az vm run-command invoke \
  --resource-group $RG \
  --name $MGMT_VM \
  --command-id RunShellScript \
  --scripts "
nslookup $STORAGE_NAME.blob.core.windows.net
"
az vm run-command invoke \
  --resource-group $RG \
  --name $MGMT_VM \
  --command-id RunShellScript \
  --scripts "
nslookup $MYSQL_NAME.mysql.database.azure.com
"

VM1 / VM2単体確認

az vm run-command invoke \
  --resource-group $RG \
  --name $VM1 \
  --command-id RunShellScript \
  --scripts "
curl -s http://localhost:$APP_PORT/health
"
az vm run-command invoke \
  --resource-group $RG \
  --name $VM2 \
  --command-id RunShellScript \
  --scripts "
curl -s http://localhost:$APP_PORT/health
"

Internal Load Balancer確認

az vm run-command invoke \
  --resource-group $RG \
  --name $MGMT_VM \
  --command-id RunShellScript \
  --scripts "
curl -s http://$ILB_PRIVATE_IP:$APP_PORT/health
"

Application Gateway Backend Health確認

az network application-gateway show-backend-health \
  --resource-group $RG \
  --name $APPGW_NAME

Application Gateway Public IP確認(ブラウザ確認)

Internet → Application Gateway → Internal Load Balancer → VM1 / VM2 の経路でWeb画面を確認

APPGW_PUBLIC_IP=$(az network public-ip show \
  --resource-group $RG \
  --name pip-appgw \
  --query ipAddress \
  -o tsv)

echo "https://$APPGW_PUBLIC_IP"

Bation経由でManegementVmに入る

az network bastion update \
  --name $BASTION_NAME \
  --resource-group $RG \
  --location $LOCATION \
  --sku name=Standard \
  --enable-tunneling true \
  --enable-ip-connect true
  
MGMT_VM_ID=$(az vm show \
  --resource-group $RG \
  --name $MGMT_VM \
  --query id \
  -o tsv)

az network bastion ssh \
  --name $BASTION_NAME \
  --resource-group $RG \
  --target-resource-id $MGMT_VM_ID \
  --auth-type password \
  --username $ADMIN_USER

ManegementVmから各VMに入る

ssh azureuser@10.1.10.4

疑問

なぜAppSubnetとManagementSubnetのRoute Tableを別々に作っているのか

実務では、AppSubnetとManagementSubnetで通信要件が変わりやすいです。

AppSubnet

  • VM1 / VM2の外向き通信をFirewallへ集約
  • PaaSへのPrivate Endpoint通信
  • App Gateway / Load Balancerからの受信

ManagementSubnet

  • 管理用VMからVM1 / VM2へSSH
  • Storage / MySQL調査
  • Azure CLI / apt / curl用の外向き通信
  • 将来的にオンプレミス、監視基盤、運用端末への通信

Route Tableは共通機能なのにHub内に入れなくていいのか

Route TableはHub VNetの中に配置するサービスではありません。
Route TableはAzureリソースとしてResource Groupに作成し、Subnetに関連付けるものです。
つまり、構造としてはこうです。

Resource Group
├─ Route Table
│  └─ 0.0.0.0/0 → Azure Firewall
│
├─ Hub VNet
│  └─ ManagementSubnet
│     └─ Route Tableを関連付け
│
└─ Spoke VNet
   └─ AppSubnet
      └─ Route Tableを関連付け

NSGは共通機能なのにHub内に入れなくていいのか

NSGもHub VNetの中に配置して全体を守るものではありません。
NSGは、SubnetまたはNICに関連付けて、その場所の通信を許可・拒否するものです。

Hub VNet
├─ AzureBastionSubnet
│  └─ Bastion用NSGが必要ならここに関連付け
│
└─ ManagementSubnet
   └─ nsg-management-subnet

Spoke VNet
└─ AppSubnet
   └─ nsg-app-subnet

なぜDNS ZoneをHubにもSpokeにも入れる必要があるのか

正確には、DNS ZoneをHubとSpokeの両方に“入れる”のではありません。
正しくは、
Private DNS Zoneは1つ作る
その1つのPrivate DNS ZoneをHub VNetとSpoke VNetの両方にリンクする

Private DNS Zone
privatelink.blob.core.windows.net
        │
        ├─ Link → Hub VNet
        └─ Link → Spoke VNet

PeeringがあるからDNSも自動で使える、という理解は危険です。
Peeringは主にネットワーク到達性の仕組みであり、Private DNS ZoneをどのVNetから解決できるかは、基本的にPrivate DNS Zone Linkで決まります。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?