VPC間のAWS PrivateLinkを試してみた

2022.03.25

こんにちは!DA(データアナリティクス)事業本部 サービスソリューション部の大高です。

AWS PrivateLinkを利用すると、ネットワーク間をトラフィックをインターネットを経由させることなく、プライベートに通信することができます。

利用できる通信は、VPC、AWSのサービス、オンプレミスネットワークに対する通信がありますが、今回はVPC間のAWS PrivateLinkを試してみたいと思います。

やりたいこと

最終的に試したい構成は以下のような構成になります。

「Service Consumer VPC 内の EC2 インスタンス」から、「Service Provider VPC 内の EC 2インスタンス」へ簡単な通信を行い、うまく通信できているかを試したいです。

下準備

この構成への下準備として、CloudFormationテンプレートを利用して以下のような構成を作成します。

この構成を作成した後に、残りは管理コンソールから手動で設定を行い、理解を深めたいと思います。

CloudFormationテンプレートで2つのVPC環境を作成する

ということで、以下のテンプレートを利用して2つのVPC、Private Subnet、EC2を作成しました。

なお、主旨から外れるため構成図からは省略していますが、EC2への接続にセッションマネージャーを利用したかったため、セッションマネージャ用のVPC Endpointも併せて作成しています。

simple-private-server.yml

AWSTemplateFormatVersion: "2010-09-09"
Description: "Simple Private Server Template."

Parameters:
  # ------------------------------------------------------------#
  # Common
  # ------------------------------------------------------------#
  Prefix:
    Type: String
    Default: "prefix"

  # ------------------------------------------------------------#
  # Network
  # ------------------------------------------------------------#
  VpcCidr:
    Type: String
    Default: "10.0.0.0/16"

  PrivateSubnetCidr:
    Type: String
    Default: "10.0.0.0/24"

  # ------------------------------------------------------------#
  # EC2
  # ------------------------------------------------------------#
  EC2InstanceName:
    Type: String
    Default: "ec2"
  EC2InstanceAMI:
    Type: AWS::EC2::Image::Id
    Default: "ami-0ab0bbbd329f565e6" # Amazon Linux 2 AMI (HVM) - Kernel 5.10, SSD Volume Type
  EC2InstanceInstanceType:
    Type: String
    Default: "t3.nano"
  EC2InstanceVolumeType:
    Type: String
    Default: "gp2"
  EC2InstanceVolumeSize:
    Type: String
    Default: "8"

Resources:
  # ------------------------------------------------------------#
  #  Network
  # ------------------------------------------------------------#
  Vpc:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VpcCidr
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: !Sub ${Prefix}-vpc

  PrivateSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: !Ref PrivateSubnetCidr
      VpcId: !Ref Vpc
      AvailabilityZone:
        Fn::Select:
          - "0"
          - Fn::GetAZs: ""
      Tags:
        - Key: Name
          Value: !Sub ${Prefix}-private-subnet

  SecurityGroup:
    Type: "AWS::EC2::SecurityGroup"
    Properties:
      VpcId: !Ref Vpc
      GroupName: !Sub "${Prefix}-sg"
      GroupDescription: "-"
      Tags:
        - Key: "Name"
          Value: !Sub "${Prefix}-sg"

  SsmVpcEndpointSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: !Sub ${Prefix}-ssm-vpc-endpoint-sg
      GroupName: !Sub ${Prefix}-ssm-vpc-endpoint-sg
      VpcId: !Ref Vpc
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          SourceSecurityGroupId: !Ref SecurityGroup
      Tags:
        - Key: Name
          Value: !Sub ${Prefix}-ssm-vpc-endpoint-sg

  SsmVpcEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcEndpointType: Interface
      PrivateDnsEnabled: true
      ServiceName: !Sub com.amazonaws.${AWS::Region}.ssm
      VpcId: !Ref Vpc
      SubnetIds:
        - !Ref PrivateSubnet
      SecurityGroupIds:
        - !Ref SsmVpcEndpointSecurityGroup

  SsmMessagesVpcEndpointSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: !Sub ${Prefix}-ssmmessages-vpc-endpoint-sg
      GroupName: !Sub ${Prefix}-ssmmessages-vpc-endpoint-sg
      VpcId: !Ref Vpc
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          SourceSecurityGroupId: !Ref SecurityGroup
      Tags:
        - Key: Name
          Value: !Sub ${Prefix}-ssmmessages-vpc-endpoint-sg

  SsmMessagesVpcEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcEndpointType: Interface
      PrivateDnsEnabled: true
      ServiceName: !Sub com.amazonaws.${AWS::Region}.ssmmessages
      VpcId: !Ref Vpc
      SubnetIds:
        - !Ref PrivateSubnet
      SecurityGroupIds:
        - !Ref SsmMessagesVpcEndpointSecurityGroup

  # ------------------------------------------------------------#
  #  Ec2InstanceProfile
  # ------------------------------------------------------------#
  Ec2Role:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub ${Prefix}-ec2-role
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - ec2.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore

  Ec2InstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      InstanceProfileName: !Sub ${Prefix}-ec2-instance-profile
      Roles:
        - !Ref Ec2Role

  # ------------------------------------------------------------#
  #  EC2Instance
  # ------------------------------------------------------------#
  EC2Instance:
    Type: "AWS::EC2::Instance"
    Properties:
      Tags:
        - Key: Name
          Value: !Sub "${Prefix}-${EC2InstanceName}"
      ImageId: !Ref EC2InstanceAMI
      InstanceType: !Ref EC2InstanceInstanceType
      IamInstanceProfile: !Ref Ec2InstanceProfile
      DisableApiTermination: false
      EbsOptimized: false
      BlockDeviceMappings:
        - DeviceName: /dev/xvda
          Ebs:
            DeleteOnTermination: true
            VolumeType: !Ref EC2InstanceVolumeType
            VolumeSize: !Ref EC2InstanceVolumeSize
      SecurityGroupIds:
        - !Ref SecurityGroup
      SubnetId: !Ref PrivateSubnet
      UserData: !Base64 |
        #! /bin/bash
        yum update -y

通信テスト用のプログラムを用意する

次に、通信テスト用のプログラムを用意します。

今回は簡単な通信テストなので、以下のようなPythonプログラムを用意します。

app.py

from http.server import HTTPServer, SimpleHTTPRequestHandler
import socket


class MyHandler(SimpleHTTPRequestHandler):

    def do_GET(self):
        res = 'Hello!\n'.encode('utf-8')
        self.send_response(200)
        self.end_headers()
        self.wfile.write(res)


host = socket.gethostname()
port = 8000
httpd = HTTPServer((host, port), MyHandler)
print(f'Available on: http://{host}:{port}')
httpd.serve_forever()

実際に、このプログラムを動かすと以下のように表示されます。

$ python app.py 
Available on: http://HOSTNAME:8000

この状態で、サービスにアクセスすると以下のように応答が返ります。

$ curl HOSTNAME:8000
Hello!

実際には、このプログラムを「Service Provider VPC」内のEC2で動かして、VPC間での通信を試したいと思います。

Service Provider側の設定

では、追加で必要なものを手動で構築していきます。まずはService Provider側です。

Service Provider側では、「Pythonによる実際のサービス」、「Network Load Balancer」、「エンドポイントサービス」の3つを作成していきます。

Pythonによる実際のサービス

まず、EC2インスタンスで「実際のサービス」となるPythonプログラムを配置・起動しておきます。

EC2インスタンスにセッションマネージャ経由で接続してプログラムを起動させます。

# ユーザをec2-userに切り替える
$ sudo su - ec2-user

# 先程のPythonプログラムをvimで記述して保存する
$ vim app.py

# サービスを起動
$ nohup python3 app.py &
[1] 2412

念の為、別セッションで接続してプログラムが停止していないか確認しておきます。

# 別セッションから確認
$ curl 10.0.0.202:8000
Hello!

良さそうですね。

EC2インスタンスに紐づくセキュリティグループ

セキュリティグループの設定でTCPプロトコルのポート8000を解放しておきます。「ソース」は今回は0.0.0.0/0としました。

Network Load Balancer

次にNetwork Load Balancer(NLB)を作成します。

今回はインターネットを経由しない通信用なので、作成する際に「Scheme」はInternalにします。

「Network mapping」は作成済みのVPC、サブネットを指定していきます。

「Listner and routing」の設定でターゲットグループが必要となりますが、まだターゲットグループを作成していなかったので、これも同時に作成します。

それぞれ、上記のような設定としました。なお、Pythonで動かしたサービスはポート8000で待ち受けているので、対象とするインスタンスの追加設定時にはポートを8000に設定するのを忘れないようにします。

このあたりの設定ですが、ポート80で通信を受け取って、ポート8000に渡すように設定をしています。

ターゲットグループを作成したら、以下のように「Listner and routing」の設定で作成したターゲットグループを指定します。

これでNLBは作成できました。

エンドポイントサービス

最後にエンドポイントサービスを作成します。

設定は以下のように「ロードバランサーのタイプ」を「ネットワーク」にし、ロードバランサーには先程作成したNLBを指定します。

また、誰でも接続できるようにはしたくないので「承諾が必要」にはチェックを入れておきます。

作成したら「サービス名」を控えておきます。これはService Consumer側で利用します。

Service Consumer側の設定

Service Consumer側では、「エンドポイント」を作成します。

エンドポイント

エンドポイントの作成では、「サービスカテゴリ」に「その他のエンドポイントサービス」を指定して、「サービス名」に先程控えておいた名前を指定します。

VPCやサブネット、セキュリティグループにはService Consumer側のEC2が配置されているものを指定しておきます。

なお、本来であればセキュリティグループにはエンドポイント用のセキュリティグループを別途作成すべきなのですが、今回は既存のものを流用しました。

作成したエンドポイントの「ステータス」が「pendingAcceptance」になっているので、Service Provider側で許可を出しましょう。

Service Provider側のエンドポイントサービスを開いて、「エンドポイント接続」のタブから「エンドポイント接続リクエストの承諾」を行います。

しばらくすると、作成したエンドポイントの「ステータス」が「使用可能」になります。

最後に「エンドポイント」のDNS名を控えておきます。これは実際にService ConsumerからService Providerのサービスに接続する際に利用します。

セキュリティグループの設定

先程設定したVPCエンドポイントのセキュリティグループは、以下のようにService CunsumerのEC2インスタンスに設定されているプライベートIPアドレスからの通信を許可するように設定しておきます。

これで遂に以下の構成となる設定が完了しました!

早速接続できたか試してみましょう。Service Consumer側のEC2にセッションマネージャで接続します。

先程控えておいた「エンドポイント」のDNS名を指定して、アクセスしてみましょう。

$ curl vpce-XXXXXXXXXXXXXXXXX-XXXXXXXX.vpce-svc-XXXXXXXXXXXXXXXXX.ap-northeast-1.vpce.amazonaws.com
Hello!

通信できました!想定どおり、レスポンスが返ってきましたね。

まとめ

以上、VPC間のAWS PrivateLinkを試してみました。

実際に自分で作成してみると、セキュリティグループの設定などでハマることがあったのでとても勉強になりました。今後、プライベート環境でのネットワーク設定の際にうまくPrivateLinkを活用できればと思います。

どなたかのお役に立てば幸いです。それでは!

参考