1
votes

Using this AWS walkthrough, I can successfully add a vpc peering connection between different aws accounts.

The connection is accepted automagically because of the IAM role setup in the accepter account is given that permission and referenced in the requester account when requesting the connection.

That's all fine and good, but without route table entries in both VPC's, this connection is meaningless.

Looking at the second template in the example; the one that creates the AWS::EC2::VPCPeeringConnection, is there a way to add a route in the route table entry of the VPC created in the first template?

I can of course pass the route table id to the second template, but I don't think this is enough. I think there must be an additional trust relationship between the accounts that would allow this.

Any idea on how to do that?

3
I am going to explore the SNS/Lambda way to do thisSam Hammamy
I'm curious about the "referenced in the requestor account". I'm trying to automate a peering connection that I have to initiate as the acceptor, through the requestor's web interface. They also expose and api for initiating the request, but I'm not clear on your statement above, and how I'd have to go about informing the requestor of that. I hope that made sense?wkhatch

3 Answers

1
votes

I was able to do this using the following combination of things:

1) In the accepter account
 -- added an SNS topic
 -- added a lambda function that uses boto3 to create the route entry.  It receives the peering connection id and CIDR and adds it to the route table of the accepter VPC
 -- added a permission to tirgger the lambda from the SNS topic
 -- updated the PeerRole to allow cross account access to sns:Publish on Arn of the topic
  -- added a topic policy to allow cross account publish on sns topic

2) On the requester account
  -- added a lambda that sends a message to the SNS topic with the VPC Peer Id, and the CIDR

It's a bit verbose, but here is part of the json for the accepter template

"AcceptVPCPeerLambdaExecutionRole": {
            "Type": "AWS::IAM::Role",
            "Properties": {
                "Path": "/",
                "Policies": [
                    {
                        "PolicyName": "CrossAccountVPCPeering",
                        "PolicyDocument": {
                            "Statement": [
                            {
                                "Action": [
                                    "logs:CreateLogGroup",
                                    "logs:CreateLogStream",
                                    "logs:GetLogEvents",
                                    "logs:PutLogEvents",
                                ],
                                "Resource": [ "arn:aws:logs:*:*:*" ],
                                "Effect": "Allow"
                            },
                            {
                                "Effect":"Allow",
                                "Action":["ec2:*Route*"],
                                "Resource":"*"
                            }
                        ]
                        }
                    }
                ],
                "AssumeRolePolicyDocument": {
                    "Statement": [
                    {
                        "Action": [ "sts:AssumeRole" ],
                        "Effect": "Allow",
                        "Principal": {
                          "Service": [ "lambda.amazonaws.com" ]
                        }
                    }]
                }
            }
        },
        "AcceptVPCPeerLambdaFunction": {
            "DependsOn": ["AcceptVPCPeerLambdaExecutionRole"],
            "Type": "AWS::Lambda::Function",
            "Properties": {
                "Code": {
                    "ZipFile" : { "Fn::Join" : ["\n", [
                          "import json",
                          "import boto3",
                          "import logging",
                          "logger = logging.getLogger()",
                          "logger.setLevel(logging.INFO)",
                          "def handler(event, context):",
                          "    message = json.loads(event['Records'][0]['Sns']['Message'])",
                          "    #logger.info('got event {}'.format(event))",
                          "    logger.info('message {}'.format(message))",
                          "    client = boto3.client('ec2')",
                          "    response = client.create_route(",
                          "        DestinationCidrBlock=message.get('destCidrBlock'),",
                          "        VpcPeeringConnectionId=message.get('ReqVpcPeeringId'),",
                          {"Fn::Sub" : ["        RouteTableId='${RouteTableId}'", {"RouteTableId" : {"Ref" : "PrivateRouteTable"}}]},
                          "    )",
                          "    logger.info('response code is {} '.format(",
                          "       response['Return']",
                          "    ))",

                        ]]
                    }
                },
                "Description": "Accept A VPC Peering Connection From Requested By Another Account",
                "MemorySize": 128,
                "Handler": "index.handler",
                "Role": {
                    "Fn::GetAtt": [ "AcceptVPCPeerLambdaExecutionRole", "Arn" ]
                },
                "Timeout": 300,
                "Runtime": "python2.7"
            }
        },
        "AcceptVPCPeerSNSTopic": {
            "DependsOn": [ "AcceptVPCPeerLambdaFunction" ],
            "Type": "AWS::SNS::Topic",
            "Properties": {
                "Subscription": [{
                    "Endpoint": {"Fn::GetAtt": [ "AcceptVPCPeerLambdaFunction", "Arn" ]},
                    "Protocol": "lambda"
                }]
            }
        },
"SNSTopicPolicy" : {
            "Type" : "AWS::SNS::TopicPolicy",
            "Properties" :{
                "PolicyDocument" : {
                    "Version":"2012-10-17",
                    "Id":"AWSAccountTopicAccess",
                    "Statement" :[
                        {
                            "Sid":"allow-publish-vpc-peering",
                            "Effect":"Allow",           
                            "Principal" :{
                            "AWS": {"Ref": "PeerRequesterAccounts"}
                        },
                        "Action":["sns:Publish"],
                        "Resource" : "*"
                        }
                    ]
                },
                "Topics" : [ {"Ref" : "AcceptVPCPeerSNSTopic"}]
            }
        }

And the Lambda for the requester template is

"VpcPeeringConnection": {
            "Type": "AWS::EC2::VPCPeeringConnection",
            "DependsOn" : ["VPC"],
            "Properties": {
                "VpcId": {
                    "Ref": "VPC"
                },
                "PeerVpcId": {
                    "Ref": "PeerVPCId"
                },
                "PeerOwnerId": {
                    "Ref": "PeerVPCAccountId"
                },
                "PeerRoleArn": {
                    "Ref": "PeerRoleArn"
                },
                "Tags" : [
                    {"Key" : "Name", "Value" : "DevOps Account To VPN Account"}
                ]
            }
        },
"RequesterVPCPeerLambdaFunction": {
            "DependsOn": ["RequesterVPCPeerLambdaExecutionRole", "VPC", "VpcPeeringConnection"],
            "Type": "AWS::Lambda::Function",
            "Properties": {
                "Code": {
                    "ZipFile" : { "Fn::Join" : ["\n", [
                          "import json",
                          "import boto3",
                          "import cfnresponse",
                          "def handler(event, context):",
                          "    message = {",
                          { "Fn::Sub": [ " 'ReqVpcPeeringId' : '${VpcPeeringId}',", { "VpcPeeringId": {"Ref" : "VpcPeeringConnection" }} ]},
                          { "Fn::Sub": [ " 'destCidrBlock' : '${destCidrBlock}'", { "destCidrBlock": {"Ref" : "TestPrivateSubnet1Cidr" }} ]},
                          "    }",
                          "    client = boto3.client('sns')",
                          "    response = client.publish(",
                          { "Fn::Sub": [ "        TargetArn='${TargetArn}',", { "TargetArn": {"Ref" : "AcceptVPCPeerSNSTopicArn" }} ]},
                          "        Message=json.dumps({'default': json.dumps(message)}),",
                          "        MessageStructure='json'",
                          "    )"
                        ]]
                    }
                },
                "Description": "Lambda Function To Publish the VPC Peering Connection Id to The VPN Accepter SNS Topic",
                "MemorySize": 128,
                "Handler": "index.handler",
                "Role": {
                    "Fn::GetAtt": [ "RequesterVPCPeerLambdaExecutionRole", "Arn" ]
                },
                "Timeout": 300,
                "Runtime": "python2.7"
            }
        }
1
votes

It is possible to create the route table entries in the first VPC from within the second template. Example of relevant CloudFormation resources you might include in second template:

Resources:
  IsolationVPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: "10.1.0.0/16"

  PrimaryPrivateSubnet:
    DependsOn:
      - IsolationVPC
    Type: AWS::EC2::Subnet
    Properties:
      VpcId:
        Ref: IsolationVPC
      AvailabilityZone: ${self:provider.region}a
      CidrBlock: 10.1.1.0/24
  PrimaryPrivateSubnetRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId:
        Ref: IsolationVPC
    DependsOn:
      - IsolationVPC

  PrimaryPublicSubnet:
    DependsOn:
      - IsolationVPC
    Type: AWS::EC2::Subnet
    Properties:
      VpcId:
        Ref: IsolationVPC
      AvailabilityZone: ${self:provider.region}a
      CidrBlock: 10.1.2.0/24
  PrimaryPublicSubnetRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId:
        Ref: IsolationVPC
    DependsOn:
      - IsolationVPC

  PeeringConnection:
    Type: AWS::EC2::VPCPeeringConnection
    DependsOn:
      - IsolationVPC
    Properties: 
      PeerVpcId: <first VPC ID goes here>
      VpcId:
        Ref: IsolationVPC
  PublicRoutingTableEntry:
    Type: AWS::EC2::Route
    DependsOn:
      - PrimaryPublicSubnetRouteTable
      - PeeringConnection
    Properties:
      RouteTableId:
        Ref: PrimaryPublicSubnetRouteTable
      DestinationCidrBlock: <first VPC CIDR block goes here>
      VpcPeeringConnectionId:
        Ref: PeeringConnection
  PrivateRoutingTableEntry:
    Type: AWS::EC2::Route
    DependsOn:
      - PrimaryPrivateSubnetRouteTable
      - PeeringConnection
    Properties:
      RouteTableId:
        Ref: PrimaryPrivateSubnetRouteTable
      DestinationCidrBlock: <first VPC CIDR block goes here>
      VpcPeeringConnectionId:
        Ref: PeeringConnection
  ReversePublicRoutingTableEntry:
    Type: AWS::EC2::Route
    DependsOn:
      - PeeringConnection
    Properties:
      RouteTableId: <first VPC public route table ID goes here>
      DestinationCidrBlock: 10.1.0.0/16
      VpcPeeringConnectionId:
        Ref: PeeringConnection
  ReversePrivateRoutingTableEntry:
    Type: AWS::EC2::Route
    DependsOn:
      - PeeringConnection
    Properties:
      RouteTableId: <first VPC private route table ID goes here>
      DestinationCidrBlock: 10.1.0.0/16
      VpcPeeringConnectionId:
        Ref: PeeringConnection

I did not realize this until I read the examples provided here: https://github.com/lizduke/cloudformationexamples but have since tested it successfully.

0
votes

For posterity, we've been using this to create peering between VPCs.

It uses our generic custom resource provider to create remote peering routes and tags as well as optionally authorize ingress into remote security group (e.g.):

  RemotePeeringRoute:
    Type: 'Custom::CreatePeeringRoute'
    Version: 1.0
    DependsOn: PeeringConnection
    Properties:
      ServiceToken: !Sub 'arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:generic-custom-resource-provider'
      RoleArn: !Sub 'arn:${AWS::Partition}:iam::${TargetAccountId}:role/VPCPeeringRole'
      AgentService: ec2
      AgentType: client
      AgentRegion: !Sub '${TargetRegion}'
      AgentCreateMethod: create_route
      AgentDeleteMethod: delete_route
      AgentCreateArgs:
        DestinationCidrBlock: !Sub '${RequesterCidrBlock.CidrBlock}'
        RouteTableId: !Select [ 0, !Split [ ',', !Ref 'TargetRouteTableIds' ]]
        VpcPeeringConnectionId: !Sub '${PeeringConnection}'
      AgentDeleteArgs:
        DestinationCidrBlock: !Sub '${RequesterCidrBlock.CidrBlock}'
        RouteTableId: !Select [ 0, !Split [ ',', !Ref 'TargetRouteTableIds' ]]

Can stand on its own or be nested inside another stack.