# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 # # Permission is hereby granted, free of charge, to any person obtaining a copy of this # software and associated documentation files (the "Software"), to deal in the Software # without restriction, including without limitation the rights to use, copy, modify, # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --- AWSTemplateFormatVersion: '2010-09-09' Description: 'CloudFormation template to deploy the Transit Gateway Attachment Tagger' Parameters: awsOrganizationsRootAccountId: Description: The AWS Account Id for the AWS Organizations root account Type: String Default: "123456789012" AllowedPattern: "\\d{12}" ConstraintDescription: Enter a valid AWS Account Id TGWRegions: Description: Comma-Seperated list of regions with a Transit Gateway in our core network Type: String Default: "eu-west-1,us-east-1,us-west-2,ap-southeast-1" TGWExclusionList: Description: Comma-Seperated list of TGW Ids to exclude from processing (leave blank to process all TGWs) Type: String Default: "" Metadata: AWS::CloudFormation::Interface: ParameterGroups: - Label: default: "AWS Organization Configuration" Parameters: - awsOrganizationsRootAccountId - Label: default: "Transit Gateway Configuration" Parameters: - TGWRegions - TGWExclusionList Resources: LambdaOrganizationsAccountQueryLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: '/aws/lambda/tgw-tagger-organizations-account-query' RetentionInDays: 90 Metadata: cfn_nag: rules_to_suppress: - id: W84 reason: "The data stored in CloudWatch Logs does not contain sensitive information, using default protections provided by CloudWatch logs" LambdaOrganizationsAccountQueryRole: Type: AWS::IAM::Role Properties: RoleName: tgw-attachment-tagger-organizations-lambda-role AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: lambda.amazonaws.com Action: sts:AssumeRole Path: / Policies: - PolicyName: tgw-attachment-tagger-organizations-lambda-inline-policy PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Resource: - !GetAtt 'LambdaOrganizationsAccountQueryLogGroup.Arn' - Effect: Allow Action: - sts:AssumeRole Resource: - !Sub arn:aws:iam::${awsOrganizationsRootAccountId}:role/tgw-attachment-tagger-organization-query-role - Effect: Allow Action: - xray:PutTraceSegments - xray:PutTelemetryRecords Resource: "*" Metadata: cfn_nag: rules_to_suppress: - id: W28 reason: "A single copy of this solution is to be deployed globally, resources have static names for ease of use" - id: W11 reason: "Xray requires the use of a wildcard resource value" LambdaOrganizationsAccountQuery: Type: AWS::Lambda::Function Properties: FunctionName: tgw-tagger-organizations-account-query Description: Queries the AWS Organizations API in the Organization Root for account Ids and Names Handler: index.lambda_handler Runtime: python3.9 Role: !GetAtt 'LambdaOrganizationsAccountQueryRole.Arn' Timeout: 60 MemorySize: 256 Environment: Variables: ORGANIZATIONS_ROLE_ARN: !Sub arn:aws:iam::${awsOrganizationsRootAccountId}:role/tgw-attachment-tagger-organization-query-role TracingConfig: Mode: Active Layers: - !Sub "arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPython:7" Code: ZipFile: | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 # # Permission is hereby granted, free of charge, to any person obtaining a copy of this # software and associated documentation files (the "Software"), to deal in the Software # without restriction, including without limitation the rights to use, copy, modify, # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. import boto3 # type: ignore import logging import sys import traceback import os import json from aws_lambda_powertools import Tracer # type: ignore from aws_lambda_powertools import Logger # type: ignore sts_client = boto3.client('sts') tracer = Tracer(service="tgw-tagger-organizations-account-query") logger = Logger(service="tgw-tagger-organizations-account-query") ORGANIZATIONS_ROLE = os.environ.get('ORGANIZATIONS_ROLE_ARN') @tracer.capture_method def assume_role(role_arn: str, boto_client): """ Function to assume an IAM Role Parameters: role_arn (str): the ARN of the role to assume boto_client: The boto client object to use, exposed to enable mocking in unit tests Returns: boto3 session object """ try: logger.info(f"Assuming Role: {role_arn}") assumed_role = boto_client.assume_role( RoleArn=role_arn, RoleSessionName='sh-notifier-send-email' ) except: logger.exception("Exception assuming role") raise RuntimeError(f"Exception assuming role: {role_arn}") return boto3.Session( aws_access_key_id=assumed_role['Credentials']['AccessKeyId'], aws_secret_access_key=assumed_role['Credentials']['SecretAccessKey'], aws_session_token=assumed_role['Credentials']['SessionToken']) @tracer.capture_method def get_account_details_from_organization(organizations_client): """ Query the Organizations API to build a list of Account Ids and Names. Parameters: organizations_client: the boto client object to use for the Organizations API calls Returns: result_object (list): List of accounts/Names in the Organization with status of ACTIVE """ result_object = [] try: paginator = organizations_client.get_paginator('list_accounts') iterator = paginator.paginate() for page in iterator: for account in page['Accounts']: if "ACTIVE" == account['Status']: logger.debug(f"Account ID {account['Id']} has status: {account['Status']}") result_object.append( { "id": account['Id'], "name": account['Name'] } ) else: logger.debug(f"Account ID {account['Id']} has status: {account['Status']}") except: logger.exception("Error calling list_accounts for the organization") raise RuntimeError(f"Error calling list_accounts for the organization") return result_object @tracer.capture_lambda_handler @logger.inject_lambda_context(log_event=True) def lambda_handler(event, context): """ Queries the AWS Organizations API to determine menmber account IDs and Names, before returning a list of dictionaries with account information. Parameters: event (dict): The Lambda event object context (dict): The Lambda context object Returns: response_data (dict): Dictionary containing a list of the accounts/names to process to the Step Function """ boto3_session_object = assume_role(ORGANIZATIONS_ROLE, sts_client) organizations_client = boto3_session_object.client('organizations') account_list = get_account_details_from_organization(organizations_client) logger.info(f"{account_list}") response_data = {} response_data['AccountDetails'] = account_list return(response_data) Metadata: cfn_nag: rules_to_suppress: - id: W89 reason: "This lambda requires access to AWS Service Endpoints so deployment into a VPC can't be guaranteed to work" - id: W92 reason: "Reserved Concurrency is not relevent nor desired for this function" LambdaTGWAttachmentQueryLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: '/aws/lambda/tgw-tagger-attachment-query' RetentionInDays: 90 Metadata: cfn_nag: rules_to_suppress: - id: W84 reason: "The data stored in CloudWatch Logs does not contain sensitive information, using default protections provided by CloudWatch logs" LambdaTGWAttachmentQueryRole: Type: AWS::IAM::Role Properties: RoleName: tgw-attachment-tagger-attachquery-lambda-role AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: lambda.amazonaws.com Action: sts:AssumeRole Path: / Policies: - PolicyName: tgw-attachment-tagger-attachquery-lambda-inline-policy PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Resource: - !GetAtt 'LambdaTGWAttachmentQueryLogGroup.Arn' - Effect: Allow Action: - ec2:DescribeTransitGatewayAttachments Resource: "*" - Effect: Allow Action: - xray:PutTraceSegments - xray:PutTelemetryRecords Resource: "*" Metadata: cfn_nag: rules_to_suppress: - id: W28 reason: "A single copy of this solution is to be deployed globally, resources have static names for ease of use" - id: W11 reason: "The DescribeTransitGatewayAttachments action cannot be locked down to specific resource Arns or paths." LambdaTGWAttachmentQuery: Type: AWS::Lambda::Function Properties: FunctionName: tgw-tagger-attachment-query Description: Queries the EC2 API for TGW attachment Ids Handler: index.lambda_handler Runtime: python3.9 Role: !GetAtt 'LambdaTGWAttachmentQueryRole.Arn' Timeout: 900 MemorySize: 512 Environment: Variables: REGION_LIST: !Ref TGWRegions TGW_LIST: !Ref TGWExclusionList TracingConfig: Mode: Active Layers: - !Sub "arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPython:7" Code: ZipFile: | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 # # Permission is hereby granted, free of charge, to any person obtaining a copy of this # software and associated documentation files (the "Software"), to deal in the Software # without restriction, including without limitation the rights to use, copy, modify, # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. import boto3 # type: ignore import logging import sys import traceback import os import json from aws_lambda_powertools import Tracer # type: ignore from aws_lambda_powertools import Logger # type: ignore tracer = Tracer(service="tgw-tagger-attachment-query") logger = Logger(service="tgw-tagger-attachment-query") REGION_LIST = os.environ.get('REGION_LIST').split(",") ORIGINAL_TGW_LIST = os.environ.get('TGW_LIST') if not REGION_LIST: raise RuntimeError("Environment Variable REGION_LIST is empty - At least one region must be specified") if ORIGINAL_TGW_LIST: tgw_list = ORIGINAL_TGW_LIST.split(",") else: tgw_list = [] @tracer.capture_method def get_ec2_client(region: str): """ Create a regional EC2 boto client Parameters: region (str): the AWS region where the client should be created Returns: boto3 ec2 client for the target region """ return boto3.client('ec2', region_name=region) @tracer.capture_method def list_transit_gateway_attachments(account_list: list, region: str): """ Returns all TGW attachments for the specified Region Parameters: account_list (list): List containing dictionaries of account IDs and their Name region (str): The AWS region to process Returns: result_object (list): List of dictionaries with TGW attachment data including the owning account name """ logger.info(f"Getting list of TGW Attachments for region {region}") ec2 = get_ec2_client(region) result_object = [] try: # Get all TGW attachments in the region which have type: vpc and are available paginator = ec2.get_paginator('describe_transit_gateway_attachments') iterator = paginator.paginate( Filters=[ { 'Name': 'state', 'Values': [ 'available', ] }, { 'Name': 'resource-type', 'Values': [ 'vpc', ] }, ] ) except: logger.exception(f"Error getting list of TGW attachments for region {region}") raise RuntimeError(f"Error getting list of TGW attachments for region {region}") for page in iterator: for attachment in page['TransitGatewayAttachments']: # Check TGW has not been excluded from processing if attachment['TransitGatewayId'] not in tgw_list: logger.info(f"Processing Attachment: {attachment['TransitGatewayAttachmentId']}") tgw_name = "MISSING" for i in attachment['Tags']: # Check whether Name tag exists if "Name" == i['Key']: tgw_name = i['Value'] account_name = "MISSING" # Check account list object for a match against the TGW resource owner for account in [x for x in account_list if x['id'] == attachment['ResourceOwnerId']]: account_name = account['name'] result_object.append( { "tgwId": attachment['TransitGatewayId'], "attachmentId": attachment['TransitGatewayAttachmentId'], "accountId": attachment['ResourceOwnerId'], "accountName": account_name, "nametag": tgw_name } ) return result_object @tracer.capture_lambda_handler @logger.inject_lambda_context(log_event=True) def lambda_handler(event, context): """ Queries the EC2 API for Transit Gateway Attachment details for each configured region, before returning a dictionary of lists with TGW attachment information. Parameters: event (dict): The Lambda event object context (dict): The Lambda context object Returns: response_data (dict): Dictionary containing a list of the TGW attachments for each processed region """ response_data = {} response_data['MapInput'] = [] if event['AccountDetails']: for region in REGION_LIST: logger.info(f"Processing Region: {region}") result = list_transit_gateway_attachments(event['AccountDetails'], region) response_data['MapInput'].append( { region: result } ) return(response_data) Metadata: cfn_nag: rules_to_suppress: - id: W89 reason: "This lambda requires access to AWS Service Endpoints so deployment into a VPC can't be guaranteed to work" - id: W92 reason: "Reserved Concurrency is not relevent nor desired for this function" LambdaTGWRTBQueryLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: '/aws/lambda/tgw-tagger-rtb-query' RetentionInDays: 90 Metadata: cfn_nag: rules_to_suppress: - id: W84 reason: "The data stored in CloudWatch Logs does not contain sensitive information, using default protections provided by CloudWatch logs" LambdaTGWRTBQueryRole: Type: AWS::IAM::Role Properties: RoleName: tgw-attachment-tagger-rtbquery-lambda-role AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: lambda.amazonaws.com Action: sts:AssumeRole Path: / Policies: - PolicyName: tgw-attachment-tagger-rtbquery-lambda-inline-policy PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Resource: - !GetAtt 'LambdaTGWRTBQueryLogGroup.Arn' - Effect: Allow Action: - ec2:DescribeTransitGatewayRouteTables - ec2:SearchTransitGatewayRoutes Resource: "*" - Effect: Allow Action: - xray:PutTraceSegments - xray:PutTelemetryRecords Resource: "*" Metadata: cfn_nag: rules_to_suppress: - id: W28 reason: "A single copy of this solution is to be deployed globally, resources have static names for ease of use" - id: W11 reason: "The DescribeTransitGatewayRouteTables & SearchTransitGatewayRoutes actions cannot be locked down to specific resource Arns or paths." LambdaTGWRTBQuery: Type: AWS::Lambda::Function Properties: FunctionName: tgw-tagger-rtb-query Description: Queries the EC2 API for TGW Route Table CIDR entries Handler: index.lambda_handler Runtime: python3.9 Role: !GetAtt 'LambdaTGWRTBQueryRole.Arn' Timeout: 900 MemorySize: 512 TracingConfig: Mode: Active Layers: - !Sub "arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPython:7" Code: ZipFile: | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 # # Permission is hereby granted, free of charge, to any person obtaining a copy of this # software and associated documentation files (the "Software"), to deal in the Software # without restriction, including without limitation the rights to use, copy, modify, # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. import boto3 # type: ignore import logging import sys import traceback import os import json from aws_lambda_powertools import Tracer # type: ignore from aws_lambda_powertools import Logger # type: ignore tracer = Tracer(service="tgw_tagger_rtb_query") logger = Logger(service="tgw_tagger_rtb_query") @tracer.capture_method def get_ec2_client(region: str): """ Create a regional EC2 boto client Parameters: region (str): the AWS region where the client should be created Returns: boto3 ec2 client for the target region """ return boto3.client('ec2', region_name=region) @tracer.capture_method def list_tgw_route_tables(region: str): """ Returns the TGW route tables for the supplied region Parameters: region (str): The AWS region to process Returns: result_object (list): List of TGW route tables (dict inc TGW and RTB IDs) """ ec2 = get_ec2_client(region) result_object = [] try: paginator = ec2.get_paginator('describe_transit_gateway_route_tables') iterator = paginator.paginate( Filters=[ { 'Name': 'state', 'Values': [ 'available', ] }, ] ) except: logger.exception(f"Error getting TGW RTB for region {region}") raise RuntimeError(f"Error getting TGW RTB for region {region}") for page in iterator: for rtb in page['TransitGatewayRouteTables']: result_object.append( { "tgwId": rtb['TransitGatewayId'], "rtbId": rtb['TransitGatewayRouteTableId'] } ) return result_object @tracer.capture_method def find_tgw_attachment_cidr(attachment_id: str, route_table_list: list, region: str): """ Returns the cidr range for a TGW attachment Parameters: attachment_id (str): The TGW attachment ID route_table_list (list): The list of route table information region (str): The AWS region to process Returns: Either the TGW cidr as a string or None """ result_object = [] for route_table in route_table_list: cidr_range = search_rtb_for_attachment(attachment_id, route_table['rtbId'], region) if cidr_range: result_object.append( { "cidr": cidr_range } ) if len(result_object) == 1: return result_object[0]['cidr'] else: return None @tracer.capture_method def search_rtb_for_attachment(attachment_id: str, route_table_id: str, region: str): """ Searches RTB for the TGW Attachment ID Parameters: attachment_id (str): The TGW attachment ID route_table_id (str): The Route Table ID region (str): The AWS region to process Returns: result_object: Either the CIDR block as a string or None """ ec2 = get_ec2_client(region) result_object = None try: response = ec2.search_transit_gateway_routes( TransitGatewayRouteTableId=route_table_id, Filters=[ { 'Name': 'attachment.transit-gateway-attachment-id', 'Values': [ attachment_id, ] }, ] ) except: logger.exception(f"Error searching TGW Route Table: {route_table_id}") raise RuntimeError(f"Error searching TGW Route Table: {route_table_id}") if response['Routes']: # An attachment may only be associated with a single Route Table, however the API returns a list containing a single element for route in response['Routes']: result_object = route['DestinationCidrBlock'] return result_object @tracer.capture_lambda_handler @logger.inject_lambda_context(log_event=True) def lambda_handler(event, context): """ Queries the TGW route tables for the supplied region, to find out the CIDR range associated with the attachment Parameters: event (dict): The Lambda event object context (dict): The Lambda context object Returns: event (dict): Updated event object, with the TGW Attachment CIDR if available """ # Get the next item in the supplied dictionary. The Map iterator in the surrounding Step Function will supply a single region at a time to this function - however we do not know which at runtime map_region = next(iter(event)) rtb = list_tgw_route_tables(map_region) for a in event[map_region]: logger.info(f"Processing attachment {a['attachmentId']}") cidr = find_tgw_attachment_cidr(a['attachmentId'], rtb, map_region) if cidr: a['cidr'] = cidr else: a['cidr'] = "MISSING" return event Metadata: cfn_nag: rules_to_suppress: - id: W89 reason: "This lambda requires access to AWS Service Endpoints so deployment into a VPC can't be guaranteed to work" - id: W92 reason: "Reserved Concurrency is not relevent nor desired for this function" LambdaTGWTagLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: '/aws/lambda/tgw-tagger-tagger' RetentionInDays: 90 Metadata: cfn_nag: rules_to_suppress: - id: W84 reason: "The data stored in CloudWatch Logs does not contain sensitive information, using default protections provided by CloudWatch logs" LambdaTGWTagRole: Type: AWS::IAM::Role Properties: RoleName: tgw-attachment-tagger-tag-lambda-role AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: lambda.amazonaws.com Action: sts:AssumeRole Path: / Policies: - PolicyName: tgw-attachment-tagger-tag-lambda-inline-policy PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Resource: - !GetAtt 'LambdaTGWTagLogGroup.Arn' - Effect: Allow Action: - ec2:CreateTags Resource: - !Sub "arn:aws:ec2:*:${AWS::AccountId}:transit-gateway-attachment/*" - Effect: Allow Action: - xray:PutTraceSegments - xray:PutTelemetryRecords Resource: "*" Metadata: cfn_nag: rules_to_suppress: - id: W28 reason: "A single copy of this solution is to be deployed globally, resources have static names for ease of use" - id: W11 reason: "Xray requires the use of a wildcard resource value" LambdaTGWTag: Type: AWS::Lambda::Function Properties: FunctionName: tgw-tagger-attachment-tagger Description: Updates the TGW Attachment Name tag Handler: index.lambda_handler Runtime: python3.9 Role: !GetAtt 'LambdaTGWTagRole.Arn' Timeout: 60 MemorySize: 128 TracingConfig: Mode: Active Layers: - !Sub "arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPython:7" Code: ZipFile: | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 # # Permission is hereby granted, free of charge, to any person obtaining a copy of this # software and associated documentation files (the "Software"), to deal in the Software # without restriction, including without limitation the rights to use, copy, modify, # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. import boto3 # type: ignore import logging import sys import traceback import os import json from aws_lambda_powertools import Tracer # type: ignore from aws_lambda_powertools import Logger # type: ignore tracer = Tracer(service="tgw_tagger_attachment_tagger") logger = Logger(service="tgw_tagger_attachment_tagger") @tracer.capture_method def get_ec2_client(region: str): """ Create a regional EC2 boto client Parameters: region (str): the AWS region where the client should be created Returns: boto3 ec2 client for the target region """ return boto3.client('ec2', region_name=region) @tracer.capture_method def tag_tgw_attachment(attachment: dict, region: str): """ Apply Tags to TGW Attachments Parameters: attachment (dict): Dictionary with TGW attachment metadata region (str): The AWS region where the attachment is found """ ec2_client = get_ec2_client(region) tagValue = f"{attachment['cidr']}-{attachment['accountName']}" try: # Add Name tag to TGW attachment ec2_client.create_tags( Resources=[ attachment['attachmentId'], ], Tags=[ { 'Key': 'Name', 'Value': tagValue }, ] ) except: logger.exception(f"Error updating TGW attachment tag for {attachment['attachmentId']}") raise RuntimeError(f"Error updating TGW attachment tag for {attachment['attachmentId']}") @tracer.capture_lambda_handler @logger.inject_lambda_context(log_event=True) def lambda_handler(event, context): """ Applies missing Name tags to TGW attachments where we have the necessary information and there is no existing Name tag Parameters: event (dict): The Lambda event object context (dict): The Lambda context object Returns: event (dict): Updated event object, with the TGW Attachment CIDR if available """ # Get the next item in the supplied dictionary. # The Map iterator in the surrounding Step Function will supply a single region at a time to this function, # however we do not know which at runtime map_region = next(iter(event)) logger.info(f"Processing region {map_region}") for attachment in event[map_region]: # Logic to determine whether we should tag the attachment if ("MISSING" == attachment['nametag']) and ("MISSING" != attachment['cidr']): # Attachment has no Name tag and we were able find the CIDR from the propagated Route Table entry logger.info(f"Tagging attachment {attachment['attachmentId']}") tag_tgw_attachment(attachment, map_region) attachment['tagCreated'] = True else: logger.info(f"Skipping attachment {attachment['attachmentId']}") attachment['tagCreated'] = False return event Metadata: cfn_nag: rules_to_suppress: - id: W89 reason: "This lambda requires access to AWS Service Endpoints so deployment into a VPC can't be guaranteed to work" - id: W92 reason: "Reserved Concurrency is not relevent nor desired for this function" TGWTaggerStateMachineRole: Type: AWS::IAM::Role Properties: RoleName: tgw-attachment-tagger-state-machine-role AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Principal: Service: - !Sub "states.${AWS::Region}.amazonaws.com" Action: - "sts:AssumeRole" Path: "/" Policies: - PolicyName: tgw-attachment-tagger-state-machine-inline-policy PolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Action: - lambda:InvokeFunction Resource: - !GetAtt 'LambdaOrganizationsAccountQuery.Arn' - !GetAtt 'LambdaTGWAttachmentQuery.Arn' - !GetAtt 'LambdaTGWRTBQuery.Arn' - !GetAtt 'LambdaTGWTag.Arn' - Effect: "Allow" Action: - states:DescribeStateMachine - states:ListExecutions - states:StartExecution - states:StopExecution - states:DescribeExecution Resource: - !Sub arn:${AWS::Partition}:states:${AWS::Region}:${AWS::AccountId}:stateMachine:tgw-attachment-tagger-state-machine - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Resource: - !GetAtt 'TGWTaggerStateMachineLogGroup.Arn' - Effect: Allow Action: - logs:DescribeLogGroups - logs:PutResourcePolicy - logs:DescribeResourcePolicies - logs:CreateLogDelivery - logs:GetLogDelivery - logs:UpdateLogDelivery - logs:DeleteLogDelivery - logs:ListLogDeliveries Resource: "*" - Effect: Allow Action: - xray:PutTraceSegments - xray:PutTelemetryRecords Resource: "*" Metadata: cfn_nag: rules_to_suppress: - id: W28 reason: "A single copy of this solution is to be deployed globally, resources have static names for ease of use" - id: W11 reason: "Step Functions requires the ability to create/update log resource policies to enable CloudWatch Logging" TGWTaggerStateMachineLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: '/aws/stepfunction/tgw-tagger-state-machine' RetentionInDays: 90 Metadata: cfn_nag: rules_to_suppress: - id: W84 reason: "The data stored in CloudWatch Logs does not contain sensitive information, using default protections provided by CloudWatch logs" TGWTaggerStepFunction: Type: 'AWS::StepFunctions::StateMachine' Properties: StateMachineName: tgw-attachment-tagger-state-machine StateMachineType: STANDARD RoleArn: !GetAtt 'TGWTaggerStateMachineRole.Arn' LoggingConfiguration: Destinations: - CloudWatchLogsLogGroup: LogGroupArn: !GetAtt 'TGWTaggerStateMachineLogGroup.Arn' IncludeExecutionData: True Level: ALL TracingConfiguration: Enabled: True DefinitionString: Fn::Sub: |- { "Comment": "A state machine to orchestrate the tagging of transit gateway attachments", "StartAt": "get-account-data", "States": { "get-account-data": { "Type": "Task", "Resource": "${LambdaOrganizationsAccountQuery.Arn}", "Retry": [ { "ErrorEquals": [ "Lambda.ServiceException", "Lambda.AWSLambdaException", "Lambda.SdkClientException"], "IntervalSeconds": 2, "MaxAttempts": 3, "BackoffRate": 2 } ], "Catch": [ { "ErrorEquals": [ "States.ALL" ], "Next": "get-account-data-failure", "ResultPath": "$.RuntimeError" } ], "Next": "get-tgw-attachments" }, "get-account-data-failure": { "Type": "Pass", "Result": "Error retrieving account data from AWS Organizations", "ResultPath": "$.FailureReason", "Next": "failed" }, "get-tgw-attachments": { "Type": "Task", "Resource": "${LambdaTGWAttachmentQuery.Arn}", "Retry": [ { "ErrorEquals": [ "Lambda.ServiceException", "Lambda.AWSLambdaException", "Lambda.SdkClientException"], "IntervalSeconds": 2, "MaxAttempts": 3, "BackoffRate": 2 } ], "Catch": [ { "ErrorEquals": [ "States.ALL" ], "Next": "get-tgw-attachments-failure", "ResultPath": "$.RuntimeError" } ], "Next": "process-regions-map" }, "get-tgw-attachments-failure": { "Type": "Pass", "Result": "Error retrieving TGW Attachments", "ResultPath": "$.FailureReason", "Next": "failed" }, "process-regions-map": { "Type": "Map", "ItemsPath": "$.MapInput", "MaxConcurrency": 0, "Iterator": { "StartAt": "get-tgw-attachment-cidr", "States": { "get-tgw-attachment-cidr": { "Type": "Task", "Resource": "${LambdaTGWRTBQuery.Arn}", "Retry": [ { "ErrorEquals": [ "Lambda.ServiceException", "Lambda.AWSLambdaException", "Lambda.SdkClientException"], "IntervalSeconds": 2, "MaxAttempts": 3, "BackoffRate": 2 } ], "Catch": [ { "ErrorEquals": [ "States.ALL" ], "Next": "get-tgw-attachment-cidr-failure", "ResultPath": "$.RuntimeError" } ], "Next": "tag-tgw-attachments" }, "get-tgw-attachment-cidr-failure": { "Type": "Pass", "Result": "Error getting TGW attachment CIDR ranges", "ResultPath": "$.FailureReason", "Next": "map-failed" }, "tag-tgw-attachments": { "Type": "Task", "Resource": "${LambdaTGWTag.Arn}", "Retry": [ { "ErrorEquals": [ "Lambda.ServiceException", "Lambda.AWSLambdaException", "Lambda.SdkClientException"], "IntervalSeconds": 2, "MaxAttempts": 3, "BackoffRate": 2 } ], "Catch": [ { "ErrorEquals": [ "States.ALL" ], "Next": "tag-tgw-attachments-failure", "ResultPath": "$.RuntimeError" } ], "Next": "map-success" }, "tag-tgw-attachments-failure": { "Type": "Pass", "Result": "Error applying tags to TGW attachments", "ResultPath": "$.FailureReason", "Next": "map-failed" }, "map-success": { "Type": "Pass", "End": true }, "map-failed": { "Type": "Fail" } } }, "Next": "success" }, "success": { "Type": "Succeed" }, "failed": { "Type": "Fail" } } } CloudWatchEventRole: Type: AWS::IAM::Role Properties: RoleName: tgw-attachment-tagger-cloudwatch-event-role AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Principal: Service: - events.amazonaws.com Action: - "sts:AssumeRole" Policies: - PolicyName: tgw-attachment-tagger-cloudwatch-event-inline-policy PolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Action: - states:StartExecution Resource: - !Sub arn:${AWS::Partition}:states:${AWS::Region}:${AWS::AccountId}:stateMachine:tgw-attachment-tagger-state-machine Metadata: cfn_nag: rules_to_suppress: - id: W28 reason: "A single copy of this solution is to be deployed globally, resources have static names for ease of use" CloudWatchEventRule: Type: AWS::Events::Rule Properties: Description: Rule to schedule the TGW Attachment Tagger Step Function RoleArn: !GetAtt 'CloudWatchEventRole.Arn' ScheduleExpression: "cron(0 6 * * ? *)" Targets: - Arn: !GetAtt 'TGWTaggerStepFunction.Arn' RoleArn: !GetAtt CloudWatchEventRole.Arn Id: tgw-tagger-cloudwatch-event Input: |- { "Comment": "Dummy JSON" }