Site to Site VPN で Network Synthetic Monitor をサクッと試してみた。
はじめに
皆様こんにちは、あかいけです。
あなたは Network Synthetic Monitor というサービスを知っていますか?
私は AWS Summit 2025 のセッション、
「AWS-01 外部ネットワークとクラウドのつなぎ方 再入門」で知りました。
資料を見た感じだと、どうやら DirectConnect や Site to Site VP など、 AWS とオンプレミス間の通信においてグレー障害の検知をする目的で利用できるようです。
今回はこのサービスを Site to Site VPN でサクッと触ってみました。
なお記事内で使用している資材は以下リポジトリに格納しているので、お気軽にご利用ください。
グレー障害ってなんだろう。
さて、当たり前のようにグレー障害というワードが出てきました。
先ほどの資料から抜粋してみると以下を意味するようです。
- グレー障害とは
- サービスが明確に停止するわけではないが、一時的に機能が失われたり
パフォーマンスが低下する障害。検出が難しいケースがある - ネットワークの場合、レイテンシーの増加や断続的なパケットロスなど
- サービスが明確に停止するわけではないが、一時的に機能が失われたり
前述の通り検出が難しい障害なので一見すると障害原因が見つからず、
実際に遭遇した場合にノーヒントだと調査が難航する予感がします。
Network Synthetic Monitor ってなんだろう。
次に Network Synthetic Monitor についてですが、
これは AWS Direct Connect や AWS Site-to-Site VPN に対してグレー障害の検知を可能とするメトリクスやダッシュボードを提供するサービスです。
そして提供されるメトリクスに対して CloudWatch アラームを設定すれば、グレー障害の検知が可能となるわけです。
次にNetwork Synthetic Monitorは以下の要素で構成されています。
プローブから宛先に送信される通信をメトリクスとして表示してくれるわけですね。
- プローブ:AWSホストからオンプレミスの宛先IPアドレスに送信されるトラフィック測定用のテスト通信
- モニター:プローブが測定するネットワークパフォーマンスとヘルス情報を表示する管理画面
- AWS ネットワークソース:プローブの送信元となるAWS側のリソース(VPC内のサブネット)
- 宛先:プローブのターゲットとなるオンプレミスネットワーク内のIPアドレス
さらに詳しい仕様については以下の資料が大変わかりやすかったので、よければご参照ください。
今回の構成
残念ながら DirectConnect は手軽に利用できないため、今回は Site to Site VPN を利用します。
プローブはプライベートサブネットにそれぞれ一つずつ作成しており、監視対象としてVPNサーバーを指定して、通信方式としてPINGを設定しています。
またローカル側ではVPNサーバーをDockerコンテナ上で作成しています。
AWS 側リソース
今回は Terraform で設定してみます。
色々リソースを定義していますが、Network Synthetic Monitorに関連するリソースは以下です。
- aws_networkmonitor_monitor:モニターの定義
- aws_networkmonitor_probe:プローブの定義
terraform {
required_version = ">= 1.0.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 6.0"
}
http = {
source = "hashicorp/http"
version = ">= 3.0"
}
}
}
provider "aws" {
region = var.aws_region
}
data "aws_availability_zones" "available" {
state = "available"
}
data "http" "global_ip" {
url = "https://ipv4.icanhazip.com/"
}
locals {
home_global_ip = chomp(data.http.global_ip.body)
}
# VPC
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.project_name}-vpc"
}
}
# Private Subnets
resource "aws_subnet" "private" {
for_each = {
subnet_1 = {
cidr_block = var.private_subnet_1_cidr
availability_zone = data.aws_availability_zones.available.names[0]
name_suffix = "1"
}
subnet_2 = {
cidr_block = var.private_subnet_2_cidr
availability_zone = data.aws_availability_zones.available.names[1]
name_suffix = "2"
}
}
vpc_id = aws_vpc.main.id
cidr_block = each.value.cidr_block
availability_zone = each.value.availability_zone
tags = {
Name = "${var.project_name}-private-subnet-${each.value.name_suffix}"
}
}
# Route table for private subnets
resource "aws_route_table" "private" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.project_name}-private-rt"
}
}
# Route table associations
resource "aws_route_table_association" "private" {
for_each = aws_subnet.private
subnet_id = each.value.id
route_table_id = aws_route_table.private.id
}
# Virtual Private Gateway
resource "aws_vpn_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.project_name}-vgw"
}
}
# Route for VPN Gateway
resource "aws_route" "vpn" {
route_table_id = aws_route_table.private.id
destination_cidr_block = var.on_premises_cidr
gateway_id = aws_vpn_gateway.main.id
}
# Customer Gateway (represents on-premises side)
resource "aws_customer_gateway" "main" {
bgp_asn = 65000
ip_address = local.home_global_ip
type = "ipsec.1"
tags = {
Name = "${var.project_name}-cgw"
}
}
# VPN Connection
resource "aws_vpn_connection" "main" {
vpn_gateway_id = aws_vpn_gateway.main.id
customer_gateway_id = aws_customer_gateway.main.id
type = "ipsec.1"
static_routes_only = true
tags = {
Name = "${var.project_name}-vpn"
}
}
# VPN Connection Route
resource "aws_vpn_connection_route" "on_premises" {
vpn_connection_id = aws_vpn_connection.main.id
destination_cidr_block = var.on_premises_cidr
}
# AWS Network Monitor
resource "aws_networkmonitor_monitor" "main" {
monitor_name = "${var.project_name}-network-monitor"
aggregation_period = 60
tags = {
Name = "${var.project_name}-network-monitor"
}
}
# AWS Network Monitor Probes for PING monitoring
resource "aws_networkmonitor_probe" "ping_probes" {
for_each = aws_subnet.private
monitor_name = aws_networkmonitor_monitor.main.monitor_name
destination = var.monitoring_target_ip
protocol = "ICMP"
packet_size = 56
source_arn = each.value.arn
tags = {
Name = "${var.project_name}-ping-probe-${split("_", each.key)[1]}"
}
}
variable "aws_region" {
description = "AWS region"
type = string
default = "ap-northeast-1"
}
variable "project_name" {
description = "Project name used for resource naming"
type = string
default = "network-synthetic-monitor"
}
variable "vpc_cidr" {
description = "CIDR block for VPC"
type = string
default = "10.0.0.0/16"
}
variable "private_subnet_1_cidr" {
description = "CIDR block for private subnet 1"
type = string
default = "10.0.0.0/24"
}
variable "private_subnet_2_cidr" {
description = "CIDR block for private subnet 2"
type = string
default = "10.0.1.0/24"
}
variable "on_premises_cidr" {
description = "CIDR block for on-premises network"
type = string
}
variable "monitoring_target_ip" {
description = "IP address of the on-premises resource to monitor via PING"
type = string
}
output "home_global_ip" {
value = local.home_global_ip
}
output "vpc_cidr" {
value = aws_vpc.main.cidr_block
}
output "vpn_connection_tunnel_1_address" {
description = "Public IP address of VPN tunnel 1"
value = aws_vpn_connection.main.tunnel1_address
}
output "vpn_connection_tunnel_2_address" {
description = "Public IP address of VPN tunnel 2"
value = aws_vpn_connection.main.tunnel2_address
}
output "vpn_connection_tunnel_1_preshared_key" {
description = "Preshared key for VPN tunnel 1"
value = aws_vpn_connection.main.tunnel1_preshared_key
sensitive = true
}
output "vpn_connection_tunnel_2_preshared_key" {
description = "Preshared key for VPN tunnel 2"
value = aws_vpn_connection.main.tunnel2_preshared_key
sensitive = true
}
次にローカル側で利用するIPを環境変数として定義して、tfvarsを作成します。
任意の値で問題ないですが、後ほどdocker-compose.ymlで指定するのと同じものを設定します。
- DOCKER_CIDR:Dockerコンテナで利用するネットワークCIDR
- STRONGSWAN_IP:VPNサーバーで利用するIPアドレス
DOCKER_CIDR=192.168.56.0/24
STRONGSWAN_IP=192.168.56.100
cat <<EOF > terraform.tfvars
on_premises_cidr=$DOCKER_CIDR
monitoring_target_ip=$STRONGSWAN_IP
EOF
次にリソースを作成します。
terraform init;
terraform apply;
リソース作成後に、VPNサーバーの設定ファイルで利用する値を確認しておきます。
terraform output -json;
おうち側リソース
次におうち側で VPN サーバー用の Docker コンテナを起動します。
今回VPNサーバーはIPsec VPNソフトウェアのstrongswanを利用します。
FROM ubuntu:jammy
RUN apt-get update && apt-get install -y strongswan iproute2 inetutils-ping curl traceroute mtr iperf3
COPY conf/ipsec.conf /etc/ipsec.conf
COPY conf/ipsec.secrets /etc/ipsec.secrets
CMD ["ipsec", "start", "--nofork"]
version: "3.8"
services:
strongswan:
build: .
container_name: strongswan
cap_add:
- NET_RAW
- NET_ADMIN
- SYS_ADMIN
- SYS_MODULE
sysctls:
net.ipv4.ip_forward: 1
net.ipv6.conf.all.forwarding: 1
networks:
mynet:
ipv4_address: ${STRONGSWAN_IP}
deploy:
resources:
limits:
cpus: "1"
memory: "1024M"
networks:
mynet:
driver: bridge
ipam:
config:
- subnet: ${DOCKER_CIDR}
次にstrongswan用の設定ファイルを作成します。
環境変数には terraform output で確認した実際の値を設定してください。
- HOME_GLOBAL_IP:オンプレミス側のグローバルIPアドレス
- AWS_CIDR_BLOCK:VPCのIP CIDR
- AWS_VPN_GLOBAL_IP_1:Site to Site VPNのトンネルのグローバルIPアドレス(1)
- AWS_VPN_GLOBAL_IP_2:Site to Site VPNのトンネルのグローバルIPアドレス(2)
- PRE_SHARED_KEY_1:Site to Site VPNのトンネルの事前共有キー (PSK) (1)
- PRE_SHARED_KEY_2:Site to Site VPNのトンネルの事前共有キー (PSK) (2)
DOCKER_CIDR=192.168.56.0/24
STRONGSWAN_IP=192.168.56.100
HOME_GLOBAL_IP=XXX.XXX.XXX.XXX
AWS_CIDR_BLOCK=XXX.XXX.XXX.XXX
AWS_VPN_GLOBAL_IP_1=XXX.XXX.XXX.XXX
AWS_VPN_GLOBAL_IP_2=XXX.XXX.XXX.XXX
PRE_SHARED_KEY_1=XXXXXXXXX-XXXXXXXXX
PRE_SHARED_KEY_2=XXXXXXXXX-XXXXXXXXX
- ./docker/.env
cat <<EOF > ./docker/.env
DOCKER_CIDR=$DOCKER_CIDR
STRONGSWAN_IP=$STRONGSWAN_IP
EOF
- ./docker/conf/ipsec.conf
cat <<EOF > ./docker/conf/ipsec.conf
config setup
charondebug="ike 2, knl 2, cfg 2"
conn aws-tunnel1
type=tunnel
auto=start
keyexchange=ikev2
authby=psk
leftid=$HOME_GLOBAL_IP
leftsubnet=$DOCKER_CIDR
right=$AWS_VPN_GLOBAL_IP_1
rightsubnet=$AWS_CIDR_BLOCK
aggressive=no
ikelifetime=28800s
lifetime=3600s
margintime=270s
rekey=yes
rekeyfuzz=100%
fragmentation=yes
replay_window=1024
dpddelay=30s
dpdtimeout=120s
dpdaction=restart
ike=aes128-sha1-modp1024
esp=aes128-sha1-modp1024
keyingtries=%forever
conn aws-tunnel2
type=tunnel
auto=start
keyexchange=ikev2
authby=psk
leftid=$HOME_GLOBAL_IP
leftsubnet=$DOCKER_CIDR
right=$AWS_VPN_GLOBAL_IP_2
rightsubnet=$AWS_CIDR_BLOCK
aggressive=no
ikelifetime=28800s
lifetime=3600s
margintime=270s
rekey=yes
rekeyfuzz=100%
fragmentation=yes
replay_window=1024
dpddelay=30s
dpdtimeout=120s
dpdaction=restart
ike=aes128-sha1-modp1024
esp=aes128-sha1-modp1024
keyingtries=%forever
EOF
- ./docker/conf/ipsec.secrets
cat <<EOF > ./docker/conf/ipsec.secrets
$HOME_GLOBAL_IP $AWS_VPN_GLOBAL_IP_1 : PSK $PRE_SHARED_KEY_1
$HOME_GLOBAL_IP $AWS_VPN_GLOBAL_IP_2 : PSK $PRE_SHARED_KEY_2
EOF
次にコンテナを作成します。
docker-compose up --build -d
作成完了後、以下のコマンドでVPN接続のステータスを確認します。
Security Associations (2 up, 0 connecting)
と出力されれば、問題なくVPN接続ができています。
docker exec -it strongswan /bin/bash ipsec status
念の為マスクしていますが、XXXがオンプレミス側のグローバルIP、YYYがトンネル1のグローバルIP、ZZZがトンネル2のグローバルIPです。
Security Associations (2 up, 0 connecting):
aws-tunnel2[2]: ESTABLISHED 50 seconds ago, 192.168.56.100[XXX.XXX.XXX.XXX]...YYY.YYY.YYY.YYY[YYY.YYY.YYY.YYY]
aws-tunnel2{1}: INSTALLED, TUNNEL, reqid 1, ESP in UDP SPIs: c3111d76_i c8b534ba_o
aws-tunnel2{1}: 192.168.56.0/24 === 10.0.0.0/16
aws-tunnel1[1]: ESTABLISHED 50 seconds ago, 192.168.56.100[XXX.XXX.XXX.XXX]...ZZZ.ZZZ.ZZZ.ZZZ[ZZZ.ZZZ.ZZZ.ZZZ]
aws-tunnel1{2}: INSTALLED, TUNNEL, reqid 1, ESP in UDP SPIs: c22ee768_i c5417fd4_o
aws-tunnel1{2}: 192.168.56.0/24 === 10.0.0.0/16
モニタリングしてみた。
メトリクス
以下はデフォルトで作成されるメトリクスの一覧で、これがプローブごとに作成されます。
- HealthIndicator:AWS ネットワークが正常であれば0、低下している場合は1となる
- PacketLoss:パケットロスをパーセント値で表す
- RTT:ラウンドトリップ時間をマイクロ秒単位で表す
ダッシュボード
デフォルトでダッシュボードが用意されており、ネットワーク合成モニターの画面から確認できます。
ここでは前述のメトリクスがダッシュボードとして表示されます。
AWS ネットワークヘルスインジケータ
パケット損失
ラウンドトリップタイム (RTT)
アラート
最後にパケットロス率のメトリクスに対してアラートを作成してみます。
以下の条件で設定します。
- 閾値:50% 以上
- アラームを実行するデータポイント:1/1
生成後、しばらく経ってからアラームのステータスがOKにいれば問題ありません。
テストとしてVPNサーバー側を落とします。
docker-compose down
大体3~4分ぐらいでアラームが発生しました。
当然コンテナ自体が落ちているので、パケットロス率は100%になっていますね。
ダッシュボード側でもアラームがカウントされています。
さいごに
以上、Site to Site VPN で Network Synthetic Monitor をサクッと試してみました。
正直私はグレー障害の監視に謎のハードルを感じていましたが、比較的簡単に実装できる良いサービスだと思いました。
ただし、コスト面について以下の部分で発生するため、予算と相談した上で採用するか検討する必要がありそうです。
特にプローブの料金は決して安いとは言えないと思います。
(比較例:東京リージョンのEC2/Linux/m7g.large/オンデマンドがUSD 0.1054/時間)
- プローブの料金:プローブを配置するサブネットごとに、USD 0.11/時間
- CloudWatch メトリクスの料金
今回はSite to Site VPNでの検証でしたが、DirectConnectでも同様の流れで設定できると思うので、ハイブリッド環境でネットワークの安定性に課題を感じている方は一度試してみる価値があるサービスだと感じました。
特に従来は検出が困難だったグレー障害を可視化できる点は、運用面で大きなメリットになりそうです。