2
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で復元テストを自動化しよう! AWS CDK サンプルコード付き

Last updated at Posted at 2025-02-10

はじめに

この記事では、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 インスタンスの復元テストでは次の手順が必要です。

  1. バックアップから AWS リソースを復元する
  2. 復元リソースを検証する (例: アプリケーションが正常動作するか確認)
  3. 復元リソースを削除する

これらを自動化することで、AWS のベストプラクティスに基づく運用が可能です。AWS Well-Architected Framework 信頼性の柱(REL09-BP04 データの定期的な復旧を行い、バックアップの完全性とプロセスを確認する)でも、自動化された復元テストが推奨されています。

AWS Backup の復元テスト機能とは?

AWS Backup は、バックアップ管理などいくつかの機能を提供しています。その中の一つに「復元テスト機能」があります。この機能を使うと、EC2、RDS など、AWS リソースの復元テストを自動実行できます。使う際のポイントに絞って、次のように解説します。

  • 機能概要:
    • テスト頻度、実行時間枠、対象リソース、復元先などを指定すると、復元テストが自動実行されます
  • 実行タイミング:
    • 復元テストは、指定の実行時間枠内でランダムに実行されます(正確な実行時刻を制御できません)
  • 復元先:
  • 検証方法:
    • 復元リソースの検証には、追加の仕組みが必要です(復元テスト機能には含まれません)

機能概要

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 関数)

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

  1. 構成図
  2. 処理の流れ
  3. 実行確認

構成図

AWS CDK で構築する 復元テスト環境の構成図です。上部「バックアップ」、下部「復元テスト」の2つで構成されています。次のAWS 公式ブログを参考に、AWS Backup 復元テスト機能を活用できるよう工夫しています。

構成のポイント:

  • AWS Backup バックアッププランによる EC2 インスタンスのバックアップ
  • AWS Backup 復元テストプランによる EC2 インスタンスの自動復元と削除
  • Lambda 関数 (TypeScript) による復元された EC2 インスタンスの検証と結果報告

構成図

処理の流れ

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

  1. バックアップ
    AWS Backup のバックアッププランに基づいて、EC2 インスタンスのバックアップジョブが実行されます
  2. 復元
    AWS Backup の復元テストプランに基づいて、EC2 インスタンスの復元ジョブが実行されます
  3. イベント発行
    EC2 インスタンスの復元が完了すると、AWS Backup から完了イベントが発行されます
  4. Lambda 実行
    EventBridge のルールに基づいて、復元完了イベントをトリガーとして Lambda 関数(検証プログラム)が実行されます
  5. 復元リソース検証
    Lambda 関数(検証プログラム)は、復元された EC2 インスタンスの状態を確認します(例: HTTP レスポンスの確認)
  6. 検証結果報告
    AWS Backup API の PutRestoreValidationResult を使って、検証結果を AWS Backup に報告します
  7. 復元リソース削除
    復元リソース検証が完了、または保存時間に達すると、復元された EC2 インスタンスは自動的に削除されます

実行確認

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

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

  • 復元ステータス: 完了
  • 検証ステータス: 成功
  • 削除ステータス: 成功

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

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

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

  1. 採用した CDK 実装方法 (参考資料)
    CDK プロジェクト構成、使用ライブラリなど
  2. バックアップの CDK 実装 (参考資料)
    ネットワーク、EC2 インスタンス、バックアッププランなど
  3. 復元テストの CDK 実装
    復元テストプラン、復元リソース検証の仕組みを実装します
  4. 復元リソース検証の Lambdaコード
    EC2 インスタンスの検証プログラム(HTTP レスポンスの確認)を実装します
  5. 復元テストの実行
    CDK デプロイ、復元テストの自動実行を確認します

復元テストの AWS CDK 実装

構成図

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

構成図

復元テストプラン

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

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

  • CfnRestoreTestingPlan

    • scheduleExpression: テスト頻度、開始時間(時分)
    • startWindowHours: 次の時間以内に開始(時間単位)
  • CfnRestoreTestingSelection

    • protectedResourceType: リソースタイプ( EC2 )
    • restoreMetadataOverrides: 復元パラメータ
      • subnetId: サブネットID
      • securityGroupIds: '["' + セキュリティグループID + '"]'

また、セキュリティグループIDは、'["' + セキュリティグループID + '"]' の形式です。セキュリティグループIDのみを指定すると、デプロイできますが、未設定となります。

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

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

./lib/construct/restore-test-ec2.ts (コンストラクト名:RestoreTestEc2)
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 実装(参考資料)」で確認してください。

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

./lambda/ec2-validate.ts (コンストラクト名:Ec2Validate)
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コードを参考にしています。

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

  1. EC2 インスタンス IP の取得
    イベントの復元リソース ARNから EC2 インスタンス ID を抽出、EC2 API を使って EC2 インスタンス IP を取得します
  2. 復元リソースの検証
    復元された EC2 インスタンスの HTTP レスポンスを検証します
  3. 検証結果の報告
    イベントの復元ジョブ IDを基に、AWS Backup API の PutRestoreValidationResult を使って、検証結果を AWS Backup の復元ジョブに設定します

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

./lambda/ec2-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 { 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 コードを元に、次の実行までを確認します。

  1. AWS CDK デプロイ実行
  2. バックアップジョブ実行

それでは最後に、復元テストの実行です。次の確認をします。

  1. 復元ジョブ実行
  2. 復元リソース検証
  3. 復元リソース削除

復元ジョブ実行

  • AWS Backup の復元テストプランに基づき、復元ジョブが実行されます
    • 復元パラメータ: サブネット、セキュリティグループ
  • リソース( EC2 )が復元され、復元ジョブが完了すると、AWS Backup の完了イベントが発行されます
  • ステータス(復元)が「実行中」→「完了」に変更されます

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

  • リソース(EC2)の復元が確認できます

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

復元リソース検証

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

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

復元リソース削除

  • 検証が完了、または時限超過すると、復元リソース( EC2 )が削除されます
  • 検証ステータスが「削除中」→「成功」に変更されます

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

  • リソース( EC2 )の削除(終了)が確認できます

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

おわりに

ここまで、なぜ復元テストの自動化が必要なのか、AWS Backup の復元テスト機能とは何か、どのように復元テストを自動化するのか、AWS CDK (TypeScript) のサンプルコード付きで紹介しました。復元テストの自動化に興味のある開発者の方々にとって、少しでも参考になれば幸いです。

私は AWS CDK の経験が浅く、復元テストプランの CDK 実装では、L1 コンストラクトのデプロイエラーに悩まされました。AWS CDK コードの理解だけでなく、合成される CloudFormation テンプレートを確認したり、マネジメントコンソールで設定した内容を AWS CLI で出力して比較したりすることが、解決につながることを学びました。

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

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

参考資料

AWS Document

AWS Blog

Powertools for AWS Lambda

採用した CDK 実装方法

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

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

全体方針

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

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

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

工夫点:

  • 試行錯誤を行うコンストラクトは、パラメータでデプロイ有無を制御
    • EC2 インスタンス
    • AWS Backup バックアッププラン
    • AWS Backup 復元テストプラン
    • 復元リソース検証( Lambda, EventBridge ルール)
  • 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

ファイル構成

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 実装は、次のとおり解説します。

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

構成図

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

構成図

パラメータ

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

  • AWSアカウント
  • リージョン
  • VPC CIDR
  • デプロイフラグ
コードは、折りたたみを展開して表示して下さい。
./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 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 の起点として、以下を実施します。

  • コード検証ツール設定
  • スタックのインスタンス化
コードは、折りたたみを展開して表示して下さい。
./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 EC2、AWS Backup 用の IAM ロール
3 BackupVaults AWS Backup Vaults
4 Ec2App EC2 インスタンス
5 BackupEc2 AWS Backup のバックアッププラン
6 RestoreTestEc2 AWS Backup の復元テストプラン
7 Ec2Validate 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';

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

テストコード

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

コードは、折りたたみを展開して表示して下さい。
./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 { 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 ロールでは、バックアップだけでなく、復元の権限が必要です。

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

  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 (復元テスト対象)
コードは、折りたたみを展開して表示して下さい。
./lib/construct/ec2-app.ts (コンストラクト名:Ec2App)
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
コードは、折りたたみを展開して表示して下さい。
./lib/construct/backup-ec2.ts (コンストラクト名:BackupEc2)
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/*'],
      },
    });
  }
}
2
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
2
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?