初投稿になります。自身の理解をより深めるためのアウトプットをするために投稿しました。
今月1日からIPv4のパブリックIPアドレスが全て課金対象になったため、これはIPv6が使える環境を用意できるようにならなければという危機感を感じました。(今更感...)
本稿のテンプレートファイルはそれに対応するために作成しました。
切り替えについては単純に興味本位で試みたものです。
本稿のテンプレートファイルで作成する構成はお勉強用のものだと思っています。
現場ではこんな単純な構成は稀でしょうし、きちんとした要件定義の元にIPプロトコル等の仕様を決定するものと思います。
概要
- 本稿のテンプレートファイルを全てデプロイすることで、ネットワークからEC2 Instance Connect Endpointまで手軽に準備できます
- ご利用は自己責任でお願いいたします
- 本稿のリソースのみ展開する場合は、2024/2/23時点において12ヶ月の無料利用枠内で全て作成可能です
- 筆者は12ヶ月の無料利用枠がある状態です。2月初旬にリソース作成を行いましたが今のところ請求情報に課金されているものは見当たりません
- defaultのVPCやらサブネットやらは存在している状態で大丈夫です
- 一応、ネットワークレイヤーとセキュリティレイヤーは12ヶ月の無料利用枠が切れていても課金は発生しないと思います
- 東京リージョンでの作成を想定しています
- CloudShellもしくはローカルのAWS CLIからコマンドでデプロイすることを前提にしていますが、マネジメントコンソールからの作成も可能です
- EC2 Instance Connect、EC2 Instance Connect Endpointはおまけです
- !Refや!Cidr等の組み込み関数、AWS::StackNameやAWS::NoValue等の疑似パラメータ等は下記の公式ユーザーガイドを参照願います
想定読者
- CloudFormationの知見を深めたい方
- IPv6やDual-Stackにする方法を知りたい方
- CloudFormationでサクッとEC2を使える環境を作りたい方
- EC2 Instance Connect、EC2 Instance Connect Endpointを試してみたい方
内容の理解には、サーバーやネットワークについてある程度の知識が必要かと思います。
リソースの全体構成
スタックのレイヤー分け
レイヤー | スタック名 | テンプレートファイル名 |
---|---|---|
アプリケーション | ipv46d-eicendpoint-cfn ipv46d-ec2-cfn |
ipv46d-eicendpoint-cfn.yml ipv46d-ec2-cfn.yml |
セキュリティ | ipv46d-securitygroup-cfn | ipv46d-securitygroup-cfn.yml |
ネットワーク | ipv46d-network-cfn | ipv46d-network-cfn.yml |
スタック作成の際はネットワークレイヤーからアプリケーションレイヤーの順番でデプロイしてください。削除の場合はその逆になります。
ネットワークレイヤーのIPプロトコル切り替えを行う際は、アプリケーションレイヤーを削除してから更新してください。
あくまで作成時に任意に選択できるという認識をするとよろしいかと思います。
下層レイヤーのリソースの参照にはクロススタック参照を利用しています。
例えばipv46d-securitygroup-cfn.ymlは、BaseNetworkStackNameのDefaultとして「ipv46d-network-cfn」というスタック名を参照しています。
スタック作成の際にスタック名を任意のものに設定できますが、参照している上層レイヤーでは任意に設定したスタック名をパラメータ指定する必要があります。
ipv46d-network-cfn.ymlのスタック名を「exampleNet」とした場合、ipv46d-securitygroup-cfn.ymlのBaseNetworkStackNameを「exampleNet」というパラメータ指定にします。
ネットワーク作成
AWSTemplateFormatVersion: "2010-09-09"
Description: "Create Network. IPv4, IPv6, or Dual-Stack can be selected"
# -----------------
# Input Parameters
# -----------------
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
-
Parameters:
- SystemName
- CidrBlock
- IpProtocolEnv
Parameters:
SystemName:
Description: Please type the SystemName.
Type: String
Default: IPv46D
CidrBlock:
Description: Please type the CidrBlock.
Type: String
Default: 10.1.0.0/16
IpProtocolEnv:
Description: Please type either IPv4-Only, IPv6-Only, or Dual-Stack for IpProtocolEnv.
Type: String
Default: IPv6-Only
AllowedValues:
- IPv4-Only
- IPv6-Only
- Dual-Stack
Conditions:
CreateIPv4Route: !Not
- !Equals
- !Ref IpProtocolEnv
- IPv6-Only
CreateIPv6Network: !Not
- !Equals
- !Ref IpProtocolEnv
- IPv4-Only
Resources:
# -----------------
# VPC
# -----------------
BaseVPC:
Type: "AWS::EC2::VPC"
Properties:
CidrBlock: !Sub ${CidrBlock}
Tags:
- Key: Name
Value: !Sub ${SystemName}-vpc
BaseVPCCidrBlockIPv6:
Type: "AWS::EC2::VPCCidrBlock"
Properties:
AmazonProvidedIpv6CidrBlock: true
VpcId: !Ref BaseVPC
# -----------------
# Subnet
# -----------------
PublicSubnet:
DependsOn: BaseVPCCidrBlockIPv6
Type: "AWS::EC2::Subnet"
Properties:
VpcId: !Ref BaseVPC
AvailabilityZone: ap-northeast-1c
CidrBlock: !Select [ 1, !Cidr [ !GetAtt BaseVPC.CidrBlock, 3, 8 ]]
Tags:
- Key: Name
Value: !Sub ${SystemName}-subnet-public
PublicSubnetCidrBlockIPv6:
Type: "AWS::EC2::SubnetCidrBlock"
Condition: CreateIPv6Network
Properties:
Ipv6CidrBlock: !Select [ 1, !Cidr [ !Select [ 0, !GetAtt BaseVPC.Ipv6CidrBlocks ], 3, 64 ]]
SubnetId: !Ref PublicSubnet
PrivateSubnet:
DependsOn: BaseVPCCidrBlockIPv6
Type: "AWS::EC2::Subnet"
Properties:
VpcId: !Ref BaseVPC
AvailabilityZone: ap-northeast-1c
CidrBlock: !Select [ 2, !Cidr [ !GetAtt BaseVPC.CidrBlock, 3, 8 ]]
Tags:
- Key: Name
Value: !Sub ${SystemName}-subnet-private
PrivateSubnetCidrBlockIPv6:
Type: "AWS::EC2::SubnetCidrBlock"
Condition: CreateIPv6Network
Properties:
Ipv6CidrBlock: !Select [ 2, !Cidr [ !Select [ 0, !GetAtt BaseVPC.Ipv6CidrBlocks ], 3, 64 ]]
SubnetId: !Ref PrivateSubnet
# -----------------
# InternetGateway
# -----------------
BaseIGW:
Type: "AWS::EC2::InternetGateway"
Properties:
Tags:
- Key: Name
Value: !Sub ${SystemName}-igw
AttachBaseIGW:
Type: "AWS::EC2::VPCGatewayAttachment"
Properties:
VpcId: !Ref BaseVPC
InternetGatewayId: !Ref BaseIGW
# -----------------
# RouteTable
# -----------------
PublicRouteTable:
Type: "AWS::EC2::RouteTable"
Properties:
VpcId: !Ref BaseVPC
Tags:
- Key: Name
Value: !Sub ${SystemName}-rt-public
PublicRouteTableAssociation:
Type: "AWS::EC2::SubnetRouteTableAssociation"
Properties:
RouteTableId: !Ref PublicRouteTable
SubnetId: !Ref PublicSubnet
PublicRoute:
Type: "AWS::EC2::Route"
Condition: CreateIPv4Route
Properties:
RouteTableId: !Ref PublicRouteTable
GatewayId: !Ref BaseIGW
DestinationCidrBlock: 0.0.0.0/0
PublicRouteIPv6:
Type: "AWS::EC2::Route"
Condition: CreateIPv6Network
Properties:
RouteTableId: !Ref PublicRouteTable
GatewayId: !Ref BaseIGW
DestinationIpv6CidrBlock: ::/0
PrivateRouteTable:
Type: "AWS::EC2::RouteTable"
Properties:
VpcId: !Ref BaseVPC
Tags:
- Key: Name
Value: !Sub ${SystemName}-rt-private
PrivateRouteTableAssociation:
Type: "AWS::EC2::SubnetRouteTableAssociation"
Properties:
RouteTableId: !Ref PrivateRouteTable
SubnetId: !Ref PrivateSubnet
PrivateRoute:
Type: "AWS::EC2::Route"
Condition: CreateIPv4Route
Properties:
RouteTableId: !Ref PrivateRouteTable
GatewayId: !Ref BaseIGW
DestinationCidrBlock: 0.0.0.0/0
PrivateRouteIPv6:
Type: "AWS::EC2::Route"
Condition: CreateIPv6Network
Properties:
RouteTableId: !Ref PrivateRouteTable
GatewayId: !Ref BaseIGW
DestinationIpv6CidrBlock: ::/0
# -----------------
# Output
# -----------------
Outputs:
BaseVPCId:
Value: !Ref BaseVPC
Export:
Name: !Sub "${AWS::StackName}-BaseVPC"
PublicSubnetId:
Value: !Ref PublicSubnet
Export:
Name: !Sub "${AWS::StackName}-PublicSubnet"
PrivateSubnetId:
Value: !Ref PrivateSubnet
Export:
Name: !Sub "${AWS::StackName}-PrivateSubnet"
- Conditionsセクションの条件で切り替えを行っています
- VPCでIPv6を有効にするためAWS::EC2::VPCCidrBlockを定義しています
- AWS::EC2::Subnetでは、スタック削除の失敗を防ぐためにDependsOnを指定しています
- 切り替えの際にサブネットの再作成が発生しないようAWS::EC2::SubnetCidrBlockでIPv6を定義しています
- そのため、サブネットにはIPv6-OnlyのときでもIPv4アドレス範囲を割り当てる形になっています
サブネットについて、AWS::EC2::Subnetのみによる下記のような実装も可能ですが、これだとIPv6←→Dual-Stackの切り替えが失敗します。
# -----------------
# Subnet
# -----------------
PublicSubnet:
Type: "AWS::EC2::Subnet"
Properties:
VpcId: !Ref BaseVPC
AvailabilityZone: ap-northeast-1c
CidrBlock: !If
- CreateIPv4Route
- !Select [ 1, !Cidr [ !GetAtt BaseVPC.CidrBlock, 3, 8 ]]
- !Ref "AWS::NoValue"
Ipv6CidrBlock: !If
- CreateIPv6Network
- !Select [ 1, !Cidr [ !Select [ 0, !GetAtt BaseVPC.Ipv6CidrBlocks ], 3, 64 ]]
- !Ref "AWS::NoValue"
Ipv6Native: !If
- IsIPv6Native
- true
- !Ref "AWS::NoValue"
- スタック更新の際にサブネットの再作成が発生する
- 同じIPv6アドレス範囲を持ったサブネットの作成が試みられる
- 古いサブネットのIPv6アドレス範囲が衝突する
- スタック更新に失敗する
という流れのようです。
CloudFormationのスタック更新の挙動として、Update requiresがReplacementになっているPropertiesが変更される場合、新しいリソースを作成してから古いリソースを削除するというものが影響しているようです。
IPv6アドレス範囲の定義をAWS::EC2::SubnetCidrBlockで分けたipv46d-network-cfn.ymlの構成なら、サブネットの再作成が発生しないため切り替えが上手く行きます。
セキュリティグループ作成
AWSTemplateFormatVersion: "2010-09-09"
Description: "Create SecurityGroup. IPv4, IPv6, or Dual-Stack can be selected"
# -----------------
# Input Parameters
# -----------------
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
-
Parameters:
- SystemName
- EnvType
- IpProtocolEnv
- EC2InstanceConnect
- IngressSSHCidr
- IngressSSHCidrIPv6
- EICEndpoint
- BaseNetworkStackName
Parameters:
SystemName:
Description: Please type the SystemName.
Type: String
Default: IPv46D
EnvType:
Description: Please type the Environment Type.
Type: String
Default: develop
AllowedValues:
- develop
- product
IpProtocolEnv:
Description: Please type either IPv4-Only, IPv6-Only, or Dual-Stack for IpProtocolEnv.
Type: String
Default: IPv6-Only
AllowedValues:
- IPv4-Only
- IPv6-Only
- Dual-Stack
EC2InstanceConnect:
Description: Please type whether you want to activate EC2InstanceConnect. Only ap-northeast-1 is assumed.
Type: String
Default: enable
AllowedValues:
- enable
- disable
IngressSSHCidr:
Description: Please type the IngressSSH to IPv4 address range, in CIDR format.
Type: String
Default: 0.0.0.0/0
IngressSSHCidrIPv6:
Description: Please type the IngressSSH to IPv6 address range, in CIDR format.
Type: String
Default: ::/0
EICEndpoint:
Description: Please type whether you want to activate EC2InstanceConnectEndpoint.
Type: String
Default: enable
AllowedValues:
- enable
- disable
BaseNetworkStackName:
Description: Please type the SharedServices stack name.
Type: String
Default: ipv46d-network-cfn
Conditions:
IsProduction: !Equals
- !Ref EnvType
- product
CreateEC2InstanceConnectRule: !Equals
- !Ref EC2InstanceConnect
- enable
CreateIPv4Rule: !Not
- !Equals
- !Ref IpProtocolEnv
- IPv6-Only
CreateIPv6Rule: !Not
- !Equals
- !Ref IpProtocolEnv
- IPv4-Only
CreatePrivateInternetPolicy: !And
- !Not
- !Condition IsProduction
- !Condition CreateIPv4Rule
CreatePrivateInternetIPv6Policy: !And
- !Not
- !Condition IsProduction
- !Condition CreateIPv6Rule
CreateEICEndpointRule: !Equals
- !Ref EICEndpoint
- enable
DisableEICEndpointRule: !Equals
- !Ref EICEndpoint
- disable
Resources:
# -----------------
# SecurityGroup
# -----------------
PublicSG:
Type: "AWS::EC2::SecurityGroup"
Properties:
GroupDescription: SecurityGroup for BastionEC2.
GroupName: ipv46d-sg-public
VpcId: !ImportValue
'Fn::Join':
- '-'
- - !Ref BaseNetworkStackName
- BaseVPC
Tags:
- Key: Name
Value: !Sub ${SystemName}-${EnvType}-sg
PublicIngressEC2InstanceConnect:
Type: "AWS::EC2::SecurityGroupIngress"
Condition: CreateEC2InstanceConnectRule
Properties:
IpProtocol: tcp
FromPort: 22
ToPort: 22
GroupId: !Ref PublicSG
CidrIp: 3.112.23.0/29
PublicIngressSSH:
Type: "AWS::EC2::SecurityGroupIngress"
Condition: CreateIPv4Rule
Properties:
IpProtocol: tcp
FromPort: 22
ToPort: 22
GroupId: !Ref PublicSG
CidrIp: !Ref IngressSSHCidr
PublicIngressSSHIPv6:
Type: "AWS::EC2::SecurityGroupIngress"
Condition: CreateIPv6Rule
Properties:
IpProtocol: tcp
FromPort: 22
ToPort: 22
GroupId: !Ref PublicSG
CidrIpv6: !Ref IngressSSHCidrIPv6
PublicEgressSSH:
Type: "AWS::EC2::SecurityGroupEgress"
Properties:
IpProtocol: tcp
FromPort: 22
ToPort: 22
GroupId: !Ref PublicSG
DestinationSecurityGroupId: !Ref PrivateSG
PublicEgressICMP:
Type: "AWS::EC2::SecurityGroupEgress"
Properties:
IpProtocol: icmp
FromPort: -1
ToPort: -1
GroupId: !Ref PublicSG
DestinationSecurityGroupId: !Ref PrivateSG
PublicEgressInternet:
Type: "AWS::EC2::SecurityGroupEgress"
Condition: CreateIPv4Rule
Properties:
IpProtocol: tcp
FromPort: 0
ToPort: 65535
GroupId: !Ref PublicSG
CidrIp: 0.0.0.0/0
PublicEgressInternetIPv6:
Type: "AWS::EC2::SecurityGroupEgress"
Condition: CreateIPv6Rule
Properties:
IpProtocol: tcp
FromPort: 0
ToPort: 65535
GroupId: !Ref PublicSG
CidrIpv6: ::/0
PrivateSG:
Type: "AWS::EC2::SecurityGroup"
Properties:
GroupDescription: SecurityGroup for InternalEC2.
GroupName: ipv46d-sg-private
VpcId: !ImportValue
'Fn::Join':
- '-'
- - !Ref BaseNetworkStackName
- BaseVPC
Tags:
- Key: Name
Value: !Sub ${SystemName}-${EnvType}-sg
PrivateIngressEICEndpoint:
Type: "AWS::EC2::SecurityGroupIngress"
Condition: CreateEICEndpointRule
Properties:
IpProtocol: tcp
FromPort: 22
ToPort: 22
GroupId: !Ref PrivateSG
SourceSecurityGroupId: !Ref EICEndpointSG
PrivateIngressSSH:
Type: "AWS::EC2::SecurityGroupIngress"
Properties:
IpProtocol: tcp
FromPort: 22
ToPort: 22
GroupId: !Ref PrivateSG
SourceSecurityGroupId: !Ref PublicSG
PrivateIngressICMP:
Type: "AWS::EC2::SecurityGroupIngress"
Properties:
IpProtocol: icmp
FromPort: -1
ToPort: -1
GroupId: !Ref PrivateSG
SourceSecurityGroupId: !Ref PublicSG
PrivateEgressEICEndpoint:
Type: "AWS::EC2::SecurityGroupEgress"
Condition: CreateEICEndpointRule
Properties:
IpProtocol: tcp
FromPort: 50001
ToPort: 50100
GroupId: !Ref PrivateSG
DestinationSecurityGroupId: !Ref EICEndpointSG
PrivateEgressInternet:
Type: "AWS::EC2::SecurityGroupEgress"
Condition: CreatePrivateInternetPolicy
Properties:
IpProtocol: tcp
FromPort: 0
ToPort: 65535
GroupId: !Ref PrivateSG
CidrIp: 0.0.0.0/0
PrivateEgressInternetIPv6:
Type: "AWS::EC2::SecurityGroupEgress"
Condition: CreatePrivateInternetIPv6Policy
Properties:
IpProtocol: tcp
FromPort: 0
ToPort: 65535
GroupId: !Ref PrivateSG
CidrIpv6: ::/0
PrivateEgressDefaultRemove:
Type: "AWS::EC2::SecurityGroupEgress"
Condition: IsProduction
Properties:
IpProtocol: -1
GroupId: !Ref PrivateSG
CidrIp: 127.0.0.1/32
EICEndpointSG:
Type: "AWS::EC2::SecurityGroup"
Properties:
GroupDescription: SecurityGroup for EICEndpoint.
GroupName: ipv46d-sg-endpoint
VpcId: !ImportValue
'Fn::Join':
- '-'
- - !Ref BaseNetworkStackName
- BaseVPC
Tags:
- Key: Name
Value: !Sub ${SystemName}-${EnvType}-sg
EICEndpointEgressSSH:
Type: "AWS::EC2::SecurityGroupEgress"
Condition: CreateEICEndpointRule
Properties:
IpProtocol: tcp
FromPort: 22
ToPort: 22
GroupId: !Ref EICEndpointSG
DestinationSecurityGroupId: !Ref PrivateSG
EICEndpointEgressDefaultRemove:
Type: "AWS::EC2::SecurityGroupEgress"
Condition: DisableEICEndpointRule
Properties:
IpProtocol: -1
GroupId: !Ref EICEndpointSG
CidrIp: 127.0.0.1/32
# -----------------
# Output
# -----------------
Outputs:
PublicSGId:
Value: !Ref PublicSG
Export:
Name: !Sub "${AWS::StackName}-PublicSG"
PrivateSGId:
Value: !Ref PrivateSG
Export:
Name: !Sub "${AWS::StackName}-PrivateSG"
EICEndpointSGId:
Value: !Ref EICEndpointSG
Export:
Name: !Sub "${AWS::StackName}-EICEndpointSG"
- Conditionsセクションの条件で切り替えを行っています
- EnvTypeのパラメータをproductにすることでPrivateSGのインターネット接続を制限できます
- このような設定はほとんど行わないと思いますが、アウトバウンドのデフォルトルールを削除する方法を試したかったので実装してみました
- もしかしたら、かなり特殊な状況で内部サーバーからの通信を一切インターネット側に出したくないというときに利用できるかもしれません
- このような設定はほとんど行わないと思いますが、アウトバウンドのデフォルトルールを削除する方法を試したかったので実装してみました
- EC2InstanceConnectのパラメータをenableにすることでEC2 Instance Connectを利用できます
- 公式ユーザーガイドによるとIPv6での接続はサポートされていないようです
- EICEndpointのパラメータをenableにすることでEC2 Instance Connect Endpointを利用できます
- 公式ユーザーガイドによるとIPv6での接続はサポートされていないようです
- ssh接続のみのルールにしています。RDP接続のルールは定義していません
- PrivateEgressEICEndpointのTCP 50001~50100番というのは、任意のTCPを設定できるそうなので適当に決めました
- IngressSSHCidrのパラメータ指定で、PublicIngressSSHの許可するIPv4アドレスを任意に設定できます
- Defaultでは全てのIPv4アドレス範囲としているのでセキュアではありません。自身の利用するIPアドレスをパラメータ指定することをお勧めします
- ループバックアドレス「127.0.0.1/32」を指定することでssh接続を利用不可にすることもできます
- IngressSSHCidrIPv6のパラメータ指定で、PublicIngressSSHIPv6の許可するIPv6アドレスを任意に設定できます
- Defaultでは全てのIPv6アドレス範囲としているのでセキュアではありません。自身の利用するIPアドレスをパラメータ指定することをお勧めします
- ループバックアドレス「::1/128」を指定することでssh接続を利用不可にすることもできます
- EnvTypeのパラメータをproductにすることでPrivateSGのインターネット接続を制限できます
- セキュリティグループのルールをAWS::EC2::SecurityGroupとは別に定義することで、ルールの切り替えをしやすくしています
- この方法だと、スタック更新の際にセキュリティグループ自体のリソース再作成が発生しないのも良い点だと思っています
PublicIngressEC2InstanceConnectでは、ap-northeast-1のEC2 Instance Connect用のIPアドレス範囲をベタで指定しています。
IPアドレス範囲は公式がip-ranges.jsonというファイルで公開しています。
{
"ip_prefix": "3.112.23.0/29",
"region": "ap-northeast-1",
"service": "EC2_INSTANCE_CONNECT",
"network_border_group": "ap-northeast-1"
},
EC2作成
AWSTemplateFormatVersion: "2010-09-09"
Description: "Create EC2. IPv4, IPv6, or Dual-Stack can be selected"
# -----------------
# Input Parameters
# -----------------
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
-
Parameters:
- SystemName
- EnvType
- ImageID
- EC2KeyPairBastion
- EC2KeyPairInternal
- IpProtocolEnv
- BaseNetworkStackName
- BaseSecurityGroupStackName
Parameters:
SystemName:
Description: Please type the SystemName.
Type: String
Default: IPv46D
EnvType:
Description: Please type the Environment Type.
Type: String
Default: develop
AllowedValues:
- develop
- product
ImageID:
Description: Please type the EC2 image ID.
Type: String
Default: ami-07c589821f2b353aa # Ubuntu Server 22.04 LTS AMI
#Default: ami-0a1c2ec61571737db # Amazon Linux 2 AMI
EC2KeyPairBastion:
Description: Please type BastionEC2 key name.
Type: AWS::EC2::KeyPair::KeyName
EC2KeyPairInternal:
Description: Please type InternalEC2 key name.
Type: AWS::EC2::KeyPair::KeyName
IpProtocolEnv:
Description: Please type either IPv4-Only, IPv6-Only, or Dual-Stack for IpProtocolEnv.
Type: String
Default: IPv6-Only
AllowedValues:
- IPv4-Only
- IPv6-Only
- Dual-Stack
BaseNetworkStackName:
Description: Please type the SharedServices stack name.
Type: String
Default: ipv46d-network-cfn
BaseSecurityGroupStackName:
Description: Please type the SharedServices stack name.
Type: String
Default: ipv46d-securitygroup-cfn
Mappings:
IpProtocolEnvironmentMapping:
IPv4-Only:
AssociatePublicIPv4: true
AddressCountIPv6: 0
IPv6-Only:
AssociatePublicIPv4: false
AddressCountIPv6: 1
Dual-Stack:
AssociatePublicIPv4: true
AddressCountIPv6: 1
Resources:
# -----------------
# EC2
# -----------------
BastionEC2:
Type: "AWS::EC2::Instance"
Properties:
AvailabilityZone: ap-northeast-1c
ImageId: !Ref ImageID
InstanceType: t2.micro
KeyName: !Ref EC2KeyPairBastion
BlockDeviceMappings:
- DeviceName: /dev/sda1
Ebs:
VolumeType: gp3
VolumeSize: 8
NetworkInterfaces:
- AssociatePublicIpAddress: !FindInMap [ IpProtocolEnvironmentMapping, !Ref IpProtocolEnv, AssociatePublicIPv4 ]
DeviceIndex: "0"
Ipv6AddressCount: !FindInMap [ IpProtocolEnvironmentMapping, !Ref IpProtocolEnv, AddressCountIPv6 ]
GroupSet:
- !ImportValue
'Fn::Join':
- '-'
- - !Ref BaseSecurityGroupStackName
- PublicSG
SubnetId: !ImportValue
'Fn::Join':
- '-'
- - !Ref BaseNetworkStackName
- PublicSubnet
Tags:
- Key: Name
Value: !Sub ${SystemName}-${EnvType}-ec2-bastion
InternalEC2:
Type: "AWS::EC2::Instance"
Properties:
AvailabilityZone: ap-northeast-1c
ImageId: !Ref ImageID
InstanceType: t2.micro
KeyName: !Ref EC2KeyPairInternal
BlockDeviceMappings:
- DeviceName: /dev/sda1
Ebs:
VolumeType: gp3
VolumeSize: 8
NetworkInterfaces:
- AssociatePublicIpAddress: !FindInMap [ IpProtocolEnvironmentMapping, !Ref IpProtocolEnv, AssociatePublicIPv4 ]
DeviceIndex: "0"
Ipv6AddressCount: !FindInMap [ IpProtocolEnvironmentMapping, !Ref IpProtocolEnv, AddressCountIPv6 ]
GroupSet:
- !ImportValue
'Fn::Join':
- '-'
- - !Ref BaseSecurityGroupStackName
- PrivateSG
SubnetId: !ImportValue
'Fn::Join':
- '-'
- - !Ref BaseNetworkStackName
- PrivateSubnet
Tags:
- Key: Name
Value: !Sub ${SystemName}-${EnvType}-ec2-internal
- こちらはEnvTypeを切り替えても特に変化はありません
- 本来は開発環境と本番環境で何かを分けることもあるかなと思って記載してあるだけです(インスタンスタイプやEBS容量を分けるとか)
- EC2KeyPairBastionとEC2KeyPairInternalには存在するキーペアをパラメータ指定する必要があることに留意ください
- 両方同じキーペアを指定しても大丈夫です
- MappingsセクションによるIPアドレスの選択を行っています
- IPv6アドレスは1つだけ割り当てるようにしています
InternalEC2にssh接続する際は~/.ssh/configに下記のような記載をすると楽です。
Host BastionEC2
HostName BastionEC2のパブリックIPアドレス
IdentityFile ~/.ssh/キーペア名.pem
User ubuntu
Host InternalEC2
HostName InternalEC2のプライベートIPアドレス
IdentityFile ~/.ssh/キーペア名.pem
User ubuntu
ProxyJump BastionEC2
これでPowerShellやターミナル等からssh InternalEC2
を使えば接続できます。
EC2 Instance Connect Endpoint作成
AWSTemplateFormatVersion: "2010-09-09"
Description: "Create EICEndpoint. IPv4, IPv6, or Dual-Stack can be selected"
# -----------------
# Input Parameters
# -----------------
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
-
Parameters:
- SystemName
- UsePreserveClientIp
- BaseNetworkStackName
- BaseSecurityGroupStackName
Parameters:
SystemName:
Description: Please type the SystemName.
Type: String
Default: IPv46D
UsePreserveClientIp:
Description: Please type use PreserveClientIp or not.
Type: String
Default: false
AllowedValues:
- true
- false
BaseNetworkStackName:
Description: Please type the SharedServices stack name.
Type: String
Default: ipv46d-network-cfn
BaseSecurityGroupStackName:
Description: Please type the SharedServices stack name.
Type: String
Default: ipv46d-securitygroup-cfn
Resources:
# -----------------
# EICEndpoint
# -----------------
EICEndpoint:
Type: "AWS::EC2::InstanceConnectEndpoint"
Properties:
PreserveClientIp: !Ref UsePreserveClientIp
SecurityGroupIds:
- !ImportValue
'Fn::Join':
- '-'
- - !Ref BaseSecurityGroupStackName
- EICEndpointSG
SubnetId: !ImportValue
'Fn::Join':
- '-'
- - !Ref BaseNetworkStackName
- PrivateSubnet
Tags:
- Key: Name
Value: !Sub ${SystemName}-eic-endpoint
EC2 Instance Connectを試す際はエンドポイントのデプロイ前に行うとよろしいかと思います。
理由は不明ですが、EC2 Instance ConnectとEC2 Instance Connect Endpointを同時に使えません。
抜粋ですが、エラーメッセージとして確認したのは以下の2種類になります。上記のスタックで作成したエンドポイントが存在する状況でのものです。
- The specified Instance Connect Endpoint does not exist or is not in create-complete state.
- Error establishing SSH connection to your instance. Try again later.
ガチャガチャ試してみると下記の特徴があることがわかりました。
- エンドポイントのスタックを削除してから10数分くらい経過すると改めてEC2 Instance Connectが使えるようになる
- EC2 Instance ConnectでBastionEC2に接続した状態でエンドポイントを作成可能
- その状態でEC2 Instance Connect EndpointでInternalEC2に接続可能
- 10分くらい経過してもEC2 Instance Connectのセッションは維持されていた
- 通常のssh接続はできる
何か上手く行かないときは自分がやらかしていることが往々にしてあるので、もし誤りを発見した方はご指摘いただけると幸いです。
参考にしたサイト
- はじめてのCloudFormation
- 【AWS】CloudFormationまとめ
- 【AWS】CloudFormationで必要最低限なCLIをまとめてみた
- [AWS]CloudFormationでVPCのIPv6 CIDRを取得してサブネットに割り当てる
- 【備忘録】CloudFormationで IPv6 対応のVPCとサブネットを作る【CFn】
- パブリック IPv4 の料金の開始に伴い...
- EC2 Instance Connect Endpoint を使用した...
終わりに
AWS自体に触ってから3ヶ月余りでわからないことがたくさんあります。
試行錯誤を繰り返してより良いものが作れるように、これからも知見を積み上げて行きたいです。
本稿の内容が少しでも誰かのお役に立てば幸いです。