VPC Peering で SSM の VPC エンドポイントを集約してみた

2023.06.21

はじめに

こんにちは!体内の 6 割は水分ではなく、えびだと思うくらいえび好きな kaz です。

検証で EC2 を起動することがあるのですが、AWS 環境内のプライベートなアクセスのみに留める場合、AWS Systems Manager(以下、SSM)の Session Manager から EC2 へ接続しています。 そのとき、AWS PrivateLink を使用して SSM Session Manager の以下のような VPC エンドポイントを用意する必要があります。

  • ssm.《region》.amazonaws.com
  • ssmmessages.《region》.amazonaws.com
  • ec2messages.《region》.amazonaws.com

しかし、AWS PrivateLink の料金表 にもある通り、「各 AZ の VPC エンドポイント 1 つあたりの料金」が発生してしまいます。

複数の VPC を利用する場合などでは、その分 SSM Session Manager の VPC エンドポイントが必要です。 こうなると、コスト増や、管理の手間も増えるので「VPC エンドポイントって集約することができるのかな?」と思ったのがキッカケで検証をしてみました。

やりたいこととしては、以下のようなイメージです。

このように、ある VPC 内の「ネットワーク領域」リソースを共有(いわゆる、Shared VPC)することで、他の VPC 環境はその Shared VPC を経由して、集約された VPC エンドポイントにアクセスすることができます。 なお、SSM への通信は DNS 名を介して名前解決が行われるので、Amazon Route 53 Private Hosted Zone が必要になります(SSM 関連で必要なドメインは Shared VPC に向けます)。

やってみる

それでは、実際に AWS に各種環境を揃えてみます。 なお、AWS CLI でリソース作成をしますが、リージョンは「ap-northeast-1(東京)」とし、AWS CLI の初期設定が完了しているものとします。

また、SSM Session Manager の前提条件はこちらを確認ください。

Session Manager のセットアップ - AWS Systems Manager

Shared VPC 側のリソース作成

まずは、Shared VPC を作成します。

# Shared VPC の作成(出力された VPC ID は環境変数に代入) & DNS ホスト名を有効化
$ SHARED_VPC_ID=$( \
  aws ec2 create-vpc \
    --cidr-block 172.16.0.0/16 \
    --tag-specifications "ResourceType=vpc,Tags=[{Key=Name,Value=Shared-VPC}]" \
    --query Vpc.VpcId --output text \
) && \
aws ec2 modify-vpc-attribute \
  --vpc-id ${SHARED_VPC_ID} \
  --enable-dns-hostnames "{\"Value\":true}"

# AZ - A に Subnet を作成(出力された Subnet ID は環境変数に代入))
$ SHARED_VPC_Subnet_A_ID=$( \
  aws ec2 create-subnet \
    --vpc-id ${SHARED_VPC_ID} \
    --cidr-block 172.16.100.0/24 \
    --availability-zone ap-northeast-1a \
    --tag-specifications "ResourceType=subnet,Tags=[{Key=Name,Value=Shared-VPC-Subnet-A}]" \
    --query Subnet.SubnetId --output text \
)

# AZ - C に Subnet を作成(出力された Subnet ID は環境変数に代入))
$ SHARED_VPC_Subnet_C_ID=$( \
  aws ec2 create-subnet \
    --vpc-id ${SHARED_VPC_ID} \
    --cidr-block 172.16.200.0/24 \
    --availability-zone ap-northeast-1c \
    --tag-specifications "ResourceType=subnet,Tags=[{Key=Name,Value=Shared-VPC-Subnet-C}]" \
    --query Subnet.SubnetId --output text \
)

# EC2 インスタンス用のセキュリティグループの作成 ※インバウンドルールは設定しません
$ SHARED_VPC_SG_EC2_ID=$( \
  aws ec2 create-security-group \
    --vpc-id ${SHARED_VPC_ID} \
    --group-name Shared-VPC-SG-EC2 \
    --description "EC2 security group" \
    --tag-specifications "ResourceType=security-group,Tags=[{Key=Name,Value=Shared-VPC-SG-EC2}]" \
    --query GroupId --output text \
)

# VPC エンドポイント用のセキュリティグループの作成(出力された SG ID は環境変数に代入) & インバウンドルールを設定
$ SHARED_VPC_SG_VPCE_ID=$( \
  aws ec2 create-security-group \
    --vpc-id ${SHARED_VPC_ID} \
    --group-name Shared-VPC-SG-VPCE \
    --description "VPC Endpoint security group" \
    --tag-specifications "ResourceType=security-group,Tags=[{Key=Name,Value=Shared-VPC-SG-VPCE}]" \
    --query GroupId --output text \
) && \
aws ec2 authorize-security-group-ingress \
  --group-id ${SHARED_VPC_SG_VPCE_ID} --protocol tcp --port 443 --cidr 172.16.0.0/16

次に、SSM で必要となる 3 つの VPC エンドポイントを作成します。

# `ssm.region.amazonaws.com` エンドポイントの作成(対象サブネット: Shared-VPC-Subnet-A, Shared-VPC-Subnet-C)
$ SSM_ENDPOINT_ID=$( \
  aws ec2 create-vpc-endpoint \
    --vpc-id ${SHARED_VPC_ID} \
    --vpc-endpoint-type Interface \
    --service-name com.amazonaws.ap-northeast-1.ssm \
    --subnet-ids ${SHARED_VPC_Subnet_A_ID} ${SHARED_VPC_Subnet_C_ID} \
    --security-group-id ${SHARED_VPC_SG_VPCE_ID} \
    --tag-specifications "ResourceType=vpc-endpoint,Tags=[{Key=Name,Value=Shared-VPC-VPCE-ssm}]" \
    --query VpcEndpoint.VpcEndpointId --output text \
)

# `ssmmessages.region.amazonaws.com` エンドポイントの作成(対象サブネット: Shared-VPC-Subnet-A, Shared-VPC-Subnet-C)
$ SSMMESSAGES_ENDPOINT_ID=$( \
  aws ec2 create-vpc-endpoint \
    --vpc-id ${SHARED_VPC_ID} \
    --vpc-endpoint-type Interface \
    --service-name com.amazonaws.ap-northeast-1.ssmmessages \
    --subnet-ids ${SHARED_VPC_Subnet_A_ID} ${SHARED_VPC_Subnet_C_ID} \
    --security-group-id ${SHARED_VPC_SG_VPCE_ID} \
    --tag-specifications "ResourceType=vpc-endpoint,Tags=[{Key=Name,Value=Shared-VPC-VPCE-ssmmessages}]" \
    --query VpcEndpoint.VpcEndpointId --output text \
)

# `ec2messages.region.amazonaws.com` エンドポイントの作成(対象サブネット: Shared-VPC-Subnet-A, Shared-VPC-Subnet-C)
$ EC2MESSAGES_ENDPOINT_ID=$( \
  aws ec2 create-vpc-endpoint \
    --vpc-id ${SHARED_VPC_ID} \
    --vpc-endpoint-type Interface \
    --service-name com.amazonaws.ap-northeast-1.ec2messages \
    --subnet-ids ${SHARED_VPC_Subnet_A_ID} ${SHARED_VPC_Subnet_C_ID} \
    --security-group-id ${SHARED_VPC_SG_VPCE_ID} \
    --tag-specifications "ResourceType=vpc-endpoint,Tags=[{Key=Name,Value=Shared-VPC-VPCE-ec2messages}]" \
    --query VpcEndpoint.VpcEndpointId --output text \
)

以上で、Shared VPC で必要なリソースが揃いました。 ただ、本当に AWS 環境内の EC2 から、プライベートアクセスで SSM Session Manager が利用できるかを確認してみます。

また、EC2 から SSM サービスにアクセスするために、インスタンスプロファイルが必要になるので作成しておきます。 AWS CLI から作るのがちょっと面倒だったので、ここだけマネージメントコンソールから作成しましたw

ポリシーは、AmazonSSMManagedInstanceCore というマネージドポリシーをアタッチしておけば OK です。

それでは、プライベートサブネットに適当な EC2 インスタンスを作成します。 ※ SSM Agent がデフォルトで導入されている Amazon Linux 2023 (OS) を利用しています。

# Shared-VPC-Subnet-A に EC2 (Amazon Linux 2023) を起動(出力された EC2 ID は環境変数に代入)
# ※ `--iam-instance-profile` には、作成した Role 名を入れてください
$ SHARED_VPC_EC2_ID=$( \
  aws ec2 run-instances \
    --image-id ami-0f9816f78187c68fb \
    --instance-type t2.micro \
    --subnet-id ${SHARED_VPC_Subnet_A_ID} \
    --security-group-ids ${SHARED_VPC_SG_EC2_ID} \
    --iam-instance-profile Name="<Role名を入力>"\
    --tag-specifications \
      'ResourceType=instance,Tags=[{Key=Name,Value=Shared-VPC-EC2-Sub-A}]' \
      'ResourceType=volume,Tags=[{Key=Name,Value=Shared-VPC-EBS}]' \
    --query Instances[].InstanceId --output text \
)

EC2 を起動してからしばらくすると、以下のように SSM Fleet Manager にインスタンスが登録されているのがわかります。

ssm-03

SSM Session Manager を経由してアクセスしたい場合は、マネージメントコンソールからもできますし、AWS CLI からでもできます。

今回は、AWS CLI で接続して、接続状況などを確認してみました。

# SSM Session Manager を経由して EC2 にアクセスする
$ aws ssm start-session --target ${SHARED_VPC_EC2_ID}

Starting session with SessionId: botocore-session-1687240502-0d9ad18641597481d
sh-5.2$

# ssm-cli でエンドポイントの接続をチェック
$ sudo ssm-cli get-diagnostics --output table
...(Snip)...
├──────────────────────────────────────┼─────────┼──────────────────────────────────────────┤
│ Connectivity to ssm endpoint         │ Success │ ssm.ap-northeast-1.amazonaws.com is      │
│                                      │         │ reachable                                │
├──────────────────────────────────────┼─────────┼──────────────────────────────────────────┤
│ Connectivity to ec2messages endpoint │ Success │ ec2messages.ap-northeast-1.amazonaws.com │
│                                      │         │ is reachable                             │
├──────────────────────────────────────┼─────────┼──────────────────────────────────────────┤
│ Connectivity to ssmmessages endpoint │ Success │ ssmmessages.ap-northeast-1.amazonaws.com │
│                                      │         │ is reachable                             │
├──────────────────────────────────────┼─────────┼──────────────────────────────────────────┤
...(Snip)...

# エンドポイントのドメイン名を確認(ssm.ap-northeast-1.amazonaws.com)
$ dig +short ssm.ap-northeast-1.amazonaws.com
172.16.100.126
172.16.200.45

# エンドポイントのドメイン名を確認(ssmmessages.ap-northeast-1.amazonaws.com)
$ dig +short ssmmessages.ap-northeast-1.amazonaws.com
172.16.100.253
172.16.200.141

# エンドポイントのドメイン名を確認(ec2messages.ap-northeast-1.amazonaws.com)
$ dig +short ec2messages.ap-northeast-1.amazonaws.com
172.16.200.200
172.16.100.169

VPC エンドポイントへの接続や、ドメイン名もちゃんとアドレスを引けていることがわかります。 各エンドポイントで 2 つのアドレスを引けていることから、リージョン内での可用性も確保できていますね。

これで、Shared VPC 側の確認は終了です。

User VPC 側のリソース作成

VPC 関連と EC2 のリソース作成は、だいたいやることが同じなのでパパッと作成します。

# User VPC の作成(出力された VPC ID は環境変数に代入) & DNS ホスト名を有効化
$ USER_VPC_ID=$( \
  aws ec2 create-vpc \
    --cidr-block 192.168.0.0/16 \
    --tag-specifications "ResourceType=vpc,Tags=[{Key=Name,Value=User-VPC}]" \
    --query Vpc.VpcId --output text \
) && \
aws ec2 modify-vpc-attribute \
  --vpc-id ${USER_VPC_ID} \
  --enable-dns-hostnames "{\"Value\":true}"

# AZ - A に Subnet を作成(出力された Subnet ID は環境変数に代入))
$ USER_VPC_Subnet_A_ID=$( \
  aws ec2 create-subnet \
    --vpc-id ${USER_VPC_ID} \
    --cidr-block 192.168.100.0/24 \
    --availability-zone ap-northeast-1a \
    --tag-specifications "ResourceType=subnet,Tags=[{Key=Name,Value=User-VPC-Subnet-A}]" \
    --query Subnet.SubnetId --output text \
)

# EC2 インスタンス用のセキュリティグループの作成 & インバウンドルールを設定(Shared VPC の EC2 からの SSH 接続を許可)
$ USER_VPC_SG_EC2_ID=$( \
  aws ec2 create-security-group \
    --vpc-id ${USER_VPC_ID} \
    --group-name Shared-VPC-SG-EC2 \
    --description "EC2 security group" \
    --tag-specifications "ResourceType=security-group,Tags=[{Key=Name,Value=User-VPC-SG-EC2}]" \
    --query GroupId --output text \
) && \
aws ec2 authorize-security-group-ingress \
  --group-id ${USER_VPC_SG_EC2_ID} --protocol tcp --port 22 --cidr 172.16.0.0/16

# Shared VPC リソースに関連付けた Shared-VPC-SG-VPCE のセキュリティグループ(インバウンドルール)に User VPC の CIDR 範囲を許可します
$ aws ec2 authorize-security-group-ingress \
    --group-id ${SHARED_VPC_SG_VPCE_ID} --protocol tcp --port 443 --cidr 192.168.0.0/16

# User-Data ファイルを作成
$ cat <<EOL >> user-data.txt
#cloud-config
ssh_pwauth: True
password: PassW0rd!
chpasswd: { expire: False }
EOL

# User-VPC-Subnet-A に EC2 (Amazon Linux 2023) を起動(出力された EC2 ID は環境変数に代入)
# ※ `--iam-instance-profile` には、作成した Role 名を入れてください
# ※ 検証のため、パスワード認証でログインできるように User-Data を指定します
$ USER_VPC_EC2_ID=$( \
  aws ec2 run-instances \
    --image-id ami-0f9816f78187c68fb \
    --instance-type t2.micro \
    --subnet-id ${USER_VPC_Subnet_A_ID} \
    --security-group-ids ${USER_VPC_SG_EC2_ID} \
    --iam-instance-profile Name="EC2_SSM_Role"\
    --tag-specifications \
      'ResourceType=instance,Tags=[{Key=Name,Value=User-VPC-EC2-Sub-A}]' \
      'ResourceType=volume,Tags=[{Key=Name,Value=User-VPC-EBS}]' \
    --user-data file://user-data.txt \
    --query Instances[].InstanceId --output text \
)

次に、Shared VPC と、User VPC 間を繋げるための VPC Peering を設定します。

# Shared VPC と、User VPC の Peering Connection を作成
$ PEERING_ID=$( \
  aws ec2 create-vpc-peering-connection \
    --vpc-id ${SHARED_VPC_ID} \
    --peer-vpc-id ${USER_VPC_ID} \
    --tag-specifications "ResourceType=vpc-peering-connection,Tags=[{Key=Name,Value=SharedVPC-UserVPC}]" \
    --query VpcPeeringConnection.VpcPeeringConnectionId --output text \
)

# Peering Connection の承認
$ aws ec2 accept-vpc-peering-connection \
  --vpc-peering-connection-id ${PEERING_ID}

# Shared VPC のメインルートテーブルに User VPC 向けのルートを作成
$ SHARED_VPC_MAIN_RTB_ID=$( \
  aws ec2 describe-route-tables \
    --filters "Name=vpc-id,Values=${SHARED_VPC_ID}" \
    --query RouteTables[].RouteTableId --output text \
) && \
aws ec2 create-route \
  --route-table-id ${SHARED_VPC_MAIN_RTB_ID} \
  --destination-cidr-block 192.168.0.0/16 \
  --vpc-peering-connection-id ${PEERING_ID}

# User VPC のメインルートテーブルに Shared VPC 向けのルートを作成
$ USER_VPC_MAIN_RTB_ID=$( \
  aws ec2 describe-route-tables \
    --filters "Name=vpc-id,Values=${USER_VPC_ID}" \
    --query RouteTables[].RouteTableId --output text \
) && \
aws ec2 create-route \
  --route-table-id ${USER_VPC_MAIN_RTB_ID} \
  --destination-cidr-block 172.16.0.0/16 \
  --vpc-peering-connection-id ${PEERING_ID}

この時点で、お互いの VPC 間は接続されているので、Shared VPC 内の EC2 インスタンスから、User VPC 内の EC2 に SSH 接続できるようになっています。 では、Amazon Route 53 Private Hosted Zone を作成する前に User-VPC-EC2-Sub-A から SSM 向けのエンドポイントがどのように見えているのかを確認してみます。

# Shared VPC 内の EC2 インスタンスに Session Manager でアクセス
$ aws ssm start-session --target ${SHARED_VPC_EC2_ID}

# User-VPC-EC2-Sub-A に SSH 接続(パスワードは、User-Data で指定したもの)
$ ssh ec2-user@<User-VPC-EC2-Sub-A のプライベート IP>
ec2-user@192.168.100.58's password: 

   ,     #_
   ~\_  ####_        Amazon Linux 2023
  ~~  \_#####\
  ~~     \###|
  ~~       \#/ ___   https://aws.amazon.com/linux/amazon-linux-2023
   ~~       V~' '->
    ~~~         /
      ~~._.   _/
         _/ _/
       _/m/'
[ec2-user@ip-192-168-100-58 ~]$ 

# ssm-cli でエンドポイントの接続をチェック
$ sudo ssm-cli get-diagnostics --output table
...(Snip)...
├──────────────────────────────────────┼─────────┼──────────────────────────────────────────┤
│ Connectivity to ssm endpoint         │ Failed  │ ssm.ap-northeast-1.amazonaws.com is not  │
│                                      │         │ reachable: dial tcp 52.119.223.48:443:   │
│                                      │         │ i/o timeout                              │
├──────────────────────────────────────┼─────────┼──────────────────────────────────────────┤
│ Connectivity to ec2messages endpoint │ Failed  │ ec2messages.ap-northeast-1.amazonaws.com │
│                                      │         │ is not reachable: dial tcp               │
│                                      │         │ 52.119.223.180:443: i/o timeout          │
├──────────────────────────────────────┼─────────┼──────────────────────────────────────────┤
│ Connectivity to ssmmessages endpoint │ Failed  │ ssmmessages.ap-northeast-1.amazonaws.com │
│                                      │         │ is not reachable: dial tcp               │
│                                      │         │ 52.119.222.59:443: i/o timeout           │
├──────────────────────────────────────┼─────────┼──────────────────────────────────────────┤
...(Snip)...

# エンドポイントのドメイン名を確認(ssm.ap-northeast-1.amazonaws.com)
$ dig +short ssm.ap-northeast-1.amazonaws.com
52.119.223.48

# エンドポイントのドメイン名を確認(ssmmessages.ap-northeast-1.amazonaws.com)
$ dig +short ssmmessages.ap-northeast-1.amazonaws.com
52.119.222.59

# エンドポイントのドメイン名を確認(ec2messages.ap-northeast-1.amazonaws.com)
$ dig +short ec2messages.ap-northeast-1.amazonaws.com
99.77.62.64

各種エンドポイントは、パブリックに向いておりアクセスに失敗していますね。 当然ながら、User VPC には SSM 関連の VPC エンドポイントは作成していないですし、インターネットゲートウェイなどインターネットにアクセスする経路もないためです。

それでは、Amazon Route 53 Private Hosted Zone を作成して、VPC エンドポイントを内部に向ける設定をします(作成時に User VPC へ関連付けします)。 また、レコードの登録をするために「レコードファイル(JSON ファイル)」を作成しておく必要があるので、こちらも準備します。

※ 各種エンドポイントの DNS 名と、ホストゾーン ID の取得ですが、かなり無理やりやっています・・・(良い方法あったら教えてください)。

ssm.ap-northeast-1.amazonaws.com ドメインの作成

# VPC エンドポイントの DNS 名を取得
$ SSM_ENDPOINT_DNS=$( \
  aws ec2 describe-vpc-endpoints \
    --vpc-endpoint-ids ${SSM_ENDPOINT_ID} \
    --query VpcEndpoints[].DnsEntries[].[DnsName] --output text \
    | grep -E "vpce-([0-9a-z]+)-([0-9a-z]+)\.ssm\.ap-northeast-1\.vpce\.amazonaws\.com$" \
)

# VPC エンドポイントの ホストゾーン ID を取得
$ SSM_ENDPOINT_HOSTEDZONEID=$( \
  aws ec2 describe-vpc-endpoints \
    --vpc-endpoint-ids ${SSM_ENDPOINT_ID} \
    --query "VpcEndpoints[].DnsEntries[?DnsName==\`$(echo ${SSM_ENDPOINT_DNS})\`].HostedZoneId" --output text \
)

# レコードファイルを作成
$ cat <<EOL >> ssm-record.json
{
  "Comment": "CREATE a record",
  "Changes": [{
    "Action": "CREATE",
    "ResourceRecordSet": {
      "Name": "ssm.ap-northeast-1.amazonaws.com",
      "Type": "A",
      "AliasTarget": {
        "HostedZoneId": "${SSM_ENDPOINT_HOSTEDZONEID}",
        "DNSName": "${SSM_ENDPOINT_DNS}",
        "EvaluateTargetHealth": true
      }
    }
  }]
}
EOL

# Amazon Route 53 Private Hosted Zone を作成(ドメイン名: ssm.ap-northeast-1.amazonaws.com)
$ SSM_ROUTE53_ZONE_ID=$( \
  aws route53 create-hosted-zone \
    --name ssm.ap-northeast-1.amazonaws.com \
    --vpc VPCRegion=ap-northeast-1,VPCId=${USER_VPC_ID} \
    --hosted-zone-config Comment="ssm endpoint.",PrivateZone=true \
    --caller-reference `date +%Y-%m-%d-%H:%M` \
    --query HostedZone.Id --output text \
)

# ホストゾーンへレコードを作成
$ aws route53 change-resource-record-sets \
    --hosted-zone-id ${SSM_ROUTE53_ZONE_ID} \
    --change-batch file://ssm-record.json

ssmmessages.ap-northeast-1.amazonaws.com ドメインの作成

# VPC エンドポイントの DNS 名を取得
$ SSMMESSAGES_ENDPOINT_DNS=$( \
  aws ec2 describe-vpc-endpoints \
    --vpc-endpoint-ids ${SSMMESSAGES_ENDPOINT_ID} \
    --query VpcEndpoints[].DnsEntries[].[DnsName] --output text \
    | grep -E "vpce-([0-9a-z]+)-([0-9a-z]+)\.ssmmessages\.ap-northeast-1\.vpce\.amazonaws\.com$" \
)

# VPC エンドポイントの ホストゾーン ID を取得
$ SSMMESSAGES_ENDPOINT_HOSTEDZONEID=$( \
  aws ec2 describe-vpc-endpoints \
    --vpc-endpoint-ids ${SSMMESSAGES_ENDPOINT_ID} \
    --query "VpcEndpoints[].DnsEntries[?DnsName==\`$(echo ${SSMMESSAGES_ENDPOINT_DNS})\`].HostedZoneId" --output text \
)

# レコードファイルを作成
$ cat <<EOL >> ssmmessages-record.json
{
  "Comment": "CREATE a record",
  "Changes": [{
    "Action": "CREATE",
    "ResourceRecordSet": {
      "Name": "ssmmessages.ap-northeast-1.amazonaws.com",
      "Type": "A",
      "AliasTarget": {
        "HostedZoneId": "${SSMMESSAGES_ENDPOINT_HOSTEDZONEID}",
        "DNSName": "${SSMMESSAGES_ENDPOINT_DNS}",
        "EvaluateTargetHealth": true
      }
    }
  }]
}
EOL

# Amazon Route 53 Private Hosted Zone を作成(ドメイン名: ssmmessages.ap-northeast-1.amazonaws.com)
$ SSMMESSAGES_ROUTE53_ZONE_ID=$( \
  aws route53 create-hosted-zone \
    --name ssmmessages.ap-northeast-1.amazonaws.com \
    --vpc VPCRegion=ap-northeast-1,VPCId=${USER_VPC_ID} \
    --hosted-zone-config Comment="ssmmessages endpoint.",PrivateZone=true \
    --caller-reference `date +%Y-%m-%d-%H:%M` \
    --query HostedZone.Id --output text \
)

# ホストゾーンへレコードを作成
$ aws route53 change-resource-record-sets \
    --hosted-zone-id ${SSMMESSAGES_ROUTE53_ZONE_ID} \
    --change-batch file://ssmmessages-record.json

ec2messages.ap-northeast-1.amazonaws.com ドメインの作成

# VPC エンドポイントの DNS 名を取得
$ EC2MESSAGES_ENDPOINT_DNS=$( \
  aws ec2 describe-vpc-endpoints \
    --vpc-endpoint-ids ${EC2MESSAGES_ENDPOINT_ID} \
    --query VpcEndpoints[].DnsEntries[].[DnsName] --output text \
    | grep -E "vpce-([0-9a-z]+)-([0-9a-z]+)\.ec2messages\.ap-northeast-1\.vpce\.amazonaws\.com$" \
)

# VPC エンドポイントの ホストゾーン ID を取得
$ EC2MESSAGES_ENDPOINT_HOSTEDZONEID=$( \
  aws ec2 describe-vpc-endpoints \
    --vpc-endpoint-ids ${EC2MESSAGES_ENDPOINT_ID} \
    --query "VpcEndpoints[].DnsEntries[?DnsName==\`$(echo ${EC2MESSAGES_ENDPOINT_DNS})\`].HostedZoneId" --output text \
)

# レコードファイルを作成
$ cat <<EOL >> ec2messages-record.json
{
  "Comment": "CREATE a record",
  "Changes": [{
    "Action": "CREATE",
    "ResourceRecordSet": {
      "Name": "ec2messages.ap-northeast-1.amazonaws.com",
      "Type": "A",
      "AliasTarget": {
        "HostedZoneId": "${EC2MESSAGES_ENDPOINT_HOSTEDZONEID}",
        "DNSName": "${EC2MESSAGES_ENDPOINT_DNS}",
        "EvaluateTargetHealth": true
      }
    }
  }]
}
EOL

# Amazon Route 53 Private Hosted Zone を作成(ドメイン名: ec2messages.ap-northeast-1.amazonaws.com)
$ EC2MESSAGES_ROUTE53_ZONE_ID=$( \
  aws route53 create-hosted-zone \
    --name ec2messages.ap-northeast-1.amazonaws.com \
    --vpc VPCRegion=ap-northeast-1,VPCId=${USER_VPC_ID} \
    --hosted-zone-config Comment="ec2messages endpoint.",PrivateZone=true \
    --caller-reference `date +%Y-%m-%d-%H:%M` \
    --query HostedZone.Id --output text \
)

# ホストゾーンへレコードを作成
$ aws route53 change-resource-record-sets \
    --hosted-zone-id ${EC2MESSAGES_ROUTE53_ZONE_ID} \
    --change-batch file://ec2messages-record.json

これにて、一通りの準備が完了しました!

最後に、User VPC 内の EC2 インスタンス User-VPC-EC2-Sub-A に SSM Session Manager でアクセスして、SSM 向けのエンドポイントを確認してみます。

# User VPC 内の EC2 インスタンスに Session Manager でアクセス
$ aws ssm start-session --target ${USER_VPC_EC2_ID}

# ssm-cli でエンドポイントの接続をチェック
$ sudo ssm-cli get-diagnostics --output table
...(Snip)...
├──────────────────────────────────────┼─────────┼──────────────────────────────────────────┤
│ Connectivity to ssm endpoint         │ Success │ ssm.ap-northeast-1.amazonaws.com is      │
│                                      │         │ reachable                                │
├──────────────────────────────────────┼─────────┼──────────────────────────────────────────┤
│ Connectivity to ec2messages endpoint │ Success │ ec2messages.ap-northeast-1.amazonaws.com │
│                                      │         │ is reachable                             │
├──────────────────────────────────────┼─────────┼──────────────────────────────────────────┤
│ Connectivity to ssmmessages endpoint │ Success │ ssmmessages.ap-northeast-1.amazonaws.com │
│                                      │         │ is reachable                             │
├──────────────────────────────────────┼─────────┼──────────────────────────────────────────┤
...(Snip)...

# エンドポイントのドメイン名を確認(ssm.ap-northeast-1.amazonaws.com)
$ dig +short ssm.ap-northeast-1.amazonaws.com
172.16.100.126
172.16.200.45

# エンドポイントのドメイン名を確認(ssmmessages.ap-northeast-1.amazonaws.com)
$ dig +short ssmmessages.ap-northeast-1.amazonaws.com
172.16.100.253
172.16.200.141

# エンドポイントのドメイン名を確認(ec2messages.ap-northeast-1.amazonaws.com)
$ dig +short ec2messages.ap-northeast-1.amazonaws.com
172.16.200.200
172.16.100.169

今度は、各種エンドポイントが Success となっていて接続できていますね! ちゃんと、Shared VPC のプライベートアドレスを経由していることもわかります。

念のため、SSM Fleet Manager を確認してみたところ、マネージドノードに含まれていました。

まとめ

様々な AWS サービスを組み合わせることで、AWS 環境のプライベートネットワークのみに留めることができました。 非常にセキュアな接続環境を簡単に構築できるのが、SSM の便利なところですね。

ただ、可用性を考慮して 2 つの AZ にエンドポイントを作成すると、おおよそ 60USD の費用が発生します。 これを VPC ごとに作成するとなると、コストはどんどん大きくなってしまいます。

今回は、単一アカウントで 2 つの VPC を用意して検証していましたが、下記のように マルチアカウント や、Transit Gateway を組み合わせることでも実現可能です。

このように、Shared VPC の概念を用いて、役割ごとに VPC を作成することでリソース管理の煩雑性も回避できると思いますので、ぜひお試しください!

最後になりますが、リソースの削除はお忘れなく!(手動ですw)

  • Amazon Route 53 Private Hosted Zone
  • VPC Endpoint
  • Amazon EC2
  • Amazon VPC(ピアリング接続も!)

この記事が、少しでも誰かのお役にたてば幸いです。

参考情報

アノテーション株式会社について

アノテーション株式会社は、クラスメソッド社のグループ企業として「オペレーション・エクセレンス」を担える企業を目指してチャレンジを続けています。 「らしく働く、らしく生きる」のスローガンを掲げ、さまざまな背景をもつ多様なメンバーが自由度の高い働き方を通してお客様へサービスを提供し続けてきました。 現在当社では一緒に会社を盛り上げていただけるメンバーを募集中です。 少しでもご興味あれば、アノテーション株式会社WEBサイト をご覧ください。