AWSTemplateFormatVersion: '2010-09-09' Description: AWS Control Tower Lifecycle Events for Datadog (MPCT-yseaalnf) # ---------------------------------------------------------------------------------------------------------- # CloudFormation Template 1 of 1 - # # This templates allows newly added Control Tower accounts to be managed automatically by Datadog # # This template provisions infrastructure in the AWS Control Tower Management account that allows creation of Datadog # stack instances in Control Tower managed accounts whenever a new Control Tower managed account is added # # 1- Creates a Datadog Stackset in the AWS Control Tower Management Account # 2- Provisions a CloudWatchEvents Rule that is triggered based on a Control Tower Lifecycle Event # 3- Provisions a Lifecyle Lambda as a target for the CloudWatch Events Rule. # - The Lifecycle Lambda deploys a Datadog stack in the newly added Control Tower managed account--thus placing # that account under Datadog management # 4- Lifecycle Lambda performs a POST on the Datadog AWS Integration API - https://docs.datadoghq.com/api/v1/aws-integration/#generate-a-new-external-id # that registers the new AWS managed account in Datadog and generates an External ID in response for this managed acccount. # - Obtains AWS Account ID from the CreateManagedAccount or UpdateManagedAccount event. # - Uses Stack Instance Parameter Override when creating a stack instance for the managed account to override the generic External ID for the StackSet with this External ID generated by Datadog for the managed AWS account # # The infrastructure provisioned by this template in Steps 1-3 allows for a Control Tower lifecycle # event trigger (CreateManagedAccount) to # - Trigger the Lifecyle Lambda that creates Datadog stack instance in the managed account based on the # Datadog stackset in the Management account # ## ## @author: kmmahaj@amazon.com ## # # ------------------------------------------------------------............................................... Parameters: DatadogTemplateURL: Description: >- Base URL for Datadog CloudFormation templates - Datadog forwarder and Datadog Integration Role templates Type: String Default: 'https://datadog-cloudformation-template.s3.amazonaws.com/aws/main.yaml' ExternalId: Description: >- REQUIRED. Default External ID for the Datadog role (generate at https://app.datadoghq.com/account/settings#integrations/amazon-web-services) Type: String AllowedPattern: .+ Default: "11f75e0e09384e098705e9041976e4ef" ConstraintDescription: ExternalId is required DdApiKey: Description: >- REQUIRED. API key for the Datadog account (find at https://app.datadoghq.com/account/settings#api) Type: String DatadogApplicationKey: Description: >- REQUIRED. Application key for the Datadog account (find at https://app.datadoghq.com/account/settings#api) Type: String DdSite: Type: String Default: datadoghq.com Description: Define your Datadog Site to send data to. For the Datadog EU site, set to datadoghq.eu AllowedPattern: .+ ConstraintDescription: DdSite is required IAMRoleName: Description: Customize the name of IAM role for Datadog AWS integration Type: String Default: DatadogIntegrationRole Permissions: Description: >- Customize the permission level for the Datadog IAM role. Select "Core" to only grant Datadog read-only permissions (not recommended). Type: String Default: Full AllowedValues: - Full - Core LogArchives: Description: >- S3 paths to store log archives for log rehydration. Separate multiple paths with comma, e.g., "my-bucket,my-bucket-with-path/path". Permissions will be automatically added to the Datadog integration IAM role. https://docs.datadoghq.com/logs/archives/rehydrating/?tab=awss3 Type: String Default: '' CloudTrails: Description: >- S3 buckets for Datadog CloudTrail integration. Separate multiple buckets with comma, e.g., "bucket1,bucket2". Permissions will be automatically added to the Datadog integration IAM role. https://docs.datadoghq.com/integrations/amazon_cloudtrail/ Type: String Default: '' DdAWSAccountId: Description: >- Datadog AWS account ID allowed to assume the integration IAM role. DO NOT CHANGE! Type: String Default: "464622532012" DdForwarderName: Type: String Default: DatadogForwarder Description: >- The Datadog Forwarder Lambda function name. DO NOT change when updating an existing CloudFormation stack, otherwise the current forwarder function will be replaced and all the triggers will be lost. Resources: # --------------------------------------------------------------------------------------------------- # # 1- Store Parameters for the Datadog StackSet in AWS Secrets Manager # 2- Create a Datadog StackSet in the Control Tower Management Account # - The Datadog StackSet is based on the Datadog main.yaml template that is provided by Datadog. # - The Datadog main.yaml template provisions the Datadog Integration Role and the Datadog Forwarder # -------------------------------------------------------------------------------------------------- DatadogSecretString1: Type: AWS::SecretsManager::Secret Properties: Description: Datadog Parameters required for Datadog Pro Name: DatadogSecretString1 SecretString: Fn::Join: - '' - - '{"ExternalId":"' - Ref: ExternalId - '","DdApiKey": "' - Ref: DdApiKey - '","DatadogApplicationKey": "' - Ref: DatadogApplicationKey - '","DdSite": "' - Ref: DdSite - '","Permissions": "' - Ref: Permissions - '","IAMRoleName": "' - Ref: IAMRoleName - '","LogArchives": "' - Ref: LogArchives - '","CloudTrails": "' - Ref: CloudTrails - '","DdAWSAccountId": "' - Ref: DdAWSAccountId - '","DdForwarderName": "' - Ref: DdForwarderName - '"}' CreateDatadogStackSet: Type: 'Custom::DatadogStackSet' DependsOn: - DatadogStackSetLambdaExecutePermission Properties: ServiceToken: !GetAtt 'DatadogStackSetLambda.Arn' datadogUrl: !Ref DatadogTemplateURL AccountId: !Ref 'AWS::AccountId' Region: !Ref 'AWS::Region' DatadogStackSetLambdaExecutePermission: Type: 'AWS::Lambda::Permission' Properties: Action: 'lambda:InvokeFunction' FunctionName: !GetAtt 'DatadogStackSetLambda.Arn' Principal: 'cloudformation.amazonaws.com' SourceAccount: !Ref 'AWS::AccountId' DatadogStackSetLambda: Type: 'AWS::Lambda::Function' Properties: Handler: index.handler Runtime: python3.7 MemorySize: 256 Role: !GetAtt 'DatadogStackSetLambdaExecutionRole.Arn' Timeout: 60 Code: ZipFile: | import json import boto3 import botocore import os import cfnresponse import logging from botocore.vendored import requests logger = logging.getLogger() logger.setLevel(logging.INFO) def get_secret_value(key='DatadogSecretString1'): secretsmanager = boto3.client('secretsmanager') secret_list = secretsmanager.list_secrets()['SecretList'] output = {} for s in secret_list: if key in s.values(): output = secretsmanager.get_secret_value(SecretId=key)['SecretString'] return(output) def handler(event, context): AccountId = event['ResourceProperties']['AccountId'] datadogUrl = event['ResourceProperties']['datadogUrl'] Region = event['ResourceProperties']['Region'] cList = ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM', 'CAPABILITY_AUTO_EXPAND'] ExecRole = 'AWSControlTowerExecution' AdminRoleARN = 'arn:aws:iam::' + AccountId + ':role/service-role/AWSControlTowerStackSetRole' logger.info('EVENT Received: {}'.format(event)) response_data = {} eventType = event['RequestType'] if eventType != 'Delete': logger.info('Event = ' + event['RequestType']) datadog_paramList = [] secretList = json.loads(get_secret_value('DatadogSecretString1')) for s in secretList.keys(): if s != 'DatadogApplicationKey': keyDict = {} keyDict['ParameterKey'] = s keyDict['ParameterValue'] = secretList[s] datadog_paramList.append(keyDict) logger.info('Datadog ParamList:{}'.format(datadog_paramList)) cloudformation = boto3.client('cloudformation') datadog_result = cloudformation.create_stack_set(StackSetName='DataDogForwarderv1', \ Description = 'Integration Role and Forwarder for Datadog', \ TemplateURL = datadogUrl, \ Parameters = datadog_paramList, \ AdministrationRoleARN = AdminRoleARN, \ ExecutionRoleName = ExecRole, \ Capabilities = cList ) logger.info('Datadog Stackset: {}'.format(datadog_result)) cfnsend(event, context, 'SUCCESS', response_data) return "Success" else: logger.info(f'Request Type is Delete; unsupported') cfnsend(event, context, 'SUCCESS', response_data) return event cfnsend(event, context, 'SUCCESS', response_data) return "Success" def cfnsend(event, context, responseStatus, responseData, reason=None): if 'ResponseURL' in event: responseUrl = event['ResponseURL'] # Build out the response json responseBody = {} responseBody['Status'] = responseStatus responseBody['Reason'] = reason or 'CWL Log Stream =' + context.log_stream_name responseBody['PhysicalResourceId'] = context.log_stream_name responseBody['StackId'] = event['StackId'] responseBody['RequestId'] = event['RequestId'] responseBody['LogicalResourceId'] = event['LogicalResourceId'] responseBody['Data'] = responseData json_responseBody = json.dumps(responseBody) logger.info(f'Response body: + {json_responseBody}') headers = { 'content-type': '', 'content-length': str(len(json_responseBody)) } # Send response back to CFN try: response = requests.put(responseUrl, data=json_responseBody, headers=headers) logger.info(f'Status code: {response.reason}') except Exception as e: logger.info(f'send(..) failed executing requests.put(..): {str(e)}') DatadogStackSetLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub "DatadogStackSetLambdaExecutionRole-${AWS::Region}" AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: lambda.amazonaws.com Action: sts:AssumeRole Condition: {} Path: / Policies: - PolicyName: GetSecretValue PolicyDocument: Version: '2012-10-17' Statement: Sid: Secrets1 Effect: Allow Action: - secretsmanager:GetSecretValue Resource: !Ref DatadogSecretString1 - PolicyName: ListSecrets PolicyDocument: Version: '2012-10-17' Statement: Sid: Secrets2 Effect: Allow Action: - secretsmanager:ListSecrets Resource: '*' - PolicyName: CloudFormation_ops PolicyDocument: Version: '2012-10-17' Statement: Sid: VisualEditor2 Effect: Allow Action: - cloudformation:CreateStackSet Resource: !Join [':', ['arn:aws:cloudformation', !Ref 'AWS::Region', !Ref 'AWS::AccountId', 'stackset/DataDogForwarderv1:*']] - PolicyName: Pass_Role PolicyDocument: Version: '2012-10-17' Statement: Sid: VisualEditor3 Effect: Allow Action: - iam:PassRole Resource: !Join [':', ['arn:aws:iam:', !Ref "AWS::AccountId", 'role/service-role/AWSControlTowerStackSetRole']] ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSOrganizationsReadOnlyAccess - arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess # -------------------------------------------------------------------------------------------------- # # 1- Provisions a CloudWatchEvents Rule that is triggered based on a Control Tower Lifecycle Event # 2- Provisions a Lifecyle Lambda as a target for the CloudWatch Events Rule. # # -------------------------------------------------------------------------------------------------- CaptureControlTowerLifeCycleEvents: DependsOn: - TriggerCustomizationsOnLifeCycleEvent Type: AWS::Events::Rule Properties: Description: Capture Control Tower LifeCycle Events and Trigger an Action EventPattern: detail: eventName: - CreateManagedAccount - UpdateManagedAccount - EnableGuardrail - DisableGuardrail - SetupLandingZone - UpdateLandingZone - RegisterOrganizationalUnit - DeregisterOrganizationalUnit eventSource: - controltower.amazonaws.com detail-type: - AWS Service Event via CloudTrail source: - aws.controltower Name: CaptureControlTowerLifeCycleEvents State: ENABLED Targets: - Arn: !GetAtt "TriggerCustomizationsOnLifeCycleEvent.Arn" Id: IDCaptureControlTowerLifeCycleEvents LambdaRoleToCaptureEvents: Type: AWS::IAM::Role Properties: RoleName: !Sub "LambdaRoleToCaptureEvents-${AWS::Region}" AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: lambda.amazonaws.com Action: sts:AssumeRole Condition: {} Path: / Policies: - PolicyName: inline-policy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - 'cloudformation:CreateStackInstances' Resource: !Join [':',['arn:aws:cloudformation', !Ref 'AWS::Region', !Ref 'AWS::AccountId', 'stackset/DataDogForwarderv1:*']] ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole TriggerCustomizationsOnLifeCycleEvent: DependsOn: - LambdaRoleToCaptureEvents Type: AWS::Lambda::Function Properties: Code: ZipFile: | import json import os import boto3 import logging import urllib.request logger = logging.getLogger() logger.setLevel(logging.INFO) stackset_list = ['DataDogForwarderv1'] result = {"ResponseMetadata":{"HTTPStatusCode":"400"}} def get_secret_value(key='DatadogSecretString1'): secretsmanager = boto3.client('secretsmanager') secret_list = secretsmanager.list_secrets()['SecretList'] output = {} for s in secret_list: if key in s.values(): output = secretsmanager.get_secret_value(SecretId=key)['SecretString'] return(output) def get_datadog_external_id(accountid): secretList = json.loads(get_secret_value('DatadogSecretString1')) api_key="" application_key="" role_name="" dd_site="" api_key_key='DdApiKey' application_key_key='DatadogApplicationKey' role_name_key='IAMRoleName' dd_site_key= 'DdSite' if api_key_key in secretList: api_key = secretList[api_key_key] if application_key_key in secretList: application_key = secretList[application_key_key] if role_name_key in secretList: role_name = secretList[role_name_key] if dd_site_key in secretList: dd_site = secretList[dd_site_key] account_id = accountid datadogPOSTUrl = 'https://app.'+ dd_site + '/api/v1/integration/aws?api_key=' + api_key + '&application_key=' + application_key payload = { 'account_id': account_id, 'role_name': role_name } data_json=json.dumps(payload) req = urllib.request.Request(url = datadogPOSTUrl, data = bytes(data_json.encode("utf-8")), method = "POST") response_data = {} with urllib.request.urlopen(req) as resp: response_data = json.loads(resp.read().decode("utf-8")) print(response_data) output = response_data['external_id'] return output def lambda_handler(event, context): masterAcct = event['account'] eventDetails = event['detail'] regionName = eventDetails['awsRegion'] eventName = eventDetails['eventName'] srvEventDetails = eventDetails['serviceEventDetails'] if eventName == 'CreateManagedAccount' or eventName == 'UpdateManagedAccount': newAccInfo = {} logger.info('Event Processed Sucessfully') if eventName == 'CreateManagedAccount': newAccInfo = srvEventDetails['createManagedAccountStatus'] if eventName == 'UpdateManagedAccount': newAccInfo = srvEventDetails['updateManagedAccountStatus'] cmdStatus = newAccInfo['state'] if cmdStatus == 'SUCCEEDED': '''Sucessful event recieved''' accId = newAccInfo['account']['accountId'] cloudformation = boto3.client('cloudformation') for item in stackset_list: try: external_id = get_datadog_external_id(accountid=accId) datadog_overrideparamList = [] keyDict = {} keyDict['ParameterKey'] = 'ExternalId' keyDict['ParameterValue'] = external_id datadog_overrideparamList.append(keyDict) result = cloudformation.create_stack_instances(StackSetName=item, ParameterOverrides=datadog_overrideparamList,Accounts=[accId], Regions=[regionName]) logger.info('Processed {} Sucessfully'.format(item)) except Exception as e: logger.error('Unable to launch in:{}, REASON: {}'.format(item, e)) else: '''Unsucessful event recieved''' logger.info('Unsucessful Event Recieved. SKIPPING :{}'.format(event)) return(False) else: logger.info('Control Tower Event Captured :{}'.format(event)) Handler: index.lambda_handler MemorySize: 128 Role: !GetAtt "LambdaRoleToCaptureEvents.Arn" Runtime: python3.7 Timeout: 60 PermissionForEventsToInvokeLambdachk: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt "TriggerCustomizationsOnLifeCycleEvent.Arn" Principal: events.amazonaws.com SourceArn: !GetAtt "CaptureControlTowerLifeCycleEvents.Arn"