AWS PrivateLinkを使って別AWSアカウントにあるEC2にHTTP通信させる設定をやったことが無かったなと思いやってみました。
AWS PrivateLinkって何
AWSサービスやVPC内でホストされているアプリケーションをプライベートに利用できるようにするサービスです。
AWS PrivateLinkを利用することでパブリックな通信を行わずにAWSサービス (VPCエンドポイントに対応していれば) などを使用することができます。
AWS PrivateLink および VPC エンドポイント
構成
ざっくりやりたいことは以下の1~3になります。
- アカウントBでNLBとEC2を作成
- アカウントAでVPCエンドポイントとEC2を作成
- アカウント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が設定できるのでパスベースルーティングなんかも利用できるのが便利な印象です。