AWSTemplateFormatVersion: 2010-09-09 Transform: AWS::Serverless-2016-10-31 Description: Sets up AWS ControlTower. (qs-1s3rsr7lh) Parameters: AuditAWSAccountEmail: Type: String LogArchiveAWSAccountEmail: Type: String Resources: SetupControlTower: Type: AWS::CloudFormation::CustomResource Properties: ServiceToken: !GetAtt SetupControlTowerCustomResource.Arn SetupControlTowerCustomResource: Type: AWS::Serverless::Function Properties: Handler: index.handler Runtime: python3.7 MemorySize: 2048 Timeout: 900 # give it more time since it installs awsapilib and tries to deploy control tower with retries Role: !GetAtt SetupControlTowerCustomResourceRole.Arn # provide explicit role to avoid circular dependency with AwsApiLibRole Policies: - Version: 2012-10-17 Statement: - Effect: Allow Action: sts:AssumeRole Resource: !GetAtt AwsApiLibRole.Arn Environment: Variables: AWSAPILIB_CONTROL_TOWER_ROLE_ARN: !GetAtt AwsApiLibRole.Arn LOG_ARCHIVE_AWS_ACCOUNT_EMAIL: !Ref LogArchiveAWSAccountEmail AUDIT_AWS_ACCOUNT_EMAIL: !Ref AuditAWSAccountEmail InlineCode: | import boto3 import os import cfnresponse import sys import subprocess # load awsapilib in-process as long as we have no strategy for bundling assets sys.path.insert(1, '/tmp/packages') subprocess.check_call([sys.executable, "-m", "pip", "install", '--target', '/tmp/packages', 'https://github.com/superwerker/awsapilib/archive/198a3269e324455dc3cc499b61bf61e5ec095779.zip']) # workaround for install awsapilib via zip (remove me once back to official awsapilib version) with open('/tmp/packages/awsapilib/.VERSION', 'w') as version_file: version_file.write("2.3.1-ctapifix\n") import awsapilib from awsapilib import ControlTower CREATE = 'Create' DELETE = 'Delete' UPDATE = 'Update' def exception_handling(function): def catch(event, context): try: function(event, context) except Exception as e: print(e) print(event) cfnresponse.send(event, context, cfnresponse.FAILED, {}) return catch @exception_handling def handler(event, context): RequestType = event["RequestType"] Properties = event["ResourceProperties"] LogicalResourceId = event["LogicalResourceId"] PhysicalResourceId = event.get("PhysicalResourceId") print('RequestType: {}'.format(RequestType)) print('PhysicalResourceId: {}'.format(PhysicalResourceId)) print('LogicalResourceId: {}'.format(LogicalResourceId)) id = PhysicalResourceId data = {} tower = ControlTower(os.environ['AWSAPILIB_CONTROL_TOWER_ROLE_ARN']) if RequestType == CREATE: tower.deploy(logging_account_email=os.environ['LOG_ARCHIVE_AWS_ACCOUNT_EMAIL'], security_account_email=os.environ['AUDIT_AWS_ACCOUNT_EMAIL'], retries=50, wait=5) cfnresponse.send(event, context, cfnresponse.SUCCESS, data, id) SetupControlTowerCustomResourceRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: lambda.amazonaws.com Action: sts:AssumeRole ManagedPolicyArns: - !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole SetupControlTowerCustomResourceRolePolicy: Type: AWS::IAM::Policy Properties: PolicyDocument: Statement: - Effect: Allow Action: sts:AssumeRole Resource: !GetAtt AwsApiLibRole.Arn Version: 2012-10-17 PolicyName: !Sub ${SetupControlTowerCustomResourceRole}Policy Roles: - !Ref SetupControlTowerCustomResourceRole AwsApiLibRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: AWS: !GetAtt SetupControlTowerCustomResourceRole.Arn Action: sts:AssumeRole ManagedPolicyArns: - !Sub arn:${AWS::Partition}:iam::aws:policy/AdministratorAccess ControlTowerReadyHandle: Type: AWS::CloudFormation::WaitConditionHandle ControlTowerReadyHandleWaitCondition: Type: AWS::CloudFormation::WaitCondition Properties: Handle: !Ref ControlTowerReadyHandle Timeout: "7200" SuperwerkerBootstrapFunction: Type: AWS::Serverless::Function Properties: Events: SetupLandingZone: # event from entirely fresh landing zone Type: CloudWatchEvent Properties: InputPath: $.detail.serviceEventDetails.setupLandingZoneStatus Pattern: detail-type: - AWS Service Event via CloudTrail source: - aws.controltower detail: serviceEventDetails: setupLandingZoneStatus: state: - SUCCEEDED eventName: - SetupLandingZone Handler: index.handler Runtime: python3.7 Environment: Variables: SIGNAL_URL: !Ref ControlTowerReadyHandle InlineCode: |- import boto3 import json ssm = boto3.client('ssm') events = boto3.client('events') import urllib3 import os def handler(event, context): for account in event['accounts']: ssm.put_parameter( Name='/superwerker/account_id_{}'.format(account['accountName'].lower().replace(' ', '')), Value=account['accountId'], Overwrite=True, Type='String', ) # signal cloudformation stack that control tower setup is complete encoded_body = json.dumps({ "Status": "SUCCESS", "Reason": "Control Tower Setup completed", "UniqueId": "doesthisreallyhavetobeunique", "Data": "Control Tower Setup completed" }) http = urllib3.PoolManager() http.request('PUT', os.environ['SIGNAL_URL'], body=encoded_body) # signal Control Tower Landing ZOne Setup/Update has finished events.put_events( Entries=[ { 'DetailType': 'superwerker-event', 'Detail': json.dumps( { 'eventName': 'LandingZoneSetupOrUpdateFinished', } ), 'Source': 'superwerker' } ] ) Policies: - Version: 2012-10-17 Statement: - Action: - ssm:PutParameter Effect: Allow Resource: - !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter/superwerker* - Action: events:PutEvents Effect: Allow Resource: !Sub 'arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:event-bus/default' Metadata: SuperwerkerVersion: 0.13.2 cfn-lint: config: ignore_checks: - E9007