こしあん
2025-03-08

AWS Network ManagerのReachability Analyzerを試す


7{icon} {views}


AWSのReachability Analyzerを使うと、NATゲートウェイやネットワークACLなどの通信経路を自動解析して接続障害の原因を即座に突き止められる。プライベートNATの誤設定やACLのブロックなど、セキュリティ設定の見落としも簡単に見つかり、Session Managerの接続トラブルも素早く解消できる。

はじめに

  • 「よくわからないけどVPC内でネットワークが通じない!」ってときに使えるReachability Analyzerを試してみた
  • これはTransit GatewayやVPNとの通信も含めてなんでもできる機能(わざわざEC2立ててtracerouteみたいなことしなくていい)で、割と便利そうなので試してみた

作るもの

怪しいVPCを作る

「あれ?全然怪しくないじゃん」と思うかもしれないが、パブリックサブネットにあるNAT GatewayはプライベートNATゲートウェイである(Elastic IPをNATゲートウェイに割り当てない)。これではインターネットに出ていけるわけがない

# Public route table
resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.igw.id
  }

  route {
    ipv6_cidr_block = "::/0"
    gateway_id      = aws_internet_gateway.igw.id
  }

  tags = {
    Name = "${var.vpc_name}-public-route-table"
  }
}

# # Elastic IPの割り当て(NATゲートウェイ用)
# resource "aws_eip" "nat" {
#   domain = "vpc"

#   tags = {
#     Name = "${var.vpc_name}-nat-eip"
#   }
# }

# NATゲートウェイ(パブリックサブネットの1個目のAZのみおく。あえてPrivateにしてみる)
resource "aws_nat_gateway" "nat_gw" {
  connectivity_type = "private"
  subnet_id     = aws_subnet.public[0].id

  tags = {
    Name = "${var.vpc_name}-nat"
  }
}

このぐらいだったらコード見れば一発だが、間にサブネットがもう一個入ると(例:Network Firewall)なにがなんだかわからなくなるので、ポチポチでネットワークをデバッグしてくれるのは非常に便利。

この怪しいVPCのプライベートサブネットにEC2を作ると、セキュリティグループやIAMロールで付与しているのにSession Managerに接続できないとなる。「なんで?」となるのがスタートポイント。

Network Manager

そもそもマネジメントコンソールのどこにあるのか?

VPCのダッシュボードの横にある(サイドバーではなくてわかりづらい)

あるいは「Network Manager」で検索する(そもそも名前を知らないと検索しようがない)

Network ManagerはVPCの一機能のように見えるが、その中にいろいろ機能を持っている。Systems Managerほどの知名度がないのが不思議

ここから「Reachablity Analyzer」を使う

パスの作成と分析

こんな感じでパスの送信先と送信元を選ぶ。この例はインターネットゲートウェイからインスタンス(逆も設定可能)

送信元としてはこんなのが対応している。Transit Gatewayの通信解析で使うのがメインだと思う。SSH入ってデバッグしようがないENIを起点にして切り分けられるのも良さげ

実行結果(NATゲートウェイがプライベート)

NATゲートウェイがプライベートの場合。インターネットから入っても出ていけないのであるが以下のような結果になる。

インターネット→インスタンス

明らかに「Elastic IPがないのが原因です」と言っている。NATゲートウェイに対するElastic IPではなく、EC2に対するElastic IPがないのが原因と見ているようだ。

インスタンス→インターネット

インスタンスから見ても「パブリックIPを追加しろ」と言ってくる。この程度の簡単なものだとどちらから見てもデバッグできるようだ。

実行結果(NATゲートウェイがパブリック)

Elastic IPを割り当てて正常系で実行してみる

# # Elastic IPの割り当て(NATゲートウェイ用)
resource "aws_eip" "nat" {
  domain = "vpc"

  tags = {
    Name = "${var.vpc_name}-nat-eip"
  }
}

# NATゲートウェイ(パブリックサブネットの1個目のAZのみおく。あえてPrivateにしてみる)
resource "aws_nat_gateway" "nat_gw" {
  allocation_id = aws_eip.nat.id
  subnet_id     = aws_subnet.public[0].id

  tags = {
    Name = "${var.vpc_name}-nat"
  }
}

インターネット→インスタンス

失敗しているがこれは正常な挙動で、NATゲートウェイで接続しているプライベートサブネットにあるEC2インスタンスはそもそもインターネットからアクセスできたらおかしいため。

インスタンス→インターネット

ここが改善ポイントで、インスタンスからインターネットに出て行けている。アベイラビリティゾーンをまたいだ場合も検知できているようだ。

NATの部分が正常になったので、Session Managerが接続できるようになった。

ネットワークACLをいじってみる(異常系)

正常系にしたあと、ネットワークACLで全ブロックしてみる。意地悪してインバウンドを全拒否したらどうだろうか?(メッセージに差分が出るのか)

おっ、NACLが原因だと言っている。やるじゃん!

全体コード

怪しいVPCを作るためのコード

variable "vpc_name" {
  type        = string
  description = "Name of VPC"
  default     = "strange-vpc"
}

variable "vpc_cidr_block" {
  type        = string
  description = "CIDR block of VPC"
  default = "10.10.0.0/20"
}

variable "availability_zones" {
  type        = list(string)
  description = "List of availability zones"
  default     = ["ap-northeast-1a", "ap-northeast-1c", "ap-northeast-1d"]
}

# Define a VPC
resource "aws_vpc" "main" {
  cidr_block                       = var.vpc_cidr_block
  enable_dns_support               = true
  enable_dns_hostnames             = true
  assign_generated_ipv6_cidr_block = true

  tags = {
    Name = var.vpc_name
  }
}

# Internet gateway
resource "aws_internet_gateway" "igw" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "${var.vpc_name}-igw"
  }
}

# Public subnet
# (10.0.0.0:20 -> [10.0.0.0:24, 10.0.1.0:24, 10.0.2.0:24])
resource "aws_subnet" "public" {
  count                           = length(var.availability_zones)
  vpc_id                          = aws_vpc.main.id
  cidr_block                      = cidrsubnet(aws_vpc.main.cidr_block, 4, count.index)
  ipv6_cidr_block                 = cidrsubnet(aws_vpc.main.ipv6_cidr_block, 8, count.index)
  availability_zone               = element(var.availability_zones, count.index)
  map_public_ip_on_launch         = false
  assign_ipv6_address_on_creation = true

  tags = {
    Name = "${var.vpc_name}-public-${count.index + 1}"
  }
}

# Private subnet
# (10.0.0.0:20 -> [10.0.4.0:23, 10.0.6.0:23, 10.0.8.0:23])
resource "aws_subnet" "private" {
  count                           = length(var.availability_zones)
  vpc_id                          = aws_vpc.main.id
  cidr_block                      = cidrsubnet(aws_vpc.main.cidr_block, 3, 2 + count.index)
  ipv6_cidr_block                 = cidrsubnet(aws_vpc.main.ipv6_cidr_block, 8, 4 + count.index)
  availability_zone               = element(var.availability_zones, count.index)
  map_public_ip_on_launch         = false
  assign_ipv6_address_on_creation = true

  tags = {
    Name = "${var.vpc_name}-private-${count.index + 1}"
  }
}

# Public route table
resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.igw.id
  }

  route {
    ipv6_cidr_block = "::/0"
    gateway_id      = aws_internet_gateway.igw.id
  }

  tags = {
    Name = "${var.vpc_name}-public-route-table"
  }
}

# # Elastic IPの割り当て(NATゲートウェイ用)
# resource "aws_eip" "nat" {
#   domain = "vpc"

#   tags = {
#     Name = "${var.vpc_name}-nat-eip"
#   }
# }

# NATゲートウェイ(パブリックサブネットの1個目のAZのみおく。あえてPrivateにしてみる)
resource "aws_nat_gateway" "nat_gw" {
  connectivity_type = "private"
  subnet_id     = aws_subnet.public[0].id

  tags = {
    Name = "${var.vpc_name}-nat"
  }
}

resource "aws_route_table_association" "public_assoc" {
  count          = length(aws_subnet.public)
  subnet_id      = aws_subnet.public[count.index].id
  route_table_id = aws_route_table.public.id
}


# Private route table
resource "aws_route_table" "private" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.nat_gw.id
  }

  tags = {
    Name = "${var.vpc_name}-private-route-table"
  }
}

resource "aws_route_table_association" "private_assoc" {
  count          = length(aws_subnet.private)
  subnet_id      = aws_subnet.private[count.index].id
  route_table_id = aws_route_table.private.id
}

テスト用のEC2

# Amazon Linux 2023
data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["137112412989"] # AmazonのAMI所有者ID

  filter {
    name = "name"
    # Amazon Linux 2023 AMIの名前パターン。minimumを除外する
    values = ["al2023-ami-2023*-kernel-*-x86_64"]
  }

  filter {
    name   = "architecture"
    values = ["x86_64"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
}

# Security Group for Sesson Mangaer Instances
resource "aws_security_group" "ssm_instance_sg" {
  name        = "ssm-instance-sg"
  description = "Security group for session manager instances"
  vpc_id      = aws_vpc.main.id

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
    description = "Allow all outbound traffic"
  }

  tags = {
    Name = "ssm-instance-sg"
  }
}

# Session Manager 利用用IAMロールとインスタンスプロファイル
resource "aws_iam_role" "ec2_ssm_role" {
  name = "ec2-session-manager-role"
  assume_role_policy = jsonencode({
    Version = "2012-10-17",
    Statement = [{
      Effect = "Allow",
      Principal = {
        Service = "ec2.amazonaws.com"
      },
      Action = "sts:AssumeRole"
    }]
  })
}

resource "aws_iam_role_policy_attachment" "ssm_attach" {
  role       = aws_iam_role.ec2_ssm_role.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}

resource "aws_iam_instance_profile" "ec2_instance_profile" {
  name = "ec2-instance-profile"
  role = aws_iam_role.ec2_ssm_role.name
}


# EC2 Instance to test the connection
resource "aws_instance" "test_instance" {
  for_each = { 
    for idx, subnet in aws_subnet.private : "private-${idx}" => subnet.id 
  }

  ami                    = data.aws_ami.amazon_linux.id
  instance_type          = "t3.micro"
  subnet_id              = each.value
  vpc_security_group_ids = [aws_security_group.ssm_instance_sg.id]
  iam_instance_profile   = aws_iam_instance_profile.ec2_instance_profile.name

  tags = {
    Name = "test-instance-${each.key}"
  }
}

所感

  • 普通に便利そう
  • なんか割と便利な機能なのに、AWSの資格試験だとANSになって初めて知った。もっと前のやつで聞いてきても良かったのに


Shikoan's ML Blogの中の人が運営しているサークル「じゅ~しぃ~すくりぷと」の本のご案内

技術書コーナー

北海道の駅巡りコーナー


Add a Comment

メールアドレスが公開されることはありません。 が付いている欄は必須項目です