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