How do I add routes to the main route table in my VPC with CloudFormation?
Last updated: 2022-09-09
Short description
When 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.
Resolution
Note: 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-09
Description: Template to add routes to default/main routetable of VPC
Resources:
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 MyInternetGateway
Outputs:
RouteTableID:
Value: !GetAtt Lambdatrigger.RouteTableID
The 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"
}
}
Did this article help?
Do you need billing or technical support?