AWSTemplateFormatVersion: 2010-09-09 Metadata: SuperwerkerVersion: 0.13.2 cfn-lint: config: ignore_checks: - E9007 - EPolicyWildcardPrincipal - E1029 Transform: AWS::Serverless-2016-10-31 Description: Sets up backups. (qs-1s3rsr7la) Resources: OrganizationsLookup: Type: AWS::CloudFormation::CustomResource Properties: ServiceToken: !GetAtt OrganizationsLookupCustomResource.Arn OrganizationsLookupCustomResource: Type: AWS::Serverless::Function Properties: Handler: index.handler Runtime: python3.7 Policies: - Version: 2012-10-17 Statement: - Effect: Allow Action: - organizations:ListRoots - organizations:DescribeOrganization Resource: "*" InlineCode: | import boto3 import cfnresponse org = boto3.client("organizations") 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"] LogicalResourceId = event["LogicalResourceId"] PhysicalResourceId = event.get("PhysicalResourceId") print('RequestType: {}'.format(RequestType)) print('PhysicalResourceId: {}'.format(PhysicalResourceId)) print('LogicalResourceId: {}'.format(LogicalResourceId)) id = PhysicalResourceId data = {} organization = org.describe_organization()['Organization'] data['OrgId'] = organization['Id'] roots = org.list_roots()['Roots'] data['RootId'] = roots[0]['Id'] cfnresponse.send(event, context, cfnresponse.SUCCESS, data, id) OrganizationConformancePackBucket: Type: AWS::S3::Bucket Properties: BucketName: !Sub awsconfigconforms-${AWS::AccountId} OrganizationConformancePackBucketPolicy: Type: AWS::S3::BucketPolicy Properties: Bucket: !Ref OrganizationConformancePackBucket PolicyDocument: Version: '2012-10-17' Statement: - Sid: AllowGetPutObject Effect: Allow Principal: "*" Action: - s3:GetObject - s3:PutObject Resource: !Sub ${OrganizationConformancePackBucket.Arn}/* Condition: StringEquals: aws:PrincipalOrgID: !GetAtt OrganizationsLookup.OrgId ArnLike: aws:PrincipalArn: !Sub arn:${AWS::Partition}:iam::*:role/aws-service-role/config-conforms.amazonaws.com/AWSServiceRoleForConfigConforms - Sid: AllowGetBucketAcl Effect: Allow Principal: "*" Action: s3:GetBucketAcl Resource: !Sub ${OrganizationConformancePackBucket.Arn} Condition: StringEquals: aws:PrincipalOrgID: !GetAtt OrganizationsLookup.OrgId ArnLike: aws:PrincipalArn: !Sub arn:${AWS::Partition}:iam::*:role/aws-service-role/config-conforms.amazonaws.com/AWSServiceRoleForConfigConforms BackupResources: Type: AWS::CloudFormation::StackSet DependsOn: EnableCloudFormationStacksetsOrgAccessCustomResource Properties: StackSetName: superwerker-backup PermissionModel: SERVICE_MANAGED OperationPreferences: MaxConcurrentPercentage: 50 Capabilities: - CAPABILITY_IAM - CAPABILITY_NAMED_IAM AutoDeployment: Enabled: true RetainStacksOnAccountRemoval: false StackInstancesGroup: - Regions: - !Ref AWS::Region DeploymentTargets: OrganizationalUnitIds: - !GetAtt OrganizationsLookup.RootId TemplateBody: !Sub | Resources: AWSBackupDefaultServiceRole: Type: AWS::IAM::Role Properties: RoleName: AWSBackupDefaultServiceRole Path: /service-role/ AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: backup.amazonaws.com Action: sts:AssumeRole ManagedPolicyArns: - arn:${AWS::Partition}:iam::aws:policy/service-role/AWSBackupServiceRolePolicyForBackup - arn:${AWS::Partition}:iam::aws:policy/service-role/AWSBackupServiceRolePolicyForRestores ConfigRemediationRole: Type: AWS::IAM::Role Properties: RoleName: SuperwerkerBackupTagsEnforcementRemediationRole AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: ssm.amazonaws.com Action: sts:AssumeRole Policies: - PolicyName: AllowTagging PolicyDocument: Statement: - Effect: Allow Action: - dynamodb:TagResource - ec2:CreateTags - rds:AddTagsToResource - rds:DescribeDBInstances Resource: '*' BackupTagsEnforcement: DependsOn: BackupResources Type: AWS::Config::OrganizationConformancePack Properties: ExcludedAccounts: - !Ref AWS::AccountId # exclude management account since it has no config recorder set up DeliveryS3Bucket: !Ref OrganizationConformancePackBucket OrganizationConformancePackName: superwerker-backup-enforce TemplateBody: !Sub | Resources: ConfigRuleDynamoDBTable: Type: AWS::Config::ConfigRule Properties: ConfigRuleName: superwerker-backup-enforce-dynamodb-table Scope: ComplianceResourceTypes: - AWS::DynamoDB::Table InputParameters: tag1Key: superwerker:backup tag1Value: daily,none Source: Owner: AWS SourceIdentifier: REQUIRED_TAGS ConfigRemediationDynamoDBTable: DependsOn: ConfigRuleDynamoDBTable Type: AWS::Config::RemediationConfiguration Properties: ConfigRuleName: superwerker-backup-enforce-dynamodb-table Automatic: true MaximumAutomaticAttempts: 10 RetryAttemptSeconds: 60 TargetId: arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:document/${BackupTagRemediation} TargetType: SSM_DOCUMENT Parameters: ResourceValue: ResourceValue: Value: "RESOURCE_ID" AutomationAssumeRole: StaticValue: Values: - arn:${AWS::Partition}:iam::${AWS::AccountId}:role/SuperwerkerBackupTagsEnforcementRemediationRole # ${AWS::AccountId} is magically replaced with the actual sub-account id (magic by Conformance Pack) ResourceType: StaticValue: Values: - AWS::DynamoDB::Table ConfigRuleEbsVolume: Type: AWS::Config::ConfigRule Properties: ConfigRuleName: superwerker-backup-enforce-ebs-volume Scope: ComplianceResourceTypes: - AWS::EC2::Volume InputParameters: tag1Key: superwerker:backup tag1Value: daily,none Source: Owner: AWS SourceIdentifier: REQUIRED_TAGS ConfigRemediationEbsVolume: DependsOn: ConfigRuleEbsVolume Type: AWS::Config::RemediationConfiguration Properties: ConfigRuleName: superwerker-backup-enforce-ebs-volume Automatic: true MaximumAutomaticAttempts: 10 RetryAttemptSeconds: 60 TargetId: arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:document/${BackupTagRemediation} TargetType: SSM_DOCUMENT Parameters: ResourceValue: ResourceValue: Value: "RESOURCE_ID" AutomationAssumeRole: StaticValue: Values: - arn:${AWS::Partition}:iam::${AWS::AccountId}:role/SuperwerkerBackupTagsEnforcementRemediationRole # ${AWS::AccountId} is magically replaced with the actual sub-account id (magic by Conformance Pack) ResourceType: StaticValue: Values: - AWS::EC2::Volume ConfigRuleRdsDbInstance: Type: AWS::Config::ConfigRule Properties: ConfigRuleName: superwerker-backup-enforce-rds-instance Scope: ComplianceResourceTypes: - AWS::RDS::DBInstance InputParameters: tag1Key: superwerker:backup tag1Value: daily,none Source: Owner: AWS SourceIdentifier: REQUIRED_TAGS ConfigRemediationRdsDbInstance: DependsOn: ConfigRuleRdsDbInstance Type: AWS::Config::RemediationConfiguration Properties: ConfigRuleName: superwerker-backup-enforce-rds-instance Automatic: true MaximumAutomaticAttempts: 10 RetryAttemptSeconds: 60 TargetId: arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:document/${BackupTagRemediation} TargetType: SSM_DOCUMENT Parameters: ResourceValue: ResourceValue: Value: "RESOURCE_ID" AutomationAssumeRole: StaticValue: Values: - arn:${AWS::Partition}:iam::${AWS::AccountId}:role/SuperwerkerBackupTagsEnforcementRemediationRole # ${AWS::AccountId} is magically replaced with the actual sub-account id (magic by Conformance Pack) ResourceType: StaticValue: Values: - AWS::RDS::DBInstance BackupTagRemediation: Type: AWS::SSM::Document Properties: DocumentType: Automation Content: schemaVersion: '0.3' assumeRole: '{{ AutomationAssumeRole }}' parameters: ResourceValue: type: String AutomationAssumeRole: type: String default: '' ResourceType: type: String mainSteps: - name: synthArn action: aws:branch inputs: Choices: - NextStep: tagDynamoDbTable Variable: '{{ ResourceType }}' StringEquals: AWS::DynamoDB::Table - NextStep: tagEbsVolume Variable: '{{ ResourceType }}' StringEquals: AWS::EC2::Volume - NextStep: getRdsDBInstanceArnByDbInstanceResourceIdentifier Variable: '{{ ResourceType }}' StringEquals: AWS::RDS::DBInstance - name: tagDynamoDbTable action: 'aws:executeAwsApi' inputs: Service: dynamodb Api: TagResource Tags: - Key: 'superwerker:backup' Value: daily ResourceArn: !Sub 'arn:${AWS::Partition}:dynamodb:{{ global:REGION }}:{{ global:ACCOUNT_ID }}:table/{{ ResourceValue }}' isEnd: true - name: tagEbsVolume action: 'aws:executeAwsApi' inputs: Service: ec2 Api: CreateTags Tags: - Key: 'superwerker:backup' Value: daily Resources: - '{{ ResourceValue }}' isEnd: true - name: getRdsDBInstanceArnByDbInstanceResourceIdentifier action: aws:executeAwsApi inputs: Service: rds Api: DescribeDBInstances Filters: - Name: dbi-resource-id Values: - '{{ ResourceValue }}' outputs: - Name: DBInstanceArn Selector: $.DBInstances[0].DBInstanceArn - name: tagRdsInstance action: 'aws:executeAwsApi' inputs: Service: rds Api: AddTagsToResource Tags: - Key: 'superwerker:backup' Value: daily ResourceName: '{{ getRdsDBInstanceArnByDbInstanceResourceIdentifier.DBInstanceArn }}' isEnd: true BackupTagRemediationPublic: Type: AWS::CloudFormation::CustomResource Properties: ServiceToken: !GetAtt BackupTagRemediationPublicCustomResource.Arn DocumentName: !Ref BackupTagRemediation BackupTagRemediationPublicCustomResource: Type: AWS::Serverless::Function Properties: Handler: index.handler Runtime: python3.7 Policies: - Version: 2012-10-17 Statement: - Effect: Allow Action: ssm:ModifyDocumentPermission Resource: !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:document/* InlineCode: | import boto3 import cfnresponse import os ssm = boto3.client("ssm") 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"] print('RequestType: {}'.format(RequestType)) PhysicalResourceId = event.get("PhysicalResourceId") Properties = event["ResourceProperties"] DocumentName = Properties["DocumentName"] id = "{}-{}".format(PhysicalResourceId, DocumentName) data = {} if RequestType == CREATE or RequestType == UPDATE: ssm.modify_document_permission( Name=DocumentName, PermissionType='Share', AccountIdsToAdd=['All'] ) elif RequestType == DELETE: ssm.modify_document_permission( Name=DocumentName, PermissionType='Share', AccountIdsToRemove=['All'] ) cfnresponse.send(event, context, cfnresponse.SUCCESS, data, id) EnableCloudFormationStacksetsOrgAccessCustomResource: Type: AWS::CloudFormation::CustomResource Properties: ServiceToken: !GetAtt EnableCloudFormationStacksetsOrgAccessCustomResourceFunction.Arn EnableCloudFormationStacksetsOrgAccessCustomResourceFunction: Type: AWS::Serverless::Function Properties: Handler: index.handler Runtime: python3.7 Timeout: 900 # give it more time since it installs dependencies on the fly Role: !GetAtt EnableCloudFormationStacksetsOrgAccessCustomResourceRole.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_ROLE_ARN: !GetAtt AwsApiLibRole.Arn 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', 'awsapilib==0.10.1']) import awsapilib from awsapilib import Cloudformation 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 = {} cf = Cloudformation(os.environ['AWSAPILIB_ROLE_ARN']) if RequestType == CREATE: cf.stacksets.enable_organizations_trusted_access() cfnresponse.send(event, context, cfnresponse.SUCCESS, data, id) EnableCloudFormationStacksetsOrgAccessCustomResourceRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: lambda.amazonaws.com Action: sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole EnableCloudFormationStacksetsOrgAccessCustomResourceRolePolicy: Type: AWS::IAM::Policy Properties: PolicyDocument: Statement: - Effect: Allow Action: sts:AssumeRole Resource: !GetAtt AwsApiLibRole.Arn Version: 2012-10-17 PolicyName: !Sub ${EnableCloudFormationStacksetsOrgAccessCustomResourceRole}Policy Roles: - !Ref EnableCloudFormationStacksetsOrgAccessCustomResourceRole AwsApiLibRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: AWS: !GetAtt EnableCloudFormationStacksetsOrgAccessCustomResourceRole.Arn Action: sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/AdministratorAccess # Proudly found elsewhere and partially copied from: # https://github.com/theserverlessway/aws-baseline TagPolicy: DependsOn: TagPolicyEnable Type: AWS::CloudFormation::CustomResource Properties: ServiceToken: !GetAtt TagPolicyCustomResource.Arn Policy: | { "tags": { "superwerker:backup": { "tag_value": { "@@assign": [ "none", "daily" ] }, "enforced_for": { "@@assign": [ "dynamodb:table", "ec2:volume" ] } } } } Attach: true TagPolicyCustomResource: Type: AWS::Serverless::Function Metadata: cfn-lint: config: ignore_checks: - EIAMPolicyWildcardResource Properties: Timeout: 200 Runtime: python3.7 Handler: index.handler Policies: - Version: 2012-10-17 Statement: - Effect: Allow Action: - organizations:CreatePolicy - organizations:UpdatePolicy - organizations:DeletePolicy - organizations:AttachPolicy - organizations:DetachPolicy - organizations:ListRoots - organizations:ListPolicies - organizations:ListPoliciesForTarget Resource: "*" InlineCode: | import boto3 import cfnresponse import time import random import re o = boto3.client("organizations") CREATE = 'Create' UPDATE = 'Update' DELETE = 'Delete' TAG_POLICY = "TAG_POLICY" def root(): return o.list_roots()['Roots'][0] def root_id(): return root()['Id'] def with_retry(function, **kwargs): for i in [0, 3, 9, 15, 30]: # Random sleep to not run into concurrency problems when adding or attaching multiple TAG_POLICYs # They have to be added/updated/deleted one after the other sleeptime = i + random.randint(0, 5) print('Running {} with Sleep of {}'.format(function.__name__, sleeptime)) time.sleep(sleeptime) try: response = function(**kwargs) print("Response for {}: {}".format(function.__name__, response)) return response except o.exceptions.ConcurrentModificationException as e: print('Exception: {}'.format(e)) raise Exception def handler(event, context): RequestType = event["RequestType"] Properties = event["ResourceProperties"] LogicalResourceId = event["LogicalResourceId"] PhysicalResourceId = event.get("PhysicalResourceId") Policy = Properties["Policy"] Attach = Properties["Attach"] == 'true' print('RequestType: {}'.format(RequestType)) print('PhysicalResourceId: {}'.format(PhysicalResourceId)) print('LogicalResourceId: {}'.format(LogicalResourceId)) print('Attach: {}'.format(Attach)) parameters = dict( Content=Policy, Description="superwerker - {}".format(LogicalResourceId), Name=LogicalResourceId, ) policy_id = PhysicalResourceId try: if RequestType == CREATE: print('Creating Policy: {}'.format(LogicalResourceId)) response = with_retry(o.create_policy, **parameters, Type=TAG_POLICY ) policy_id = response["Policy"]["PolicySummary"]["Id"] if Attach: with_retry(o.attach_policy, PolicyId=policy_id, TargetId=root_id()) elif RequestType == UPDATE: print('Updating Policy: {}'.format(LogicalResourceId)) with_retry(o.update_policy, PolicyId=policy_id, **parameters) elif RequestType == DELETE: print('Deleting Policy: {}'.format(LogicalResourceId)) # Same as above if re.match('p-[0-9a-z]+', policy_id): if policy_attached(policy_id): with_retry(o.detach_policy, PolicyId=policy_id, TargetId=root_id()) with_retry(o.delete_policy, PolicyId=policy_id) else: print('{} is no valid PolicyId'.format(policy_id)) else: raise Exception('Unexpected RequestType: {}'.format(RequestType)) cfnresponse.send(event, context, cfnresponse.SUCCESS, {}, policy_id) except Exception as e: print(e) print(event) cfnresponse.send(event, context, cfnresponse.FAILED, {}, policy_id) def policy_attached(policy_id): return [p['Id'] for p in o.list_policies_for_target(TargetId=root_id(), Filter='TAG_POLICY')['Policies'] if p['Id'] == policy_id] TagPolicyEnable: Type: AWS::CloudFormation::CustomResource Properties: ServiceToken: !GetAtt TagPolicyEnableCustomResource.Arn TagPolicyEnableCustomResource: Type: AWS::Serverless::Function Properties: Timeout: 200 Handler: index.enable_tag_policies Runtime: python3.7 Policies: - Version: 2012-10-17 Statement: - Effect: Allow Action: - organizations:EnablePolicyType - organizations:DisablePolicyType - organizations:ListRoots Resource: "*" InlineCode: | import boto3 import cfnresponse import time import random import re o = boto3.client("organizations") CREATE = 'Create' UPDATE = 'Update' DELETE = 'Delete' TAG_POLICY = "TAG_POLICY" def root(): return o.list_roots()['Roots'][0] def root_id(): return root()['Id'] def tag_policy_enabled(): enabled_policies = root()['PolicyTypes'] return {"Type": TAG_POLICY, "Status": "ENABLED"} in enabled_policies 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 enable_tag_policies(event, context): RequestType = event["RequestType"] if RequestType == CREATE and not tag_policy_enabled(): r_id = root_id() print('Enable TAG_POLICY for root: {}'.format(r_id)) o.enable_policy_type(RootId=r_id, PolicyType=TAG_POLICY) cfnresponse.send(event, context, cfnresponse.SUCCESS, {}, 'TAG_POLICY') def with_retry(function, **kwargs): for i in [0, 3, 9, 15, 30]: # Random sleep to not run into concurrency problems when adding or attaching multiple TAG_POLICYs # They have to be added/updated/deleted one after the other sleeptime = i + random.randint(0, 5) print('Running {} with Sleep of {}'.format(function.__name__, sleeptime)) time.sleep(sleeptime) try: response = function(**kwargs) print("Response for {}: {}".format(function.__name__, response)) return response except o.exceptions.ConcurrentModificationException as e: print('Exception: {}'.format(e)) raise Exception def policy_attached(policy_id): return [p['Id'] for p in o.list_policies_for_target(TargetId=root_id(), Filter='TAG_POLICY')['Policies'] if p['Id'] == policy_id] BackupPolicy: DependsOn: BackupPolicyEnable Type: AWS::CloudFormation::CustomResource Properties: ServiceToken: !GetAtt BackupPolicyCustomResource.Arn Policy: !Sub | { "plans": { "superwerker-backup": { "regions": { "@@assign": [ "${AWS::Region}" ] }, "rules": { "backup-daily": { "lifecycle": { "delete_after_days": { "@@assign": "30" } }, "target_backup_vault_name": { "@@assign": "Default" } } }, "selections": { "tags": { "backup-daily": { "iam_role_arn": { "@@assign": "arn:${AWS::Partition}:iam::$account:role/service-role/AWSBackupDefaultServiceRole" }, "tag_key": { "@@assign": "superwerker:backup" }, "tag_value": { "@@assign": [ "daily" ] } } } } } } } Attach: true BackupPolicyCustomResource: Type: AWS::Serverless::Function Properties: Timeout: 200 Runtime: python3.7 Handler: index.handler Policies: - Version: 2012-10-17 Statement: - Effect: Allow Action: - organizations:CreatePolicy - organizations:UpdatePolicy - organizations:DeletePolicy - organizations:AttachPolicy - organizations:DetachPolicy - organizations:ListRoots - organizations:ListPolicies - organizations:ListPoliciesForTarget Resource: "*" InlineCode: | import boto3 import cfnresponse import time import random import re o = boto3.client("organizations") CREATE = 'Create' UPDATE = 'Update' DELETE = 'Delete' BACKUP_POLICY = "BACKUP_POLICY" def root(): return o.list_roots()['Roots'][0] def root_id(): return root()['Id'] def with_retry(function, **kwargs): for i in [0, 3, 9, 15, 30]: # Random sleep to not run into concurrency problems when adding or attaching multiple BACKUP_POLICYs # They have to be added/updated/deleted one after the other sleeptime = i + random.randint(0, 5) print('Running {} with Sleep of {}'.format(function.__name__, sleeptime)) time.sleep(sleeptime) try: response = function(**kwargs) print("Response for {}: {}".format(function.__name__, response)) return response except o.exceptions.ConcurrentModificationException as e: print('Exception: {}'.format(e)) raise Exception def handler(event, context): RequestType = event["RequestType"] Properties = event["ResourceProperties"] LogicalResourceId = event["LogicalResourceId"] PhysicalResourceId = event.get("PhysicalResourceId") Policy = Properties["Policy"] Attach = Properties["Attach"] == 'true' print('RequestType: {}'.format(RequestType)) print('PhysicalResourceId: {}'.format(PhysicalResourceId)) print('LogicalResourceId: {}'.format(LogicalResourceId)) print('Attach: {}'.format(Attach)) parameters = dict( Content=Policy, Description="superwerker - {}".format(LogicalResourceId), Name=LogicalResourceId, ) policy_id = PhysicalResourceId try: if RequestType == CREATE: print('Creating Policy: {}'.format(LogicalResourceId)) response = with_retry(o.create_policy, **parameters, Type=BACKUP_POLICY ) policy_id = response["Policy"]["PolicySummary"]["Id"] if Attach: with_retry(o.attach_policy, PolicyId=policy_id, TargetId=root_id()) elif RequestType == UPDATE: print('Updating Policy: {}'.format(LogicalResourceId)) with_retry(o.update_policy, PolicyId=policy_id, **parameters) elif RequestType == DELETE: print('Deleting Policy: {}'.format(LogicalResourceId)) # Same as above if re.match('p-[0-9a-z]+', policy_id): if policy_attached(policy_id): with_retry(o.detach_policy, PolicyId=policy_id, TargetId=root_id()) with_retry(o.delete_policy, PolicyId=policy_id) else: print('{} is no valid PolicyId'.format(policy_id)) else: raise Exception('Unexpected RequestType: {}'.format(RequestType)) cfnresponse.send(event, context, cfnresponse.SUCCESS, {}, policy_id) except Exception as e: print(e) print(event) cfnresponse.send(event, context, cfnresponse.FAILED, {}, policy_id) def policy_attached(policy_id): return [p['Id'] for p in o.list_policies_for_target(TargetId=root_id(), Filter='BACKUP_POLICY')['Policies'] if p['Id'] == policy_id] BackupPolicyEnable: Type: AWS::CloudFormation::CustomResource Properties: ServiceToken: !GetAtt BackupPolicyEnableCustomResource.Arn BackupPolicyEnableCustomResource: Type: AWS::Serverless::Function Properties: Timeout: 200 Handler: index.enable_tag_policies Runtime: python3.7 Policies: - Version: 2012-10-17 Statement: - Effect: Allow Action: - organizations:EnablePolicyType - organizations:DisablePolicyType - organizations:ListRoots Resource: "*" InlineCode: | import boto3 import cfnresponse import time import random import re o = boto3.client("organizations") CREATE = 'Create' UPDATE = 'Update' DELETE = 'Delete' BACKUP_POLICY = "BACKUP_POLICY" def root(): return o.list_roots()['Roots'][0] def root_id(): return root()['Id'] def backup_policy_enabled(): enabled_policies = root()['PolicyTypes'] return {"Type": BACKUP_POLICY, "Status": "ENABLED"} in enabled_policies 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 enable_tag_policies(event, context): RequestType = event["RequestType"] if RequestType == CREATE and not backup_policy_enabled(): r_id = root_id() print('Enable BACKUP_POLICY for root: {}'.format(r_id)) o.enable_policy_type(RootId=r_id, PolicyType=BACKUP_POLICY) cfnresponse.send(event, context, cfnresponse.SUCCESS, {}, 'BACKUP_POLICY') def with_retry(function, **kwargs): for i in [0, 3, 9, 15, 30]: # Random sleep to not run into concurrency problems when adding or attaching multiple BACKUP_POLICYs # They have to be added/updated/deleted one after the other sleeptime = i + random.randint(0, 5) print('Running {} with Sleep of {}'.format(function.__name__, sleeptime)) time.sleep(sleeptime) try: response = function(**kwargs) print("Response for {}: {}".format(function.__name__, response)) return response except o.exceptions.ConcurrentModificationException as e: print('Exception: {}'.format(e)) raise Exception def policy_attached(policy_id): return [p['Id'] for p in o.list_policies_for_target(TargetId=root_id(), Filter='BACKUP_POLICY')['Policies'] if p['Id'] == policy_id]