AWSTemplateFormatVersion: '2010-09-09' Description: AWS CloudFormation template to Delete AWS config resources across Regions and Accounts ############# Parmeter Sections ############# Parameters: ExecutionRoleName: Type: String Default: AWSConfigDeletion-ExecutionRole Description: >- this IAM role is assumed by the Automation document and has the required permission to remove AWS config rules and settings. TargetAccounts: Type: String Description: >- Comma separated list of AWS Account Ids for the target account(s). TargetRegionIds: Type: String Description: >- Comma separated list of AWS Regions to target. For example: us-east-1,ap-south-1. TargetLocationMaxConcurrency: Type: String Default: '1' Description: >- Specify the number or percentage of locations (account-Region pairs) on which to execute the task at the same time. You can specify a number, such as 10, or a percentage, such as 10%. The default value is 1. TargetLocationMaxErrors: Type: String Default: '1' Description: >- Specify an error threshold which will stop the task after the task fails on a specific number or percentage of locations. You can specify either an absolute number of errors, for example 10, or a percentage of the locations, for example 10%. The default value is 1. ############# Resources Creation ############# Resources: ##------------ Lambda Dead Letter Queues ------------## AWSConfigDeletionLambdaDLQ: Type: AWS::SQS::Queue Properties: QueueName: !Sub AWSConfigDeletionLambdaDLQ-${AWS::StackName} ##------------ Lambda IAM ROLE ------------## AWSConfigDeletionLambdaRole: Type: AWS::IAM::Role Properties: RoleName: !Sub AWSConfigDeletion-Lambda-Role-${AWS::StackName} AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: sts:AssumeRole Path: "/" Policies: - PolicyName: AWSConfigDeletionSSMAutomationPolicy PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - ssm:StartAutomationExecution Resource: - Fn::Sub: arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:automation-definition/${AWSConfigAutomationRunbook}:$DEFAULT - Action: iam:PassRole Resource: Fn::Sub: arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${SSMAutomationServiceRole} Effect: Allow - Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Resource: Fn::Sub: arn:aws:logs:*:*:* Effect: Allow - PolicyName: IAMAssumeRole-AWSConfigDeletion-ExecutionRole PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - sqs:SendMessage Resource: - !GetAtt AWSConfigDeletionLambdaDLQ.Arn ##------------ Lambda Function Creation ------------## AWSConfigDeletionLambda: Type: AWS::Lambda::Function Properties: Code: ZipFile: !Sub | import boto3 import os import cfnresponse import logging import json logger = logging.getLogger() logger.setLevel(logging.INFO) client = boto3.client('ssm') def handler(event,context): request_type = event['RequestType'] print(request_type) logger.info("Received event: %s" % json.dumps(event)) if request_type == 'Create': create_response = create(event) cfnresponse.send(event, context, cfnresponse.SUCCESS, create_response, 'CustomResourcePhysicalID') return create_response if request_type == 'Update': update_response = update(event) cfnresponse.send(event, context, cfnresponse.SUCCESS, update_response, 'CustomResourcePhysicalID') return update_response if request_type == 'Delete': result_status = delete(event) cfnresponse.send(event, context, result_status, {}) return result_status else: logger.info("Unexpected error - Request Type: %s" % request_type) logger.error("Error", exc_info=True) cfnresponse.send(event, context, cfnresponse.FAILED, {}) def create(event): TargetAccounts=os.environ['TargetAccounts'] b = str(TargetAccounts) TargetAccountsList = b.split(",") TargetRegionIds=os.environ['TargetRegionIds'] b = str(TargetRegionIds) TargetRegionIdsList = b.split(",") TargetLocationMaxConcurrency=os.environ['TargetLocationMaxConcurrency'] TargetLocationMaxErrors=os.environ['TargetLocationMaxErrors'] ExecutionRoleName=os.environ['ExecutionRoleName'] MasterAccountID=os.environ['MasterAccountID'] SSMAutomationAssumeRole=os.environ['SSMAutomationAssumeRole'] response = client.start_automation_execution( DocumentName=os.environ['DeleteConfigSSMAutomationRunbook'], Parameters={ 'AutomationAssumeRole':[f'arn:aws:iam::{MasterAccountID}:role/{SSMAutomationAssumeRole}']}, TargetLocations=[ { 'Accounts': TargetAccountsList, 'Regions': TargetRegionIdsList, 'TargetLocationMaxConcurrency': f'{TargetLocationMaxConcurrency}', 'TargetLocationMaxErrors': f'{TargetLocationMaxErrors}', 'ExecutionRoleName': f'{ExecutionRoleName}' } ] ) logger.info('Invoking DeleteConfigSSMAutomationRunbook') return response def update(event): logger.info('Update Operation') return create(event) def delete(event): # Delete never returns anything. Should not fail if the underlying resources are already deleted. Desired state. logger.info("Delete Operation") return cfnresponse.SUCCESS Environment: Variables: SSMAutomationAssumeRole: !Ref SSMAutomationServiceRole TargetAccounts: !Ref TargetAccounts TargetRegionIds: !Ref TargetRegionIds TargetLocationMaxConcurrency: !Ref TargetLocationMaxConcurrency TargetLocationMaxErrors: !Ref TargetLocationMaxErrors ExecutionRoleName: !Ref ExecutionRoleName MasterAccountID: !Sub ${AWS::AccountId} DeleteConfigSSMAutomationRunbook: !Ref AWSConfigAutomationRunbook FunctionName: !Sub AWSConfig-Deletion-${AWS::StackName} Handler: index.handler Role: !GetAtt AWSConfigDeletionLambdaRole.Arn Runtime: python3.7 DeadLetterConfig: TargetArn: !GetAtt AWSConfigDeletionLambdaDLQ.Arn ##------------ SSM Automation - IAM Service role ------------## SSMAutomationServiceRole: Type: AWS::IAM::Role Properties: RoleName: !Sub AWSConfigDeletion-ssm-role-${AWS::StackName} AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: ssm.amazonaws.com Action: - sts:AssumeRole Path: "/" Policies: - PolicyName: IAMAssumeRole-AWSConfigDeletion-ExecutionRole PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - sts:AssumeRole Resource: Fn::Sub: arn:${AWS::Partition}:iam::*:role/AWSConfigDeletion-ExecutionRole - Effect: Allow Action: - organizations:ListAccountsForParent Resource: - "*" ##------------ SSM Automation RunBook ------------## AWSConfigAutomationRunbook: Type: AWS::SSM::Document Properties: DocumentFormat: YAML Name: !Sub AWSConfigDeletion-AutomationDocument-${AWS::StackName} DocumentType: Automation Content: description: Automation runbook to delete an existing AWS Config recorder and delivery channel. schemaVersion: '0.3' assumeRole: '{{AutomationAssumeRole}}' outputs: - deleteConfig.OutputPayload parameters: AutomationAssumeRole: type: String description: '(Required) The Amazon Resource Name (ARN) of the IAM role that allows Automation to perform the actions on your behalf. If no role is specified, Systems Manager Automation uses your IAM permissions to operate this runbook.' default: !GetAtt SSMAutomationServiceRole.Arn mainSteps: - name: deleteConfig action: 'aws:executeScript' timeoutSeconds: 120 onFailure: Abort inputs: Runtime: python3.7 Handler: deleteConfig Script: |- import boto3 config = boto3.client('config') ssm = boto3.client('ssm') def deleteConfig(event, context): # Describe Config recorder configuration recorderConfigResponse = config.describe_configuration_recorders() # Check if Config recorder exists if recorderConfigResponse['ConfigurationRecorders']: # Describe Config recorder status recorderStatusResponse = config.describe_configuration_recorder_status( ConfigurationRecorderNames=[ recorderConfigResponse['ConfigurationRecorders'][0]['name'] ] ) print("Storing previous Config recorder configuration and status in Parameter Store") # Put Config recorder configuration to Parameter Store configRecorderConfigParameterResponse = ssm.put_parameter( Name='previous-config-recorder-configuration', Description='Previous Config recorder configuration', Value=str(recorderConfigResponse['ConfigurationRecorders']), Type='String', Overwrite=True, Tier='Standard', DataType='text' ) # Put Config recorder status to Parameter Store configRecorderStatusParameterResponse = ssm.put_parameter( Name='previous-config-recorder-status', Description='Previous Config recorder status', Value=str(recorderStatusResponse['ConfigurationRecordersStatus']), Type='String', Overwrite=True, Tier='Standard', DataType='text' ) # Disable recording for Config recorder stopRecorderResponse = config.stop_configuration_recorder( ConfigurationRecorderName=recorderConfigResponse['ConfigurationRecorders'][0]['name'] ) # Delete Config recorder deleteRecorderResponse = config.delete_configuration_recorder( ConfigurationRecorderName=recorderConfigResponse['ConfigurationRecorders'][0]['name'] ) else: print("Config recorder does not exist.") # Describe Config delivery channel configuration deliveryChannelConfigResponse = config.describe_delivery_channels() # Check if Config delivery channel exists if deliveryChannelConfigResponse['DeliveryChannels']: # Describe Config delivery channel status deliveryChannelStatusResponse = config.describe_delivery_channel_status( DeliveryChannelNames=[ deliveryChannelConfigResponse['DeliveryChannels'][0]['name'] ] ) print("Storing previous Config delivery channel and status in Parameter Store") # Put Config delivery channel configuration to Parameter Store configDeliveryChannelConfigurationParameterResponse = ssm.put_parameter( Name='previous-config-delivery-channel-configuration', Description='Previous Config delivery channel configuration', Value=str(deliveryChannelConfigResponse['DeliveryChannels']), Type='String', Overwrite=True, Tier='Standard', DataType='text' ) # Put Config delivery channel status to Parameter Store configDeliveryChannelStatusParameterResponse = ssm.put_parameter( Name='previous-config-delivery-channel-status', Description='Previous Config delivery channel status', Value=str(deliveryChannelStatusResponse['DeliveryChannelsStatus']), Type='String', Overwrite=True, Tier='Standard', DataType='text' ) # Delete Config delivery channel deleteDeliveryChannelResponse = config.delete_delivery_channel( DeliveryChannelName=deliveryChannelConfigResponse['DeliveryChannels'][0]['name'] ) else: print("Config delivery channel does not exist.") ##------------ Lambda Invoker ------------## Primerinvoke: Type: AWS::CloudFormation::CustomResource DependsOn: - AWSConfigDeletionLambdaRole - AWSConfigDeletionLambda - AWSConfigAutomationRunbook - SSMAutomationServiceRole Version: "1.0" Properties: ServiceToken: !GetAtt AWSConfigDeletionLambda.Arn TargetAccounts: !Ref TargetAccounts TargetRegionIds: !Ref TargetRegionIds TargetLocationMaxConcurrency: !Ref TargetLocationMaxConcurrency TargetLocationMaxErrors: !Ref TargetLocationMaxErrors