Using AWS CloudFormation templates with AWS Backup - AWS Backup

Using AWS CloudFormation templates with AWS Backup

In general

With AWS CloudFormation, you can provision and manage your AWS resources in a safe, repeatable manner using templates that you create. You can use AWS CloudFormation templates to manage your backup plans, backup resource selections, and backup vaults. For information about using AWS CloudFormation, see How Does AWS CloudFormation Work? in the AWS CloudFormation User Guide.

Before you create your AWS CloudFormation stack, you should consider the following:

  • We recommend that you create separate templates for your backup plans and your backup vaults. Because backup vaults can be deleted only if they are empty, you can't delete a stack that includes backup vaults if they contain any recovery points.

  • Be sure that you have a service role available before you create your stack. The AWS Backup default service role is created for you the first time you assign resources to a backup plan. If you haven't done this yet, the default service role is not available. You can also specify a custom role that you create. For more information about roles, see IAM service roles.

We provide two sample AWS CloudFormation templates for your reference. The first template creates a simple backup plan. The second template enables VSS backups in a backup plan.

Description: Backup Plan template to back up all resources tagged with backup=daily daily at 5am UTC. Resources: KMSKey: Type: AWS::KMS::Key Properties: Description: "Encryption key for daily" EnableKeyRotation: True Enabled: True KeyPolicy: Version: "2012-10-17" Statement: - Effect: Allow Principal: "AWS": { "Fn::Sub": "arn:${AWS::Partition}:iam::${AWS::AccountId}:root" } Action: - kms:* Resource: "*" BackupVaultWithDailyBackups: Type: "AWS::Backup::BackupVault" Properties: BackupVaultName: "BackupVaultWithDailyBackups" EncryptionKeyArn: !GetAtt KMSKey.Arn BackupPlanWithDailyBackups: Type: "AWS::Backup::BackupPlan" Properties: BackupPlan: BackupPlanName: "BackupPlanWithDailyBackups" BackupPlanRule: - RuleName: "RuleForDailyBackups" TargetBackupVault: !Ref BackupVaultWithDailyBackups ScheduleExpression: "cron(0 5 ? * * *)" DependsOn: BackupVaultWithDailyBackups DDBTableWithDailyBackupTag: Type: "AWS::DynamoDB::Table" Properties: TableName: "TestTable" AttributeDefinitions: - AttributeName: "Album" AttributeType: "S" KeySchema: - AttributeName: "Album" KeyType: "HASH" ProvisionedThroughput: ReadCapacityUnits: "5" WriteCapacityUnits: "5" Tags: - Key: "backup" Value: "daily" BackupRole: Type: "AWS::IAM::Role" Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Principal: Service: - "backup.amazonaws.com" Action: - "sts:AssumeRole" ManagedPolicyArns: - "arn:aws:iam::aws:policy/service-role/service-role" TagBasedBackupSelection: Type: "AWS::Backup::BackupSelection" Properties: BackupSelection: SelectionName: "TagBasedBackupSelection" IamRoleArn: !GetAtt BackupRole.Arn ListOfTags: - ConditionType: "STRINGEQUALS" ConditionKey: "backup" ConditionValue: "daily" BackupPlanId: !Ref BackupPlanWithDailyBackups DependsOn: BackupPlanWithDailyBackups
Note

If you are using the default service role, replace service-role with AWSBackupServiceRolePolicyForBackup.

Description: Backup Plan template to enable Windows VSS and add backup rule to take backup of assigned resources daily at 5am UTC. Resources: KMSKey: Type: AWS::KMS::Key Properties: Description: "Encryption key for daily" EnableKeyRotation: True Enabled: True KeyPolicy: Version: "2012-10-17" Statement: - Effect: Allow Principal: "AWS": { "Fn::Sub": "arn:${AWS::Partition}:iam::${AWS::AccountId}:root" } Action: - kms:* Resource: "*" BackupVaultWithDailyBackups: Type: "AWS::Backup::BackupVault" Properties: BackupVaultName: "BackupVaultWithDailyBackups" EncryptionKeyArn: !GetAtt KMSKey.Arn BackupPlanWithDailyBackups: Type: "AWS::Backup::BackupPlan" Properties: BackupPlan: BackupPlanName: "BackupPlanWithDailyBackups" AdvancedBackupSettings: - ResourceType: EC2 BackupOptions: WindowsVSS: enabled BackupPlanRule: - RuleName: "RuleForDailyBackups" TargetBackupVault: !Ref BackupVaultWithDailyBackups ScheduleExpression: "cron(0 5 ? * * *)" DependsOn: BackupVaultWithDailyBackups

For information about using AWS CloudFormation with AWS Backup, see AWS Backup Resource Type Reference in the AWS CloudFormation User Guide.

For information about controlling access to AWS service resources when using AWS CloudFormation, see Controlling Access with AWS Identity and Access Management in the AWS CloudFormation User Guide.

Using AWS CloudFormation with Organizations

Use the following YAML and Python files with AWS CloudFormation to deploy AWS Backup policies at the Organizations level.

AWSTemplateFormatVersion: '2010-09-09' Description: This template deploys Backup Policies required to manage backups at an organization level. Parameters: ImpactedAccounts: Description: "CSV list of the Org Ids" Type: CommaDelimitedList Default: "" ConfigBucket: Description: S3 Bucket for the Custom Lambda Code and Templates Type: String Default: mb3-venkitas ConfigBucketKey: Description: S3 Key for the Custom Lambda Code and Templates Type: String Default: IaC/cfn-templates/Backup/ Resources: #Type='SERVICE_CONTROL_POLICY'|'TAG_POLICY'|'BACKUP_POLICY'|'AISERVICES_OPT_OUT_POLICY' GoldDailyBackupPolicySyd: Type: Custom::OrgPolicy Properties: PolicyName: GoldDailyBackupPolicySyd PolicyType: BACKUP_POLICY PolicyTargets : !Ref ImpactedAccounts PolicyDescription: >- BackupPolicy for Daily Backup as per the resource selection criteria PolicyContents: |- { "plans": { "OrgDailyBackupPlan": { "regions": { "@@assign": [ "REGION" ] }, "rules": { "OrgDailyBackupRule": { "schedule_expression": { "@@assign": "SCHEDULE_EXPRESSION" }, "start_backup_window_minutes": { "@@assign": "480" }, "complete_backup_window_minutes": { "@@assign": "720" }, "lifecycle": { "delete_after_days": { "@@assign": "1" } }, "target_backup_vault_name": { "@@assign": "DailyBackupVault" }, "recovery_point_tags": { "project": { "tag_key": { "@@assign": "TAG_KEY" }, "tag_value": { "@@assign": "TAG_VALUE" } } } } }, "backup_plan_tags": { "project": { "tag_key": { "@@assign": "TAG_KEY" }, "tag_value": { "@@assign": "TAG_VALUE" } } }, "selections": { "tags": { "OrgDailyBackupSelection": { "iam_role_arn": { "@@assign": "arn:aws:iam::$account:role/BackupRole" }, "tag_key": { "@@assign": "TAG_KEY" }, "tag_value": { "@@assign": [ "TAG_VALUE" ] } } } } } } } Variables: - REGION : !Ref 'AWS::Region' - TAG_KEY : project - TAG_VALUE : aws-backup-demo - SCHEDULE_EXPRESSION : "cron(0 5 ? * * *)" ServiceToken: !GetAtt OrgPolicyCustomResourceManager.Arn DenyVaultAndLogBucketOperationsSCP: Type: Custom::OrgPolicy Properties: PolicyName: SCP_DENY_VAULT_AND_LOG_BUCKET_OPERATIONS PolicyType: SERVICE_CONTROL_POLICY PolicyTargets : !Ref ImpactedAccounts PolicyDescription: >- This SCP denies operations on Vault and log bucket that are tagged with a specific project name. PolicyContents: |- { "Version": "2012-10-17", "Statement": [ { "Sid": "DenyLogBucketOperations", "Effect": "Deny", "Action": [ "s3:DeleteBucket", "s3:DeleteBucketPolicy", "s3:DeleteJobTagging", "s3:DeleteAccessPointPolicy", "s3:DeleteAccessPoint", "s3:DeleteBucketWebsite" ], "Resource": [ "arn:aws:s3:::LOG_BUCKET-*" ] }, { "Sid": "DenyLogBucketObjectOperations", "Effect": "Deny", "Action": [ "s3:DeleteObject", "s3:DeleteObjectTagging", "s3:DeleteObjectVersion", "s3:DeleteObjectVersionTagging" ], "Resource": [ "arn:aws:s3:::LOG_BUCKET-*/*" ] }, { "Sid": "DenyVaultOperations", "Effect": "Deny", "Action": [ "backup:DeleteBackupVault", "backup:DeleteBackupSelection", "backup:DeleteBackupPlan", "backup:DeleteBackupVaultAccessPolicy", "backup:DeleteBackupVaultNotifications", "backup:DeleteRecoveryPoint", "backup:UntagResource" ], "Resource": [ "*" ], "Condition": { "StringEquals": { "aws:ResourceTag/TAG_KEY": [ "TAG_VALUE" ] } } } ] } Variables: - REGION : !Ref 'AWS::Region' - LOG_BUCKET : backup-log-bucket - TAG_KEY : project - TAG_VALUE : aws-backup-demo ServiceToken: !GetAtt OrgPolicyCustomResourceManager.Arn OrgPolicyCustomResourceManager: Type: AWS::Lambda::Function Properties: FunctionName: OrgPolicyCustomResourceManager Description: Lambda function to deploy CloudFormation custom resources for Organization SCPs. Handler: OrgPolicyCustomResourceManager.lambda_handler Code: S3Bucket: !Ref ConfigBucket S3Key: !Sub - '${S3Prefix}OrgPolicyCustomResourceManager.zip' - { S3Prefix: !Ref ConfigBucketKey } Role: !GetAtt OrgPolicyCustomResourceManagerRole.Arn Runtime: python3.7 MemorySize: 256 Timeout: 300 Tags: - Key: Name Value: OrgPolicyCustomResourceManager OrgPolicyCustomResourceManagerRole: Type: 'AWS::IAM::Role' Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: 'lambda.amazonaws.com' Action: - 'sts:AssumeRole' Path: '/' ManagedPolicyArns: - 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole' Policies: - PolicyName: AssumeOrgRole PolicyDocument: Statement: - Effect: Allow Action: - sts:AssumeRole Resource: '*' - PolicyName: OrgPermissions PolicyDocument: Statement: - Effect: Allow Action: - organizations:CreatePolicy - organizations:DeletePolicy - organizations:AttachPolicy - organizations:DetachPolicy - organizations:ListPolicies Resource: '*' - PolicyName: S3Permissions PolicyDocument: Statement: - Effect: Allow Action: - s3:Get* Resource: !Sub - 'arn:aws:s3:::${Bucket}/*' - { Bucket: !Ref ConfigBucket }
import boto3 #https://pypi.org/project/cfnresponse/ import cfnresponse as cfn import logging import uuid logger = logging.getLogger() logger.setLevel(logging.INFO) client = boto3.client('organizations') def get_policy(event): policy_contents = '' if 'PolicyContents' in event['ResourceProperties']: policy_contents = event['ResourceProperties']['PolicyContents'] else: s3_bucket = event['ResourceProperties']['PolicyBucket'] s3_object = event['ResourceProperties']['PolicyLocation'] s3 = boto3.resource('s3') policy_file = s3.Object(s3_bucket, s3_object) policy_contents = policy_file.get()['Body'].read().decode('utf-8') #Check for replacement variables if 'Variables' in event['ResourceProperties']: variables= event['ResourceProperties']['Variables'] logger.info(f"variables : {variables}") for variable in variables: for key, value in variable.items(): logger.info(f"Replacing Key : {key} with value : {value}") policy_contents = policy_contents.replace(key,value ) return policy_contents def lambda_handler(event, context): try: #create physical resource id customResourcePhysicalID = uuid.uuid4().hex if 'PhysicalResourceId' in event: customResourcePhysicalID = event['PhysicalResourceId'] logger.info(f"OrgPolicyCustomResourceManager Request: {event}") resource_action = event['RequestType'] policy_name = event['ResourceProperties']['PolicyName'] policy_contents = get_policy(event) policy_type = event['ResourceProperties']['PolicyType'] policy_description = event['ResourceProperties']['PolicyDescription'] policyTargetList=[] if 'PolicyTargets' in event['ResourceProperties']: policy_targets = event['ResourceProperties']['PolicyTargets'] policyTargetList = policy_targets logger.info(f"policy_targets: {policy_targets}") if resource_action == 'Create': logger.info(f"Action : {resource_action} policy") response = client.create_policy( Content=policy_contents, Description=policy_description, Name=policy_name, Type=policy_type ) logger.info(f"Response: {response}") policyId = response['Policy']['PolicySummary']['Id'] #Attach the policy in target accounts for policyTarget in policyTargetList: try: logger.info(f"Attaching {policyId} on Account {policyTarget}") response = client.attach_policy(PolicyId=policyId, TargetId=policyTarget) logger.info(f"Attached {policyId} on Account {policyTarget}") except Exception as e: logger.error(str(e)) cfn.send(event, context, cfn.SUCCESS, {'Message': "Policy created successfully."}, customResourcePhysicalID) elif resource_action == 'Update' or resource_action == 'Delete': logger.info(f" Action: {resource_action} policy, policy_type: {policy_type}, policy_name {policy_name}") response = client.list_policies(Filter=policy_type) policy = list(filter(lambda item: item['Name'] == policy_name, response["Policies"])) logger.info(f"Policy Found : {policy}") if len(policy) == 0: return {'Status': 'Policy not found for name ' + policy_name} policyId=policy[0]['Id'] if resource_action == 'Delete': #Detach the policy detachPolicy(policyTargetList,policyId) while True: try: response = client.delete_policy(PolicyId=policyId) logger.info(f"deletePolicy response: {response}") cfn.send(event, context, cfn.SUCCESS, {'Message': "Policy modified successfully."}, customResourcePhysicalID) break except client.exceptions.PolicyInUseException as e: #try detaching the policy again detachPolicy(policyTargetList,policyId) logger.error(str(e)) else: response = updatePolicy(resource_action,policyId, policy_contents) logger.info(f"updatePolicy response: {response}") cfn.send(event, context, cfn.SUCCESS, {'Message': "Policy modified successfully."}, customResourcePhysicalID) else: logger.error(f"Unexpected Action : {resource_action}") cfn.send(event, context, cfn.FAILED, {'Message': 'Unexpected event received from CloudFormation'}, customResourcePhysicalID) except Exception as exc: logger.error(f"Exception: {str(exc)}") cfn.send(event, context, cfn.FAILED, {'Message': str(exc)}, customResourcePhysicalID) def detachPolicy(policyTargetList,policyId): #Detach the policy in target accounts for policyTarget in policyTargetList: try: logger.info(f"Detaching {policyId} from Account {policyTarget}") response = client.detach_policy(PolicyId=policyId, TargetId=policyTarget) logger.info(f"Detached {policyId} from Account {policyTarget}") except Exception as e: logger.error(str(e)) def updatePolicy(resource_action, policyId, policy_contents): logger.info(f"updatePolicy with action : {resource_action}, policyId : {policyId}") if (resource_action == 'Update'): response = client.update_policy( PolicyId=policyId, Content=policy_contents ) return response