はじめに
こんにちは、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
ソースコード
lib/stack/backup-role-stack.ts
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
ソースコード
lib/stack/primary-key-stack.ts
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
ソースコード
lib/stack/replica-key-stack.ts
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
ソースコード
lib/stack/sub-region-backup-stack.ts
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
ソースコード
lib/stack/main-region-backup-stack.ts
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
ソースコード
lib/stage/backup-stage.ts
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アプリケーションを作成してみました。
このブログが誰かの助けになれば幸いです。