AWS BackupでのクロスリージョンバックアップをCDKで一撃でデプロイする
はじめに
こんにちは、AWS事業本部のイシザワです。
先日、AWS BackupでのクロスリージョンバックアップをCloudFormationで構築する機会がありました。
CloudFormationでは1つのスタックには1つのリージョンのリソースしか含めることができないため、クロスリージョンの構成をとると複数のスタックを作成する必要があります。
CloudFormationでのスタックのFn::ImportValue
での参照や、SSMパラメータの動的参照では異なるリージョンの値を参照できません。
そのため、クロスリージョンの構成の場合はリージョン間での値の受け渡しは、テンプレートのパラメータ経由で入力する等の間接的な方法を取る必要があります。
一方、CDKでは cdk-remote-stack を使うことで、異なるリージョンのスタックの値を参照することが出来ます。
今回は、これを利用してクロスリージョンのAWS Backupの構成を一撃で構築するCDKアプリケーションを作成してみました。
構成
以下のアーキテクチャを構築します。
作成するスタックは以下の通りです。矢印は依存先のスタックを示しています。
ソースコード
ソースコードは以下のリポジトリに置いてあります。
https://github.com/cm-ishizawa-takuya/cross-region-backup
各コンポーネントの構成を解説します。
BackupRoleStack
ソースコード
import * as iam from 'aws-cdk-lib/aws-iam'; import { Construct } from 'constructs'; import { Stack, StackProps, CfnOutput } from 'aws-cdk-lib'; export interface BackupRoleStackProps extends StackProps { backupRoleName?: string; } export class BackupRoleStack extends Stack { public readonly backupRole: iam.IRole; public static readonly BACKUP_ROLE_ARN_OUTPUT_KEY = 'BackupRoleArn'; constructor(scope: Construct, id: string, props: BackupRoleStackProps) { super(scope, id, props); const roleName = props.backupRoleName ?? 'cross-region-backup-role'; this.backupRole = new iam.Role(this, 'BackupRole', { roleName, assumedBy: new iam.ServicePrincipal('backup.amazonaws.com'), managedPolicies: [ iam.ManagedPolicy.fromAwsManagedPolicyName( 'service-role/AWSBackupServiceRolePolicyForBackup', ), iam.ManagedPolicy.fromAwsManagedPolicyName( 'service-role/AWSBackupServiceRolePolicyForRestores', ), ], inlinePolicies: { 'iam-pass-policy': new iam.PolicyDocument({ statements: [ new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: ['iam:PassRole'], resources: ['*'], }), ], }), }, }); // クロスリージョン参照用 new CfnOutput(this, BackupRoleStack.BACKUP_ROLE_ARN_OUTPUT_KEY, { value: this.backupRole.roleArn, }); } }
BackupRoleStack
はAWS Backupのバックアップジョブを実行するためのIAMロールを作成します。- 22~29行目:バックアップとリストア用のAWS Managed Policyをアタッチします。
- 30~40行目:リストアに
iam:PassRole
が必要なため、iam:PassRole
を許可するインラインポリシーをアタッチします。 - 43~46行目:クロスリージョン参照用の出力を作成します。
PrimaryKeyStack
ソースコード
import * as kms from 'aws-cdk-lib/aws-kms'; import * as iam from 'aws-cdk-lib/aws-iam'; import { Stack, StackProps, Duration, RemovalPolicy, Aws, CfnOutput } from 'aws-cdk-lib'; import { Construct } from 'constructs'; export interface PrimaryKeyStackProps extends StackProps { keyAdminUserArns: string[]; subRegion: string; backupRole: iam.IRole; pendingWindow?: Duration; removalPolicy?: RemovalPolicy; } export class PrimaryKeyStack extends Stack { public readonly key: kms.IKey; public static readonly KEY_ARN_OUTPUT_KEY = 'KeyArn'; constructor(scope: Construct, id: string, props: PrimaryKeyStackProps) { super(scope, id, props); this.key = new kms.Key(this, 'Key', { enableKeyRotation: true, pendingWindow: props.pendingWindow, alias: 'alias/backup-key', keySpec: kms.KeySpec.SYMMETRIC_DEFAULT, keyUsage: kms.KeyUsage.ENCRYPT_DECRYPT, policy: new iam.PolicyDocument({ statements: [ new iam.PolicyStatement({ effect: iam.Effect.ALLOW, principals: props.keyAdminUserArns.map((arn) => new iam.ArnPrincipal(arn)), actions: ['kms:*'], resources: ['*'], }), new iam.PolicyStatement({ effect: iam.Effect.ALLOW, principals: [new iam.AccountRootPrincipal()], actions: ['kms:*'], resources: ['*'], conditions: { ArnLike: { 'aws:PrincipalArn': [ `arn:${Aws.PARTITION}:iam::${Aws.ACCOUNT_ID}:role/cdk-*-cfn-exec-role-${Aws.ACCOUNT_ID}-${Aws.REGION}`, `arn:${Aws.PARTITION}:iam::${Aws.ACCOUNT_ID}:role/cdk-*-cfn-exec-role-${Aws.ACCOUNT_ID}-${props.subRegion}`, ], }, }, }), new iam.PolicyStatement({ effect: iam.Effect.ALLOW, principals: [new iam.ArnPrincipal(props.backupRole.roleArn)], actions: [ 'kms:Encrypt', 'kms:Decrypt', 'kms:ReEncrypt*', 'kms:GenerateDataKey*', 'kms:DescribeKey', ], resources: ['*'], }), new iam.PolicyStatement({ effect: iam.Effect.ALLOW, principals: [new iam.ArnPrincipal(props.backupRole.roleArn)], actions: ['kms:CreateGrant'], resources: ['*'], conditions: { StringEquals: { 'kms:GrantIsForAWSResource': true, }, }, }), ], }), removalPolicy: props.removalPolicy, }); const cfnKey = this.key.node.defaultChild as kms.CfnKey; cfnKey.addPropertyOverride('MultiRegion', true); // クロスリージョン参照用 new CfnOutput(this, PrimaryKeyStack.KEY_ARN_OUTPUT_KEY, { value: this.key.keyArn }); } }
PrimaryKeyStack
はAWS Backupのバックアップジョブで使用するキーを作成します。- 30~35行目:キーの管理者として指定されたユーザーに対して
kms:*
を許可するキーポリシーを作成します。 - 36~49行目:CDKによるスタックのデプロイ時に利用されるIAMロールに対して
kms:*
を許可するキーポリシーを作成します。- バックアップ先リージョンのスタックのデプロイ時にも参照されるため、バックアップ元とバックアップ先の2つのロールに対して許可する必要があります。
- 50~72行目:AWS Backupのバックアップジョブに対して必要な権限を許可するキーポリシーを作成します。
- 81~82行目:クロスリージョン参照用の出力を作成します。
ReplicaKeyStack
ソースコード
import * as kms from 'aws-cdk-lib/aws-kms'; import * as iam from 'aws-cdk-lib/aws-iam'; import { Stack, StackProps, Duration, RemovalPolicy, Aws } from 'aws-cdk-lib'; import { Construct } from 'constructs'; import { RemoteOutputs } from 'cdk-remote-stack'; import { BackupRoleStack } from './backup-role-stack'; import { PrimaryKeyStack } from './primary-key-stack'; export interface ReplicaKeyStackProps extends StackProps { keyAdminUserArns: string[]; backupRoleStack: BackupRoleStack; primaryKeyStack: PrimaryKeyStack; pendingWindow?: Duration; removalPolicy?: RemovalPolicy; } export class ReplicaKeyStack extends Stack { public readonly key: kms.CfnReplicaKey; constructor(scope: Construct, id: string, props: ReplicaKeyStackProps) { super(scope, id, props); const backupRoleStackOutputs = new RemoteOutputs(this, 'BackupRoleStackOutputs', { stack: props.backupRoleStack, }); const backupRoleArn = backupRoleStackOutputs.get(BackupRoleStack.BACKUP_ROLE_ARN_OUTPUT_KEY); const primaryKeyStackOutputs = new RemoteOutputs(this, 'PrimaryKeyStackOutputs', { stack: props.primaryKeyStack, }); const primaryKeyArn = primaryKeyStackOutputs.get(PrimaryKeyStack.KEY_ARN_OUTPUT_KEY); this.key = new kms.CfnReplicaKey(this, 'Key', { pendingWindowInDays: props.pendingWindow?.toDays(), primaryKeyArn, keyPolicy: JSON.stringify( new iam.PolicyDocument({ statements: [ new iam.PolicyStatement({ effect: iam.Effect.ALLOW, principals: props.keyAdminUserArns.map((arn) => new iam.ArnPrincipal(arn)), actions: ['kms:*'], resources: ['*'], }), new iam.PolicyStatement({ effect: iam.Effect.ALLOW, principals: [new iam.AccountRootPrincipal()], actions: ['kms:*'], resources: ['*'], conditions: { ArnLike: { 'aws:PrincipalArn': `arn:${Aws.PARTITION}:iam::${Aws.ACCOUNT_ID}:role/cdk-*-cfn-exec-role-${Aws.ACCOUNT_ID}-${Aws.REGION}`, }, }, }), new iam.PolicyStatement({ effect: iam.Effect.ALLOW, principals: [new iam.ArnPrincipal(backupRoleArn)], actions: [ 'kms:Encrypt', 'kms:Decrypt', 'kms:ReEncrypt*', 'kms:GenerateDataKey*', 'kms:DescribeKey', ], resources: ['*'], }), new iam.PolicyStatement({ effect: iam.Effect.ALLOW, principals: [new iam.ArnPrincipal(backupRoleArn)], actions: ['kms:CreateGrant'], resources: ['*'], conditions: { StringEquals: { 'kms:GrantIsForAWSResource': true, }, }, }), ], }).toJSON(), ), }); this.key.applyRemovalPolicy(props.removalPolicy); } }
ReplicaKeyStack
はバックアップ先リージョンで使用するキーを作成します。このキーはバックアップ元リージョンのキーをレプリケーションしたものです。- 25~28行目:
RemoteOutputs
を使用してバックアップ元リージョンのスタックの出力からバックアップ元リージョンのバックアップロールのARNを取得します。 - 30~33行目:
RemoteOutputs
を使用してバックアップ元リージョンのスタックの出力からバックアップ元リージョンのキーのARNを取得します。 - 38~83行目:キーポリシーを設定しています。内容は
PrimaryKeyStack
とほぼ同様です。
SubRegionBackupStack
ソースコード
import * as backup from 'aws-cdk-lib/aws-backup'; import { Stack, StackProps, Aws, RemovalPolicy, CfnOutput } from 'aws-cdk-lib'; import { Construct } from 'constructs'; export interface SubRegionBackupStackProps extends StackProps { keyArn: string; backupVaultName?: string; removalPolicy?: RemovalPolicy; } export class SubRegionBackupStack extends Stack { public readonly vault: backup.CfnBackupVault; public static readonly BACKUP_VAULT_ARN_OUTPUT_KEY = 'BackupVaultArn'; constructor(scope: Construct, id: string, props: SubRegionBackupStackProps) { super(scope, id, props); const vaultName = props.backupVaultName ?? `backup-vault-${Aws.REGION}`; this.vault = new backup.CfnBackupVault(this, 'Vault', { backupVaultName: vaultName, encryptionKeyArn: props.keyArn, }); this.vault.applyRemovalPolicy(props.removalPolicy); // クロスリージョン参照用 new CfnOutput(this, SubRegionBackupStack.BACKUP_VAULT_ARN_OUTPUT_KEY, { value: this.vault.attrBackupVaultArn, }); } }
SubRegionBackupStack
はバックアップ先リージョンのAWS Backupのバックアップジョブで使用するバックアップボールトを作成します。- 27~30行目:クロスリージョン参照用の出力を作成します。
MainRegionBackupStack
ソースコード
import * as backup from 'aws-cdk-lib/aws-backup'; import { Stack, StackProps, Aws, RemovalPolicy } from 'aws-cdk-lib'; import { Construct } from 'constructs'; import { RemoteOutputs } from 'cdk-remote-stack'; import { SubRegionBackupStack } from './sub-region-backup-stack'; export interface MainRegionBackupStackProps extends StackProps { subRegionBackupStack: SubRegionBackupStack; keyArn: string; backupRoleArn: string; backupVaultName?: string; cronExpression: string; backupTagKey: string; backupTagValue: string; deleteAfterDays?: number; removalPolicy?: RemovalPolicy; } export class MainRegionBackupStack extends Stack { vault: backup.CfnBackupVault; constructor(scope: Construct, id: string, props: MainRegionBackupStackProps) { super(scope, id, props); const subRegionBackupStackOutputs = new RemoteOutputs(this, 'SubRegionBackupStackOutputs', { stack: props.subRegionBackupStack, }); const subRegionBackupVaultArn = subRegionBackupStackOutputs.get( SubRegionBackupStack.BACKUP_VAULT_ARN_OUTPUT_KEY, ); const vaultName = props.backupVaultName ?? `backup-vault-${Aws.REGION}`; const deleteAfterDays = props.deleteAfterDays ?? 7; this.vault = new backup.CfnBackupVault(this, 'Vault', { backupVaultName: vaultName, encryptionKeyArn: props.keyArn, }); this.vault.applyRemovalPolicy(props.removalPolicy); const plan = new backup.CfnBackupPlan(this, 'Plan', { backupPlan: { backupPlanName: 'cross-region-backup-plan', backupPlanRule: [ { ruleName: 'cross-region-backup-plan-rule', targetBackupVault: this.vault.ref, scheduleExpression: props.cronExpression, startWindowMinutes: 60, completionWindowMinutes: 180, lifecycle: { deleteAfterDays, }, copyActions: [ { destinationBackupVaultArn: subRegionBackupVaultArn, }, ], }, ], }, }); new backup.CfnBackupSelection(this, 'Selection', { backupPlanId: plan.ref, backupSelection: { selectionName: 'cross-region-backup-selection', iamRoleArn: props.backupRoleArn, resources: ['arn:aws:ec2:*:*:instance/*'], listOfTags: [ { conditionType: 'STRINGEQUALS', conditionKey: props.backupTagKey, conditionValue: props.backupTagValue, }, ], conditions: { StringEquals: [ { ConditionKey: `aws:ResourceTag/${props.backupTagKey}`, ConditionValue: props.backupTagValue, }, ], }, }, }); } }
MainRegionBackupStack
はバックアップ元リージョンのAWS Backupのバックアップジョブを作成します。- 27~32行目:
RemoteOutputs
を使用してバックアップ先リージョンのスタックの出力からバックアップ先リージョンのバックアップボールトのARNを取得します。 - 49行目:上記で取得したバックアップ先リージョンのバックアップボールトのARNを指定します。
BackupStage
ソースコード
import { Stage, StageProps, Duration, RemovalPolicy } from 'aws-cdk-lib'; import { Construct } from 'constructs'; import { BackupRoleStack } from '../stack/backup-role-stack'; import { PrimaryKeyStack } from '../stack/primary-key-stack'; import { ReplicaKeyStack } from '../stack/replica-key-stack'; import { SubRegionBackupStack } from '../stack/sub-region-backup-stack'; import { MainRegionBackupStack } from '../stack/main-region-backup-stack'; export interface BackupStageProps extends StageProps { keyAdminUserArns: string[]; mainRegion: string; subRegion: string; backupRoleName?: string; primaryKeyPendingWindow?: Duration; replicaKeyPendingWindow?: Duration; mainRegionBackupVaultName?: string; subRegionBackupVaultName?: string; cronExpression: string; backupTagKey: string; backupTagValue: string; removalPolicy?: RemovalPolicy; } export class BackupStage extends Stage { constructor(scope: Construct, id: string, props: BackupStageProps) { super(scope, id, props); const backupRoleStack = new BackupRoleStack(this, 'BackupRoleStack', { backupRoleName: props.backupRoleName, env: { region: props.mainRegion, }, }); const primaryKeyStack = new PrimaryKeyStack(this, 'PrimaryKeyStack', { keyAdminUserArns: props.keyAdminUserArns, subRegion: props.subRegion, backupRole: backupRoleStack.backupRole, pendingWindow: props.primaryKeyPendingWindow, removalPolicy: props.removalPolicy, env: { region: props.mainRegion, }, }); const replicaKeyStack = new ReplicaKeyStack(this, 'ReplicaKeyStack', { keyAdminUserArns: props.keyAdminUserArns, backupRoleStack, primaryKeyStack, pendingWindow: props.replicaKeyPendingWindow, removalPolicy: props.removalPolicy, env: { region: props.subRegion, }, }); replicaKeyStack.addDependency(backupRoleStack); replicaKeyStack.addDependency(primaryKeyStack); const subRegionBackupStack = new SubRegionBackupStack(this, 'SubRegionBackupStack', { keyArn: replicaKeyStack.key.attrArn, backupVaultName: props.subRegionBackupVaultName, removalPolicy: props.removalPolicy, env: { region: props.subRegion, }, }); const mainRegionBackupStack = new MainRegionBackupStack(this, 'MainRegionBackupStack', { subRegionBackupStack, keyArn: primaryKeyStack.key.keyArn, backupRoleArn: backupRoleStack.backupRole.roleArn, backupVaultName: props.mainRegionBackupVaultName, cronExpression: props.cronExpression, backupTagKey: props.backupTagKey, backupTagValue: props.backupTagValue, removalPolicy: props.removalPolicy, env: { region: props.mainRegion, }, }); mainRegionBackupStack.addDependency(subRegionBackupStack); } }
BackupStage
はAWS Backupのクロスリージョンバックアップの構成を一括で構築するためのステージです。- 29~34行目:バックアップロールを作成するスタック
BackupRoleStack
を作成します。 - 36~45行目:バックアップ元リージョンで使用するキーを作成するスタック
PrimaryKeyStack
を作成します。 - 47~56行目:バックアップ先リージョンで使用するキーを作成するスタック
ReplicaKeyStack
を作成します。 - 57~58行目:
ReplicaKeyStack
はBackupRoleStack
とPrimaryKeyStack
に暗黙的に依存するため、依存関係を追加します。- この暗黙的な依存はクロスリージョン参照によるものです。
- 60~67行目:バックアップ先リージョンのバックアップボールトを作成するスタック
SubRegionBackupStack
を作成します。 - 69~81行目:バックアップ元リージョンのバックアップジョブを作成するスタック
MainRegionBackupStack
を作成します。 - 82行目:
MainRegionBackupStack
はSubRegionBackupStack
に暗黙的に依存するため、依存関係を追加します。- この暗黙的な依存もクロスリージョン参照によるものです。
デプロイ
bin/cross-region-backup.ts
内のBackupStage
のパラメータkeyAdminUserArns
を適切な値に変更してから、以下のコマンドを実行することでデプロイします。
npm install npm run cdk deploy BackupStage/**
デプロイ後、テスト用のEC2インスタンスを作成し、Backup
タグに値True
を設定します。(バックアップ対象のタグキーとタグ値はデプロイ時のパラメータで変更可能です)
デプロイ時に指定した時刻になるとバックアップが実行され、バックアップ元リージョンのバックアップボールトに復旧ポイントが作成されます。
その後、バックアップ先リージョンのバックアップボールトにも復旧ポイントが作成されます。
復元の際はデフォルトのIAMロールで無く、BackupRoleStack
で作成したバックアップロールを指定する必要があるためご注意ください。
おわりに
今回はAWS Backupのクロスリージョンバックアップの構成を一撃で構築するCDKアプリケーションを作成してみました。
このブログが誰かの助けになれば幸いです。