はじめに
この記事では、RDS DB インスタンスの復元テストを自動化する方法を紹介します。『AWS Backup で復元テストを自動化しよう!』の続編として、第 2 弾の記事です。
前回の記事では、次の内容を解説しました。
- 復元テストの自動化が必要な理由
- AWS Backup の復元テスト機能の概要
- EC2 インスタンスの復元テストを自動化する方法
これらを活かして、他のリソースタイプである RDS に応用します。
今回は、次の内容を解説します。
- RDS DB インスタンスの復元テストを自動化する方法
- AWS CDK (TypeScript) を使ったサンプルコード
RDS のポイントに絞って解説します。前提となる知識は、前回の記事を参照してください。
対象読者
次の開発者を対象としています。
- AWS Backup の基本機能(バックアップおよびリストア)を理解している方
- AWS Backup の復元テスト機能を知りたい、または利用を検討している方
- AWS CDK(TypeScript)の基本知識があり、利用経験がある方
- AWS CDK と AWS Backup を使った復元テストの自動化を検討している方
RDS DB インスタンスの復元テストを自動化する
前回の記事と同じように AWS Backup の復元テスト機能を活用して、RDS DB インスタンスの復元テストを自動化する方法を紹介します。復元テストを自動化するには、復元テスト環境を構築します。AWS CDK (TypeScript)を使って、次のようなリソースを構築し、最後に実行確認します。
-
バックアップ関連
- ネットワーク(VPC、セキュリティグループなど)
- RDS DB インスタンス
- AWS Backup のバックアップランなど
-
復元テスト関連
- AWS Backup の復元テストプランなど
- 復元リソース検証の仕組み(イベントルール、Lambda 関数)
さらに詳細は、次のように解説します。
- 構成図
- 処理の流れ
- 実行確認
構成図
AWS CDK で構築する復元テスト環境の構成図です。上部「バックアップ」、下部「復元テスト」の 2 つで構成されています。前回の記事との違いは、バックアップおよび復元テストの対象を RDS DB インスタンスにしています。
構成のポイント:
- AWS Backup バックアッププランによる RDS DB インスタンスのバックアップ
- AWS Backup 復元テストプランによる RDS DB インスタンスの自動復元と削除
- Lambda 関数 (TypeScript) による復元された RDS DB インスタンスの検証と結果報告
処理の流れ
構成図に沿って、処理の流れを解説します。
-
バックアップ
AWS Backup のバックアッププランに基づいて、RDS DB インスタンスのバックアップジョブが実行されます -
復元
AWS Backup の復元テストプランに基づいて、RDS DB インスタンスの復元ジョブが実行されます -
イベント発行
RDS DB インスタンスの復元が完了すると、AWS Backup から完了イベントが発行されます -
Lambda 実行
EventBridge のルールに基づいて、復元完了イベントをトリガーとして Lambda 関数(検証プログラム)が実行されます -
復元リソース検証
Lambda 関数(検証プログラム)は、復元された RDS DB インスタンスの状態を確認します(Lambda からの DB 接続を確認) -
検証結果報告
AWS Backup API の PutRestoreValidationResult を使って、検証結果を AWS Backup に報告します -
復元リソース削除
復元リソース検証が完了、または保存時間に達すると、復元された RDS DB インスタンスは自動的に削除されます
実行確認
AWS CDK で構築した環境にて、復元テストの実行を確認します。実行タイミングになると、はじめに復元ジョブが自動実行され、DB インスタンスが復元されます。つぎに検証がおこなわれ、最後に自動削除されます。
たとえば、AWS Backup マネジメントコンソールで、次の状態を確認できます。
- 復元ステータス: 完了
- 検証ステータス: 成功
- 削除ステータス: 成功
CDK で復元テスト環境を構築する
AWS CDK (TypeScript) を使用して、復元テスト環境を構築します。ここでは、復元テストにポイントを絞って、3 ~ 5 を解説します。1 ~ 2 を詳しく知りたい方は、参考資料で確認してください。
-
採用した CDK 実装方法 (参考資料)
CDK プロジェクト構成、使用ライブラリなど -
バックアップの CDK 実装 (参考資料)
ネットワーク、RDS DB インスタンス、バックアッププランなど -
復元テストの CDK 実装
復元テストプラン、復元リソース検証の仕組みを実装します -
復元リソース検証の Lambda コード
RDS DB インスタンスの検証プログラム(Lambda からの DB 接続を確認)を実装します -
復元テストの実行
CDK デプロイ、復元テストの自動実行を確認します
復元テストの AWS CDK 実装
構成図
復元テストの AWS CDK 実装は、次の構成図を対象とします。
復元テストプラン
構成図に基づいて、AWS Backup の復元テストプランを作成します。復元テストプランの作成には、AWS CDK L1 コンストラクトを使います。L2 コンストラクトは未対応です。(2025 年 2 月 6 日時点)
復元テストプランの設定ポイントは、次の項目です。
-
- scheduleExpression:
テスト頻度、開始時間(時分)
- startWindowHours:
次の時間以内に開始(時間単位)
- scheduleExpression:
-
- protectedResourceType:
リソースタイプ( RDS )
- restoreMetadataOverrides:
復元パラメータ
- dbSubnetGroupName:
サブネットグループ
- securityGroupIds:
'["' + セキュリティグループID + '"]'
- dbSubnetGroupName:
- protectedResourceType:
ここでは、AWS CDK の全サンプルコードから、「復元テストプラン」の実装(コンストラクト名:RestoreTestRds)に絞って紹介します。コンストラクトの入力パラメータなど、詳しく知りたい方は「バックアップの CDK 実装(参考資料)」で確認してください。
それでは、次のサンプルコードを見てみましょう。
import { Construct } from 'constructs';
import * as backup from 'aws-cdk-lib/aws-backup';
import * as cwe from 'aws-cdk-lib/aws-events';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as rds from 'aws-cdk-lib/aws-rds';
// Interface for RestoreTestRds
export interface RestoreTestRdsProps {
readonly backupRole: iam.IRole;
readonly backupVault: backup.IBackupVault;
readonly scheduleExpression: cwe.Schedule;
readonly vpc: ec2.IVpc;
readonly rdsSg: ec2.ISecurityGroup;
readonly rdsSubnetGroup: rds.ISubnetGroup;
}
// Class for RestoreTestRds
export class RestoreTestRds extends Construct {
constructor(scope: Construct, id: string, props: RestoreTestRdsProps) {
super(scope, id);
// CfnRestoreTestingPlan
const restoreTestingPlan = new backup.CfnRestoreTestingPlan(this, 'RestoreTestingPlanRds', {
restoreTestingPlanName: 'RestoreTestingPlanRds',
scheduleExpression: props.scheduleExpression.expressionString, // 例:第2金曜日 00:45 from prameter.ts
startWindowHours: 1,
recoveryPointSelection: {
algorithm: 'LATEST_WITHIN_WINDOW',
includeVaults: [props.backupVault.backupVaultArn],
recoveryPointTypes: ['SNAPSHOT'],
selectionWindowDays: 31,
},
});
// CfnRestoreTestingSelection
const restoreTestingSelection = new backup.CfnRestoreTestingSelection(this, 'RestoreTestingSelectionRds', {
restoreTestingPlanName: 'RestoreTestingPlanRds',
restoreTestingSelectionName: 'RestoreTestingSelectionRds',
iamRoleArn: props.backupRole.roleArn,
validationWindowHours: 1,
protectedResourceType: 'RDS',
protectedResourceConditions: {
stringEquals: [
{
key: 'aws:ResourceTag/Restore-test-rds',
value: 'true',
},
],
},
restoreMetadataOverrides: {
availabilityZone: props.vpc.selectSubnets({
subnetGroupName: 'Protected',
}).availabilityZones[1],
dbSubnetGroupName: props.rdsSubnetGroup.subnetGroupName,
multiAz: 'false',
vpcSecurityGroupIds: '["' + props.rdsSg.securityGroupId + '"]',
},
});
restoreTestingSelection.addDependency(restoreTestingPlan);
}
}
復元リソース検証の仕組み
構成図に基づいて、復元リソース検証の仕組みを作成します。AWS CDK の L2 コンストラクトを使います。Lambda 関数と EventBridge ルールを組合わせます。はじめに検証プログラムの Lambda 関数を作成して、その後 EventBridge ルールを作成します。これは依存関係があるからです。
EventBridge ルールは、次の設定をします。
- イベントパターン設定
AWS Backup 復元ジョブの完了イベント
- ターゲット設定
Lambda 関数
ここでは、AWS CDK の全サンプルコードから、「復元リソース検証の仕組み」の実装(コンストラクト名:RdsValidate)に絞って紹介します。コンストラクトの入力パラメータなど、詳しく知りたい方は「バックアップの CDK 実装(参考資料)」で確認してください。
それでは、次のサンプルコードを見てみましょう。
import { Construct } from 'constructs';
import { Duration, Stack } from 'aws-cdk-lib';
import * as cwe from 'aws-cdk-lib/aws-events';
import * as cwet from 'aws-cdk-lib/aws-events-targets';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as node_lambda from 'aws-cdk-lib/aws-lambda-nodejs';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as sm from 'aws-cdk-lib/aws-secretsmanager';
import { NagSuppressions } from 'cdk-nag';
// Interface for RdsValidateProps
export interface RdsValidateProps {
readonly vpc: ec2.IVpc;
readonly lambdaSg: ec2.ISecurityGroup;
readonly rdsSg: ec2.ISecurityGroup;
readonly rdsSecret: sm.ISecret;
}
// Class for RdsValidate
export class RdsValidate extends Construct {
constructor(scope: Construct, id: string, props: RdsValidateProps) {
super(scope, id);
// RDS Validate Function
const rdsValidateFunction = new node_lambda.NodejsFunction(this, 'RdsValidateFunction', {
runtime: lambda.Runtime.NODEJS_20_X,
entry: 'lambda/rds-validate.ts',
handler: 'handler',
timeout: Duration.seconds(30),
tracing: lambda.Tracing.ACTIVE,
insightsVersion: lambda.LambdaInsightsVersion.VERSION_1_0_98_0,
layers: [
lambda.LayerVersion.fromLayerVersionArn(
this,
'PowertoolsLayer',
`arn:aws:lambda:${Stack.of(this).region}:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:7`,
),
],
bundling: {
minify: true,
sourceMap: true,
externalModules: ['@aws-lambda-powertools/*', '@aws-sdk/*'],
},
vpc: props.vpc,
vpcSubnets: props.vpc.selectSubnets({
subnetGroupName: 'Protected',
}),
securityGroups: [props.lambdaSg],
environment: {
RDS_SECRET_ARN: props.rdsSecret.secretArn,
NODE_OPTIONS: '--enable-source-maps',
POWERTOOLS_SERVICE_NAME: 'RdsValidate',
},
});
// grantRead
props.rdsSecret.grantRead(rdsValidateFunction);
// addToRolePolicy
rdsValidateFunction.addToRolePolicy(
new iam.PolicyStatement({
actions: ['rds:DescribeDBInstances', 'backup:PutRestoreValidationResult'],
resources: ['*'],
}),
);
// Event roule "Restore Job State Change" from AWS Backup
const rdsValidateRule = new cwe.Rule(this, 'RdsValidateRule', {
description: 'Restore Job State Change',
eventPattern: {
source: ['aws.backup'],
detailType: ['Restore Job State Change'],
detail: {
resourceType: ['RDS'],
status: ['COMPLETED'],
},
},
targets: [new cwet.LambdaFunction(rdsValidateFunction)],
});
rdsValidateRule.node.addDependency(rdsValidateFunction);
// cdk-nag suppressions
NagSuppressions.addResourceSuppressions(rdsValidateFunction, [
{
id: 'AwsSolutions-L1',
reason: 'Need use node v20 for vpc Lambda',
},
]);
NagSuppressions.addResourceSuppressions(
rdsValidateFunction,
[
{
id: 'AwsSolutions-IAM4',
reason: 'Need the policy for vpc Lambda',
},
{
id: 'AwsSolutions-IAM5',
reason: 'Need the policy for vpc Lambda',
},
],
true,
);
}
}
復元リソース検証の Lambda コード
復元リソース検証の Lambda コードを解説します。はじめに復元ジョブの完了イベントを解説します。これは、Lambda コードで使うインプット項目のためです。次に Lambda コードを解説します。
-
復元ジョブの完了イベント
JSON 形式のイベントです。復元ジョブ ID、復元リソース ARN などが含まれます。Lambda コードで使います -
Lambda コード
復元ジョブの完了イベントから、復元ジョブ ID などを取得して、復元リソースの検証します。次に検証結果を復元ジョブに設定します
復元ジョブの完了イベント
AWS Backup 復元ジョブが完了すると、イベントが発生します。このイベントを使って、EventBridge ルールから Lambda 関数が自動実行されます。
EventBridge ルールの設定には、次の項目を使います。
- detailType:
Restore Job State Change
- detail: { resourceType:
RDS
} - detail: { status:
COMPLETED
}
Lambda コードでは、次の項目を使います。
- detail: { restoreJobId:
復元ジョブ ID
} - detail: { createdResourceArn:
復元リソース ARN
}
イベントのサンプルは、次の JSON です。
{
"version": "0",
"id": "36f853ab-75f2-3ed7-7181-de678c649996",
"detail-type": "Restore Job State Change",
"source": "aws.backup",
"account": "111223344556",
"time": "2025-02-14T01:34:51Z",
"region": "ap-northeast-1",
"resources": ["arn:aws:rds:ap-northeast-1:111223344556:snapshot:awsbackup:job-dd4a7563-4c40-4502-ac68-8b8b8b8b8b8b"],
"detail": {
"restoreJobId": "7A3EE9A8-F287-DCAB-048E-7F563586AB43",
"backupSizeInBytes": "0",
"creationDate": "2025-02-14T01:18:09.673Z",
"iamRoleArn": "arn:aws:iam::111223344556:role/Dev-ResearchAwsBackup-IamBackupRole95AD45E4-ZSvUgznWBuyh",
"percentDone": 0,
"resourceType": "RDS",
"status": "COMPLETED",
"createdResourceArn": "arn:aws:rds:ap-northeast-1:111223344556:db:awsbackup-restore-test-70ddc7db-be6d-4583-955a-abe12b28c4a8",
"completionDate": "2025-02-14T01:26:37.320972478Z",
"restoreTestingPlanArn": "arn:aws:backup:ap-northeast-1:111223344556:restore-testing-plan:RestoreTestingPlanRds-43d031f4-8059-4cd0-a218-a46faf9bd2cf",
"backupVaultArn": "arn:aws:backup:ap-northeast-1:111223344556:backup-vault:DevResearchAwsBackupBackupVaultsRds346435CF",
"recoveryPointArn": "arn:aws:rds:ap-northeast-1:111223344556:snapshot:awsbackup:job-dd4a7563-4c40-4502-ac68-8b8b8b8b8b8b",
"sourceResourceArn": "arn:aws:rds:ap-northeast-1:111223344556:db:dev-researchawsbackup-rdspostgresqlcc75dd8b-yd8bvuxecbdb"
}
}
Lambda コード
Lambda コードは、復元された DB インスタンスを検証します。検証処理は、DB インスタンスに接続できることを確認します。言語には TypeScript を使います。採用の理由は、型チェックやコード補完など、効率よくコーディングできるからです。
次の AWS 公式ブログの Python コードを参考にしています。
処理の流れは、次ようになります。
-
RDS DB インスタンス の エンドポイントを取得
イベントの復元リソース ARN
から RDS DB インスタンスの DB 識別子を抽出、RDS API を使って RDS DB インスタンスのエンドポイントを取得します -
復元リソースの検証
復元された DB インスタンスへの接続を検証します -
検証結果の報告
イベントの復元ジョブ ID
を基に、AWS Backup API の PutRestoreValidationResult を使って、検証結果を AWS Backup の復元ジョブに設定します
Lambda サンプルコードは、次です。
import { EventBridgeEvent } from 'aws-lambda';
import middy from '@middy/core';
import { Logger } from '@aws-lambda-powertools/logger';
import { injectLambdaContext } from '@aws-lambda-powertools/logger/middleware';
import { getSecret } from '@aws-lambda-powertools/parameters/secrets';
import { RDSClient, DescribeDBInstancesCommand } from '@aws-sdk/client-rds';
import { BackupClient, PutRestoreValidationResultCommand, RestoreValidationStatus } from '@aws-sdk/client-backup';
import { Client } from 'pg';
// Logger
const logger = new Logger();
// Restore Job event State: COMPLETED
// See: https://docs.aws.amazon.com/aws-backup/latest/devguide/eventbridge.html#restore-job-state-change-completed
// type Restore event detail for EventBridgeEvent
type RestoreEventDetail = {
restoreJobId: string;
createdResourceArn: string;
};
// Secret Value for DB credentials
type SecretValue = {
password: string;
dbname: string;
engine: string;
port: number;
dbInstanceIdentifier: string;
host: string;
username: string;
};
// lambdaHandler
const lambdaHandler = async (event: EventBridgeEvent<string, RestoreEventDetail>): Promise<void> => {
// Get db instance ID from event
const dbInstanceId = event.detail.createdResourceArn.split(':')[6];
logger.info(`DB instance ID: ${dbInstanceId}`);
// Get endpoint from RDS
let endpoint: string;
const rdsClient = new RDSClient({});
try {
const describeDBInstances = await rdsClient.send(
new DescribeDBInstancesCommand({
DBInstanceIdentifier: dbInstanceId,
}),
);
logger.info('DescribeDBInstances', { describeDBInstances: describeDBInstances });
// endpointAddress from describeDBInstances
endpoint = describeDBInstances.DBInstances?.[0].Endpoint?.Address || '';
} catch (err) {
logger.error('Error Describe DB instances', err as Error);
throw err;
}
logger.info(`DB instance endpoint: ${endpoint}`);
// Get db instance secret from Secrets Manager
const rdsSecretArn = process.env.RDS_SECRET_ARN || '';
logger.info(`RDS Secret ARN: ${rdsSecretArn}`);
let secrets: SecretValue;
try {
const response = await getSecret<string>(rdsSecretArn, { forceFetch: true });
secrets = JSON.parse(response || '');
logger.info(`DB engin: ${secrets.engine}`);
} catch (err) {
logger.error('Error getSecret', err as Error);
throw err;
}
// Connect to db and validate db instance
const client = new Client({
user: secrets.username,
host: endpoint,
database: secrets.dbname,
password: secrets.password,
port: secrets.port,
ssl: { rejectUnauthorized: false },
});
let validationStatus: RestoreValidationStatus | undefined = undefined;
try {
// Connect DB
await client.connect();
logger.info('Connection to DB successful');
const res = await client.query('SELECT CURRENT_TIMESTAMP;');
logger.info('Query Result', { res: res.rows[0] });
validationStatus = 'SUCCESSFUL';
} catch (err) {
logger.error('Error connect to DB', err as Error);
validationStatus = 'FAILED';
} finally {
await client.end();
}
// Put Restore Validation Result
const backupClient = new BackupClient({});
try {
const putRestoreValidationResult = await backupClient.send(
new PutRestoreValidationResultCommand({
RestoreJobId: event.detail.restoreJobId,
ValidationStatus: validationStatus,
}),
);
logger.info('PutRestoreValidationResult', { putRestoreValidationResult: putRestoreValidationResult });
} catch (err) {
logger.error('Error putRestoreValidationResult', err as Error);
throw err;
}
};
// handler
export const handler = middy(lambdaHandler).use(injectLambdaContext(logger, { logEvent: true }));
復元テストの実行確認
ここまでに準備した AWS CDK コードを元に、次の実行までを確認します。
- AWS CDK デプロイ実行
- バックアップジョブ実行
それでは最後に、復元テストの実行です。次の確認をします。
- 復元ジョブ実行
- 復元リソース検証
- 復元リソース削除
復元ジョブ実行
- AWS Backup の復元テストプランに基づき、復元ジョブが実行されます
- 復元パラメータ: サブネットグループ、セキュリティグループ
- リソース(RDS DB インスタンス)が復元され、復元ジョブが完了すると、AWS Backup の完了イベントが発行されます
- ステータス(復元)が「実行中」→「完了」に変更されます
- リソース(RDS DB インスタンス)の復元が確認できます
復元リソース検証
- EventBridge のルールによって、Lambda(検証プログラム)が起動されます
- 検証プログラムによって、復元されたリソースの状態が確認されます。(DB 接続の確認)
- AWS SDK(AWS Backup API)によって、検証結果が AWS Backup に報告されます
- 検証ステータスが「検証中」→「成功」に変更されます
復元リソース削除
- 検証が完了、または時限超過すると、復元リソース(RDS DB インスタンス)が削除されます
- 検証ステータスが「削除中」→「成功」に変更されます
- リソース(RDS)の削除(終了)が確認できます
おわりに
ここまで、『AWS Backup で復元テストを自動化しよう!』の続編として、RDS DB インスタンスの復元テストを自動化する方法を紹介しました。
RDS の復元テストでは、復元時間の検証も重要です。データサイズが大きい場合、復元時間が長くなることも考えられます。この記事にはその点が含まれていませんので、ぜひ皆さんも検討していただければ幸いです。
また、この記事の作成にあたり、AWS 公式ドキュメントの情報を参考にさせていただきました。AWS Blog、サンプルコードなどの詳細なドキュメントに感謝いたします。
最後までお読みいただき、ありがとうございました。
参考資料
AWS Document
-
AWS Well-Architected フレームワーク: REL09-BP04 データの定期的な復旧を行ってバックアップの完全性とプロセスを確認する
-
AWS CDK Reference: class CfnRestoreTestingSelection (construct)
-
AWS CloudFormation User Guide: EC2 推定メタデータ上書きの書式:SecurityGroupIds
-
AWS SDK for JavaScript v3: PutRestoreValidationResultCommand
AWS Blog
Powertools for AWS Lambda
採用した CDK 実装方法
CDK 実装方法は、次のとおり解説します。
- 全体方針
- プロジェクト構成
折りたたみを展開して表示して下さい。
全体方針
Baseline Environment on AWS v3 のゲストシステム を参考としてます。
- CDK コードは TypeScript を使用
- 環境ごとのパラメータは、TypeScript で定義
- 1 つのスタックと複数のコンストラクトで構成
- L2 コンストラクトを使用し、未対応の部分は L1 コンストラクトを使用
- バックアッププランのリソース選択
- 復元テストプランのリソース選択
- スナップショットテストの実施
今回、コード開発を効率化する工夫をしています。
工夫点:
- 試行錯誤が必要なコンストラクトは、パラメータでデプロイ有無を制御する
- cdk-nagを採用してコードチェックする
- Lambda 関数コードは、TypeScript を使用する
- Powertools for AWS Lambdaを採用して標準化する
プロジェクト構成
プロジェクト構成は、次のとおり解説します。
- 実行環境
- ファイル構成
実行環境
次のライブラリを使用しています。
No.1〜4 は CDK 実装で一般的に利用されるもので、No.5〜9 は Lambda 関数(検証プログラム)で使用します。
No. | ライブラリ | バージョン |
---|---|---|
1 | aws-cdk | 2.173.2 |
2 | jest | 29.7.0 |
3 | cdk-nag | 2.28.115 |
4 | esbuild | 0.24.0 |
5 | @types/aws-lambda | 8.10.137 |
6 | @aws-lambda-powertools/logger | 2.1.1 |
7 | @middy/core | 4.7 |
8 | @aws-sdk/client-backup | 3.556.0 |
9 | @aws-sdk/client-ec2 | 3.557.0 |
ファイル構成
前回の記事に、RDS DB インスタンスの復元テストを追加したため、ファイル構成が増えています。
research-awsbackup
├── bin
│ └── research-awsbackup.ts # CDK App
├── lambda
│ ├── ec2-validate.ts # EC2: Lambdaコード
│ └── rds-validate.ts # RDS: Lambdaコード
├── lib
│ ├── construct
│ │ ├── backup-ec2.ts # EC2: AWS Backupバックアッププラン
│ │ ├── backup-rds.ts # RDS: AWS Backupバックアッププラン
│ │ ├── backup-vaults.ts # AWS Backupバックアップボールト
│ │ ├── ec2-app.ts # EC2: EC2インスタンス
│ │ ├── ec2-validate.ts # EC2: Lambda, EventBridgeルール
│ │ ├── iam.ts # IAMロール
│ │ ├── networking.ts # VPC、サブネットなどのネットワーク、セキュリティグループ
│ │ ├── rds-postgresql.ts # RDS: EC2インスタンス
│ │ ├── rds-validate.ts # RDS: Lambda, EventBridgeルール
│ │ └── restore-test-ec2.ts # EC2: AWS Backup復元テストプラン
│ │ └── restore-test-rds.ts # RDS: AWS Backup復元テストプラン
│ │ └── secrets.ts # RDS: AWS Secrets Manager シークレット
│ └── stack
│ └── research-awsbackup-stack.ts # スタック
├── test
│ └── research-awsbackup.test.ts # スナップショットテスト
└── paramater.ts # 環境パラメータ
バックアップの CDK 実装
バックアップの CDK 実装は、次のとおり解説します。
- 構成図
- パラメータ
- App
- スタック
- テストコード
- コンストラクト
折りたたみを展開して表示して下さい。
構成図
バックアップの CDK 実装は、構成図に示す範囲を対象とします。
ネットワークは、復元テストの範囲も含みます。
パラメータ
稼働環境ごとのパラメータを設定します。
- AWS アカウント
- リージョン
- VPC CIDR
- デプロイフラグ
試行錯誤が必要なコンストラクトは、パラメータでデプロイ有無を制御します。
- RDS 復元テストに関連するコンストラクトは「deploy: true」でデプロイされます。
- EC2 復元テストに関連するコンストラクトは「deploy: false」でデプロイされません。
コードは、折りたたみを展開して表示して下さい。
import { Environment } from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as cwe from 'aws-cdk-lib/aws-events';
// Interface for Ec2AppParameter
export interface Ec2AppParameter {
instanceType: ec2.InstanceClass;
instanceSize: ec2.InstanceSize;
deploy: boolean;
}
// Interface for BackupParameter
export interface BackupParameter {
scheduleExpression: cwe.Schedule;
deploy: boolean;
}
// Interface for RestoreTestParameter
export interface RestoreTestParameter {
scheduleExpression: cwe.Schedule;
deploy: boolean;
}
// Interface for ValidateParameter
export interface ValidateParameter {
deploy: boolean;
}
// Interface for RdsParameter
export interface RdsParameter {
instanceType: ec2.InstanceClass;
instanceSize: ec2.InstanceSize;
deploy: boolean;
}
// Interface for App Parameter
export interface AppParameter {
env: Environment;
envName: string;
vpcCidr: string;
ec2App: Ec2AppParameter;
backupEc2: BackupParameter;
restoreTestEc2: RestoreTestParameter;
ec2Validate: ValidateParameter;
rds: RdsParameter;
backupRds: BackupParameter;
restoreTestRds: RestoreTestParameter;
rdsValidate: ValidateParameter;
}
// Define for Dev Parameter
export const devParameter: AppParameter = {
env: {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: 'ap-northeast-1',
},
envName: 'Development',
vpcCidr: '10.0.0.0/16',
ec2App: {
instanceType: ec2.InstanceClass.T3,
instanceSize: ec2.InstanceSize.MICRO,
deploy: false,
},
backupEc2: {
scheduleExpression: cwe.Schedule.cron({
minute: '00',
hour: '05',
weekDay: 'MON-FRI',
}),
deploy: false,
},
restoreTestEc2: {
scheduleExpression: cwe.Schedule.cron({
minute: '00',
hour: '06',
weekDay: 'FRI#4',
}),
deploy: false,
},
ec2Validate: {
deploy: false,
},
rds: {
instanceType: ec2.InstanceClass.T3,
instanceSize: ec2.InstanceSize.SMALL,
deploy: true,
},
backupRds: {
scheduleExpression: cwe.Schedule.cron({
minute: '00',
hour: '00',
weekDay: 'MON-FRI',
}),
deploy: true,
},
restoreTestRds: {
scheduleExpression: cwe.Schedule.cron({
minute: '45',
hour: '00',
weekDay: 'FRI#2',
}),
deploy: true,
},
rdsValidate: {
deploy: true,
},
};
App
CDK の起点として、次を実施します。
- コード検証ツール設定
- スタックのインスタンス化
コードは、折りたたみを展開して表示して下さい。
#!/usr/bin/env node
import { App, Aspects } from 'aws-cdk-lib';
import { AwsSolutionsChecks } from 'cdk-nag';
import { ResearchAwsBackupStack } from '../lib/stack/research-awsbackup-stack';
import { devParameter } from '../parameter';
const app = new App();
Aspects.of(app).add(new AwsSolutionsChecks({}));
new ResearchAwsBackupStack(app, 'Dev-ResearchAwsBackup', {
env: {
account: devParameter.env.account,
region: devParameter.env.region,
},
appParameter: devParameter,
});
スタック
1 スタック構成です。
このスタックでは、7 つのコンストラクトをインスタンス化します。
No.4 ~ 7 のコンストラクトは、デプロイフラグに応じてインスタンス化を制御します。
No. | コンストラクト | 内容 |
---|---|---|
1 | Networking | VPC、サブネットなどのネットワーク、セキュリティグループ |
2 | Iam | AWS Backup 用の IAM ロール |
3 | BackupVaults | AWS Backup Vaults |
4 | RdsPostgreSql | RDS: DB インスタンス |
5 | BackupEc2 | RDS: AWS Backup のバックアッププラン |
6 | RestoreTestRds | RDS: AWS Backup の復元テストプラン |
7 | RDSValidate | RDS: Lambda, EventBridge ルール |
コードは、折りたたみを展開して表示して下さい。
import { Construct } from 'constructs';
import { Stack, StackProps } from 'aws-cdk-lib';
import { AppParameter } from '../../parameter';
import { Networking } from '../construct/networking';
import { Iam } from '../construct/iam';
import { Ec2App } from '../construct/ec2-app';
import { BackupEc2 } from '../construct/backup-ec2';
import { Ec2Validate } from '../construct/ec2-validate';
import { BackupVaults } from '../construct/backup-vaults';
import { RestoreTestEc2 } from '../construct/restore-test-ec2';
import { RdsPostgreSql } from '../construct/rds-postgresql';
import { BackupRds } from '../construct/backup-rds';
import { RestoreTestRds } from '../construct/restore-test-rds';
import { RdsValidate } from '../construct/rds-validate';
import { Secrets } from '../construct/secrets';
// Interface for ResarchAwsBackupStackProps
export interface ResearchAwsBackupStackProps extends StackProps {
readonly appParameter: AppParameter;
}
// Class for ResarchAwsBackupStack
export class ResearchAwsBackupStack extends Stack {
constructor(scope: Construct, id: string, props: ResearchAwsBackupStackProps) {
super(scope, id, props);
// Networking
const networking = new Networking(this, 'Networking', {
vpcCidr: props.appParameter.vpcCidr,
});
// Iam
const iam = new Iam(this, 'Iam');
// Secrets
const secrets = new Secrets(this, 'Secrets');
// BackupVaults
const backupVaults = new BackupVaults(this, 'BackupVaults');
// Ec2App
if (props.appParameter.ec2App.deploy == true) {
new Ec2App(this, 'Ec2App', {
vpc: networking.vpc,
ec2AppSg: networking.ec2AppSg,
ssmInstanceRole: iam.ssmInstanceRole,
instancdType: props.appParameter.ec2App.instanceType,
instanceSize: props.appParameter.ec2App.instanceSize,
});
}
// BackupEc2
if (props.appParameter.backupEc2.deploy == true) {
new BackupEc2(this, 'BackupEc2', {
backupRole: iam.backupRole,
backupVault: backupVaults.ec2,
scheduleExpression: props.appParameter.backupEc2.scheduleExpression,
});
}
// RestoreTestEc2
if (props.appParameter.restoreTestEc2.deploy == true) {
new RestoreTestEc2(this, 'RestoreTestEc2', {
backupRole: iam.backupRole,
backupVault: backupVaults.ec2,
scheduleExpression: props.appParameter.restoreTestEc2.scheduleExpression,
vpc: networking.vpc,
ec2AppSg: networking.ec2AppSg,
});
}
// Ec2Validate
if (props.appParameter.ec2Validate.deploy == true) {
new Ec2Validate(this, 'Ec2Validate', {
vpc: networking.vpc,
lambdaSg: networking.lambdaSg,
ec2AppSg: networking.ec2AppSg,
});
}
// RdsPostgreSql
if (props.appParameter.rds.deploy == true) {
new RdsPostgreSql(this, 'RdsPostgreSql', {
vpc: networking.vpc,
rdsSg: networking.rdsSg,
rdsSubnetGroup: networking.rdsSubnetGroup,
rdsSecret: secrets.secretPostgreSql,
instancdType: props.appParameter.rds.instanceType,
instanceSize: props.appParameter.rds.instanceSize,
});
}
// BackupRds
if (props.appParameter.backupRds.deploy == true) {
new BackupRds(this, 'BackupRds', {
backupRole: iam.backupRole,
backupVault: backupVaults.rds,
scheduleExpression: props.appParameter.backupRds.scheduleExpression,
});
}
// RestoreTestRds
if (props.appParameter.restoreTestRds.deploy == true) {
new RestoreTestRds(this, 'RestoreTestRds', {
backupRole: iam.backupRole,
backupVault: backupVaults.rds,
scheduleExpression: props.appParameter.restoreTestRds.scheduleExpression,
vpc: networking.vpc,
rdsSg: networking.rdsSg,
rdsSubnetGroup: networking.rdsSubnetGroup,
});
}
// RdsValidate
if (props.appParameter.rdsValidate.deploy == true) {
new RdsValidate(this, 'RdsValidate', {
vpc: networking.vpc,
lambdaSg: networking.lambdaSg,
rdsSg: networking.rdsSg,
rdsSecret: secrets.secretPostgreSql,
});
}
}
}
テストコード
スタックのスナップショットテストを実施します。
コードは、折りたたみを展開して表示して下さい。
import * as cdk from 'aws-cdk-lib';
import { Template } from 'aws-cdk-lib/assertions';
import { ResearchAwsBackupStack } from '../lib/stack/research-awsbackup-stack';
import { devParameter } from '../parameter';
// Snapshot test for ResearchAwsbackupStack
test('Snapshot test for ResearchAwsbackupStack', () => {
const app = new cdk.App();
const stack = new ResearchAwsBackupStack(app, 'Dev-ResearchAwsBackup', {
env: {
account: devParameter.env.account,
region: devParameter.env.region,
},
appParameter: devParameter,
});
expect(Template.fromStack(stack)).toMatchSnapshot();
});
Networking
VPC、サブネットなどのネットワーク、セキュリティグループを作成します。
コードは、折りたたみを展開して表示して下さい。
import { Construct } from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as rds from 'aws-cdk-lib/aws-rds';
import { NagSuppressions } from 'cdk-nag';
// Interface for NetwokingProps
interface NetworkingProps {
vpcCidr: string;
}
// Class for Networking
export class Networking extends Construct {
public readonly vpc: ec2.IVpc;
public readonly endPointSg: ec2.ISecurityGroup;
public readonly ec2AppSg: ec2.ISecurityGroup;
public readonly rdsSg: ec2.ISecurityGroup;
public readonly lambdaSg: ec2.ISecurityGroup;
public readonly rdsSubnetGroup: rds.ISubnetGroup;
constructor(scope: Construct, id: string, props: NetworkingProps) {
super(scope, id);
// Create a new VPC with the given CIDR
const vpc = new ec2.Vpc(this, 'Vpc', {
ipAddresses: ec2.IpAddresses.cidr(props.vpcCidr),
maxAzs: 2,
flowLogs: {},
subnetConfiguration: [
{
cidrMask: 24,
name: 'Protected',
subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
},
],
});
this.vpc = vpc;
// SubnetGroup for RDS
this.rdsSubnetGroup = new rds.SubnetGroup(this, 'RdsSubnetGroup', {
vpc: vpc,
vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED },
description: 'RDS SubnetGroup',
});
// Security Group for VPC Endpoint
const endPointSg = new ec2.SecurityGroup(this, 'EndpointSg', {
vpc: vpc,
allowAllOutbound: false,
});
this.endPointSg = endPointSg;
// Security Group for RDS
this.rdsSg = new ec2.SecurityGroup(this, 'RdsSg', {
vpc: vpc,
allowAllOutbound: false,
});
// Security Group for Ec2app
this.ec2AppSg = new ec2.SecurityGroup(this, 'Ec2AppSg', {
vpc: vpc,
allowAllOutbound: false,
});
this.ec2AppSg.addEgressRule(ec2.Peer.prefixList('pl-61a54008'), ec2.Port.tcp(443), 'EC2 to S3 VPCe');
this.ec2AppSg.connections.allowTo(this.endPointSg, ec2.Port.tcp(443), 'EC2 to VPCe');
this.ec2AppSg.connections.allowTo(this.rdsSg, ec2.Port.tcp(5432), 'EC2 to RDS');
// Security Group for Lambda
this.lambdaSg = new ec2.SecurityGroup(this, 'LambdaSg', {
vpc: vpc,
allowAllOutbound: false,
});
this.lambdaSg.connections.allowTo(this.endPointSg, ec2.Port.tcp(443), 'Lambda to VPCe');
this.lambdaSg.connections.allowTo(this.ec2AppSg, ec2.Port.tcp(80), 'Lambda to Ec2App');
this.lambdaSg.connections.allowTo(this.rdsSg, ec2.Port.tcp(5432), 'Lambda to RDS');
// VPC Endpoint for S3
vpc.addGatewayEndpoint('S3Endpoint', {
service: ec2.GatewayVpcEndpointAwsService.S3,
subnets: [{ subnetType: ec2.SubnetType.PRIVATE_ISOLATED }],
});
// VPC add Inteface Endpoint for SSM
vpc.addInterfaceEndpoint('SsmEndpoint', {
service: ec2.InterfaceVpcEndpointAwsService.SSM,
subnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED },
securityGroups: [endPointSg],
});
// VPC add Inteface Endpoint for SSM Messages
vpc.addInterfaceEndpoint('SsmMsgEndpoint', {
service: ec2.InterfaceVpcEndpointAwsService.SSM_MESSAGES,
subnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED },
securityGroups: [endPointSg],
});
// VPC add Inteface Endpoint for EC2
vpc.addInterfaceEndpoint('Ec2Endpoint', {
service: ec2.InterfaceVpcEndpointAwsService.EC2,
subnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED },
securityGroups: [endPointSg],
});
// VPC add Inteface Endpoint for EC2 Messages
vpc.addInterfaceEndpoint('Ec2MsgEndpoint', {
service: ec2.InterfaceVpcEndpointAwsService.EC2_MESSAGES,
subnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED },
securityGroups: [endPointSg],
});
// VPC add Interface Endpoint for SECRETS_MANAGER
vpc.addInterfaceEndpoint('SecretEndpoint', {
service: ec2.InterfaceVpcEndpointAwsService.SECRETS_MANAGER,
subnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED },
securityGroups: [endPointSg],
});
// VPC add Interface Endpoint for RDS
vpc.addInterfaceEndpoint('RdsEndpoint', {
service: ec2.InterfaceVpcEndpointAwsService.RDS,
subnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED },
securityGroups: [endPointSg],
});
// VPC add Inteface Endpoint for Backup
vpc.addInterfaceEndpoint('BackupEndpoint', {
service: ec2.InterfaceVpcEndpointAwsService.BACKUP,
subnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED },
securityGroups: [endPointSg],
});
// cdk-nag suppressions
NagSuppressions.addResourceSuppressions(vpc, [
{
id: 'AwsSolutions-VPC7',
reason: 'For development use',
},
]);
NagSuppressions.addResourceSuppressions(endPointSg, [
{
id: 'CdkNagValidationFailure',
reason: 'https://github.com/cdklabs/cdk-nag/issues/817',
},
]);
}
}
Iam
EC2、AWS Backup 用の IAM ロールを作成します。
AWS Backup 用の IAM ロールでは、バックアップだけでなく、復元の権限が必要です。
コードは、折りたたみを展開して表示して下さい。
import { Construct } from 'constructs';
import * as iam from 'aws-cdk-lib/aws-iam';
import { NagSuppressions } from 'cdk-nag';
// Class Construct for iam
export class Iam extends Construct {
public readonly ssmInstanceRole: iam.IRole;
public readonly backupRole: iam.IRole;
constructor(scope: Construct, id: string) {
super(scope, id);
// SsmInstanceRole
const ssmInstanceRole = new iam.Role(this, 'SsmInstanceRole', {
assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore'),
iam.ManagedPolicy.fromAwsManagedPolicyName('CloudWatchAgentServerPolicy'),
],
});
this.ssmInstanceRole = ssmInstanceRole;
// BackupRole
const backupRole = new iam.Role(this, 'BackupRole', {
assumedBy: new iam.ServicePrincipal('backup.amazonaws.com'),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSBackupServiceRolePolicyForBackup'),
iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSBackupServiceRolePolicyForRestores'),
],
// inlinePolices for PassRole ssmInstanceRole
inlinePolicies: {
ForRestorePolicy: new iam.PolicyDocument({
statements: [
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ['iam:PassRole'],
resources: [ssmInstanceRole.roleArn],
}),
],
}),
},
});
this.backupRole = backupRole;
// cdk-nag suppressions
NagSuppressions.addResourceSuppressions(
[this.ssmInstanceRole, this.backupRole],
[
{
id: 'AwsSolutions-IAM4',
reason: 'To use SSM for instance, this managed policy is required.',
},
],
);
}
}
BackupVaults
EC2 バックアップを管理するための AWS Backup ボールトを作成します。
コードは、折りたたみを展開して表示して下さい。
import { Construct } from 'constructs';
import { RemovalPolicy } from 'aws-cdk-lib';
import * as backup from 'aws-cdk-lib/aws-backup';
// Class for BackupVaults
export class BackupVaults extends Construct {
public readonly ec2: backup.IBackupVault;
public readonly rds: backup.IBackupVault;
constructor(scope: Construct, id: string) {
super(scope, id);
// Backup Vault for Ec2
this.ec2 = new backup.BackupVault(this, 'Ec2', {
removalPolicy: RemovalPolicy.DESTROY,
});
// Backup Vault for Rds
this.rds = new backup.BackupVault(this, 'Rds', {
removalPolicy: RemovalPolicy.DESTROY,
});
}
}
RdsPostgreSql
RDS DB インスタンスを作成します。
復元テスト向けに以下の設定します。
- タグ:
Daily-backup-rds = true
(バックアップ対象) - タグ:
Restore-test-rds = true
(復元テスト対象)
コードは、折りたたみを展開して表示して下さい。
import { Construct } from 'constructs';
import { RemovalPolicy, Tags, Duration } from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as rds from 'aws-cdk-lib/aws-rds';
import * as sm from 'aws-cdk-lib/aws-secretsmanager';
import { NagSuppressions } from 'cdk-nag';
// Interface for RdsPostgreSqlProps
export interface RdsPostgreSqlProps {
readonly vpc: ec2.IVpc;
readonly rdsSubnetGroup: rds.ISubnetGroup;
readonly rdsSg: ec2.ISecurityGroup;
readonly rdsSecret: sm.ISecret;
readonly instancdType: ec2.InstanceClass;
readonly instanceSize: ec2.InstanceSize;
}
// Class for PostgreSql
export class RdsPostgreSql extends Construct {
public readonly rdsSg: ec2.ISecurityGroup;
constructor(scope: Construct, id: string, props: RdsPostgreSqlProps) {
super(scope, id);
const PostgreSql = new rds.DatabaseInstance(this, 'PostgreSql', {
engine: rds.DatabaseInstanceEngine.postgres({
version: rds.PostgresEngineVersion.VER_15_10,
}),
instanceType: ec2.InstanceType.of(props.instancdType, props.instanceSize),
vpc: props.vpc,
subnetGroup: props.rdsSubnetGroup,
availabilityZone: props.vpc.selectSubnets({
subnetGroupName: 'Protected',
}).availabilityZones[0],
securityGroups: [props.rdsSg],
credentials: rds.Credentials.fromSecret(props.rdsSecret),
databaseName: 'mydb',
multiAz: false,
allocatedStorage: 20,
storageEncrypted: true,
backupRetention: Duration.days(1),
removalPolicy: RemovalPolicy.DESTROY, // For development env only
deletionProtection: false, // In production, we have to set true.
});
// Tags for Rds
Tags.of(PostgreSql).add('Daily-backup-rds', 'true');
Tags.of(PostgreSql).add('Restore-test-rds', 'true');
// cdk-nag suppressions
NagSuppressions.addResourceSuppressions(
PostgreSql,
[
{
id: 'AwsSolutions-RDS3',
reason: 'This database is for development purpose.',
},
{
id: 'AwsSolutions-RDS10',
reason: 'This database is for development purpose.',
},
{
id: 'AwsSolutions-RDS11',
reason: 'This database is for development purpose.',
},
],
true,
);
}
}
BackupRds
RDS DB インスタンスの AWS Backup バックアッププランを作成します。
- 頻度: 毎日(月~金曜日)
- リソース選択
- タイプ:
RDS
- タグ:
Daily-backup-rds = true
- タイプ:
コードは、折りたたみを展開して表示して下さい。
import { Construct } from 'constructs';
import { Duration } from 'aws-cdk-lib';
import * as backup from 'aws-cdk-lib/aws-backup';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as events from 'aws-cdk-lib/aws-events';
import * as cwe from 'aws-cdk-lib/aws-events';
// Interface for BackupRds
export interface BackupRdsProps {
readonly backupRole: iam.IRole;
readonly backupVault: backup.IBackupVault;
readonly scheduleExpression: cwe.Schedule;
}
// Class for BackupRds
export class BackupRds extends Construct {
constructor(scope: Construct, id: string, props: BackupRdsProps) {
super(scope, id);
// Backup Plan Rds
const backupPlan = new backup.BackupPlan(this, 'BackupPlanRds', {
backupVault: props.backupVault,
// Backup Plan Rule
backupPlanRules: [
new backup.BackupPlanRule({
ruleName: 'RDS',
scheduleExpression: props.scheduleExpression,
startWindow: Duration.hours(1),
completionWindow: Duration.hours(2),
}),
],
});
// Backup Plan Selection RDS and Tag
new backup.CfnBackupSelection(this, 'Selection', {
backupPlanId: backupPlan.backupPlanId,
backupSelection: {
iamRoleArn: props.backupRole.roleArn,
selectionName: 'Selection',
conditions: {
StringEquals: [
{
ConditionKey: 'aws:ResourceTag/Daily-backup-rds',
ConditionValue: 'true',
},
],
},
resources: ['arn:aws:rds:*:*:db:*'],
},
});
}
}