はじめに
この記事では、AWS Backup の復元テスト機能を活用し、EC2 インスタンスの復元テストを自動化する方法を紹介します。復元テストの自動化は、バックアップ運用の信頼性を向上させる重要な取り組みです。
AWS Backup は多くのシステムで利用されているバックアップ管理サービスです。AWS Cloud Development Kit (AWS CDK) を使った設定も広く採用されています。ただし、2023年11月に追加された 復元テスト機能に関する事例や、AWS CDK を使った構築例はまだ少ない状況です。
そこで、次の内容を解説します。
- 復元テストの自動化が必要な理由
- AWS Backup の復元テスト機能の概要
- 復元テストを自動化する方法
- AWS CDK (TypeScript) を使ったサンプルコード
対象読者
次の開発者を対象としています。
- AWS Backup の基本機能(バックアップおよびリストア)を理解している方
- AWS Backup の復元テスト機能を知りたい、または利用を検討している方
- AWS CDK(TypeScript)の基本知識があり、利用経験がある方
- AWS CDK と AWS Backup を使った復元テストの自動化を検討している方
用語
次の用語を使用します。
用語 | 説明 |
---|---|
復元 | バックアップからのリストアを指します。AWS 公式ドキュメントに基づき「復元」と表記します。 |
復元テスト | バックアップから正しく復元できるかを検証するテストです。 |
AWS Backup の復元テスト機能 | AWS Backup から提供される機能です。EC2、 RDS などの復元テストと区別するため、この表記とします。 |
なぜ復元テストの自動化が必要なのか?
EC2、RDS など、AWS リソースの復元テストを自動化することは重要です。復元テストは、システム障害やデータ損失時にバックアップから正確に復元できるかを確認する作業です。バックアップは常に取得されるため、復元テストも定期的に必要になります。手動では時間と手間がかかり、継続的な実施は難しくなります。
たとえば、EC2 インスタンスの復元テストでは次の手順が必要です。
- バックアップから AWS リソースを復元する
- 復元リソースを検証する (例: アプリケーションが正常動作するか確認)
- 復元リソースを削除する
これらを自動化することで、AWS のベストプラクティスに基づく運用が可能です。AWS Well-Architected Framework 信頼性の柱(REL09-BP04 データの定期的な復旧を行い、バックアップの完全性とプロセスを確認する)でも、自動化された復元テストが推奨されています。
AWS Backup の復元テスト機能とは?
AWS Backup は、バックアップ管理などいくつかの機能を提供しています。その中の一つに「復元テスト機能」があります。この機能を使うと、EC2、RDS など、AWS リソースの復元テストを自動実行できます。使う際のポイントに絞って、次のように解説します。
-
機能概要:
- テスト頻度、実行時間枠、対象リソース、復元先などを指定すると、復元テストが自動実行されます
-
実行タイミング:
- 復元テストは、指定の実行時間枠内でランダムに実行されます(正確な実行時刻を制御できません)
-
復元先:
- 復元先を指定しないと、AWS Backup が推定した場所に復元します(明示的な指定を強く推奨します)
-
検証方法:
- 復元リソースの検証には、追加の仕組みが必要です(復元テスト機能には含まれません)
機能概要
AWS Backup の復元テスト機能を使うと、EC2、RDS などのバックアップデータを基に、定期的な復元テストを自動実行できます。
復元テストを設定するには、「復元テストプラン」を作成します。このプランでは次の項目などを指定します。
- テスト頻度 (例: 毎日、毎週、毎月)
- 実行時間枠
- 対象リソース
- 復元リソースの保存期間
実行タイミングになると、復元ジョブが開始され、対象の AWS リソースが復元されます。復元リソースは、検証完了後や指定の保存期間が終わると自動削除されます。
なお、リソースタイプは、Aurora、Amazon DocumentDB、Amazon DynamoDB、Amazon EBS、Amazon EC2、Amazon EFS、Amazon FSx (Lustre、ONTAP、OpenZFS、Windows)、Amazon Neptune、Amazon RDS、Amazon S3 が指定できます。(2025年2月6日時点)
最新情報は、AWS Backup 開発ガイド: 復元テストをご確認ください。
実行タイミング
復元テストは、指定の実行時間枠内でランダムに実行されます。正確な実行時刻を制御することはできません。
実行時間枠は、「復元テストプラン」の設定項目である 開始時間(時分)
と 次の時間以内に開始(時間単位)
の組み合わせで指定します。設定例は、次のようになります。
開始時間(時分) | 次の時間以内に開始(時間単位) | 実行時間枠 |
---|---|---|
10:00 | 1 | 10:00 - 11:00 |
10:00 | 5 | 10:00 - 15:00 |
復元先
バックアップからの復元先は、「復元テストプラン」の設定項目である 復元パラメータ
に指定します。復元先を指定しない場合、AWS Backup が復元先を推定します。これは、意図しない場所に復元される可能性があるため、明示的に指定することを強く推奨します。
またEC2、RDSなど、サービスによって指定可能な復元パラメータの種類は異なります。たとえば、EC2インスタンスの場合は、サブネットID、セキュリティグループIDなどを指定できます。
検証方法
復元リソースの検証には、追加の仕組みが必要です。AWS Backup の復元テスト機能に、検証機能は含まれません。たとえば、Amazon EventBridge や AWS Lambda など、他の AWS サービスと組み合わせて、検証機能を作成します。
追加の仕組みを作成すると、AWS Backupの復元完了イベントと連携して、復元リソースの検証が自動実行できます。検証結果は AWS Backup の API 経由で、復元ジョブに登録されます。
EC2 インスタンスの復元テストを自動化する
さて、AWS Backup の復元テスト機能を理解したところで、どのように復元テストを自動化するかに進みましょう。ここからは、EC2 インスタンスの復元テストを自動化する方法を紹介します。EC2 を選択した理由は、RDS などと比べて設定がシンプルで取り組みやすいからです。
復元テストを自動化するには、復元テスト環境を構築します。AWS CDK (TypeScript)を使って、次のようなリソースを構築し、最後に実行確認します。
-
バックアップ関連
- ネットワーク(VPC、セキュリティグループなど)
- EC2 インスタンス
- AWS Backup のバックアップランなど
-
復元テスト関連
- AWS Backup の復元テストプランなど
- 復元リソース検証の仕組み(イベントルール、Lambda 関数)
さらに詳細は、次のように解説します。
- 構成図
- 処理の流れ
- 実行確認
構成図
AWS CDK で構築する 復元テスト環境の構成図です。上部「バックアップ」、下部「復元テスト」の2つで構成されています。次のAWS 公式ブログを参考に、AWS Backup 復元テスト機能を活用できるよう工夫しています。
構成のポイント:
- AWS Backup バックアッププランによる EC2 インスタンスのバックアップ
- AWS Backup 復元テストプランによる EC2 インスタンスの自動復元と削除
- Lambda 関数 (TypeScript) による復元された EC2 インスタンスの検証と結果報告
処理の流れ
構成図に沿って、処理の流れを解説します。
-
バックアップ
AWS Backup のバックアッププランに基づいて、EC2 インスタンスのバックアップジョブが実行されます -
復元
AWS Backup の復元テストプランに基づいて、EC2 インスタンスの復元ジョブが実行されます -
イベント発行
EC2 インスタンスの復元が完了すると、AWS Backup から完了イベントが発行されます -
Lambda 実行
EventBridge のルールに基づいて、復元完了イベントをトリガーとして Lambda 関数(検証プログラム)が実行されます -
復元リソース検証
Lambda 関数(検証プログラム)は、復元された EC2 インスタンスの状態を確認します(例: HTTP レスポンスの確認) -
検証結果報告
AWS Backup API の PutRestoreValidationResult を使って、検証結果を AWS Backup に報告します -
復元リソース削除
復元リソース検証が完了、または保存時間に達すると、復元された EC2 インスタンスは自動的に削除されます
実行確認
AWS CDK で構築した環境にて、復元テストの実行を確認します。実行タイミングになると、はじめに復元ジョブが自動実行され、EC2 インスタンスが復元されます。つぎに検証がおこなわれ、最後に自動削除されます。
たとえば、AWS Backup マネジメントコンソールで、次の状態を確認できます。
- 復元ステータス: 完了
- 検証ステータス: 成功
- 削除ステータス: 成功
CDKで復元テスト環境を構築する
AWS CDK (TypeScript) を使用して、復元テスト環境を構築します。ここでは、復元テストにポイントを絞って、3~5を解説します。1~2を詳しく知りたい方は、参考資料で確認してください。
-
採用した CDK 実装方法 (参考資料)
CDK プロジェクト構成、使用ライブラリなど -
バックアップの CDK 実装 (参考資料)
ネットワーク、EC2 インスタンス、バックアッププランなど -
復元テストの CDK 実装
復元テストプラン、復元リソース検証の仕組みを実装します -
復元リソース検証の Lambdaコード
EC2 インスタンスの検証プログラム(HTTP レスポンスの確認)を実装します -
復元テストの実行
CDK デプロイ、復元テストの自動実行を確認します
復元テストの AWS CDK 実装
構成図
復元テストの AWS CDK 実装は、次の構成図を対象とします。
復元テストプラン
構成図に基づいて、AWS Backup の復元テストプランを作成します。復元テストプランの作成には、AWS CDK L1 コンストラクトを使います。L2 コンストラクトは未対応です。(2025年2月6日時点)
復元テストプランの設定ポイントは、次の項目です。
-
- scheduleExpression:
テスト頻度、開始時間(時分)
- startWindowHours:
次の時間以内に開始(時間単位)
- scheduleExpression:
-
- protectedResourceType:
リソースタイプ( EC2 )
- restoreMetadataOverrides:
復元パラメータ
- subnetId:
サブネットID
- securityGroupIds:
'["' + セキュリティグループID + '"]'
- subnetId:
- protectedResourceType:
また、セキュリティグループIDは、'["' + セキュリティグループID + '"]'
の形式です。セキュリティグループIDのみを指定すると、デプロイできますが、未設定となります。
ここでは、AWS CDK の全サンプルコードから、「復元テストプラン」の実装(コンストラクト名:RestoreTestEc2)に絞って紹介します。コンストラクトの入力パラメータなど、詳しく知りたい方は「バックアップの CDK 実装(参考資料)」で確認してください。
それでは、次のサンプルコードを見てみましょう。
import { Construct } from 'constructs';
import * as backup from 'aws-cdk-lib/aws-backup';
import * as cwe from 'aws-cdk-lib/aws-events';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
// Interface for RestoreTestEc2
export interface RestoreTestEc2Props {
readonly backupRole: iam.IRole;
readonly backupVault: backup.IBackupVault;
readonly scheduleExpression: cwe.Schedule;
readonly vpc: ec2.IVpc;
readonly ec2AppSg: ec2.ISecurityGroup;
}
// Class for RestoreTestEc2
export class RestoreTestEc2 extends Construct {
constructor(scope: Construct, id: string, props: RestoreTestEc2Props) {
super(scope, id);
// CfnRestoreTestingPlan
const restoreTestingPlan = new backup.CfnRestoreTestingPlan(this, 'RestoreTestingPlanEc2', {
restoreTestingPlanName: 'RestoreTestingPlanEc2',
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, 'RestoreTestingSelectionEc2', {
restoreTestingPlanName: 'RestoreTestingPlanEc2',
restoreTestingSelectionName: 'RestoreTestingSelection',
iamRoleArn: props.backupRole.roleArn,
validationWindowHours: 1,
protectedResourceType: 'EC2',
protectedResourceConditions: {
stringEquals: [
{
key: 'aws:ResourceTag/Restore-test-ec2',
value: 'true',
},
],
},
restoreMetadataOverrides: {
requireImdsV2: 'true',
subnetId: props.vpc.selectSubnets({
subnetGroupName: 'Protected',
}).subnetIds[1],
securityGroupIds: '["' + props.ec2AppSg.securityGroupId + '"]',
},
});
restoreTestingSelection.addDependency(restoreTestingPlan);
}
}
復元リソース検証の仕組み
構成図に基づいて、復元リソース検証の仕組みを作成します。AWS CDK の L2 コンストラクトを使います。Lambda 関数 と EventBridge ルール を組合わせます。はじめに 検証プログラムの Lambda 関数を作成して、その後EventBridge ルールを作成します。これは依存関係があるからです。
EventBridge ルールは、次の設定をします。
- イベントパターン設定
AWS Backup 復元ジョブの完了イベント
- ターゲット設定
Lambda 関数
ここでは、AWS CDK の全サンプルコードから、「復元リソース検証の仕組み」の実装(コンストラクト名:Ec2Validate)に絞って紹介します。コンストラクトの入力パラメータなど、詳しく知りたい方は「バックアップの CDK 実装(参考資料)」で確認してください。
それでは、次のサンプルコードを見てみましょう。
import { Construct } from 'constructs';
import { Duration, Stack } from 'aws-cdk-lib';
import * as cwe from 'aws-cdk-lib/aws-events';
import * as cwet from 'aws-cdk-lib/aws-events-targets';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as node_lambda from 'aws-cdk-lib/aws-lambda-nodejs';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { NagSuppressions } from 'cdk-nag';
// Interface for Ec2ValidateProps
export interface Ec2ValidateProps {
readonly vpc: ec2.IVpc;
readonly lambdaSg: ec2.ISecurityGroup;
readonly ec2AppSg: ec2.ISecurityGroup;
}
// Class for Ec2Validate
export class Ec2Validate extends Construct {
constructor(scope: Construct, id: string, props: Ec2ValidateProps) {
super(scope, id);
// EC2 Validate Function
const ec2ValidateFunction = new node_lambda.NodejsFunction(this, 'Ec2ValidateFunction', {
runtime: lambda.Runtime.NODEJS_20_X,
entry: 'lambda/ec2-validate.ts',
handler: 'handler',
memorySize: 256,
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/*'],
},
environment: {
NODE_OPTIONS: '--enable-source-maps',
POWERTOOLS_SERVICE_NAME: 'Ec2Validate',
},
vpc: props.vpc,
vpcSubnets: props.vpc.selectSubnets({
subnetGroupName: 'Protected',
}),
securityGroups: [props.lambdaSg],
});
// addToRolePolicy
ec2ValidateFunction.addToRolePolicy(
new iam.PolicyStatement({
actions: ['ec2:DescribeInstances', 'backup:PutRestoreValidationResult'],
resources: ['*'],
}),
);
// Event rule "Restore Job State Change" from AWS Backup
const ec2ValidateRule = new cwe.Rule(this, 'Ec2ValidateRule', {
description: 'Restore Job State Change',
eventPattern: {
source: ['aws.backup'],
detailType: ['Restore Job State Change'],
detail: {
resourceType: ['EC2'],
status: ['COMPLETED'],
},
},
targets: [new cwet.LambdaFunction(ec2ValidateFunction)],
});
ec2ValidateRule.node.addDependency(ec2ValidateFunction);
// cdk-nag suppressions
NagSuppressions.addResourceSuppressions(ec2ValidateFunction, [
{
id: 'AwsSolutions-L1',
reason: 'Need use node v20 for vpc Lambda',
},
]);
NagSuppressions.addResourceSuppressions(
ec2ValidateFunction,
[
{
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:
EC2
} - detail: { status:
COMPLETED
}
Lambda コードでは、次の項目を使います。
- detail: { restoreJobId:
復元ジョブ ID
} - detail: { createdResourceArn:
復元リソース ARN
}
イベントのサンプルは、次の JSON です。
{
"version": "0",
"id": "da957b0c-b849-f5f7-4d93-9ab18381c9e3",
"detail-type": "Restore Job State Change",
"source": "aws.backup",
"account": "111223344556",
"time": "2024-12-09T07:37:06Z",
"region": "ap-northeast-1",
"resources": [
"arn:aws:ec2:ap-northeast-1::image/ami-11122334455667788"
],
"detail": {
"restoreJobId": "796538D6-8D6F-E12E-3CE7-020CD7D87C80",
"backupSizeInBytes": "10737418240",
"creationDate": "2024-12-09T07:34:46.723Z",
"iamRoleArn": "arn:aws:iam::111223344556:role/Dev-ResearchAwsBackup-IamBackupRole95AD45E4-SwDJ3X816vxR",
"percentDone": 0,
"resourceType": "EC2",
"status": "COMPLETED",
"createdResourceArn": "arn:aws:ec2:ap-northeast-1:111223344556:instance/i-01c2a061fd30ef9ed",
"completionDate": "2024-12-09T07:36:03.549713059Z",
"restoreTestingPlanArn": "arn:aws:backup:ap-northeast-1:111223344556:restore-testing-plan:RestoreTestingPlanEc2-fe104f11-aad9-440f-86d1-97046f138f83",
"backupVaultArn": "arn:aws:backup:ap-northeast-1:111223344556:backup-vault:DevResearchAwsBackupBackupVaultsEc276FC1F0B",
"recoveryPointArn": "arn:aws:ec2:ap-northeast-1::image/ami-11122334455667788",
"sourceResourceArn": "arn:aws:ec2:ap-northeast-1:111223344556:instance/i-f31461a3769bc9d5"
}
}
Lambda コード
Lambda コードは、復元された EC2 インスタンスを検証します。検証処理は、EC2 インスタンスから HTTP レスポンスがあることを確認します。言語にはTypeScript を使います。採用の理由は、型チェックやコード補完など、効率よくコーディングできるからです。
次のAWS 公式ブログのPythonコードを参考にしています。
処理の流れは、次ようになります。
-
EC2 インスタンス IP の取得
イベントの復元リソース ARN
から EC2 インスタンス ID を抽出、EC2 API を使って EC2 インスタンス IP を取得します -
復元リソースの検証
復元された EC2 インスタンスの HTTP レスポンスを検証します -
検証結果の報告
イベントの復元ジョブ 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 { EC2Client, DescribeInstancesCommand } from '@aws-sdk/client-ec2';
import { BackupClient, PutRestoreValidationResultCommand, RestoreValidationStatus } from '@aws-sdk/client-backup';
// 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;
};
// lambdaHandler
const lambdaHandler = async (event: EventBridgeEvent<string, RestoreEventDetail>): Promise<void> => {
// Get instance ID from event
const instanceId = event.detail.createdResourceArn.split(':')[5].split('/')[1];
logger.info(`EC2 instance ID: ${instanceId}`);
// Get privateIp from EC2
let privateIp: string;
const ec2Client = new EC2Client({});
try {
const describeInstance = await ec2Client.send(
new DescribeInstancesCommand({
InstanceIds: [instanceId],
}),
);
logger.info('DescribeInstance', { describeInstance: describeInstance });
// privateIpAddress form describeInstance
privateIp = describeInstance.Reservations?.[0].Instances?.[0].PrivateIpAddress || '';
} catch (err) {
logger.error('Error DescribeInstancesCommandOutput', err as Error);
throw err;
}
logger.info(`EC2 instance privateIp: ${privateIp}`);
// Fetch to URL and validate EC2 instance
const url = `http://${privateIp}`;
let validationStatus: RestoreValidationStatus | undefined = undefined;
try {
logger.info(`Sending HTTP GET request URL: ${url}`);
// Fetch URL
const response = await fetch(url);
logger.info(`Recevied response status: ${response.status}`);
if (response.ok) {
logger.info('Valid response received', await response.text());
validationStatus = 'SUCCESSFUL';
} else {
logger.info(`Invalid response status: ${response.status}`);
validationStatus = 'FAILED';
}
} catch (err) {
logger.error('Error connecting to the application', err as Error);
validationStatus = 'FAILED';
}
// Put Restore Validation Result
const backupClient = new BackupClient({});
try {
const putRestoreValidationResult = await backupClient.send(
new PutRestoreValidationResultCommand({
RestoreJobId: event.detail.restoreJobId,
ValidationStatus: validationStatus,
}),
);
logger.info('PutRestoreValidationResult', { putRestoreValidationResult: putRestoreValidationResult });
} catch (err) {
logger.error('Error putRestoreValidationResult', err as Error);
throw err;
}
};
// handler
export const handler = middy(lambdaHandler).use(injectLambdaContext(logger, { logEvent: true }));
復元テストの実行確認
ここまでに準備した AWS CDK コードを元に、次の実行までを確認します。
- AWS CDK デプロイ実行
- バックアップジョブ実行
それでは最後に、復元テストの実行です。次の確認をします。
- 復元ジョブ実行
- 復元リソース検証
- 復元リソース削除
復元ジョブ実行
- AWS Backup の復元テストプランに基づき、復元ジョブが実行されます
- 復元パラメータ: サブネット、セキュリティグループ
- リソース( EC2 )が復元され、復元ジョブが完了すると、AWS Backup の完了イベントが発行されます
- ステータス(復元)が「実行中」→「完了」に変更されます
- リソース(EC2)の復元が確認できます
復元リソース検証
- EventBridge のルールによって、Lambda (検証プログラム)が起動されます
- 検証プログラムによって、復元されたリソースの状態が確認されます。( HTTP レスポンスの確認)
- AWS SDK( AWS Backup API )によって、検証結果が AWS Backup に報告されます
- 検証ステータスが「検証中」→「成功」に変更されます
復元リソース削除
- 検証が完了、または時限超過すると、復元リソース( EC2 )が削除されます
- 検証ステータスが「削除中」→「成功」に変更されます
- リソース( EC2 )の削除(終了)が確認できます
おわりに
ここまで、なぜ復元テストの自動化が必要なのか、AWS Backup の復元テスト機能とは何か、どのように復元テストを自動化するのか、AWS CDK (TypeScript) のサンプルコード付きで紹介しました。復元テストの自動化に興味のある開発者の方々にとって、少しでも参考になれば幸いです。
私は AWS CDK の経験が浅く、復元テストプランの CDK 実装では、L1 コンストラクトのデプロイエラーに悩まされました。AWS CDK コードの理解だけでなく、合成される CloudFormation テンプレートを確認したり、マネジメントコンソールで設定した内容を AWS CLI で出力して比較したりすることが、解決につながることを学びました。
また、この記事の作成にあたり、AWS 公式ドキュメントの情報を参考にさせていただきました。AWS Blog、サンプルコードなどの詳細なドキュメントに感謝いたします。
最後までお読みいただき、ありがとうございました。
参考資料
AWS Document
-
AWS Well-Architected フレームワーク: REL09-BP04 データの定期的な復旧を行ってバックアップの完全性とプロセスを確認する
-
AWS CDK Reference: class CfnRestoreTestingSelection (construct)
-
AWS CloudFormation User Guide: EC2推定メタデータ上書きの書式:SecurityGroupIds
-
AWS SDK for JavaScript v3: PutRestoreValidationResultCommand
AWS Blog
Powertools for AWS Lambda
採用した CDK 実装方法
CDK 実装方法は、次のとおり解説します。
- 全体方針
- プロジェクト構成
折りたたみを展開して表示して下さい。
全体方針
Baseline Environment on AWS v3 のゲストシステム を参考としてます。
- CDK コードは TypeScript を使用
- 環境ごとのパラメータは、TypeScriptで定義
- 1つのスタックと複数のコンストラクトで構成
- L2 コンストラクトを使用し、未対応の部分は L1 コンストラクトを使用
- バックアッププランのリソース選択
- 復元テストプランのリソース選択
- スナップショットテストの実施
今回、コード開発を効率化する工夫をしています。
工夫点:
- 試行錯誤を行うコンストラクトは、パラメータでデプロイ有無を制御
- EC2 インスタンス
- AWS Backup バックアッププラン
- AWS Backup 復元テストプラン
- 復元リソース検証( Lambda, EventBridge ルール)
- 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 |
ファイル構成
Baseline Environment on AWS v3 のゲストシステム を参考としてます。
research-awsbackup
├── bin
│ └── research-awsbackup.ts # CDK App
├── lambda
│ └── ec2-validate.ts # Lambdaコード
├── lib
│ ├── construct
│ │ ├── backup-ec2.ts # AWS Backupバックアッププラン
│ │ ├── backup-vaults.ts # AWS Backupバックアップボールト
│ │ ├── ec2-app.ts # EC2インスタンス
│ │ ├── ec2-validate.ts # Lambda, EventBridgeルール
│ │ ├── iam.ts # IAMロール
│ │ ├── networking.ts # VPC、サブネットなどのネットワーク、セキュリティグループ
│ │ └── restore-test-ec2.ts # AWS Backup復元テストプラン
│ └── stack
│ └── research-awsbackup-stack.ts # スタック
├── test
│ └── research-awsbackup.test.ts # スナップショットテスト
└── paramater.ts # 環境パラメータ
バックアップの CDK 実装
バックアップの CDK 実装は、次のとおり解説します。
- 構成図
- パラメータ
- App
- スタック
- テストコード
- コンストラクト
折りたたみを展開して表示して下さい。
構成図
バックアップの CDK 実装は、構成図に示す範囲を対象とします。
ネットワークは、復元テストの範囲も含みます。
パラメータ
稼働環境ごとのパラメータを設定します。
- AWSアカウント
- リージョン
- VPC CIDR
- デプロイフラグ
コードは、折りたたみを展開して表示して下さい。
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 App Parameter
export interface AppParameter {
env: Environment;
envName: string;
vpcCidr: string;
ec2App: Ec2AppParameter;
backupEc2: BackupParameter;
restoreTestEc2: RestoreTestParameter;
ec2Validate: ValidateParameter;
}
// Define for Dev Parameter
export const devParameter: AppParameter = {
env: {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: 'ap-northeast-1',
},
envName: 'Development',
vpcCidr: '10.0.0.0/16',
ec2App: {
instanceType: ec2.InstanceClass.T3,
instanceSize: ec2.InstanceSize.MICRO,
deploy: true,
},
backupEc2: {
scheduleExpression: cwe.Schedule.cron({
minute: '00',
hour: '05',
weekDay: 'MON-FRI',
}),
deploy: true,
},
restoreTestEc2: {
scheduleExpression: cwe.Schedule.cron({
minute: '00',
hour: '06',
weekDay: 'FRI#4',
}),
deploy: true,
},
ec2Validate: {
deploy: true,
},
};
App
CDK の起点として、以下を実施します。
- コード検証ツール設定
- スタックのインスタンス化
コードは、折りたたみを展開して表示して下さい。
#!/usr/bin/env node
import { App, Aspects } from 'aws-cdk-lib';
import { AwsSolutionsChecks } from 'cdk-nag';
import { ResearchAwsBackupStack } from '../lib/stack/research-awsbackup-stack';
import { devParameter } from '../parameter';
const app = new App();
Aspects.of(app).add(new AwsSolutionsChecks({}));
new ResearchAwsBackupStack(app, 'Dev-ResearchAwsBackup', {
env: {
account: devParameter.env.account,
region: devParameter.env.region,
},
appParameter: devParameter,
});
スタック
1スタック構成です。
このスタックでは、7つのコンストラクトをインスタンス化します。
No.4~7のコンストラクトは、デプロイフラグに応じてインスタンス化を制御します。
No. | コンストラクト | 内容 |
---|---|---|
1 | Networking | VPC、サブネットなどのネットワーク、セキュリティグループ |
2 | Iam | EC2、AWS Backup 用の IAM ロール |
3 | BackupVaults | AWS Backup Vaults |
4 | Ec2App | EC2 インスタンス |
5 | BackupEc2 | AWS Backup のバックアッププラン |
6 | RestoreTestEc2 | AWS Backup の復元テストプラン |
7 | Ec2Validate | 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';
// 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');
// 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,
});
}
}
}
テストコード
スタックのスナップショットテストを実施します。
コードは、折りたたみを展開して表示して下さい。
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 { 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 lambdaSg: ec2.ISecurityGroup;
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;
// Security Group for VPC Endpoint
const endPointSg = new ec2.SecurityGroup(this, 'EndpointSg', {
vpc: vpc,
allowAllOutbound: false,
});
this.endPointSg = endPointSg;
// 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');
// 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');
// 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 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;
constructor(scope: Construct, id: string) {
super(scope, id);
// Backup Vault for Ec2
this.ec2 = new backup.BackupVault(this, 'Ec2', {
removalPolicy: RemovalPolicy.DESTROY,
});
}
}
Ec2App
EC2 インスタンスを作成します。
復元テスト向けに以下の設定します。
- ユーザーデータ: HTTPサーバー起動スクリプト (復元検証用)
- タグ:
Daily-backup-ec2 = true
(バックアップ対象) - タグ:
Restore-test-ec2 = true
(復元テスト対象)
コードは、折りたたみを展開して表示して下さい。
import { Tags } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as iam from 'aws-cdk-lib/aws-iam';
import { NagSuppressions } from 'cdk-nag';
// Interface for Ec2AppProps
export interface Ec2AppProps {
readonly vpc: ec2.IVpc;
readonly ec2AppSg: ec2.ISecurityGroup;
readonly ssmInstanceRole: iam.IRole;
readonly instancdType: ec2.InstanceClass;
readonly instanceSize: ec2.InstanceSize;
}
// Construct for Ec2App
export class Ec2App extends Construct {
constructor(scope: Construct, id: string, props: Ec2AppProps) {
super(scope, id);
// UserData for AppServers
const userData = ec2.UserData.forLinux({ shebang: '#!/bin/bash' });
userData.addCommands(
'sudo dnf -y install postgresql15',
'sudo dnf -y install httpd',
'sudo systemctl enable httpd',
'sudo systemctl start httpd',
'echo "<h1>Hello from $(hostname)</h1>" > /var/www/html/index.html',
'chown apache.apache /var/www/html/index.html',
);
// AvailabilityZones from SubnetType.PRIVATE_ISOLATED
const privateAzs = props.vpc.selectSubnets({
subnetGroupName: 'Protected',
}).availabilityZones;
// AppInstance
const appInstance = new ec2.Instance(this, 'AppInstance', {
vpc: props.vpc,
availabilityZone: privateAzs[0],
vpcSubnets: {
subnetGroupName: 'Protected',
},
instanceType: ec2.InstanceType.of(props.instancdType, props.instanceSize),
machineImage: new ec2.AmazonLinuxImage({
generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2023,
}),
securityGroup: props.ec2AppSg,
role: props.ssmInstanceRole,
blockDevices: [
{
deviceName: '/dev/xvda',
volume: ec2.BlockDeviceVolume.ebs(10, {
encrypted: true,
}),
},
],
detailedMonitoring: true,
requireImdsv2: true,
userData: userData,
});
// Tags for appInstance
Tags.of(appInstance).add('Daily-backup-ec2', 'true');
Tags.of(appInstance).add('Restore-test-ec2', 'true');
// cdk-nag suppressions
NagSuppressions.addResourceSuppressions(appInstance, [
{
id: 'AwsSolutions-EC29',
reason: "This instance is to use for maintenance of this system. It's no problem if it was deleted.",
},
]);
}
}
BackupEc2
AWS Backup バックアッププランを作成します。
- 頻度: 毎日
- リソース選択
- タイプ:
EC2
- タグ:
Daily-backup-ec2 = 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 cwe from 'aws-cdk-lib/aws-events';
// Interface for BackupEc2: backupRole, backupVault
export interface BackupEc2Props {
readonly backupRole: iam.IRole;
readonly backupVault: backup.IBackupVault;
readonly scheduleExpression: cwe.Schedule;
}
// Class for BackupEc2
export class BackupEc2 extends Construct {
constructor(scope: Construct, id: string, props: BackupEc2Props) {
super(scope, id);
// Backup Plan Ec2
const backupPlan = new backup.BackupPlan(this, 'BackupPlanEc2', {
backupVault: props.backupVault,
// Backup Plan Rule
backupPlanRules: [
new backup.BackupPlanRule({
ruleName: 'EC2',
scheduleExpression: props.scheduleExpression,
startWindow: Duration.hours(1),
completionWindow: Duration.hours(2),
}),
],
});
// Backup Plan Selection EC2 and Tag
new backup.CfnBackupSelection(this, 'Selection', {
backupPlanId: backupPlan.backupPlanId,
backupSelection: {
iamRoleArn: props.backupRole.roleArn,
selectionName: 'Selection',
conditions: {
StringEquals: [
{
ConditionKey: 'aws:ResourceTag/Daily-backup-ec2',
ConditionValue: 'true',
},
],
},
resources: ['arn:aws:ec2:*:*:instance/*'],
},
});
}
}