ポート番号ごとに違うコンテンツを表示するAWS PrivateLinkを設定してみた

2023.03.23

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

1つのEC2 (Webサーバー) にバーチャルホストを設定してPrivateLinkでアクセスする検証を行ったのでブログに残します。

環境

以下構成図になります。

左側のサブネットで起動しているEC2 (コンテンツ確認用) から80番ポートでVPCエンドポイントへアクセスした場合は80番ポート用のコンテンツ、8080番でアクセスした場合は8080番用のコンテンツを表示するように設定を行います。

設定

AWSリソース作成

AWSリソースはCloudFormationで作成します。
まずはVPCエンドポイント以外の部分を以下のCloudFormationテンプレートで作成します。

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

Description: PrivateLink

Metadata:
# ------------------------------------------------------------#
# Metadata
# ------------------------------------------------------------# 
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label: 
          default: Parameters for VPC
        Parameters:
          - VPCCIDR1
          - VPCCIDR2
      - Label: 
          default: Parameters for Subnet
        Parameters:
          - PublicSubnet01CIDR
          - PublicSubnet02CIDR
      - Label: 
          default: Parameters for EC2
        Parameters:
          - EC2VolumeSize
          - EC2VolumeIOPS
          - EC2AMI
          - EC2InstanceType

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

  VPCCIDR2:
    Default: 10.1.0.0/16
    Type: String

  PublicSubnet01CIDR:
    Default: 10.0.0.0/24
    Type: String

  PublicSubnet02CIDR:
    Default: 10.1.0.0/24
    Type: String

  EC2VolumeSize:
    Default: 32
    Type: Number

  EC2VolumeIOPS:
    Default: 3000
    Type: Number

  EC2AMI:
    Default: ami-067871d950411e643
    Type: AWS::EC2::Image::Id

  EC2InstanceType:
    Default: t3.micro
    Type: String

Resources:
# ------------------------------------------------------------#
# VPC
# ------------------------------------------------------------# 
  VPC1:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VPCCIDR1
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags: 
        - Key: Name
          Value: private-link-service-vpc

  VPC2:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VPCCIDR2
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags: 
        - Key: Name
          Value: private-link-client-vpc

# ------------------------------------------------------------#
# Subnet
# ------------------------------------------------------------# 
  PublicSubnet01:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: ap-northeast-1a
      CidrBlock: !Ref PublicSubnet01CIDR
      MapPublicIpOnLaunch: true
      Tags: 
        - Key: Name
          Value: private-link-service-subnet
      VpcId: !Ref VPC1

  PublicSubnet02:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: ap-northeast-1a
      CidrBlock: !Ref PublicSubnet02CIDR
      MapPublicIpOnLaunch: true
      Tags: 
        - Key: Name
          Value: private-link-client-subnet
      VpcId: !Ref VPC2

# ------------------------------------------------------------#
# InternetGateway
# ------------------------------------------------------------# 
  InternetGateway1:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags: 
        - Key: Name
          Value: private-link-service-igw

  InternetGatewayAttachment1:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      InternetGatewayId: !Ref InternetGateway1
      VpcId: !Ref VPC1

  InternetGateway2:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags: 
        - Key: Name
          Value: private-link-client-igw

  InternetGatewayAttachment2:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      InternetGatewayId: !Ref InternetGateway2
      VpcId: !Ref VPC2

# ------------------------------------------------------------#
# RouteTable
# ------------------------------------------------------------# 
  PublicRouteTable1:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC1
      Tags:
        - Key: Name
          Value: private-link-service-rtb

  PublicRouteTableRoute1:
    Type: AWS::EC2::Route
    Properties:
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway1
      RouteTableId: !Ref PublicRouteTable1

  PublicRtAssociation1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicRouteTable1
      SubnetId: !Ref PublicSubnet01

  PublicRouteTable2:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC2
      Tags:
        - Key: Name
          Value: private-link-client-rtb

  PublicRouteTableRoute2:
    Type: AWS::EC2::Route
    Properties:
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway2
      RouteTableId: !Ref PublicRouteTable2

  PublicRtAssociation2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicRouteTable2
      SubnetId: !Ref PublicSubnet02

# ------------------------------------------------------------#
# Security Group
# ------------------------------------------------------------# 
  ServiceEC2SG:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: for EC2 Web
      GroupName: EC2-web-sg
      SecurityGroupEgress: 
        - CidrIp: 0.0.0.0/0
          FromPort: -1
          IpProtocol: -1
          ToPort: -1
      SecurityGroupIngress: 
        - FromPort: 80
          IpProtocol: tcp
          ToPort: 80
          CidrIp: !Ref VPCCIDR1
        - FromPort: 8080
          IpProtocol: tcp
          ToPort: 8080
          CidrIp: !Ref VPCCIDR1
      Tags: 
        - Key: Name
          Value: EC2-web-sg
      VpcId: !Ref VPC1

  ClientEC2SG:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: for EC2 Client
      GroupName: EC2-client-sg
      SecurityGroupEgress: 
        - CidrIp: 0.0.0.0/0
          FromPort: -1
          IpProtocol: -1
          ToPort: -1
      Tags: 
        - Key: Name
          Value: EC2-client-sg
      VpcId: !Ref VPC2

  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 ClientEC2SG
          FromPort: 80
          IpProtocol: tcp
          ToPort: 80
        - SourceSecurityGroupId: !Ref ClientEC2SG
          FromPort: 8080
          IpProtocol: tcp
          ToPort: 8080
      Tags: 
        - Key: Name
          Value: privatelink-sg
      VpcId: !Ref VPC2

# ------------------------------------------------------------#
# 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-role-ec2
      Tags:
        - Key: Name
          Value: iam-role-ec2

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

# ------------------------------------------------------------#
# EC2
# ------------------------------------------------------------# 
  ServiceEC2:
    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 ServiceEC2SG
          SubnetId: !Ref PublicSubnet01
      Tags:
        - Key: Name
          Value: private-link-service-ec2
      UserData: !Base64 |
        #!/bin/bash
        dnf install httpd -y
        systemctl start httpd
        systemctl enable httpd
        echo "Private Link Port 80" > /var/www/html/index.html
        mkdir /var/www/port8080
        echo "Private Link Port 8080" > /var/www/port8080/index.html
        touch /etc/httpd/conf.d/port8080.conf
        echo "Listen 8080" > /etc/httpd/conf.d/port8080.conf
        echo "<VirtualHost *:8080>" >> /etc/httpd/conf.d/port8080.conf
        echo " DocumentRoot /var/www/port8080" >> /etc/httpd/conf.d/port8080.conf
        echo "</VirtualHost>" >> /etc/httpd/conf.d/port8080.conf
        systemctl reload httpd

  ClientEC2:
    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 ClientEC2SG
          SubnetId: !Ref PublicSubnet02
      Tags:
        - Key: Name
          Value: private-link-client-ec2

# ------------------------------------------------------------#
# NLB
# ------------------------------------------------------------# 
  NLB:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      IpAddressType: ipv4
      LoadBalancerAttributes:
        - Key: deletion_protection.enabled
          Value: false
      Name: private-link-service-nlb
      Scheme: internal
      Subnets: 
        - !Ref PublicSubnet01
      Tags: 
        - Key: Name
          Value: private-link-service-nlb
      Type: network

  TargetGroup1: 
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    DependsOn:
      - ServiceEC2
    Properties:
      HealthCheckEnabled: true
      HealthCheckIntervalSeconds: 30
      HealthCheckPort: traffic-port
      HealthCheckProtocol: TCP
      HealthyThresholdCount: 5
      IpAddressType: ipv4
      Name: private-link-service-nlb-tg-80
      Port: 80
      Protocol: TCP
      TargetGroupAttributes: 
        - Key: preserve_client_ip.enabled
          Value: true
      Targets:
        - Id: !Ref ServiceEC2
          Port: 80
      TargetType: instance
      VpcId: !Ref VPC1

  TargetGroup2: 
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    DependsOn:
      - ServiceEC2
    Properties:
      HealthCheckEnabled: true
      HealthCheckIntervalSeconds: 30
      HealthCheckPort: traffic-port
      HealthCheckProtocol: TCP
      HealthyThresholdCount: 5
      IpAddressType: ipv4
      Name: private-link-service-nlb-tg-8080
      Port: 8080
      Protocol: TCP
      TargetGroupAttributes: 
        - Key: preserve_client_ip.enabled
          Value: true
      Targets:
        - Id: !Ref ServiceEC2
          Port: 8080
      TargetType: instance
      VpcId: !Ref VPC1

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

  Listener2:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties: 
      DefaultActions:
        - TargetGroupArn: !Ref TargetGroup2
          Type: forward
      LoadBalancerArn: !Ref NLB
      Port: 8080
      Protocol: TCP

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

Outputs:
# ------------------------------------------------------------#
# Outputs
# ------------------------------------------------------------# 
  VPC2ID:
    Value: !Ref VPC2
    Export: 
      Name: VPC2ID

  PrivateLinkSGID:
    Value: !Ref PrivateLinkSG
    Export: 
      Name: PrivateLinkSGID

  PublicSubnet02ID:
    Value: !Ref PublicSubnet02
    Export: 
      Name: PublicSubnet02ID

上記CloudFormationテンプレートではVPCエンドポイント以外の部分が作成されます。
369~433行目のターゲットグループ、リスナールールで80番ポート、8080番ポートを振り分ける設定を入れています。

デプロイは以下のコマンドを実行します。

aws cloudformation create-stack --stack-name CloudFormationスタック名 --template-body file://CloudFormationテンプレートファイル名 --capabilities CAPABILITY_NAMED_IAM

デプロイが完了したらVPCエンドポイントを作成していきます。
以下のCloudFormationテンプレートで作成します。

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

Description: Client Stack

Parameters:
# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------# 
  ServiceName:
    Type: String

Resources:
# ------------------------------------------------------------#
# VPC Endpoint
# ------------------------------------------------------------# 
  PrivateLink:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcEndpointType: Interface
      PrivateDnsEnabled: false
      ServiceName: !Ref ServiceName
      VpcId: !ImportValue VPC2ID
      SubnetIds:
        - !ImportValue PublicSubnet02ID
      SecurityGroupIds:
        - !ImportValue PrivateLinkSGID

上記CloudFormationテンプレートではVPCエンドポイントを作成しています。
パラメータにあるServiceNameはマネジメントコンソールからVPC>エンドポイントサービスの詳細から確認が可能です。

デプロイは以下のコマンドを実行します。

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

デプロイが完了したらマネジメントコンソールからVPC>エンドポイントサービス>該当のVPCエンドポイントサービス選択>エンドポイント接続>エンドポイント接続リクエストの承諾でリクエストの承諾を行います。
しばらくすると「状態」がPendingからAvailableになります。
ここまででリソースの作成が完了です。

動作確認

以下の手順でSession Managerを使用してEC2 (private-link-client-ec2) へ接続します。
セッションを開始する (Amazon EC2 コンソール)

接続ができたら以下のコマンドで80番ポートと8080番ポートにアクセスします。

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

成功すると80番ポートの時は「Private Link Port 80」、8080番ポートの時は「Private Link Port 8080」とレスポンスが返ってきます。

さいごに

PrivateLinkの後ろにいるのがNLBなのでポート番号での振り分けは可能だろうと思いやってみました。
NLBのターゲットにALBが設定可能なのでURLでの振り分けができるのか今度検証してみたいと思いました。