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つ立てます。
構成図
構成方針
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で決まります。
