I’ve been doing a lot with the Amazon Web Services Cloud Development Kit (CDK) over the last few months. CDK lets you define all your infrastructure in code, such as Typescript, which is then compiled into a CloudFormation template. It’s pretty cool, but it’s also a new production changing rapidly and the documentation hasn’t always kept up. Below are my notes for setting up a backup strategy for an Elastic File System (EFS) using Amazon Backup.

High-level overview

With Amazon Backup, you will define a Backup Plan with one or more rules targeting various resources, and then store those backups in a Backup Vault. This means you’re going to need the following:

  1. A Backup Vault
  2. A Backup Plan
  3. A Backup Selection
  4. A resource to be backed up. For this example, an EFS instance.

You’re also going to need an Identity and Access Management (IAM) role with sufficient permissions to provision all the backup infrastructure.

IAM permissions

This was the trickiest aspect of the implementation. CDK build failures do not generally indicate which permission failed; sometimes they don’t even indicate which resource was in play. I encountered this error repeatedly trying to create a Backup Vault:

 2/16 | 3:03:21 PM | CREATE_FAILED | AWS::Backup::BackupVault | <VaultName> Insufficient privileges to perform this action. (Service: AWSBackup; Status Code: 403; Error Code: AccessDeniedException; Request ID: <id>)

Not much to go on, and especially puzzling given that I’d given my IAM user the backup:CreateBackupVault permission. Amazon has a page describing the permissions involved to manage AWS Backup programmatically, and there are actually seven different permissions that you need. Confusingly, several of the ARN examples given on that page are incorrect. To create the Backup Vault, the Backup Plan, the Backup Selection, and delegate the backup role correctly, I eventually crafted this policy snippet:

        {
            "Sid": "BackupPolicy",
            "Effect": "Allow",
            "Action": [
                "backup:CreateBackupPlan",
                "backup:CreateBackupSelection",
                "backup:CreateBackupVault",
                "backup:DeleteBackupPlan",
                "backup:DeleteBackupSelection",
                "backup:DeleteBackupVault",
                "backup:DescribeBackupVault",
                "backup:GetBackupPlan",
                "backup:UpdateBackupPlan",
                "iam:PassRole",
                "kms:CreateGrant",
                "kms:GenerateDataKey",
                "kms:Decrypt",
                "kms:RetireGrant",
                "kms:DescribeKey"
            ],
            "Resource": [
                "arn:aws:backup:<region>:<account-id>:backup-plan:*",
                "arn:aws:backup:<region>:<account-id>:backup-vault:<Name>*",
                "arn:aws:backup:<region>:<account-id>:key:*",
                "arn:aws:iam::<region>:<account-id>:role/<Name>*"
            ]
        },
        {
            "Sid": "BackupStoragePolicy",
            "Effect": "Allow",
            "Action": [
                "backup-storage:MountCapsule"
            ],
            "Resource": "*"
        }

Something to note here is that you can’t qualify everything by ARN, not easily anyway. The generated ARNs for backup plans appear to be completely random.

CDK code

This example presupposes that you have an EFS filesystem defined elsewhere, named efsUploads. The following code creates a Backup Vault, a Backup Plan, and targets that EFS filesystem by ARN. It also creates an IAM service role for running backups. The backup rules create daily backups in warm storage, retained for 35 days, and monthly backups in cold storage, retained for a year.

    const efsVault = new backup.CfnBackupVault(this, opts.siteName + 'BackupVault', {
      backupVaultName: opts.siteName,
    });
  
    const efsBackupRole = new iam.Role(this, opts.siteName + 'EFSBackupRole', {
      assumedBy: new iam.ServicePrincipal('backup.amazonaws.com'),
      managedPolicies: [
        iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSBackupServiceRolePolicyForBackup'),
        iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSBackupServiceRolePolicyForRestores'),
      ],
    });
    const efsBackup = new backup.CfnBackupPlan(this, opts.siteName + 'EFSBackupPlan', {
      backupPlan: {
        backupPlanName: opts.siteName + 'EFSBackupPlan',
        backupPlanRule: [
          {
            ruleName: opts.siteName + 'DailyWarmBackup',
            lifecycle: {
              deleteAfterDays: 35
            },
            targetBackupVault: efsVault.attrBackupVaultName,
            scheduleExpression: 'cron(0 8 * * ? *)',
          },
          {
            ruleName: opts.siteName + 'MonthlyColdBackup',
            lifecycle: {
              deleteAfterDays: 365,
              moveToColdStorageAfterDays: 30
            },
            targetBackupVault: efsVault.attrBackupVaultName,
            scheduleExpression: 'cron(0 8 1 * ? *)',
          }
        ]
      }
    });
    const efsBackupPlanSelection = new backup.CfnBackupSelection(this, opts.siteName + 'EFSBackupPlanSelection', {
      backupPlanId: efsBackup.attrBackupPlanId,
      backupSelection: {
        iamRoleArn: efsBackupRole.roleArn,
        selectionName: opts.siteName + 'EFS',
        resources: [
          'arn:aws:elasticfilesystem:' + this.region + ':' + this.account + ':file-system/' + efsUploads.ref,
        ]
      }
    });

There’s no way (that I know of) to get the ARN of the EFS instance programmatically, so you have to construct it. The AWS Backup library in CDK is still in developer preview, and all the constructs are still pretty low-level.