この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
こんにちは!AWS事業本部のおつまみです。
皆さん、AWS CloudFormationでVPCEndpoint + NLB + ALB + EC2の構成を作ってみたいなぁと思ったことはありますか?私はあります。
ちなみに2021年9月よりNLBのターゲットにALBを登録することができるようになってます。
今回同じようなシステム構成の案件に携わったので、せっかくなら自分でAWS CloudFormationを使って構築してみようと思い、やってみました。
ちなみにAWS CDKを使ったNLB + ALB + EC2の構成はこちらが参考になります。
実際にやってみた
構成図
ゴールはVPC AのEC2からVPC BのEC2に接続されることです。
- VPC AのEC2にはSession Manager経由で接続します。
- VPC BのEC2には事前にユーザデータで下記を設定しています。
- apacheをインストール
- ドキュメントルート配下(/var/www/html)にindex.htmlを作成し、「Hello World」を設定。
CloudFormationテンプレート
CloudFormationテンプレートはこのように3つに分けて作成しました。
VPCA.yml
AWSTemplateFormatVersion: "2010-09-09"
Description: "VPC-A Template."
Parameters:
# ------------------------------------------------------------#
# Common
# ------------------------------------------------------------#
Prefix:
Type: String
Default: "vpc-a"
# ------------------------------------------------------------#
# 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
EC2SecurityGroup:
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
# ------------------------------------------------------------#
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 EC2SecurityGroup
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 EC2SecurityGroup
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 EC2SecurityGroup
SubnetId: !Ref PrivateSubnet
UserData: !Base64 |
#! /bin/bash
yum update -y
# ------------------------------------------------------------#
# Output Parameters
# ------------------------------------------------------------#
Outputs:
Vpc:
Value: !Ref Vpc
Export:
Name: VpcA
PrivateSubnet:
Value: !Ref PrivateSubnet
Export:
Name: PrivateSubnet-VpcA
EC2SecurityGroup:
Value: !Ref EC2SecurityGroup
Export:
Name: EC2SecurityGroup-VpcA
VPCB.yml
AWSTemplateFormatVersion: "2010-09-09"
Description: "VPC-B Template."
Parameters:
# ------------------------------------------------------------#
# Common
# ------------------------------------------------------------#
Prefix:
Type: String
Default: "vpc-b"
# ------------------------------------------------------------#
# Network
# ------------------------------------------------------------#
VpcCidr:
Type: String
Default: "10.1.0.0/16"
PrivateSubnetCidrA:
Type: String
Default: "10.1.0.0/24"
PrivateSubnetCidrC:
Type: String
Default: "10.1.2.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
PrivateSubnetA:
Type: AWS::EC2::Subnet
Properties:
CidrBlock: !Ref PrivateSubnetCidrA
VpcId: !Ref Vpc
AvailabilityZone:
Fn::Select:
- "0"
- Fn::GetAZs: ""
Tags:
- Key: Name
Value: !Sub ${Prefix}-private-subnet-a
PrivateSubnetC:
Type: AWS::EC2::Subnet
Properties:
CidrBlock: !Ref PrivateSubnetCidrC
VpcId: !Ref Vpc
AvailabilityZone:
Fn::Select:
- "1"
- Fn::GetAZs: ""
Tags:
- Key: Name
Value: !Sub ${Prefix}-private-subnet-c
PrivateRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref Vpc
Tags:
- Key: Name
Value: !Sub "${Prefix}-table"
PrivateSubnetTableAssociationA:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnetA
RouteTableId: !Ref PrivateRouteTable
PrivateSubnetTableAssociationC:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnetC
RouteTableId: !Ref PrivateRouteTable
EC2SecurityGroup:
Type: "AWS::EC2::SecurityGroup"
Properties:
VpcId: !Ref Vpc
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: "0.0.0.0/0"
GroupName: !Sub "${Prefix}-ec2-sg"
GroupDescription: "-"
Tags:
- Key: "Name"
Value: !Sub "${Prefix}-ec2-sg"
ALBSecurityGroup:
Type: "AWS::EC2::SecurityGroup"
Properties:
VpcId: !Ref Vpc
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: "0.0.0.0/0"
GroupName: !Sub "${Prefix}-alb-sg"
GroupDescription: "-"
Tags:
- Key: "Name"
Value: !Sub "${Prefix}-alb-sg"
# ------------------------------------------------------------#
# VPC Endpoint
# ------------------------------------------------------------#
S3VPCEndpoint:
Type: "AWS::EC2::VPCEndpoint"
Properties:
RouteTableIds:
- !Ref PrivateRouteTable
ServiceName: !Sub "com.amazonaws.${AWS::Region}.s3"
VpcEndpointType: Gateway
VpcId: !Ref Vpc
# ------------------------------------------------------------#
# EC2Instance
# ------------------------------------------------------------#
EC2Instance1:
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 EC2SecurityGroup
SubnetId: !Ref PrivateSubnetA
UserData: !Base64 |
#! /bin/bash
sudo yum update -y
sudo yum -y install httpd
sudo systemctl start httpd
sudo systemctl enable httpd
sudo echo "Hello World" >> /var/www/html/index.html
EC2Instance2:
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 EC2SecurityGroup
SubnetId: !Ref PrivateSubnetC
UserData: !Base64 |
#! /bin/bash
sudo yum update -y
sudo yum -y install httpd
sudo systemctl start httpd
sudo systemctl enable httpd
sudo echo "Hello World" >> /var/www/html/index.html
# ------------------------------------------------------------#
# ALB
# ------------------------------------------------------------#
ALBTargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
VpcId: !Ref Vpc
Name: !Sub "${Prefix}-alb-tg"
Protocol: HTTP
Port: 80
Tags:
- Key: Name
Value: !Sub "${Prefix}-alb-tg"
Targets:
- Id: !Ref EC2Instance1
Port: 80
- Id: !Ref EC2Instance2
Port: 80
ALB:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
IpAddressType: ipv4
Name: !Sub "${Prefix}-alb"
Tags:
- Key: Name
Value: !Sub "${Prefix}-alb"
Scheme: "internal"
SecurityGroups:
- !Ref ALBSecurityGroup
Subnets:
- !Ref PrivateSubnetA
- !Ref PrivateSubnetC
Type: application
ALBListener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
DefaultActions:
- TargetGroupArn: !Ref ALBTargetGroup
Type: forward
LoadBalancerArn: !Ref ALB
Port: 80
Protocol: HTTP
# ------------------------------------------------------------#
# NLB
# ------------------------------------------------------------#
NLBTargetGroup:
DependsOn:
- ALBListener
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
VpcId: !Ref Vpc
Name: !Sub "${Prefix}-nlb-tg"
Protocol: TCP
Port: 80
Tags:
- Key: Name
Value: !Sub "${Prefix}-nlb-tg"
Targets:
- Id: !Ref ALB
Port: 80
TargetType: alb
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 PrivateSubnetA
- !Ref PrivateSubnetC
Type: network
NLBListener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
DefaultActions:
- TargetGroupArn: !Ref NLBTargetGroup
Type: forward
LoadBalancerArn: !Ref NLB
Port: 80
Protocol: TCP
# ------------------------------------------------------------#
# Output Parameters
# ------------------------------------------------------------#
Outputs:
NLB:
Value: !Ref NLB
Export:
Name: NLB
NLBVPCEndpoint.yml
AWSTemplateFormatVersion: "2010-09-09"
Description: "NLBVPCEndpoint Template."
Parameters:
# ------------------------------------------------------------#
# Common
# ------------------------------------------------------------#
Prefix:
Type: String
Default: "nlb-vpcendpoint"
Resources:
# ------------------------------------------------------------#
# VPC Endpoint
# ------------------------------------------------------------#
NLBVPCEndpointService:
Type: "AWS::EC2::VPCEndpointService"
Properties:
AcceptanceRequired: true
NetworkLoadBalancerArns:
- !ImportValue NLB
NLBVPCEndpointSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: !Sub ${Prefix}-nlb-vpc-endpoint-sg
GroupName: !Sub ${Prefix}-nlb-vpc-endpoint-sg
VpcId: !ImportValue VpcA
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
SourceSecurityGroupId: !ImportValue EC2SecurityGroup-VpcA
Tags:
- Key: Name
Value: !Sub ${Prefix}-nlb-vpc-endpoint-sg
NLBVPCEndpoint:
DependsOn:
- NLBVPCEndpointService
Type: AWS::EC2::VPCEndpoint
Properties:
VpcEndpointType: Interface
PrivateDnsEnabled: false
ServiceName: !Sub com.amazonaws.vpce.${AWS::Region}.${NLBVPCEndpointService}
VpcId: !ImportValue VpcA
SubnetIds:
- !ImportValue PrivateSubnet-VpcA
SecurityGroupIds:
- !Ref NLBVPCEndpointSecurityGroup
CloudFormation実行後には、エンドポイントサービスからエンドポイント接続を承認する必要があります。
そのため、下記の公式ドキュメントに記載の通りマネジメントコンソールもしくはAWS CLI経由で承認してあげましょう。
接続確認
VPC AのEC2にSession Manager経由で接続します。
下記のコマンド通りNLBのエンドポイントにcurlをして、VPC Bへの接続を確認します。
% curl <NLBのエンドポイント>
無事、Hello Worldと出力されました!
ハマったところ
ALBのヘルスチェックに403エラーで失敗する。
理由はVPC B側のEC2にindex.htmlを作成していないためでした。
そのため、ヘルスチェックをデフォルトの"/"にしている場合、apacheだと初期設定の/var/www/htmlにインデックスページを作成する必要があります。
NLBのターゲットグループ作成に失敗してしまう。
このようにCloudFormation実行が失敗しました。
理由はALB作成前にNLBのターゲットグループを作成しようとしているためでした。 そのため、NLBのターゲットグループにDependsOn属性で依存関係を明示してあげましょう。
NLBTargetGroup:
DependsOn:
- ALBListener
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
VpcId: !Ref Vpc
Name: !Sub "${Prefix}-nlb-tg"
Protocol: TCP
Port: 80
Tags:
- Key: Name
Value: !Sub "${Prefix}-nlb-tg"
Targets:
- Id: !Ref ALB
Port: 80
TargetType: alb
最後に
今回はAWS CloudFormationでVPCEndpoint + NLB + ALB + EC2の構成を作ってみました。
所々はまるポイントがあったため、同じような構成をAWS CloudFormationで構築する際にお役に立てれば幸いです。
最後までお読みいただきありがとうございました!
以上、おつまみ(@AWS11077)でした!