1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【CDK】AWS Backupで復元テストを自動化しよう! CDK Pipelines編

Last updated at Posted at 2025-04-04

はじめに

この記事では、CDK CodePipelines を活用し、復元テスト環境のデプロイを自動化する方法を紹介します。『AWS Backup で復元テストを自動化しよう! RDS DB インスタンス編』の続編として、第 3 弾の記事です。

前回の記事では、次の内容を解説しました。

  • AWS CDK で復元テスト環境を構築する方法
  • AWS CDK (TypeScript) を使ったサンプルコード

その際、復元テスト環境の構築は、コンソールから CDK コマンド (CDK CLI) を使って手動デプロイしていました。今回は、GitHub と CDK CodePipelines を使って、復元テスト環境のデプロイを自動化します。

具体的には、次の内容を解説します。

  • デプロイの自動化が必要な理由
  • CDK CodePipelines の機能概要
  • 復元テスト環境のデプロイを自動化する方法
  • AWS CDK (TypeScript) を使ったサンプルコード

対象読者

次の開発者を対象としています。

  • AWS CDK(TypeScript)の基本知識があり、利用経験がある方
  • GitHub の基本知識があり、利用経験がある方
  • GitHub、CDK CodePipelines を使ったデプロイの自動化を検討している方

用語

次の用語を使用します。

用語 説明
CI/CD パイプライン CodePipeline に設定するパイプライン(ワークフロー)です。 複数のステージで構成されます。
ステージ CodePipeline に設定するパイプラインの各処理ステップです。あとの Stage と分けて「ステージ」と表記します。
CDK コード AWS CDK の App、Stack、Construct を実装したソースコードです。
CDK CodePipelines AWS CDK の L3 コンストラクトです。CodePipeline および CodeBuild の設定ができます。
Stage CDK の Stage は、複数の Stack をまとめて論理的なアプリ(サービス)のかたまりを表現します。CDK CodePipelines では、必ず使います
復元テスト Stack 復元テスト関連のスタックです。AWS Backup、AWS EventBridge、Lambda の設定です。
パイプライン Stack CI/CD パイプライン関連のスタックです。CodePipeline および CodeBuild の設定です。

なぜデプロイの自動化が必要なのか?

CDK のデプロイを自動化することは重要です。CDK では、次のような理由が挙げられます。

  • Git リポジトリの最新 CDK コードとインフラ環境との間で一貫性を維持できる
  • 同じ AWS アカウントで複数の開発者がデプロイするときの競合を回避できる

これらの理由をそれぞれ解説します。

また、AWS の DevOps Guidance([DL.CD.4] Automate the entire deployment process) でも、デプロイプロセスの自動化が推奨されています。

CDK コードとインフラ環境の一貫性を維持する

Git リポジトリの最新 CDK コードとインフラ環境を常に一致させることが重要です。
一貫性を維持するためには、次の 2 つの手順が必要です。

  1. 更新した CDK コードから CloudFormation テンプレートを合成し、手動デプロイする
  2. 手動デプロイ済みの CDK コードを Git リポジトリに登録して、最新バージョンを管理する

なお、ここでの「Git リポジトリ登録」は、コミット、プルリクエスト、レビュー、マージなどの一連の操作を指します。

手動デプロイの際に Git リポジトリへの登録を失念すると、Git リポジトリにある最新 CDK コードとインフラ環境の間で一貫性が損なわれます。

CI/CD パイプラインにより、デプロイを自動化すると、更新した CDK コードを Git リポジトリに登録した時点でデプロイが自動実行されます。これにより、Git リポジトリにある最新 CDK コードとインフラ環境の一貫性を確保できます。

複数の開発者がデプロイするときの競合を回避する

同じ AWS アカウントで複数の開発者がデプロイするときは、次のような競合が考えられます。

  1. 開発者 A と開発者 B が、Git リポジトリから最新 CDK コードを取得します
  2. 開発者 A が EC2 設定を変更してデプロイすると、インフラ環境は新しい設定に更新されます
  3. その後、開発者 B がまだ古い CDK コードをベースに変更を加えて手動デプロイすると、開発者 A の設定が意図せず上書きされます

CI/CD パイプラインにより、デプロイを自動化すると、次のように競合を回避できます。

  • 更新した CDK コードを Git リポジトリに登録しようとしたとき、すでに登録済みの CDK コードに最新変更がある場合は、コンフリクトが検出されます
  • このコンフリクトが解消されないかぎり、Git リポジトリに登録は完了できません
  • 意図しない上書きを防ぎながら、常に最新 CDK コードがデプロイされます

この仕組みによって、同じ AWS アカウントで複数の開発者が作業しても、デプロイの衝突リスクを最小化できます。

CDK CodePipelines とは?

CDK CodePipelines は、CI/CD パイプラインを構築できる、AWS CDK の L3 コンストラクトです。このコンストラクトを使用することで、GitHub リポジトリに登録した CDK コードを基に、デプロイまでを自動化できます。ここでは、復元テスト環境を構築する際のポイントに絞って解説します。

機能概要

CDK CodePipelines コンストラクトを使用すると、CodePipeline および CodeBuild を用いた CI/CD パイプラインを構築できます。この CI/CD パイプラインは、5 つのステージで構成されており、GitHub リポジトリからの CDK コード取得、CloudFormation テンプレートの合成、デプロイなどのプロセスを自動化します。

CDK CodePipelines コンストラクトには、特徴的な機能として「Self Mutation」があります。これは、自身の CI/CD パイプラインをデプロイ対象として処理する機能です。

この記事では、「復元テストスタック」のデプロイを自動化します。CDK CodePipelines コンストラクトを使用して CI/CD パイプラインを構築すると、「Self Mutation」機能により「パイプライン Stack」のデプロイも自動化されます。

各ステージの処理対象は、「復元テスト Stack」、「パイプライン Stack」の両方、またはどちらか一方となります。各ステージの処理内容は、次のように解説します。

# ステージ 復元テスト Stack パイプライン Stack 処理内容
1 Source 対象 対象 GitHub リポジトリへ CDK コードを登録すると、パイプラインが開始され、GitHub リポジトリから CDK コードが取得されます
2 Build 対象 対象 CDK コードを基に CloudFormation テンプレートが合成されます
3 UpdatePipeline - 対象 処理 1:パイプラインに変更がない場合は、次の Assets ステージに移ります
処理 2:CodePipeline のパイプライン設定が更新(Self Mutation)されます、その後 Source ステージからパイプラインが再実行されます
4 Assets 対象 - CloudFormation テンプレート、Lambda コードなどのアセットが、 S3 バケットに登録されます
5 Deploy(Dev) 対象 - CloudFormation から、アセットを基にデプロイされます

ステージ

さらに詳しく知りたい方は、AWS CDK (v2) Developer Guide: CDK Pipelines を使用した継続的インテグレーションと継続的デリバリー (CI/CD)をご確認ください。

復元テスト環境のデプロイを自動化する

さて、CDK CodePipelines を理解したところで、どのように復元テスト環境のデプロイを自動化するかに進みましょう。

復元テスト環境のデプロイを自動化するには、CI/CD パイプライン環境を構築します。GitHub、マネジメントコンソール、CDK CodePipelines コンストラクトを使って、次のようなリソースを構築し、最後に実行確認します。

  • GitHub 関連
    • リポジトリの作成
    • CDK コードをリポジトリに登録(AWS 関連の設定後)
  • AWS 関連(マネジメントコンソール)
    • GitHub と AWS CodePipeline の連携設定
  • AWS 関連( CDK コード)
    • CodePipeline のパイプライン設定
    • CDK の Stage 設定(複数スタックをまとめる CDK の仕組み、CDK CodePipelines の利用条件)
      • 対象スタック:「復元テスト Stack」

さらに詳細は、次のように解説します。

  • 構成図
  • 処理の流れ

構成図

AWS CDK で構築する CI/CD パイプライン環境の構成図です。上部「復元テストの前回記事版」、下部「復元テストの CDK Pipelines 版」の 2 つで構成されています。今回の記事は、下部「復元テストの CDK Pipelines 版」を対象に構築します。

次の AWS ドキュメントを参考に、前回記事おける復元テスト環境のデプロイを自動化できるよう工夫しています。

構成のポイント:

  • 「復元テスト Stack」の CDK コードは変更なし、手動デプロイも継続してできるようにする
  • 「パイプライン Stack」の CDK コードを追加して、デプロイを自動化する

構成図

処理の流れ

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

  1. リポジトリ登録
    対象:「復元テスト Stack」、「パイプライン Stack」
    処理:復元テストと CDK Pipelines の CDK コードをリポジトリに登録します
  2. パイプラインのデプロイ(手動)
    対象:「パイプライン Stack」
    処理:手動デプロイします
  3. コード取得(初回時、変更時)
    対象:「復元テスト Stack」、「パイプライン Stack」
    処理:GitHub リポジトリと CodePipeline の連携により、CDK コードが取得されて CI/CD パイプラインが実行されます
  4. パイプラインのデプロイ(自動)
    対象:「パイプライン Stack」
    処理 1:パイプラインに変更がない場合は、次の処理「5. 復元テストのデプロイ(自動)」に移ります
    処理 2:自動デプロイされます(Self Mutation)、その後「3. コード取得(初回時、変更時)」から再実行されます
  5. 復元テストのデプロイ(自動)
    対象:「復元テスト Stack」
    処理:自動デプロイされます

実行確認

AWS CDK で構築した環境にて、CI/CD パイプラインの実行を確認します。たとえば、AWS CodePipeline マネジメントコンソールで、次の状態を確認できます。

  • Source: すべてのアクションが成功しました
  • Build: すべてのアクションが成功しました
  • UpdatePipeline: すべてのアクションが成功しました
  • Assets: すべてのアクションが成功しました
  • Deploy(Dev): すべてのアクションが成功しました

ステージ

CDK CodePipelines で CI/CD パイプライン環境を構築する

CDK CodePipelines を使って、CI/CD パイプライン環境を構築します。ここでは、パイプラインの実装にポイントを絞って、2 ~ 4 を解説します。1 を詳しく知りたい方は、参考資料で確認してください。

  1. 採用した CDK 実装方法 (参考資料)
    CDK プロジェクト構成、使用ライブラリ、復元テストの CDK 実装など
  2. GitHub と AWS CodePipeline の連携
    GitHub と AWS CodePipeline の連携設定します
  3. CDK CodePipelines の実装
    CI/CD パイプラインを実装します
  4. テストコード
    スナップショットテストを実装します
  5. パイプラインの実行
    パイプラインのデプロイ、CI/CD パイプラインの実行を確認します

GitHub と AWS CodePipeline の連携設定

GitHub と AWS CodePipeline の連携設定します。次の AWS ドキュメントを参考に連携設定できます。

AWS CodePipeline

DeveloperTools Connections の設定を確認します。

  • 「接続 ARN」は、CDK CodePipelines コンストラクトのパラメータ設定に使います

マネジメントコンソールCodePipeline

GitHub

リポジトリの Settings タブから、GitHub Apps の設定を確認します。

  • 「AWS Connector for GitHub」が設定されます

GitHubコンソール

CDK CodePipelines の実装

CDK CodePipelines を使って、CI/CD パイプライン環境を構築します。
CDK コードの詳細は、次のように解説します。

  • パラメータ
  • App
  • Stack
  • Stage
  • テストコード

パラメータ

稼働環境ごとのパラメータを設定します。
GitHub と AWS CodePipeline 連携の設定ポイントは、次の項目です。

  • devPipelineParameter
    • sourceRepository: リポジトリ名
    • sourceBranch: ブランチ名
    • sourceConnectionArn: 接続 ARN

ここでは、CI/CD パイプラインのパラメータに絞って、掲載します。
全パラメータ設定を詳しく知りたい方は、(参考資料)で確認してください。

./paramater.ts
import { Environment } from 'aws-cdk-lib';

// 復元テストのimport,interfaceは省略してます。

// Interface Parameter for Pipelines
export interface PipelineParameter {
  env: Environment;
  envName: string;
  sourceRepository: string;
  sourceBranch: string;
  sourceConnectionArn: string;
}

// 復元テストのパラメータ設定は省略してます。

// Parameters for Pipeline Account
export const devPipelineParameter: PipelineParameter = {
  env: {
    account: process.env.CDK_DEFAULT_ACCOUNT,
    region: 'ap-northeast-1',
  },
  envName: 'DevPipeline',
  sourceRepository: 'xxxxxxx/research-awsbackup',
  sourceBranch: 'main',
  sourceConnectionArn:
    'arn:aws:codestar-connections:ap-northeast-1:111223344556:connection/cfdb983e-fe03-472a-a175-099d8a48eec8',
};

App

CDK の起点として、「パイプライン Stack」をインスタンス化します。

./bin/research-awsbackup-via-cdk-pipelines.ts
#!/usr/bin/env node
import { App, Aspects } from 'aws-cdk-lib';
import { AwsSolutionsChecks } from 'cdk-nag';
import { ResearchAwsBackupPipelineStack } from '../lib/stack/research-awsbackup-via-cdk-pipelines-stack';
import { devParameter, devPipelineParameter } from '../parameter';

const app = new App();
Aspects.of(app).add(new AwsSolutionsChecks({}));

// ResearchAwsBackupPipelinesStack
new ResearchAwsBackupPipelineStack(app, 'Dev-ResearchAwsBackupPipeline', {
  env: devPipelineParameter.env,
  targetParameters: [devParameter],
  sourceRepository: devPipelineParameter.sourceRepository,
  sourceBranch: devPipelineParameter.sourceBranch,
  sourceConnectionArn: devPipelineParameter.sourceConnectionArn,
  tags: {
    Repository: 'xxxxxxx/research-awsbackup',
    Environment: devParameter.envName,
  },
});

Stack

「パイプライン Stack」です。このスタックでは、CDK Pipelines のコンストラクトをインスタンス化します。
さらに、「復元テストの Stage」をインスタンス化して、CDK Pipelines のインスタンスに設定します。
設定ポイントは、次の項目です。

さらに詳しく知りたい方は、AWS CDK Reference: CDK Pipelinesをご確認ください。

./lib/stack/research-awsbackup-via-cdk-pipelines-stack.ts (スタック名:ResearchAwsBackupPipelineStack)
import { Construct } from 'constructs';
import { Environment, Stack, StackProps } from 'aws-cdk-lib';
import * as pipelines from 'aws-cdk-lib/pipelines';
import * as iam from 'aws-cdk-lib/aws-iam';
import { AppParameter } from '../../parameter';
import { ResearchAwsBackupStage } from '../stage/research-awsbackup-stage';
import { NagSuppressions } from 'cdk-nag';

// Interface for ResearchAwsBackupPipelineStackProps
export interface ResearchAwsBackupPipelineStackProps extends StackProps {
  targetParameters: AppParameter[];
  env: Environment;
  sourceRepository: string;
  sourceBranch: string;
  sourceConnectionArn: string;
}

// Class for ResearchAwsBackupPipelineStack
export class ResearchAwsBackupPipelineStack extends Stack {
  constructor(scope: Construct, id: string, props: ResearchAwsBackupPipelineStackProps) {
    super(scope, id, props);

    // Role for CodeBuild
    const codeBuildRole = new iam.Role(this, 'CodeBuildRole', {
      assumedBy: new iam.ServicePrincipal('codebuild.amazonaws.com'),
      managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName('AdministratorAccess')],
    });

    // Pipeline for Restore Testing
    const pipeline = new pipelines.CodePipeline(this, 'Pipeline', {
      crossAccountKeys: true,
      enableKeyRotation: true,
      synth: new pipelines.CodeBuildStep('SynthStep', {
        input: pipelines.CodePipelineSource.connection(props.sourceRepository, props.sourceBranch, {
          connectionArn: props.sourceConnectionArn,
        }),
        installCommands: ['n stable', 'node --version', 'npm i -g npm', 'npm --version'],
        commands: [
          'npm ci',
          'npx aws-cdk synth --app "npx ts-node --prefer-ts-exts bin/research-awsbackup-via-cdk-pipelines.ts"',
        ],
        role: codeBuildRole,
        primaryOutputDirectory: 'cdk.out',
      }),
    });

    // Add Dev Stage ResearchAwsBackupStage for each targetParameters
    props.targetParameters.forEach((params) => {
      pipeline.addStage(new ResearchAwsBackupStage(this, 'Dev', params));
    });

    // cdk-nag suppressions
    NagSuppressions.addResourceSuppressions(codeBuildRole, [
      {
        id: 'AwsSolutions-IAM4',
        reason: 'CodeBild Need ManagedPolicies',
      },
    ]);

    // Force the pipeline construct creation forward before applying suppressions.
    // @See https://github.com/cdklabs/cdk-nag/blob/main/README.md#suppressing-aws-cdk-libpipelines-violations
    pipeline.buildPipeline();
    NagSuppressions.addStackSuppressions(this, [
      {
        id: 'AwsSolutions-S1',
        reason: "This bucket doesn't store sensitive data",
      },
      {
        id: 'AwsSolutions-IAM5',
        reason: 'The managed policy is automatically generated by AWS',
      },
    ]);
  }
}

Stage

「復元テストの Stage」です。「復元テスト Stack」をインスタンス化して、CDK の Stage にまとめます。
CDK の Stage は、複数の Stack をまとめて論理的なアプリ(サービス)のかたまりを表現します。
今回ように、CDK Pipelines 使うときは、Stage にまとめます。

./lib/stage/research-awsbackup-stage.ts (ステージ名:ResearchAwsBackupStage)
import { Stage } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { AppParameter } from '../../parameter';
import { ResearchAwsBackupStack } from '../stack/research-awsbackup-stack';

// Class for ResearchAwsBackupStage
export class ResearchAwsBackupStage extends Stage {
  constructor(scope: Construct, id: string, props: AppParameter) {
    super(scope, id, props);

    // ResearchAwsBackupStack
    new ResearchAwsBackupStack(this, 'ResearchAwsBackup', {
      env: props.env,
      appParameter: props,
    });
  }
}

テストコード

「パイプライン Stack」のスナップショットテストです。

./test/research-awsbackup-via-cdk-pipelines-stack.ts
import { App } from 'aws-cdk-lib';
import { Template } from 'aws-cdk-lib/assertions';
import { ResearchAwsBackupPipelineStack } from '../lib/stack/research-awsbackup-via-cdk-pipelines-stack';
import { devParameter, devPipelineParameter } from '../parameter';

// SnapShot test for ResearchAwsBackup
test('Snapshot test for ResearchAwsBackup', () => {
  const app = new App();
  const stack = new ResearchAwsBackupPipelineStack(app, 'Dev-ResearchAwsBackupPipeline', {
    env: devPipelineParameter.env,
    targetParameters: [devParameter],
    sourceRepository: devPipelineParameter.sourceRepository,
    sourceBranch: devPipelineParameter.sourceBranch,
    sourceConnectionArn: devPipelineParameter.sourceConnectionArn,
    tags: {
      Repository: 'xxxxxxx/research-awsbackup',
      Environment: devParameter.envName,
    },
  });
  expect(Template.fromStack(stack)).toMatchSnapshot();
});

デプロイの実行確認

ここまでに準備した CDK コードを元に、デプロイの実行確認をします。
確認方法の詳細は、次のように解説します。

  1. GitHub リポジトリへ CDK コード登録(手動)
  2. 「パイプライン Stack」のデプロイ(手動)
  3. 「復元テスト Stack」のデプロイ(自動)

GitHub リポジトリへ CDK コード登録(手動)

  • git コマンドを使って、ここまでに準備した CDK コードを GitHub リポジトリへ登録します
  • GitHub リポジトリに登録すると、次のように確認できます

GitHubコンソール

「パイプライン Stack」のデプロイ(手動)

  • パイプラインのデプロイ(手動)します
  • CDK コマンドでは、--app オプションを使って「パイプライン Stack」の CDK コードを指定します
npx aws-cdk deploy --profile xxxxxxx --app "npx ts-node --prefer-ts-exts bin/research-awsbackup-via-cdk-pipelines.ts"
  • 「パイプライン Stack」(Dev-ResearchAwsBackupPipeline)が作成されます
  • マネジメントコンソールの CloudFormation にて、次のように確認できます

マネジメントコンソールCloudFormation

「復元テスト Stack」のデプロイ(自動)

  • 「パイプライン Stack」に基き、CodePipeline の CI/CD パイプラインが設定されます
  • マネジメントコンソールの CodePipeline にて、次のように確認できます
  • はじめに Source ステージ、Build ステージが実行されます
  • GitHub リポジトリから CDK コードが取得され、CloudFormation テンプレートが合成されます

マネジメントコンソールCodepipeline

  • つぎに、UpdatePipeline ステージが実行されます
  • 今回はパイプラインに変更がないため、次の Assets ステージに移ります

マネジメントコンソールCodepipeline

  • さらに Assets ステージ、Deploy(Dev) ステージが実行されます
  • CloudFormation テンプレート、Lambda コードを基に、CloudFormation から「復元テスト Stack」がデプロイされます

マネジメントコンソールCodepipeline

  • CI/CD パイプラインの全体が完了すると、「復元テスト Stack」(Dev-ResearchAwsBackup)が作成されます
  • マネジメントコンソールの CloudFormation にて、次のように確認できます

マネジメントコンソールCloudFormation

  • さらに、復元テストプラン(RestoreTestingPlanRds)の設定がされます
  • マネジメントコンソールの Backup にて、次のように確認できます

マネジメントコンソールBackup

おわりに

ここまで、『AWS Backup で復元テストを自動化しよう! RDS DB インスタンス編』の続編として、復元テスト環境のデプロイを自動化する方法を紹介しました。

今回、はじめて CDK Pipelines を使用して CI/CD パイプラインを構築してみましたが、マネジメントコンソールから設定するよりも、手軽かつ簡単に環境構築ができることに驚きました。さらに、復元テスト環境の CDK コードに影響を与えずにデプロイの自動化を追加できる点も、とても導入しやすいと感じています。デプロイの自動化に興味がある方は、是非この記事を参考にしていただけると幸いです。

また、この記事の作成にあたり、AWS 公式ドキュメントの情報を参考にさせていただきました。AWS Blog、サンプルコードなどの詳細なドキュメントに感謝いたします。

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

参考資料

AWS Document

AWS Blog

採用した CDK 実装方法

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

  1. 全体方針
  2. プロジェクト構成
折りたたみを展開して表示して下さい。

全体方針

Baseline Environment on AWS v3 のゲストシステム を参考としてます。

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

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

工夫点:

  • 試行錯誤が必要なコンストラクトは、パラメータでデプロイ有無を制御する
  • cdk-nagを採用してコードチェックする
  • Lambda 関数コードは、TypeScript を使用する

プロジェクト構成

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

  1. 実行環境
  2. ファイル構成

実行環境

次のライブラリを使用しています。
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

ファイル構成

前回の記事に、CI/CD パイプラインを追加したため、ファイル構成が増えています。

ツリー構造
research-awsbackup
  ├── bin
  │   ├── research-awsbackup-via-cdk-pipelines.ts  # CDK App: CI/CDパイプライン
  │   └── 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                   # スタック: 復元テスト
  │       └── research-awsbackup-via-cdk-pipelines-stack.ts # スタック: CI/CDパイプライン
  │   └── stage
  │       └── research-awsbackup-stage.ts                   # ステージ: CI/CDパイプライン
  ├── test
  │   ├── research-awsbackup.test.ts                        # スナップショットテスト
  │   └── research-awsbackup-via-cdk-pipelines-stack.ts     # スナップショットテスト
  └── paramater.ts                                          # 環境パラメータ

バックアップと復元テストの CDK 実装

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

  1. 構成図
  2. パラメータ
  3. App
  4. スタック
  5. テストコード
  6. コンストラクト
折りたたみを展開して表示して下さい。

構成図

バックアップと復元テストの CDK 実装は、それぞれ構成図に示す範囲を対象とします。

構成図

パラメータ

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

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

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

  • RDS 復元テストに関連するコンストラクトは「deploy: true」でデプロイされます
  • EC2 復元テストに関連するコンストラクトは「deploy: false」でデプロイされません
コードは、折りたたみを展開して表示して下さい。
./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;
}

// Interface Parameter for Pipelines
export interface PipelineParameter {
  env: Environment;
  envName: string;
  sourceRepository: string;
  sourceBranch: string;
  sourceConnectionArn: string;
}

// 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,
  },
};

// Parameters for Pipeline Account
export const devPipelineParameter: PipelineParameter = {
  env: {
    account: process.env.CDK_DEFAULT_ACCOUNT,
    region: 'ap-northeast-1',
  },
  envName: 'DevPipeline',
  sourceRepository: 'xxxxxxx/research-awsbackup',
  sourceBranch: 'main',
  sourceConnectionArn:
    'arn:aws:codestar-connections:ap-northeast-1:111223344556:connection/cfdb983e-fe03-472a-a175-099d8a48eec8',
};

App

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

  • コード検証ツール設定
  • スタックのインスタンス化
コードは、折りたたみを展開して表示して下さい。
./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,
});

スタック

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 ルール
コードは、折りたたみを展開して表示して下さい。
./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,
      });
    }
  }
}

テストコード

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

コードは、折りたたみを展開して表示して下さい。
./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();
});

コンストラクト

Networking

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

コードは、折りたたみを展開して表示して下さい。
./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',
      },
    ]);
  }
}

Iam

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

コードは、折りたたみを展開して表示して下さい。
./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.',
        },
      ],
    );
  }
}

BackupVaults

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

コードは、折りたたみを展開して表示して下さい。
./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,
    });
  }
}

RdsPostgreSql

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

  • タグ: Daily-backup-rds = true (バックアップ対象)
  • タグ: Restore-test-rds = true (復元テスト対象)
コードは、折りたたみを展開して表示して下さい。
./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,
    );
  }
}

BackupRds

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

  • 頻度: 毎日(月~金曜日)
  • リソース選択
    • タイプ: RDS
    • タグ: Daily-backup-rds = true
コードは、折りたたみを展開して表示して下さい。
./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:*'],
      },
    });
  }
}

RestoreTestRds

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

  • 頻度: 毎月(第 2 金曜日)
  • リソース選択
    • リソースタイプ: RDS
    • タグ: Restore-test-rds = true
コードは、折りたたみを展開して表示して下さい。
./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,
      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);
  }
}

RdsValidate

復元リソース検証の仕組みを作成します。

  • EventBridge ルールは、次の設定をします
    • イベントパターン設定: AWS Backup 復元ジョブの完了イベント
    • ターゲット設定: Lambda 関数
コードは、折りたたみを展開して表示して下さい。
./lib/construct/restore-test-rds.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 です。

コードは、折りたたみを展開して表示して下さい。
{
  "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 コードを参考にしています。

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

  1. RDS DB インスタンス の エンドポイントを取得
    イベントの復元リソース ARNから RDS DB インスタンスの DB 識別子を抽出、RDS API を使って RDS DB インスタンスのエンドポイントを取得します
  2. 復元リソースの検証
    復元された DB インスタンスへの接続を検証します
  3. 検証結果の報告
    イベントの復元ジョブ IDを基に、AWS Backup API の PutRestoreValidationResult を使って、検証結果を AWS Backup の復元ジョブに設定します

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

コードは、折りたたみを展開して表示して下さい。
./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 }));
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?