Site to Site VPN で Network Synthetic Monitor をサクッと試してみた。

Site to Site VPN で Network Synthetic Monitor をサクッと試してみた。

2025.09.03

はじめに

皆様こんにちは、あかいけです。

あなたは Network Synthetic Monitor というサービスを知っていますか?
私は AWS Summit 2025 のセッション、
「AWS-01 外部ネットワークとクラウドのつなぎ方 再入門」で知りました。

https://pages.awscloud.com/rs/112-TZM-766/images/AWS-01_Networking_AWS-Summit-JP-2025.pdf

資料を見た感じだと、どうやら DirectConnect や Site to Site VP など、 AWS とオンプレミス間の通信においてグレー障害の検知をする目的で利用できるようです。
今回はこのサービスを Site to Site VPN でサクッと触ってみました。

なお記事内で使用している資材は以下リポジトリに格納しているので、お気軽にご利用ください。

https://github.com/Lamaglama39/network-synthetic-monitor-vpn

グレー障害ってなんだろう。

さて、当たり前のようにグレー障害というワードが出てきました。
先ほどの資料から抜粋してみると以下を意味するようです。

  • グレー障害とは
    • サービスが明確に停止するわけではないが、一時的に機能が失われたり
      パフォーマンスが低下する障害。検出が難しいケースがある
    • ネットワークの場合、レイテンシーの増加や断続的なパケットロスなど

前述の通り検出が難しい障害なので一見すると障害原因が見つからず、
実際に遭遇した場合にノーヒントだと調査が難航する予感がします。

Network Synthetic Monitor ってなんだろう。

次に Network Synthetic Monitor についてですが、
これは AWS Direct Connect や AWS Site-to-Site VPN に対してグレー障害の検知を可能とするメトリクスやダッシュボードを提供するサービスです。

そして提供されるメトリクスに対して CloudWatch アラームを設定すれば、グレー障害の検知が可能となるわけです。

https://aws.amazon.com/jp/blogs/news/monitor-hybrid-connectivity-with-amazon-cloudwatch-network-monitor/

次にNetwork Synthetic Monitorは以下の要素で構成されています。
プローブから宛先に送信される通信をメトリクスとして表示してくれるわけですね。

  • プローブ:AWSホストからオンプレミスの宛先IPアドレスに送信されるトラフィック測定用のテスト通信
  • モニター:プローブが測定するネットワークパフォーマンスとヘルス情報を表示する管理画面
  • AWS ネットワークソース:プローブの送信元となるAWS側のリソース(VPC内のサブネット)
  • 宛先:プローブのターゲットとなるオンプレミスネットワーク内のIPアドレス

さらに詳しい仕様については以下の資料が大変わかりやすかったので、よければご参照ください。

https://speakerdeck.com/yukimmmm/amazon-cloudwatch-network-monitor-dao-ru-gaido-demoshuo-ming-fu-ki

今回の構成

残念ながら DirectConnect は手軽に利用できないため、今回は Site to Site VPN を利用します。

Network-Synthetic-Monitor-image.drawio

プローブはプライベートサブネットにそれぞれ一つずつ作成しており、監視対象としてVPNサーバーを指定して、通信方式としてPINGを設定しています。
またローカル側ではVPNサーバーをDockerコンテナ上で作成しています。

AWS 側リソース

今回は Terraform で設定してみます。
色々リソースを定義していますが、Network Synthetic Monitorに関連するリソースは以下です。

  • aws_networkmonitor_monitor:モニターの定義
  • aws_networkmonitor_probe:プローブの定義
main.tf
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]}"
  }
}
variables.tf
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
}
outputs.tf
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を利用します。

https://github.com/strongswan/strongswan

Dockerfile
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"]
docker-compose.yml
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:ラウンドトリップ時間をマイクロ秒単位で表す

スクリーンショット 2025-09-01 3.45.40

ダッシュボード

デフォルトでダッシュボードが用意されており、ネットワーク合成モニターの画面から確認できます。
ここでは前述のメトリクスがダッシュボードとして表示されます。

AWS ネットワークヘルスインジケータ

スクリーンショット 2025-09-01 3.36.34

パケット損失

スクリーンショット 2025-09-01 3.36.47

ラウンドトリップタイム (RTT)

スクリーンショット 2025-09-01 3.36.57

アラート

最後にパケットロス率のメトリクスに対してアラートを作成してみます。

スクリーンショット 2025-09-01 3.50.09

以下の条件で設定します。

  • 閾値:50% 以上
  • アラームを実行するデータポイント:1/1

スクリーンショット 2025-09-01 3.52.37

スクリーンショット 2025-09-01 3.53.46

生成後、しばらく経ってからアラームのステータスがOKにいれば問題ありません。

スクリーンショット 2025-09-01 3.57.13

テストとしてVPNサーバー側を落とします。

docker-compose down

大体3~4分ぐらいでアラームが発生しました。
当然コンテナ自体が落ちているので、パケットロス率は100%になっていますね。

スクリーンショット 2025-09-01 4.04.00

ダッシュボード側でもアラームがカウントされています。

スクリーンショット 2025-09-01 4.04.13

さいごに

以上、Site to Site VPN で Network Synthetic Monitor をサクッと試してみました。
正直私はグレー障害の監視に謎のハードルを感じていましたが、比較的簡単に実装できる良いサービスだと思いました。

ただし、コスト面について以下の部分で発生するため、予算と相談した上で採用するか検討する必要がありそうです。
特にプローブの料金は決して安いとは言えないと思います。
(比較例:東京リージョンのEC2/Linux/m7g.large/オンデマンドがUSD 0.1054/時間)

  • プローブの料金:プローブを配置するサブネットごとに、USD 0.11/時間
  • CloudWatch メトリクスの料金

https://aws.amazon.com/jp/cloudwatch/pricing/

今回はSite to Site VPNでの検証でしたが、DirectConnectでも同様の流れで設定できると思うので、ハイブリッド環境でネットワークの安定性に課題を感じている方は一度試してみる価値があるサービスだと感じました。
特に従来は検出が困難だったグレー障害を可視化できる点は、運用面で大きなメリットになりそうです。

この記事をシェアする

facebookのロゴhatenaのロゴtwitterのロゴ

© Classmethod, Inc. All rights reserved.