【初心者向け】VPCエンドポイントとAWS PrivateLinkの違いを実際に構築して理解してみた

VPCエンドポイントとAWS PrivateLinkの違い。あなたはを説明できますか?
2022.09.29

もう2度とググりたくない

こんにちは!AWS事業本部のおつまみです。

皆さん「VPCエンドポイントとAWS PrivateLinkの違い」を自分の言葉で説明できますか?私は時折、記憶喪失になり違いを忘れてしまいます。

もう2度とググりたくないという思いがあり、今回は違いをまとめました。
実際にハンズオンできるよう初心者向けの内容となっているため、VPCエンドポイントやPrivateLinkの知識が全くない方のお役に立てればと思います!

結論

  • VPCエンドポイント
    • VPCと他サービス間でプライベートな接続を提供するコンポーネント
    • サービス利用側のVPC内で作成
  • AWS PrivateLink
    • プライベート接続を介したサービスを提供するためのサービス
    • 以下の2つがセットとなり、AWS PrivateLinkが提供されている。
        - VPCエンドポイント(サービス利用側のVPC内で作成)
        - VPCエンドポイントサービス(サービス提供側のVPC内で作成)

すみません。文章だけはわかりにくいですね。
簡単に図で表しました。

ざっくりAWS PrivateLinkの接続にVPCエンドポイントが使われていると理解できればOKです。
もう少し深く掘り下げてみます。

VPCエンドポイントとは

VPCと他サービス間でプライベートな接続を提供するコンポーネントです。
サービス利用側のVPC内に作成されます。

2022/9/29時点では、これら3種類のVPCエンドポイントが提供されています。

1. インターフェイスエンドポイント
一部のAWSサービスやNLBを介した独自サービス、サポートされているAWS Marketplaceサービスにプライベートに接続する

2. Gateway Load Balancerエンドポイント
Gateway Load Balancerを介したサービスにプライベートに接続する

3. ゲートウェイエンドポイント
S3及びDynamoDBへのアクセスをプライベートに接続する

ポイントは各タイプによって通信経路が異なることです。

1のインターフェイスエンドポイント及び2のGateway Load Balancerエンドポイントの場合は、VPC内にプライベートアドレスをもつENI(Elastic Network Interface)が作成されます。
このENI経由でサービス提供側のVPCエンドポイントサービスに接続されます。

3のゲートウェイエンドポイントの場合は、サービス利用側のルートテーブルにエンドポイントのルーティング設定が必要となります。VPCエンドポイントサービスは作成されません。

詳細な違いは、こちらのブログが大変わかりやすかったです。

次にVPCエンドポイントサービスをみてみましょう。

VPCエンドポイントサービスとは

サービス提供側のVPC内にあるサービスをPrivateLink経由で公開する場合の設定です。
この設定がないと、インターフェイスエンドポイントもしくはGateway Load Balancerエンドポイントにサービスを提供できません。
またゲートウェイエンドポイントの場合はVPCエンドポイントサービスは作成されません。(2回目)

次にAWS PrivateLinkをみてみましょう。

プライベート接続を介したサービスを提供するためのサービスです。
ネットワーク間のトラフィックをインターネット経由せずに、プライベートに通信することができます。
上記で述べた、以下の2つがセットとなり提供されています。

  ・VPCエンドポイント(サービス利用側のVPC内で作成)
  ・VPCエンドポイントサービス(サービス提供側のVPC内で作成)

ちなみにAWS PrivateLinkはAWSのサービス名でありません!
マネジメントコンソールで「AWS PrivateLink」と検索すると、[機能]に以下の2つがヒットします。

これがPrivateLinkの実体ということです。

実際にやってみた

VPCエンドポイントとAWS PrivateLinkの違いは理解できたところで、VPCエンドポイントによるAWS PrivateLink接続を試したいと思います!

今回の構成図です。
ゴールはサービス利用側(Consumer)のEC2からサービス提供側(Provider)のEC2に接続し、Apacheのテストページが表示されることです。

事前準備

ただ全てマネジメントコンソールで作成すると、かなり骨が折れます。 そのため、この構成を下準備として、CloudFormationで以下の構成を事前に作成しておきます。

Consumer.yml

AWSTemplateFormatVersion: "2010-09-09"
Description: "Consumer 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-078296f82eb463377" # 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

  PrivateRouteTable: 
    Type: AWS::EC2::RouteTable
    Properties: 
      VpcId: !Ref Vpc 
      Tags: 
        - Key: Name
          Value: !Sub "${Prefix}-table"
  
  PrivateSubnetTableAssociation: 
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties: 
      SubnetId: !Ref PrivateSubnet
      RouteTableId: !Ref PrivateRouteTable
  
  SecurityGroup:
    Type: "AWS::EC2::SecurityGroup"
    Properties:
      VpcId: !Ref Vpc
      GroupName: !Sub "${Prefix}-sg"
      GroupDescription: "-"
      Tags:
        - Key: "Name"
          Value: !Sub "${Prefix}-sg"

  # ------------------------------------------------------------#
  #  VPC Endpoint
  # ------------------------------------------------------------#
  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

Provider.yml

AWSTemplateFormatVersion: "2010-09-09"
Description: "Provider Template."

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

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

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

  # ------------------------------------------------------------#
  # EC2
  # ------------------------------------------------------------#
  EC2InstanceName:
    Type: String
    Default: "ec2"
  EC2InstanceAMI:
    Type: AWS::EC2::Image::Id
    Default: "ami-078296f82eb463377" # 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

  PrivateRouteTable: 
    Type: AWS::EC2::RouteTable
    Properties: 
      VpcId: !Ref Vpc 
      Tags: 
        - Key: Name
          Value: !Sub "${Prefix}-table"
  
  PrivateSubnetTableAssociation: 
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties: 
      SubnetId: !Ref PrivateSubnet
      RouteTableId: !Ref PrivateRouteTable

  SecurityGroup:
    Type: "AWS::EC2::SecurityGroup"
    Properties:
      VpcId: !Ref Vpc
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: "0.0.0.0/0"
      GroupName: !Sub "${Prefix}-sg"
      GroupDescription: "-"
      Tags:
        - Key: "Name"
          Value: !Sub "${Prefix}-sg"

  # ------------------------------------------------------------#
  #  VPC Endpoint
  # ------------------------------------------------------------#
  VPCS3Endpoint:
    Type: "AWS::EC2::VPCEndpoint"
    Properties:
      RouteTableIds: 
        - !Ref PrivateRouteTable
      ServiceName: !Sub "com.amazonaws.${AWS::Region}.s3"
      VpcEndpointType: Gateway
      VpcId: !Ref Vpc 

  # ------------------------------------------------------------#
  #  EC2Instance
  # ------------------------------------------------------------#
  EC2Instance:
    Type: "AWS::EC2::Instance"
    Properties:
      Tags:
        - Key: Name
          Value: !Sub "${Prefix}-${EC2InstanceName}"
      ImageId: !Ref EC2InstanceAMI
      InstanceType: !Ref EC2InstanceInstanceType
      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
        sudo yum update -y
        sudo yum -y install httpd
        sudo systemctl start httpd
        sudo systemctl enable httpd

  # ------------------------------------------------------------#
  #  NLB
  # ------------------------------------------------------------#  
  TargetGroup: 
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties: 
      VpcId: !Ref Vpc
      Name: !Sub "${Prefix}-tg"
      Protocol: TCP
      Port: 80
      Tags: 
        - Key: Name
          Value: !Sub "${Prefix}-tg"
      Targets: 
        - Id: !Ref EC2Instance
          Port: 80
   
  NLB: 
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties: 
      Name: !Sub "${Prefix}-nlb"
      Tags: 
        - Key: Name
          Value: !Sub "${Prefix}-nlb"
      Scheme: "internal"
      LoadBalancerAttributes: 
        - Key: "deletion_protection.enabled"
          Value: false
      Subnets: 
        - !Ref PrivateSubnet
      Type: network

  NLBListener: 
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties: 
      DefaultActions: 
        - TargetGroupArn: !Ref TargetGroup
          Type: forward
      LoadBalancerArn: !Ref NLB
      Port: 80
      Protocol: TCP

CloudFormationテンプレートはこちらの記事で公開されているものをカスタマイズさせていただきました。  ありがとうございます!!

いよいよAWS PrivateLink接続のための手順となります。今回は吹き出しの部分をマネジメントコンソールを触りながら、理解を深めたいと思います。

1. VPCエンドポイントサービスの作成

VPCサービスより「エンドポイントサービス」を選択し、「エンドポイントサービスの作成」をクリックします。

以下のように任意の名前を入力し、「ロードバランサーのタイプ」を「ネットワーク」にし、ロードバランサーにはCloudFormationで作成されたNLBを指定します。

「承諾が必要」にチェックを入れておきます。
ここにチェックを入れることで、第3者からの接続を防ぐことができます。
チェック後、「作成」をクリックします。

Consumer側でエンドポイント作成時に使用する「サービス名」を控えておきます。

2. VPCエンドポイントの作成

VPCサービスより「エンドポイント」を選択します。
既にCloudFormationで作成された S3のゲートウェイエンドポイントとSession Managerを使用するためのエンドポイントが作成されています。「エンドポイントの作成」をクリックします。

以下のように任意の名前を入力し、「サービスカテゴリ」に「その他のエンドポイントサービス」を指定します。「サービス名」には先程控えておいたサービス名を指定します。入力後、「サービスの検証」をクリックします。  

「サービス名が検証されました」が出力されればOKです。

VPCやサブネットはConsumer側のEC2が配置されているものを指定します。

NLBのエンドポイント用のセキュリティグループを作成していなかったため、今回はdefaultを指定しました。
※エンドポイント用のセキュリティグループを作成することを推奨します。  
後ほど、 Consumer側のEC2インスタンスから接続できるようにセキュリティグループを修正します。  
セキュリティグループ指定後、「エンドポイントを作成」をクリックします。  

作成したエンドポイントの「ステータス」が「pendingAcceptance」になりました。

話は逸れますが、この画面からインターフェイスエンドポイントのみENIが作成され、ゲートウェイエンドポイントにはENIが作成されてないことがわかります。

先にVPCエンドポイントのセキュリティグループを修正しておきます。 エンドポイントのセキュリティグループよりグループIDを指定します。

セキュリティグループのインバウンドルールにCunsumer側のEC2インスタンスに設定されているプライベートIPアドレスからのHTTP通信を許可するように設定します。

3. エンドポイントの承諾

再度VPCサービスより「エンドポイントサービス」を選択し、作成されているエンドポイントサービスの「エンドポイント接続」のタブをクリックします。
状態が「pendingAcceptance」中のエンドポイントが1つ紐づいていることがわかります。

「アクション」から「エンドポイント接続リクエストの承諾」を行います。

承諾確認画面が表示されるため、「承諾」を入力し、「承諾」をクリックします。

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

作成された「エンドポイント」のDNS名より、NLBを経由してProvider側のEC2にアクセスできるようになります。  

ちなみにDNS名には、「リージョン固有のDNSホスト名」と「ゾーンごとのDNSホスト名」の2つが発行されています。どちらからでも接続できますが、原則的にはリージョンDNS名を利用し、AZ跨ぎのレイテンシや通信料金が気になる場合のみゾーン固有のDNSを利用するのがよいようです。

4. サービス利用側のEC2から接続確認

最後にConsumer側のEC2インスタンスからの接続を確認します。 EC2サービスからConsumer側のEC2インスタンスを選択し、「接続」をクリックします。

「セッションマネージャー」のタブから「接続」をクリックします。

先程控えておいた「エンドポイント」のDNS名を指定して、アクセスしてみます。 無事、Apacheのテストページが表示されることが確認できました!

最後に

今回はVPCエンドポイントとAWS PrivateLinkの違いを実際に構築して理解してみました。
恥ずかしながら、今までインターフェースエンドポイントとAWS PrivateLinkは同義だと思い込んでいました。。。
実際に構築したことで違いを知るきっかけになったので、やはり実践が1番ですね!
おそらくもう2度とググることはないと思います。

今回は触れませんでしが、AWS PrivateLink・VPCエンドポイントを利用するメリットやユースケースはこちらの記事がわかりやすかったので参考にしてください。

最後までお読みいただきありがとうございました! この記事が誰かのお役に立てば幸いです。

以上、おつまみ(@AWS11077)でした!

参考

AWS PrivateLink および VPC エンドポイント - Amazon Virtual Private Cloud