AWSTemplateFormatVersion: '2010-09-09' Description: AWS Control Tower Lifecycle Events for Datadog # ---------------------------------------------------------------------------------------------------------- # CloudFormation Template 1 of 2 - # # This templates allows newly added Control Tower accounts to be managed automatically by Datadog # # This template provisions infrastructure in the Control Tower Master 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 Control Tower Master 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. # - Invokes Datadog External ID API with new Integration Role Name to be created and this Account ID to retrieve an External ID in response # - 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 master account # ## ## License: ## This code is made available under the MIT-0 license. See the LICENSE file. # ------------------------------------------------------------............................................... 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: >- 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: >- API key for the Datadog account (find at https://app.datadoghq.com/account/settings#api) Type: String Default: "16d7d2476a1d5187d14a0c0803d94e0d" AllowedPattern: .+ ConstraintDescription: DdApiKey is required 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. DatadogAPIKey: Description: Datadog API Key for the AWS Control Tower Master Account Type: String Default: '16d7d2476a1d5187d14a0c0803d94e0d' DatadogApplicationKey: Description: Datadog Application Key for the AWS Control Tower Master Account Type: String Default: '271798b8a8a9907afbfd7d61233f982352152b17' InstallDatadogPolicyMacro: Type: String Default: true AllowedValues: - true - false Description: If you already deployed a stack using this template, set this parameter to false to skip the installation of the DatadogPolicy Macro again. Conditions: ShouldInstallDatadogPolicyMacro: Fn::Equals: - Ref: InstallDatadogPolicyMacro - true Resources: # --------------------------------------------------------------------------------------------------- # # 1- Store Parameters for the Datadog StackSet in AWS Secrets Manager # 2- Create a Datadog StackSet in the Control Tower Master 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 - '","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 event['RequestType'] == "Delete": logger.info(f'Request Type is Delete; unsupported') cfnsend(event, context, 'SUCCESS', response_data) return event if eventType != 'Delete': logger.info('Event = ' + event['RequestType']) datadog_paramList = [] secretList = json.loads(get_secret_value('DatadogSecretString1')) for s in secretList.keys(): 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" 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 - cloudformation:CreateStackInstances - cloudformation:DescribeStackSet - cloudformation:ListStackInstances - cloudformation:DeleteStackInstances - cloudformation:DeleteStackSet Resource: !Join [':', ['arn:aws:cloudformation', !Ref 'AWS::Region', !Ref 'AWS::AccountId', 'stackset/*:*']] - 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/*:*']] 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_datadog_external_id(accountid): api_key = os.environ['API_KEY'] account_id = accountid application_key = os.environ['APPLICATION_KEY'] datadogPOSTUrl = 'https://app.datadoghq.com/api/v1/integration/aws?api_key=' + api_key + '&application_key=' + application_key role_name = os.environ['INTEGRATIONROLE_NAME'] 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): # TODO implement 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''' ouInfo = newAccInfo['organizationalUnit'] ouName = ouInfo['organizationalUnitName'] odId = ouInfo['organizationalUnitId'] accId = newAccInfo['account']['accountId'] accName = newAccInfo['account']['accountName'] CFT = 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 = CFT.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 Environment: Variables: API_KEY : !Ref DatadogAPIKey APPLICATION_KEY : !Ref DatadogApplicationKey INTEGRATIONROLE_NAME : !Ref IAMRoleName PermissionForEventsToInvokeLambdachk: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt "TriggerCustomizationsOnLifeCycleEvent.Arn" Principal: events.amazonaws.com SourceArn: !GetAtt "CaptureControlTowerLifeCycleEvents.Arn"