AWS PrivateLinkを使って別AWSアカウントにあるEC2にHTTP通信してみた

2023.01.27

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

AWS PrivateLinkを使って別AWSアカウントにあるEC2にHTTP通信させる設定をやったことが無かったなと思いやってみました。

AWSサービスやVPC内でホストされているアプリケーションをプライベートに利用できるようにするサービスです。
AWS PrivateLinkを利用することでパブリックな通信を行わずにAWSサービス (VPCエンドポイントに対応していれば) などを使用することができます。
AWS PrivateLink および VPC エンドポイント

構成

ざっくりやりたいことは以下の1~3になります。

  1. アカウントBでNLBとEC2を作成
  2. アカウントAでVPCエンドポイントとEC2を作成
  3. アカウントAのEC2からVPCエンドポイントへHTTPアクセス

  • 構成図は今回メインとなる部分を抜粋して記載しています。
  • EC2への接続はSession Managerを使用するのでインターフェイス型VPCエンドポイントを作成します。
  • EC2ではAmazon Linux2を使用してWebサーバーにApacheを使用します。
  • こちらのドキュメントに記載されている通りAmazon Linux2のリポジトリはS3にあるのでゲートウェイ型VPCエンドポイントを作成します。

設定

アカウントBでAWSリソース作成

AWSリソースはCloudFormationで作成します。
まずはアカウントBでリソースを作成していきます。
VPCエンドポイントをアカウントAで作成できるようにするためアカウントBではVPCエンドポイントサービスというものを作成しています。

CloudFormationテンプレート (ここをクリックしてください)
AWSTemplateFormatVersion: "2010-09-09"

Description: Service Stack

Metadata:
# ------------------------------------------------------------#
# Metadata
# ------------------------------------------------------------# 
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label: 
          default: Parameters for VPC
        Parameters:
          - VPCCIDR
      - Label: 
          default: Parameters for Subnet
        Parameters:
          - PrivateSubnet01CIDR
      - Label: 
          default: Parameters for VPC Endpoint
        Parameters:
          - AWSAccountId
      - Label: 
          default: Parameters for EC2
        Parameters:
          - EC2VolumeSize
          - EC2VolumeIOPS
          - EC2AMI
          - EC2InstanceType

Parameters:
# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------# 
  VPCCIDR:
    Default: 10.0.0.0/16
    Type: String

  PrivateSubnet01CIDR:
    Default: 10.0.0.0/24
    Type: String

  AWSAccountId:
    Type: String
    Description: AWS AccountId

  EC2VolumeSize:
    Default: 32
    Type: Number

  EC2VolumeIOPS:
    Default: 3000
    Type: Number

  EC2AMI:
    Default: ami-0bba69335379e17f8
    Type: AWS::EC2::Image::Id

  EC2InstanceType:
    Default: t3.micro
    Type: String

Resources:
# ------------------------------------------------------------#
# VPC
# ------------------------------------------------------------# 
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VPCCIDR
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags: 
        - Key: Name
          Value: account-b-vpc

# ------------------------------------------------------------#
# Subnet
# ------------------------------------------------------------# 
  PrivateSubnet01:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: ap-northeast-1a
      CidrBlock: !Ref PrivateSubnet01CIDR
      Tags: 
        - Key: Name
          Value: account-b-private-01
      VpcId: !Ref VPC

# ------------------------------------------------------------#
# RouteTable
# ------------------------------------------------------------# 
  PrivateRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: account-b-private-rtb

  PrivateRtAssociation1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PrivateRouteTable
      SubnetId: !Ref PrivateSubnet01

# ------------------------------------------------------------#
# Security Group
# ------------------------------------------------------------# 
  EC2Sg:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: for EC2
      GroupName: EC2-sg
      SecurityGroupEgress: 
        - CidrIp: 0.0.0.0/0
          FromPort: -1
          IpProtocol: -1
          ToPort: -1
      SecurityGroupIngress: 
        - FromPort: 80
          IpProtocol: tcp
          ToPort: 80
          CidrIp: !Ref VPCCIDR
      Tags: 
        - Key: Name
          Value: EC2-sg
      VpcId: !Ref VPC

  VPCEndpointSG:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: for Systems Manager
      GroupName: SystemsManager-sg
      SecurityGroupEgress: 
        - CidrIp: 0.0.0.0/0
          FromPort: -1
          IpProtocol: -1
          ToPort: -1
      SecurityGroupIngress: 
        - SourceSecurityGroupId: !Ref EC2Sg
          FromPort: 443
          IpProtocol: tcp
          ToPort: 443
      Tags: 
        - Key: Name
          Value: SystemsManager-sg
      VpcId: !Ref VPC

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

  SystemsManagerEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcEndpointType: Interface
      PrivateDnsEnabled: true
      ServiceName: !Sub com.amazonaws.${AWS::Region}.ssm
      VpcId: !Ref VPC
      SubnetIds:
        - !Ref PrivateSubnet01
      SecurityGroupIds:
        - !Ref VPCEndpointSG

  SystemsManagerMessageEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcEndpointType: Interface
      PrivateDnsEnabled: true
      ServiceName: !Sub com.amazonaws.${AWS::Region}.ssmmessages
      VpcId: !Ref VPC
      SubnetIds:
        - !Ref PrivateSubnet01
      SecurityGroupIds:
        - !Ref VPCEndpointSG

# ------------------------------------------------------------#
# VPC Endpoint Service
# ------------------------------------------------------------# 
  VPCEndpointService:
    DependsOn:
      - NLB
    Type: AWS::EC2::VPCEndpointService
    Properties: 
      AcceptanceRequired: true
      NetworkLoadBalancerArns: 
        - !Ref NLB

  VPCEndpointServicePermissions:
    DependsOn:
      - VPCEndpointService
    Type: AWS::EC2::VPCEndpointServicePermissions
    Properties: 
      AllowedPrincipals:
        - !Join 
          - ''
          - - 'arn:aws:iam::'
            - !Ref AWSAccountId
            - ':root'
      ServiceId: !Ref VPCEndpointService

# ------------------------------------------------------------#
# IAM
# ------------------------------------------------------------# 
  EC2IAMPolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties: 
      PolicyDocument:
        Version: "2012-10-17"
        Statement: 
          - Effect: Allow
            Action:
              - "s3:GetObject"
            Resource: 
              - !Join 
                - ''
                - - 'arn:aws:s3:::amazonlinux.'
                  - !Sub ${AWS::Region}
                  - '.amazonaws.com/*'
              - !Join 
                - ''
                - - 'arn:aws:s3:::amazonlinux-2-repos-'
                  - !Sub ${AWS::Region}
                  - '/*'
      ManagedPolicyName: iam-repository-access-policy-ec2

  EC2IAMRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument: 
        Version: "2012-10-17"
        Statement: 
          - Effect: Allow
            Principal: 
              Service: 
                - ec2.amazonaws.com
            Action: 
              - 'sts:AssumeRole'
      ManagedPolicyArns: 
        - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
        - !Ref EC2IAMPolicy
      RoleName: iam-repository-access-role-ec2
      Tags:
        - Key: Name
          Value: iam-repository-access-role-ec2

  EC2IAMInstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      InstanceProfileName: iam-repository-access-instanceprofile-ec2
      Roles: 
        - !Ref EC2IAMRole

# ------------------------------------------------------------#
# EC2
# ------------------------------------------------------------# 
  EC2:
    Type: AWS::EC2::Instance
    Properties:
      BlockDeviceMappings: 
        - DeviceName: /dev/xvda
          Ebs:
            DeleteOnTermination: true
            Encrypted: true
            Iops: !Ref EC2VolumeIOPS
            VolumeSize: !Ref EC2VolumeSize
            VolumeType: gp3
      DisableApiTermination: false
      IamInstanceProfile: !Ref EC2IAMInstanceProfile
      ImageId: !Ref EC2AMI
      InstanceType: !Ref EC2InstanceType
      NetworkInterfaces: 
        - DeleteOnTermination: true
          DeviceIndex: 0
          GroupSet: 
            - !Ref EC2Sg
          SubnetId: !Ref PrivateSubnet01
      Tags:
        - Key: Name
          Value: account-b-ec2
      UserData: !Base64 |
        #!/bin/bash
        yum update -y
        yum install httpd -y
        systemctl start httpd
        systemctl enable httpd
        echo "Private Link Test" > /var/www/html/index.html

# ------------------------------------------------------------#
# NLB
# ------------------------------------------------------------# 
  NLB:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      IpAddressType: ipv4
      LoadBalancerAttributes:
        - Key: deletion_protection.enabled
          Value: false
      Name: account-b-nlb
      Scheme: internal
      Subnets: 
        - !Ref PrivateSubnet01
      Tags: 
        - Key: Name
          Value: account-b-nlb
      Type: network

  TargetGroup: 
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      HealthCheckEnabled: true
      HealthCheckIntervalSeconds: 30
      HealthCheckPort: traffic-port
      HealthCheckProtocol: TCP
      HealthyThresholdCount: 5
      IpAddressType: ipv4
      Name: account-b-nlb-tg
      Port: 80
      Protocol: TCP
      TargetGroupAttributes: 
        - Key: preserve_client_ip.enabled
          Value: true
      Targets:
        - Id: !Ref EC2
          Port: 80
      TargetType: instance
      VpcId: !Ref VPC

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

S3やSystems Managerで利用するVPCエンドポイントは153行目~184行目で作成しています。
Session Managerを利用するだけであれば以下の2つのVPCエンドポイントを作成すれば問題ありません。

  • com.amazonaws.ap-northeast-1.ssm
  • com.amazonaws.ap-northeast-1.ssmmessages

189行目~209行目でVPCエンドポイントサービスを作成しています。
また、アカウントAから利用できるように権限を追加しています。
デプロイは以下のコマンドを実行します。

aws cloudformation create-stack --stack-name CloudFormationスタック名 --template-body file://CloudFormationテンプレートファイル名 --parameters ParameterKey=AWSAccountId,ParameterValue=アカウントID --capabilities CAPABILITY_NAMED_IAM

デプロイが完了したらVPCエンドポイントサービスの画面へ移動してサービス名を確認します。
このサービス名はアカウントAでVPCエンドポイントを作成する際に使用するものになります。

アカウントAでリソース作成

アカウントAではアカウントBで作成したVPCエンドポイントサービス名を使用してVPCエンドポイントを作成します。

CloudFormationテンプレート (ここをクリックしてください)
AWSTemplateFormatVersion: "2010-09-09"

Description: Client Stack

Metadata:
# ------------------------------------------------------------#
# Metadata
# ------------------------------------------------------------# 
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label: 
          default: Parameters for VPC
        Parameters:
          - VPCCIDR
      - Label: 
          default: Parameters for Subnet
        Parameters:
          - PrivateSubnet01CIDR
      - Label: 
          default: Parameters for VPC Endpoint
        Parameters:
          - ServiceName
      - Label: 
          default: Parameters for EC2
        Parameters:
          - EC2VolumeSize
          - EC2VolumeIOPS
          - EC2AMI
          - EC2InstanceType

Parameters:
# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------# 
  VPCCIDR:
    Default: 10.1.0.0/16
    Type: String

  PrivateSubnet01CIDR:
    Default: 10.1.0.0/24
    Type: String

  ServiceName:
    Type: String

  EC2VolumeSize:
    Default: 32
    Type: Number

  EC2VolumeIOPS:
    Default: 3000
    Type: Number

  EC2AMI:
    Default: ami-0bba69335379e17f8
    Type: AWS::EC2::Image::Id

  EC2InstanceType:
    Default: t3.micro
    Type: String

Resources:
# ------------------------------------------------------------#
# VPC
# ------------------------------------------------------------# 
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VPCCIDR
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags: 
        - Key: Name
          Value: account-a-vpc

# ------------------------------------------------------------#
# Subnet
# ------------------------------------------------------------# 
  PrivateSubnet01:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: ap-northeast-1a
      CidrBlock: !Ref PrivateSubnet01CIDR
      Tags: 
        - Key: Name
          Value: account-a-private-01
      VpcId: !Ref VPC

# ------------------------------------------------------------#
# RouteTable
# ------------------------------------------------------------# 
  PrivateRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: account-a-private-rtb

  PrivateRtAssociation1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PrivateRouteTable
      SubnetId: !Ref PrivateSubnet01

# ------------------------------------------------------------#
# Security Group
# ------------------------------------------------------------# 
  EC2Sg:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: for EC2
      GroupName: EC2-sg
      SecurityGroupEgress: 
        - CidrIp: 0.0.0.0/0
          FromPort: -1
          IpProtocol: -1
          ToPort: -1
      Tags: 
        - Key: Name
          Value: EC2-sg
      VpcId: !Ref VPC

  VPCEndpointSG:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: for Systems Manager
      GroupName: SystemsManager-sg
      SecurityGroupEgress: 
        - CidrIp: 0.0.0.0/0
          FromPort: -1
          IpProtocol: -1
          ToPort: -1
      SecurityGroupIngress: 
        - SourceSecurityGroupId: !Ref EC2Sg
          FromPort: 443
          IpProtocol: tcp
          ToPort: 443
      Tags: 
        - Key: Name
          Value: SystemsManager-sg
      VpcId: !Ref VPC

  PrivateLinkSG:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: for Private Link
      GroupName: PrivateLink-sg
      SecurityGroupEgress: 
        - CidrIp: 0.0.0.0/0
          FromPort: -1
          IpProtocol: -1
          ToPort: -1
      SecurityGroupIngress: 
        - SourceSecurityGroupId: !Ref EC2Sg
          FromPort: 80
          IpProtocol: tcp
          ToPort: 80
      Tags: 
        - Key: Name
          Value: PrivateLink-sg
      VpcId: !Ref VPC

# ------------------------------------------------------------#
# VPC Endpoint
# ------------------------------------------------------------# 
  SystemsManagerEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcEndpointType: Interface
      PrivateDnsEnabled: true
      ServiceName: !Sub com.amazonaws.${AWS::Region}.ssm
      VpcId: !Ref VPC
      SubnetIds:
        - !Ref PrivateSubnet01
      SecurityGroupIds:
        - !Ref VPCEndpointSG

  SystemsManagerMessageEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcEndpointType: Interface
      PrivateDnsEnabled: true
      ServiceName: !Sub com.amazonaws.${AWS::Region}.ssmmessages
      VpcId: !Ref VPC
      SubnetIds:
        - !Ref PrivateSubnet01
      SecurityGroupIds:
        - !Ref VPCEndpointSG

  PrivateLink:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcEndpointType: Interface
      PrivateDnsEnabled: false
      ServiceName: !Ref ServiceName
      VpcId: !Ref VPC
      SubnetIds:
        - !Ref PrivateSubnet01
      SecurityGroupIds:
        - !Ref PrivateLinkSG

# ------------------------------------------------------------#
# IAM
# ------------------------------------------------------------# 
  EC2IAMRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument: 
        Version: "2012-10-17"
        Statement: 
          - Effect: Allow
            Principal: 
              Service: 
                - ec2.amazonaws.com
            Action: 
              - 'sts:AssumeRole'
      ManagedPolicyArns: 
        - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
      RoleName: iam-client-access-role-ec2
      Tags:
        - Key: Name
          Value: iam-client-access-role-ec2

  EC2IAMInstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      InstanceProfileName: iam-client-access-instanceprofile-ec2
      Roles: 
        - !Ref EC2IAMRole

# ------------------------------------------------------------#
# EC2
# ------------------------------------------------------------# 
  EC2:
    Type: AWS::EC2::Instance
    Properties:
      BlockDeviceMappings: 
        - DeviceName: /dev/xvda
          Ebs:
            DeleteOnTermination: true
            Encrypted: true
            Iops: !Ref EC2VolumeIOPS
            VolumeSize: !Ref EC2VolumeSize
            VolumeType: gp3
      DisableApiTermination: false
      IamInstanceProfile: !Ref EC2IAMInstanceProfile
      ImageId: !Ref EC2AMI
      InstanceType: !Ref EC2InstanceType
      NetworkInterfaces: 
        - DeleteOnTermination: true
          DeviceIndex: 0
          GroupSet: 
            - !Ref EC2Sg
          SubnetId: !Ref PrivateSubnet01
      Tags:
        - Key: Name
          Value: account-a-ec2

191行目から201行目でアカウントBのVPCエンドポイントサービスに対応するVPCエンドポイントを作成しています。
今回はサブネットが1つだけなので「SubnetIds」が1つになっていますが、他のサブネットからも使用したい場合は追加する必要があります。
デプロイは以下のコマンドを実行します。
こちらのCloudFormationはアカウントAで実行するものなので実行場所 (クレデンシャル) などに気を付けてください。

aws cloudformation create-stack --stack-name CloudFormationスタック名 --template-body file://CloudFormationテンプレートファイル名 --parameters ParameterKey=ServiceName,ParameterValue=VPCエンドポイントサービス名 --capabilities CAPABILITY_NAMED_IAM

デプロイが完了したらアカウントBのVPCエンドポイントサービスの画面に移動してCloudFormationで作成したVPCエンドポイントサービスを選択してください。
選択したら「エンドポイント接続」タブをクリックします。

クリックすると「所有者」の欄がアカウントAのIDになった接続があるので選択します。
選択後、「アクション」から「エンドポイント接続リクエストの承諾」をクリックします。

クリックすると確認画面が出るので承諾と入力して「承諾」をクリックします。
クリック後、しばらくすると「状態欄」がAvailableになります。
ここまでで作成は完了です。

動作確認

アカウントAで作成したEC2にSession Managerで接続します。
Session Managerの利用方法は以下の公式ドキュメントをご確認ください。
セッションを開始する (Amazon EC2 コンソール)
EC2に接続できたら以下のコマンドを実行します。
VPCエンドポイントDNS名はVPCエンドポイントの詳細タブから確認できます。

curl http://VPCエンドポイントDNS名

成功すると「Private Link Test」とレスポンスが返ってきます。

アカウントBのEC2に接続してApacheのアクセスログを確認すると以下のようにアクセスされていることが確認できます。
IPアドレス「10.0.0.236」はNLBのネットワークインターフェイスのIPアドレスです。
ソースIPが確認したい場合は以下のドキュメントに記載されている通りProxy Protocolを使用する必要があります。
接続情報のプロキシプロトコルを使用する

tail -f /var/log/httpd/access_log
10.0.0.236 - - [27/Jan/2023:09:03:18 +0000] "GET / HTTP/1.1" 200 18 "-" "curl/7.79.1"
10.0.0.236 - - [27/Jan/2023:09:03:18 +0000] "GET / HTTP/1.1" 200 18 "-" "curl/7.79.1"
10.0.0.236 - - [27/Jan/2023:09:03:19 +0000] "GET / HTTP/1.1" 200 18 "-" "curl/7.79.1"
10.0.0.236 - - [27/Jan/2023:09:03:19 +0000] "GET / HTTP/1.1" 200 18 "-" "curl/7.79.1"
10.0.0.236 - - [27/Jan/2023:09:03:20 +0000] "GET / HTTP/1.1" 200 18 "-" "curl/7.79.1"

さいごに

複数アカウントにまたがると管理が複雑化するのかと思っていましたが、許可設定を入れてあげるだけで利用できるので簡単にサービスへアクセス可能になるといった感じでした。
NLBのターゲットにALBが設定できるのでパスベースルーティングなんかも利用できるのが便利な印象です。