Question: How do I add routes to the main route table in my VPC with CloudFormation?

I want to add routes to the main route table when I create my Amazon Virtual Private Cloud (Amazon VPC) in AWS CloudFormation.

Short descriptionWhen you use CloudFormation to create an Amazon VPC, CloudFormation doesn't recognize the main route table that's created by default. Therefore, information about the route table can't be passed between your Amazon VPC and CloudFormation. As a result, you can't add or remove routes from the main route table because you can't reference the route table from your CloudFormation template.

ResolutionNote: If you receive errors when running AWS Command Line Interface (AWS CLI) commands, make sure that you're using the most recent AWS CLI version.To resolve this issue, you can use an AWS Lambda-backed custom resource in the CloudFormation template. The CloudFormation stack creates an Amazon VPC. Then, the custom resource uses an AWS Lambda function to retrieve the main route table ID that's associated with your Amazon VPC.Create a CloudFormation stack using the following RouteTable-template.yml template:AWSTemplateFormatVersion: 2010-09-09Description: Template to add routes to default/main routetable of VPCResources:  MyVPC:    Type: 'AWS::EC2::VPC'    Properties:      CidrBlock: 10.0.0.0/16      Tags:        - Key: Env          Value: Test  LambdaIAMRole:    Type: 'AWS::IAM::Role'    DependsOn: MyVPC    Properties:      AssumeRolePolicyDocument:        Version: 2012-10-17        Statement:          - Effect: Allow            Principal:              Service:                - lambda.amazonaws.com            Action:              - 'sts:AssumeRole'      Path: /      Policies:        - PolicyName: root          PolicyDocument:            Version: 2012-10-17            Statement:              - Effect: Allow                Action:                  - 'ec2:Describe*'                Resource: '*'              - Effect: Allow                Action:                  - 'logs:CreateLogGroup'                  - 'logs:CreateLogStream'                  - 'logs:PutLogEvents'                Resource: 'arn:aws:logs:*:*:*'  LambdaFunction:    Type: 'AWS::Lambda::Function'    Properties:      Handler: index.lambda_handler      Role: !GetAtt LambdaIAMRole.Arn      Runtime: python3.9      Timeout: 50      Code:        ZipFile: |            from __future__ import print_function            import json            import boto3            import urllib3            import cfnresponse            SUCCESS = "SUCCESS"            FAILED = "FAILED"            http = urllib3.PoolManager()            print('Loading function')            ec2 = boto3.client('ec2')            def lambda_handler(event, context):                print("Received event: " + json.dumps(event, indent=2))                responseData={}                try:                    if event['RequestType'] == 'Delete':                        print("Request Type:",event['RequestType'])                        print("Delete Request - No Physical resources to delete")                    elif event['RequestType'] == 'Create':                        print("Request Type:",event['RequestType'])                        VPCID=event['ResourceProperties']['VPCID']                        RouteTableID=get_vpc(VPCID)                        responseData={'RouteTableID':RouteTableID}                        print("Sending response to custom resource")                    elif event['RequestType'] == 'Update':                        print("Request Type:",event['RequestType'])                        VPCID=event['ResourceProperties']['VPCID']                        RouteTableID=get_vpc(VPCID)                        responseData={'RouteTableID':RouteTableID}                        print("Sending response to custom resource")                    responseStatus = 'SUCCESS'                    print("responseStatus: " + responseStatus)                    cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData, "CustomResourcePhysicalID")                except Exception as e:                    print('Failed to process:', e)                    responseStatus = 'FAILURE'                    responseData = {'Failure': 'Something bad happened.'}                    cfnresponse.send(event, context, cfnresponse.FAILURE, responseData, "CustomResourcePhysicalID")            def get_vpc(VPCID):                response = ec2.describe_route_tables (                  Filters=[                    {                      'Name': 'association.main',                      'Values': [ 'true' ]                    },                    {                      'Name': 'vpc-id',                      'Values': [ VPCID ]                    }                  ]                )                print("Printing the VPC Route Table ID ....")                RouteTableID=response['RouteTables'][0]['RouteTableId']                print(RouteTableID)                return RouteTableID            def send(event, context, responseStatus, responseData, physicalResourceId=None, noEcho=False):                responseUrl = event['ResponseURL']                print(responseUrl)                responseBody = {'Status': responseStatus,                                'Reason': 'See the details in CloudWatch Log Stream: ' + context.log_stream_name,                                'PhysicalResourceId': physicalResourceId or context.log_stream_name,                                'StackId': event['StackId'],                                'RequestId': event['RequestId'],                                'LogicalResourceId': event['LogicalResourceId'],                                'Data': responseData}                json_responseBody = json.dumps(responseBody)                print("Response body:\n" + json_responseBody)                headers = {                    'content-type' : '',                    'content-length' : str(len(json_responseBody))                }                try:                    response = http.request('PUT', responseUrl, headers=headers, body=json_responseBody)                    print("Status code: " + response.reason)                except Exception as e:                    print("send(..) failed executing requests.put(..): " + str(e))  Lambdatrigger:    Type: 'Custom::RouteTableLambda'    Properties:      ServiceToken: !GetAtt LambdaFunction.Arn      VPCID: !Ref MyVPC  MyInternetGateway:    Type: 'AWS::EC2::InternetGateway'    Properties:      Tags:        - Key: Env          Value: Test  AttachGateway:    Type: 'AWS::EC2::VPCGatewayAttachment'    Properties:      VpcId: !Ref MyVPC      InternetGatewayId: !Ref MyInternetGateway  MyRoute:    Type: 'AWS::EC2::Route'    Properties:      RouteTableId: !GetAtt Lambdatrigger.RouteTableID      DestinationCidrBlock: 0.0.0.0/0      GatewayId: !Ref MyInternetGatewayOutputs:  RouteTableID:    Value: !GetAtt Lambdatrigger.RouteTableIDThe data that the custom resource returns contains the main route table ID. You can reference the ID using GetAtt in AWS::EC2::Route to add routes to the main route table. Also, the outputs of the stack display the route table ID.The following is an example of the SUCCESS response body that the custom resource sends to the CloudFormation stack:{  "Status": "SUCCESS",  "Reason": "See the details in CloudWatch Log Stream: 2022/08/31/[$LATEST]c48b90efb3944c11ad3fb6e1ce5e1f45",  "PhysicalResourceId": "CustomResourcePhysicalID",  "StackId": "arn:aws:cloudformation:us-west-1:XXXX:stack/VPC-RT/06c957b0-297e-11ed-afb5-02ca6fd67f8d",  "RequestId": "55c0f2b8-3044-47f7-aba4-84502b4ef632",  "LogicalResourceId": "Lambdatrigger",  "NoEcho": false,  "Data": {    "RouteTableID": "rtb-0fba8d15701234567a"  }}