🚌

[CFn]2アカウント間のVPCをVPC Peeringで接続する

2023/06/07に公開

2アカウント間のVPCをVPC Peeringで接続する

DevelopersIO BASECAMP一期参加者の加藤です。

今回はアカウント跨ぎで2つのVPCをピアリング接続してみたいと思います。

以下が構成図になります。


VPC Peeringの概要

以下ドキュメントになります。
https://docs.aws.amazon.com/vpc/latest/peering/what-is-vpc-peering.html


作成手順

アカウント1でこのテンプレートを実行してください。

「PJPrefix」パラメーターは好きな文字列、「Account2AccountId」には接続する先のアカウントのアカウントIDを入力してください。

Account1.yml
AWSTemplateFormatVersion: "2010-09-09"
# ------------------------------------------------------------#
# Metadata
# ------------------------------------------------------------#
Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: Stack
        Parameters:
          - PJPrefix

      - Label:
          default: for This Account
        Parameters:
          - VPCCidrBlock
          - PrivateSubnetCidrBlock
          - ImageId
          - InstanceType

      - Label:
          default: for opposite Account
        Parameters:
          - Account2AccountId
          - Account2PrivateSubnetCidrBlock
# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------#
Parameters:
  PJPrefix: # リソース名やNameタグ値の接頭辞に利用
    Type: String

  VPCCidrBlock: # VPCのCidr
    Type: String
    Default: 10.1.0.0/16

  PrivateSubnetCidrBlock: # EC2インスタンスのCidr
    Type: String
    Default: 10.1.0.0/24

  ImageId: # EC2のImageId
    Type: String
    Default: ami-079a2a9ac6ed876fc

  InstanceType: # EC2のInstanceType
    Type: String
    Default: t2.micro

  Account2AccountId: # 対向アカウントのアカウントID
    Type: String
    MinLength: 12
    MaxLength: 12

  Account2PrivateSubnetCidrBlock: 
    Type: String
    Default: 10.2.0.0/24
# ------------------------------------------------------------#
# Resources
# ------------------------------------------------------------#
Resources:
  # ------------------------------------------------------------#
  # Role
  # ------------------------------------------------------------#
  VPCPeeringRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub ${PJPrefix}-vpcpeeringrole
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Action: sts:AssumeRole
            Principal:
              AWS: !Ref Account2AccountId
      Policies:
        - PolicyName: 'AcceptVpcPeering'
          PolicyDocument:
            Statement:
              - Effect: Allow
                Action: 'ec2:AcceptVpcPeeringConnection'
                Resource: '*'
#≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡#
# Role
  EC2Role:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Action: sts:AssumeRole
            Principal:
              Service:
                - ec2.amazonaws.com
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore

# InstanceProfile
  EC2InstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Path: /
      Roles:
        - !Ref EC2Role
  # ------------------------------------------------------------#
  # VPC
  # ------------------------------------------------------------#
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VPCCidrBlock
      InstanceTenancy: default
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: !Sub ${PJPrefix}-vpc
  # ------------------------------------------------------------#
  # VPCEndpoint
  # ------------------------------------------------------------#
  VPCEndpointSSM:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      PrivateDnsEnabled: true
      SecurityGroupIds:
        - !Ref VPCEndpointSecurityGroup
      ServiceName: !Sub com.amazonaws.${AWS::Region}.ssm
      SubnetIds:
        - !Ref PrivateSubnet
      VpcEndpointType: Interface
      VpcId: !Ref VPC

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

  VPCEndpointEC2Messages:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      PrivateDnsEnabled: true
      SecurityGroupIds:
        - !Ref VPCEndpointSecurityGroup
      ServiceName: !Sub com.amazonaws.${AWS::Region}.ec2messages
      SubnetIds:
        - !Ref PrivateSubnet
      VpcEndpointType: Interface
      VpcId: !Ref VPC

  VPCEndpointS3:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      RouteTableIds:
        - !Ref PrivateSubnetRouteTable
      ServiceName: !Sub com.amazonaws.${AWS::Region}.s3
      VpcEndpointType: Gateway
      VpcId: !Ref VPC
  # ------------------------------------------------------------#
  # RouteTable
  # ------------------------------------------------------------#
  PrivateSubnetRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub ${PJPrefix}-rtb-for-private-subnet

  PrivateSubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet
      RouteTableId: !Ref PrivateSubnetRouteTable
  # ------------------------------------------------------------#
  # Subnet
  # ------------------------------------------------------------#
  PrivateSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: !Ref PrivateSubnetCidrBlock
      VpcId: !Ref VPC
      AvailabilityZone: !Select [ 0, !GetAZs ]
      Tags:
        - Key: Name
          Value: !Sub ${PJPrefix}-subnet-private
  # ------------------------------------------------------------#
  # EC2
  # ------------------------------------------------------------#
# for VPC
  EC2:
    Type: AWS::EC2::Instance
    Properties:
      IamInstanceProfile: !Ref EC2InstanceProfile
      ImageId: !Ref ImageId
      InstanceType: !Ref InstanceType
      NetworkInterfaces:
        - DeviceIndex: 0
          SubnetId: !Ref PrivateSubnet
          GroupSet:
            - !Ref EC2SecurityGroup
      Tags:
        - Key: Name
          Value: !Sub ${PJPrefix}-ec2
  # ------------------------------------------------------------#
  # SecurityGroup
  # ------------------------------------------------------------#
# for VPC
  # SecurityGroup
  VPCEndpointSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      VpcId: !Ref VPC
      GroupName: !Sub ${PJPrefix}-sg-for-vpce-
      GroupDescription: for VPCE
      Tags:
        - Key: Name
          Value: !Sub ${PJPrefix}-sg-for-vpce

  # Ingress
  VPCEndpointSecurityGroupIngress:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      FromPort: 443
      ToPort: 443
      IpProtocol: tcp
      SourceSecurityGroupId: !Ref EC2SecurityGroup
      GroupId: !GetAtt VPCEndpointSecurityGroup.GroupId
#≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡#
  # SecurityGroup
  EC2SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: EC2SecurityGroup
      GroupName: !Sub ${PJPrefix}-sg-for-ec2
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub ${PJPrefix}-sg-for-ec2

  # Ingress
  EC2SecurityGroupIngressFromAccount2PrivateSubnetCidrBlock:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      FromPort: -1
      ToPort: -1
      IpProtocol: icmp
      CidrIp: !Ref Account2PrivateSubnetCidrBlock
      GroupId: !GetAtt EC2SecurityGroup.GroupId
# ------------------------------------------------------------#
# Outputs
# ------------------------------------------------------------#
Outputs:
  PeerRoleArn:
    Value: !GetAtt VPCPeeringRole.Arn

  Account1AccountId:
    Value: !Ref AWS::AccountId

  Account1VPCId:
    Value: !Ref VPC

  pingcommand:
    Value: !Sub ping -c 3 ${EC2.PrivateIp}
    Description: Ping command to be executed on the opposite account.

アカウント2でこのテンプレートを実行してください。

※「PJPrefix」パラメーターは好きな文字列、その他の空欄パラメーターはAccount1.ymlの同名出力値をコピペしてください。

Account2.yml
AWSTemplateFormatVersion: "2010-09-09"
Description: VPC Peering Inviter
# ------------------------------------------------------------#
# Metadata
# ------------------------------------------------------------#
Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: Stack
        Parameters:
          - PJPrefix

      - Label:
          default: for This Account
        Parameters:
          - VPCCidrBlock
          - PrivateSubnetCidrBlock
          - ImageId
          - InstanceType

      - Label:
          default: for opposite Account
        Parameters:
          - PeerRoleArn
          - Account1AccountId
          - Account1VPCId
          - Account1VPCCidrBlock
          - Account1PrivateSubnetCidrBlock
# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------#
Parameters:
  PJPrefix: # リソース名やNameタグ値の接頭辞に利用
    Type: String

  VPCCidrBlock: # VPCのCidr
    Type: String
    Default: 10.2.0.0/16

  PrivateSubnetCidrBlock: # EC2インスタンスのCidr
    Type: String
    Default: 10.2.0.0/24

  ImageId: # EC2のImageId
    Type: String
    Default: ami-079a2a9ac6ed876fc

  InstanceType: # EC2のInstanceType
    Type: String
    Default: t2.micro

  Account1VPCCidrBlock:  
    Type: String
    Default: 10.1.0.0/16

  Account1PrivateSubnetCidrBlock: 
    Type: String
    Default: 10.1.0.0/24

  Account1VPCId:
    Type: String

  Account1AccountId:
    Type: String
    MinLength: 12
    MaxLength: 12

  PeerRoleArn:
    Type: String
# ------------------------------------------------------------#
# Resources
# ------------------------------------------------------------#
Resources:
  # ------------------------------------------------------------#
  # VPCPeeringConnection
  # ------------------------------------------------------------#
  VPCPeeringConnection: 
    Type: AWS::EC2::VPCPeeringConnection
    Properties: 
      VpcId: !Ref VPC
      PeerVpcId: !Ref Account1VPCId
      PeerOwnerId: !Ref Account1AccountId
      PeerRoleArn: !Ref PeerRoleArn
  # ------------------------------------------------------------#
  # Role
  # ------------------------------------------------------------#
# Role
  EC2Role:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Action: sts:AssumeRole
            Principal:
              Service:
                - ec2.amazonaws.com
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore

# InstanceProfile
  EC2InstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Path: /
      Roles:
        - !Ref EC2Role
  # ------------------------------------------------------------#
  # VPC
  # ------------------------------------------------------------#
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VPCCidrBlock
      InstanceTenancy: default
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: !Sub ${PJPrefix}-vpc
  # ------------------------------------------------------------#
  # VPCEndpoint
  # ------------------------------------------------------------#
  VPCEndpointSSM:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      PrivateDnsEnabled: true
      SecurityGroupIds:
        - !Ref VPCEndpointSecurityGroup
      ServiceName: !Sub com.amazonaws.${AWS::Region}.ssm
      SubnetIds:
        - !Ref PrivateSubnet
      VpcEndpointType: Interface
      VpcId: !Ref VPC

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

  VPCEndpointEC2Messages:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      PrivateDnsEnabled: true
      SecurityGroupIds:
        - !Ref VPCEndpointSecurityGroup
      ServiceName: !Sub com.amazonaws.${AWS::Region}.ec2messages
      SubnetIds:
        - !Ref PrivateSubnet
      VpcEndpointType: Interface
      VpcId: !Ref VPC

  VPCEndpointS3:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      RouteTableIds:
        - !Ref PrivateSubnetRouteTable
      ServiceName: !Sub com.amazonaws.${AWS::Region}.s3
      VpcEndpointType: Gateway
      VpcId: !Ref VPC
  # ------------------------------------------------------------#
  # RouteTable
  # ------------------------------------------------------------#
  PrivateSubnetRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub ${PJPrefix}-rtb-for-private-subnet

  PrivateSubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet
      RouteTableId: !Ref PrivateSubnetRouteTable

  RouteforPeer:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateSubnetRouteTable
      DestinationCidrBlock: !Ref Account1VPCCidrBlock
      VpcPeeringConnectionId: !Ref VPCPeeringConnection
  # ------------------------------------------------------------#
  # Subnet
  # ------------------------------------------------------------#
  PrivateSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: !Ref PrivateSubnetCidrBlock
      VpcId: !Ref VPC
      AvailabilityZone: !Select [ 0, !GetAZs ]
      Tags:
        - Key: Name
          Value: !Sub ${PJPrefix}-subnet-private
  # ------------------------------------------------------------#
  # EC2
  # ------------------------------------------------------------#
# for VPC
  EC2:
    Type: AWS::EC2::Instance
    Properties:
      IamInstanceProfile: !Ref EC2InstanceProfile
      ImageId: !Ref ImageId
      InstanceType: !Ref InstanceType
      NetworkInterfaces:
        - DeviceIndex: 0
          SubnetId: !Ref PrivateSubnet
          GroupSet:
            - !Ref EC2SecurityGroup
      Tags:
        - Key: Name
          Value: !Sub ${PJPrefix}-ec2
  # ------------------------------------------------------------#
  # SecurityGroup
  # ------------------------------------------------------------#
# for VPC
  # SecurityGroup
  VPCEndpointSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      VpcId: !Ref VPC
      GroupName: !Sub ${PJPrefix}-sg-for-vpce-
      GroupDescription: for VPCE
      Tags:
        - Key: Name
          Value: !Sub ${PJPrefix}-sg-for-vpce

  # Ingress
  VPCEndpointSecurityGroupIngress:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      FromPort: 443
      ToPort: 443
      IpProtocol: tcp
      SourceSecurityGroupId: !Ref EC2SecurityGroup
      GroupId: !GetAtt VPCEndpointSecurityGroup.GroupId
#≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡#
  # SecurityGroup
  EC2SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: EC2SecurityGroup
      GroupName: !Sub ${PJPrefix}-sg-for-ec2
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub ${PJPrefix}-sg-for-ec2

  # Ingress
  EC2SecurityGroupIngressFromAccount1PrivateSubnetCidrBlock:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      FromPort: -1
      ToPort: -1
      IpProtocol: icmp
      CidrIp: !Ref Account1PrivateSubnetCidrBlock
      GroupId: !GetAtt EC2SecurityGroup.GroupId
# ------------------------------------------------------------#
# Outputs
# ------------------------------------------------------------#
Outputs:
  pingcommand:
    Value: !Sub ping -c 3 ${EC2.PrivateIp}
    Description: Ping command to be executed on the opposite account.


アカウント1の「xxxxxx-rtb-for-private-subnet」というルートテーブルに、
送信先:「10.2.0.0/16」と入力。
ターゲット:「ピアリング接続」→「pcx-xxxxxxxxxxxxxxxxx」を選択。
し、「変更を保存」

以上となります。


詳細説明

① Account1.ymlでは、アカウント2をプリンシパルとして、「ec2:AcceptVpcPeeringConnection」というアクションを許可するクロスアカウントロールを作成しています。

② Account2.ymlでは両方のVPCと、(オーナーとして)アカウント1を指定したVPCピアリングを作成しています。

③ Account2.ymlではピアリングを通じてアカウント1のVPCのCidrへの経路を指定するルートをルートテーブルに追加しています。

④ 両方のテンプレートでは
・VPC
・Subnet
・EC2
・対向アカウントのSubnetのからの通信を許可するSecurityGroup
・EC2に接続する為のVPCエンドポイント×4
・VPCエンドポイント用のSecurityGroup
・セッションマネージャーで接続する為に必要なIAMロール

を作成しています。

⑤ 最後に、Account1.yml実行時点では存在しない為、テンプレートの中で向き先として指定する事が出来なかったアカウント1→アカウント2のルート(③の反対にあたる経路)を手動で追加していただくようにしています。


疎通確認

※両方のスタックの「出力」値に「pingcommand」という値を出しておきました。
コピーして、”反対側の”アカウントのEC2で実行ください。

アカウント1→アカウント2

アカウント2→アカウント1

無事疎通確認が出来ました。


別リージョンに存在するVPCに接続するには

アカウント1のVPCをap-northeast-1のまま、アカウント2のVPCをus-east-1にした状態で接続するにはどうすべきかです。

以下プロパティをAWS::EC2::VPCPeeringConnectionリソースに追加すれば接続可能となります。

      PeerRegion: ap-northeast-1 # 反対側のVPCがあるリージョンを指定

※先ほどのテンプレートではEC2用のAMIがus-east-1に対応していませんので、パラメーター入力画面で変更の必要があります。


今回体験出来たエラー

その①

「識別子「pcx-xxxxxxxxxxxxxxxxx」を持つAWS
Resource of type 'AWS::EC2::VPCPeeringConnection' with identifier 'pcx-xxxxxxxxxxxxxxxxx' did not stabilize.

こちらに原因が書いてあります。
https://repost.aws/knowledge-center/cloudformation-vpc-peering-error


その②

「rootアカウントでは、ロールを引き受けることはできません。」
Roles may not be assumed by root accounts. 

ルートユーザーではなくIAMユーザーでログインし直し、スタックを実行した所当該エラーは発生しませんでした。

上記エラーはルートユーザー、或いはAdmin権限をしっかり理解する意味で重要な気がしますが、素直にいって現状私の理解が説明を可能にするレベルに到達していません。

心苦しいですが、判明次第別で書かせてください。


終わりに

お読みいいただき有難うございました!


参考にさせていただいた記事

https://dev.classmethod.jp/articles/vpc-peering-cfn/

デベキャン

Discussion