【CDK】AWS Backupで復元テストを自動化しよう！ RDS DB インスタンス編

# はじめに

この記事では、RDS DB インスタンスの復元テストを自動化する方法を紹介します。[『AWS Backup で復元テストを自動化しよう！』](https://qiita.com/shigeharu_takagi/items/13db911b126db0537300)の続編として、第 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 インスタンスの検証と結果報告

![構成図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3782210/1f587676-2b61-442d-9f4e-4ba6377bb97a.png)

## 処理の流れ

構成図に沿って、処理の流れを解説します。

1. **バックアップ**
   AWS Backup のバックアッププランに基づいて、RDS DB インスタンスのバックアップジョブが実行されます
2. **復元**
   AWS Backup の復元テストプランに基づいて、RDS DB インスタンスの復元ジョブが実行されます
3. **イベント発行**
   RDS DB インスタンスの復元が完了すると、AWS Backup から完了イベントが発行されます
4. **Lambda 実行**
   EventBridge のルールに基づいて、復元完了イベントをトリガーとして Lambda 関数（検証プログラム）が実行されます
5. **復元リソース検証**
   Lambda 関数（検証プログラム）は、復元された RDS DB インスタンスの状態を確認します（Lambda からの DB 接続を確認）
6. **検証結果報告**
   AWS Backup API の [PutRestoreValidationResult](https://docs.aws.amazon.com/ja_jp/aws-backup/latest/devguide/API_PutRestoreValidationResult.html) を使って、検証結果を AWS Backup に報告します
7. **復元リソース削除**
   復元リソース検証が完了、または保存時間に達すると、復元された RDS DB インスタンスは自動的に削除されます

## 実行確認

AWS CDK で構築した環境にて、復元テストの実行を確認します。実行タイミングになると、はじめに復元ジョブが自動実行され、DB インスタンスが復元されます。つぎに検証がおこなわれ、最後に自動削除されます。

たとえば、AWS Backup マネジメントコンソールで、次の状態を確認できます。

- **復元ステータス：** 完了
- **検証ステータス：** 成功
- **削除ステータス：** 成功

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3782210/410d4e9a-7682-4749-beea-d400da981c0d.png)

# CDK で復元テスト環境を構築する

AWS CDK (TypeScript) を使用して、復元テスト環境を構築します。ここでは、復元テストにポイントを絞って、3 ～ 5 を解説します。1 ～ 2 を詳しく知りたい方は、参考資料で確認してください。

1. **採用した CDK 実装方法** [（参考資料）](#採用した-cdk-実装方法)
   CDK プロジェクト構成、使用ライブラリなど
2. **バックアップの CDK 実装** [（参考資料）](#バックアップの-cdk-実装)
   ネットワーク、RDS DB インスタンス、バックアッププランなど
3. **復元テストの CDK 実装**
   復元テストプラン、復元リソース検証の仕組みを実装します
4. **復元リソース検証の Lambda コード**
   RDS DB インスタンスの検証プログラム（Lambda からの DB 接続を確認）を実装します
5. **復元テストの実行**
   CDK デプロイ、復元テストの自動実行を確認します

## 復元テストの AWS CDK 実装

### 構成図

復元テストの AWS CDK 実装は、次の構成図を対象とします。

![構成図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3782210/84708f35-eb0d-45ae-8ee1-b2edfaba4ecb.png)

### 復元テストプラン

構成図に基づいて、AWS Backup の復元テストプランを作成します。復元テストプランの作成には、AWS CDK L1 コンストラクトを使います。L2 コンストラクトは未対応です。（2025 年 2 月 6 日時点）

復元テストプランの設定ポイントは、次の項目です。

- [CfnRestoreTestingPlan](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_backup.CfnRestoreTestingPlan.html)

  - scheduleExpression: `テスト頻度、開始時間（時分）`
  - startWindowHours: `次の時間以内に開始（時間単位）`

- [CfnRestoreTestingSelection](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_backup.CfnRestoreTestingSelection.html)

  - protectedResourceType: `リソースタイプ（ RDS ）`
  - restoreMetadataOverrides: `復元パラメータ`
    - dbSubnetGroupName: `サブネットグループ`
    - securityGroupIds: **`'["' + セキュリティグループID + '"]'`**

ここでは、AWS CDK の全サンプルコードから、「復元テストプラン」の実装（コンストラクト名:RestoreTestRds）に絞って紹介します。コンストラクトの入力パラメータなど、詳しく知りたい方は「バックアップの CDK 実装[（参考資料）](#バックアップの-cdk-実装)」で確認してください。

それでは、次のサンプルコードを見てみましょう。

```ts: ./lib/construct/restore-test-rds.ts （コンストラクト名:RestoreTestRds）
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 実装[（参考資料）](#バックアップの-cdk-実装)」で確認してください。

それでは、次のサンプルコードを見てみましょう。

```ts: ./lambda/rds-validate.ts （コンストラクト名:RdsValidate）
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 です。

```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 コードを参考にしています。

- [AWS Backup で自動復元テストおよび検証が使用可能に](https://aws.amazon.com/jp/blogs/news/automatic-restore-testing-and-validation-is-now-available-in-aws-backup/)（[Python コード](https://awsstorageblogresources.s3.us-west-2.amazonaws.com/jayadevaawsbackupblog/lambda_function.zip)）

処理の流れは、次ようになります。

1. **RDS DB インスタンス の エンドポイントを取得**
   イベントの`復元リソース ARN`から RDS DB インスタンスの DB 識別子を抽出、RDS API を使って RDS DB インスタンスのエンドポイントを取得します
2. **復元リソースの検証**
   復元された DB インスタンスへの接続を検証します
3. **検証結果の報告**
   イベントの`復元ジョブ ID`を基に、AWS Backup API の [PutRestoreValidationResult](https://docs.aws.amazon.com/ja_jp/aws-backup/latest/devguide/API_PutRestoreValidationResult.html) を使って、検証結果を AWS Backup の復元ジョブに設定します

Lambda サンプルコードは、次です。

```ts: ./lambda/rds-validate.ts
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 の復元テストプランに基づき、復元ジョブが実行されます
  - 復元パラメータ: サブネットグループ、セキュリティグループ

![マネジメントコンソールBackup](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3782210/cfb02445-8ea3-480a-8b14-80a3c52c55f7.png)

- リソース（RDS DB インスタンス）が復元され、復元ジョブが完了すると、AWS Backup の完了イベントが発行されます
  - ステータス（復元）が「実行中」→「完了」に変更されます

![マネジメントコンソールBackup](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3782210/edb58eba-f285-4830-abfd-0cb439b5698f.png)

- リソース（RDS DB インスタンス）の復元が確認できます

![マネジメントコンソールEC2](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3782210/c4989775-7c72-4fb6-91f9-6006827ac154.png)

### 復元リソース検証

- EventBridge のルールによって、Lambda（検証プログラム）が起動されます
- 検証プログラムによって、復元されたリソースの状態が確認されます。（DB 接続の確認）
- AWS SDK（AWS Backup API）によって、検証結果が AWS Backup に報告されます
- 検証ステータスが「検証中」→「成功」に変更されます

![マネジメントコンソールBackup](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3782210/193ba552-353a-4868-95e4-983e0277b42e.png)

### 復元リソース削除

- 検証が完了、または時限超過すると、復元リソース（RDS DB インスタンス）が削除されます
- 検証ステータスが「削除中」→「成功」に変更されます

![マネジメントコンソールBackup](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3782210/8ad86e91-585e-466b-8aeb-1f8eac23b771.png)

- リソース（RDS）の削除（終了）が確認できます

![マネジメントコンソールEC2](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3782210/f5330222-84f9-4702-941e-379adef241cb.png)

# おわりに

ここまで、[『AWS Backup で復元テストを自動化しよう！』](https://qiita.com/shigeharu_takagi/items/13db911b126db0537300)の続編として、RDS DB インスタンスの復元テストを自動化する方法を紹介しました。

RDS の復元テストでは、復元時間の検証も重要です。データサイズが大きい場合、復元時間が長くなることも考えられます。この記事にはその点が含まれていませんので、ぜひ皆さんも検討していただければ幸いです。
 
また、この記事の作成にあたり、AWS 公式ドキュメントの情報を参考にさせていただきました。AWS Blog、サンプルコードなどの詳細なドキュメントに感謝いたします。

最後までお読みいただき、ありがとうございました。

# 参考資料

## AWS Document

- [AWS Well-Architected フレームワーク: REL09-BP04 データの定期的な復旧を行ってバックアップの完全性とプロセスを確認する](https://docs.aws.amazon.com/ja_jp/wellarchitected/latest/reliability-pillar/rel_backing_up_data_periodic_recovery_testing_data.html)

- [AWS Backup 開発ガイド: 復元テスト](https://docs.aws.amazon.com/ja_jp/aws-backup/latest/devguide/restore-testing.html)

- [AWS Backup 開発ガイド: 復元テストの推定メタデータ上書き](https://docs.aws.amazon.com/ja_jp/aws-backup/latest/devguide/restore-testing-inferred-metadata.html)

- [AWS Backup 開発ガイド: 復元完了イベント](https://docs.aws.amazon.com/ja_jp/aws-backup/latest/devguide/eventbridge.html#restore-job-state-change-completed)

- [AWS Backup 開発ガイド: PutRestoreValidationResult](https://docs.aws.amazon.com/ja_jp/aws-backup/latest/devguide/API_PutRestoreValidationResult.html)

- [Baseline Environment on AWS: Sample Web application](https://github.com/aws-samples/baseline-environment-on-aws/tree/main/usecases/blea-guest-ec2-app-sample)

- [AWS CDK Reference: class CfnRestoreTestingPlan (construct)](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_backup.CfnRestoreTestingPlan.html)

- [AWS CDK Reference: class CfnRestoreTestingSelection (construct)](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_backup.CfnRestoreTestingSelection.html)

- [AWS CloudFormation User Guide: EC2 推定メタデータ上書きの書式:SecurityGroupIds](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-instance.html#aws-resource-ec2-instance-syntax)

- [AWS SDK for JavaScript v3: PutRestoreValidationResultCommand](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/backup/command/PutRestoreValidationResultCommand/)

## AWS Blog

- [AWS Backup で復元テストおよび検証が使用可能に](https://aws.amazon.com/jp/blogs/news/automatic-restore-testing-and-validation-is-now-available-in-aws-backup/)

- [Automate data recovery validation with AWS Backup](https://aws.amazon.com/jp/blogs/storage/automate-data-recovery-validation-with-aws-backup/)

- [AWS Lambda Powertools for TypeScript でサーバーレスのベストプラクティスを簡素化する](https://aws.amazon.com/jp/blogs/news/simplifying-serverless-best-practices-with-aws-lambda-powertools-for-typescript/)

## Powertools for AWS Lambda

- [Powertools for AWS Lambda (TypeScript) Install](https://docs.powertools.aws.dev/lambda/typescript/latest/#lambda-layer_1)

## 採用した CDK 実装方法

CDK 実装方法は、次のとおり解説します。

- 全体方針
- プロジェクト構成

<details><summary>折りたたみを展開して表示して下さい。</summary>

### 全体方針

[Baseline Environment on AWS v3 のゲストシステム](https://github.com/aws-samples/baseline-environment-on-aws/tree/main/usecases/blea-guest-ec2-app-sample) を参考としてます。

- CDK コードは TypeScript を使用
- 環境ごとのパラメータは、TypeScript で定義
- 1 つのスタックと複数のコンストラクトで構成
- L2 コンストラクトを使用し、未対応の部分は L1 コンストラクトを使用
  - バックアッププランのリソース選択
  - 復元テストプランのリソース選択
- スナップショットテストの実施

今回、コード開発を効率化する工夫をしています。

工夫点:

- 試行錯誤が必要なコンストラクトは、パラメータでデプロイ有無を制御する
- [cdk-nag](https://aws.amazon.com/jp/blogs/news/manage-application-security-and-compliance-with-the-aws-cloud-development-kit-and-cdk-nag/)を採用してコードチェックする
- Lambda 関数コードは、TypeScript を使用する
  - [Powertools for AWS Lambda](https://aws.amazon.com/jp/blogs/news/simplifying-serverless-best-practices-with-aws-lambda-powertools-for-typescript/)を採用して標準化する

### プロジェクト構成

プロジェクト構成は、次のとおり解説します。

- 実行環境
- ファイル構成

#### 実行環境

次のライブラリを使用しています。
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 インスタンスの復元テストを追加したため、ファイル構成が増えています。

```zsh: ツリー構造
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                        # 環境パラメータ
```

</details>

## バックアップの CDK 実装

バックアップの CDK 実装は、次のとおり解説します。

- 構成図
- パラメータ
- App
- スタック
- テストコード
- コンストラクト

<details><summary>折りたたみを展開して表示して下さい。</summary>

### 構成図

バックアップの CDK 実装は、構成図に示す範囲を対象とします。
ネットワークは、復元テストの範囲も含みます。

![構成図](awsbackup_restoretest_rds_10.png)
![構成図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3782210/a3f57fbb-3267-4ff0-b523-dd94d2310c54.png)

### パラメータ

稼働環境ごとのパラメータを設定します。

- AWS アカウント
- リージョン
- VPC CIDR
- デプロイフラグ

試行錯誤が必要なコンストラクトは、パラメータでデプロイ有無を制御します。

- RDS 復元テストに関連するコンストラクトは「deploy: true」でデプロイされます。
- EC2 復元テストに関連するコンストラクトは「deploy: false」でデプロイされません。

<details><summary>コードは、折りたたみを展開して表示して下さい。</summary>

```ts: ./paramater.ts
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,
  },
};

```

</details>

### App

CDK の起点として、次を実施します。

- コード検証ツール設定
- スタックのインスタンス化

<details><summary>コードは、折りたたみを展開して表示して下さい。</summary>

```ts: ./bin/research-awsbackup.ts
#!/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,
});

```

</details>

### スタック

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 ルール                         |

<details><summary>コードは、折りたたみを展開して表示して下さい。</summary>

```ts: ./lib/stack/research-awsbackup-stack.ts
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,
      });
    }
  }
}

```

</details>

### テストコード

スタックのスナップショットテストを実施します。

<details><summary>コードは、折りたたみを展開して表示して下さい。</summary>

```ts: ./test/research-awsbackup.test.ts
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();
});

```

</details>

#### Networking

VPC、サブネットなどのネットワーク、セキュリティグループを作成します。

<details><summary>コードは、折りたたみを展開して表示して下さい。</summary>

```ts: ./lib/construct/networking.ts （コンストラクト名:Networking）
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',
      },
    ]);
  }
}

```

</details>

#### Iam

EC2、AWS Backup 用の IAM ロールを作成します。
AWS Backup 用の IAM ロールでは、バックアップだけでなく、復元の権限が必要です。

<details><summary>コードは、折りたたみを展開して表示して下さい。</summary>

```ts: ./lib/construct/iam.ts （コンストラクト名: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.',
        },
      ],
    );
  }
}

```

</details>

#### BackupVaults

EC2 バックアップを管理するための AWS Backup ボールトを作成します。

<details><summary>コードは、折りたたみを展開して表示して下さい。</summary>

```ts: ./lib/construct/backup-vaults.ts （コンストラクト名:BackupVaults）
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,
    });
  }
}

```

</details>

#### RdsPostgreSql

RDS DB インスタンスを作成します。
復元テスト向けに以下の設定します。

- タグ: `Daily-backup-rds = true` (バックアップ対象)
- タグ: `Restore-test-rds = true` (復元テスト対象)

<details><summary>コードは、折りたたみを展開して表示して下さい。</summary>

```ts: ./lib/construct/rds-postgresql.ts （コンストラクト名:RdsPostgreSql）
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,
    );
  }
}

```

</details>

#### BackupRds

RDS DB インスタンスの AWS Backup バックアッププランを作成します。

- 頻度: 毎日（月～金曜日）
- リソース選択
  - タイプ: `RDS`
  - タグ: `Daily-backup-rds = true`

<details><summary>コードは、折りたたみを展開して表示して下さい。</summary>

```ts: ./lib/construct/backup-rds.ts （コンストラクト名:BackupRds）
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:*'],
      },
    });
  }
}

```

</details>

</details>