GAT技術ブログ

株式会社ギブ・アンド・テイクの技術ブログ

GAT技術ブログ

AWS BLEA for FSI を参考に、ウォームスタンバイ DR 構成を構築してみた

1. はじめに

本記事では、Baseline Environment on AWS for Financial Services Institute(BLEA for FSI)の考え方を参考にしつつ、ウォームスタンバイ型のマルチリージョン DRを、AWS マネジメントコンソール中心で組み立てて検証します。

参考:BLEA for FSI

この記事で扱うのは自動切り替えではなく「手動切り替え」です。

CloudWatch Synthetics Canary と CloudWatch Alarm で異常を検知したあと、オペレーターが(大阪リージョン・監視リージョン)の両地点で異常を確認し、Step Functions を手動実行して切り替えるという運用を前提とします。

この記事でわかること

  • ウォームスタンバイ DR の「構成要素」を AWS サービスに落とし込む考え方

  • Route 53 ARC を使った「安全に切り替えるスイッチ」の作り方

  • DR 発動時の切り替えを Step Functions から実行する例

  • 切り替え中のアプリ挙動を「共有ステート」で制御する(DynamoDB グローバルテーブル)例

この記事で扱わないこと

  • ALB / ECS / Aurora / Secrets Manager の単一リージョン構築手順(前提として作成済み)

  • 監視結果をトリガーに切り替えまで自動実行する仕組み

  • 切り戻し手順・設計


2. AWS における 4 つの DR 戦略とウォームスタンバイ

2.1 4 つの DR 戦略の概要

AWS の公式ドキュメントでは、AWS における DR(Disaster Recovery)戦略は、おおまかに次の 4 つに分類されています。

それぞれは RTO(復旧時間目標)/ RPO(復旧時点目標)とコストのトレードオフで位置づけられます。

  1. バックアップ&リストア

    • 別リージョンにはバックアップのみを保持し、災害時に復旧先を新規構築して復元する。
  2. パイロットライト

    • 復旧先には最小限のコア(例:データ層)を常時起動し、災害時にアプリ層を拡張して受ける。
  3. ウォームスタンバイ(本記事で採用)

    • 復旧先に「縮小版の本番環境」を常時稼働させ、災害時は拡張+ルーティング切り替えで復旧する。
  4. マルチサイト / アクティブ-アクティブ

    • 複数リージョンで同時にトラフィックを受け、障害時も片系で継続する。

参考:クラウド内での災害対策オプション

2.2 今回の構成でウォームスタンバイを選んだ理由

金融系ワークロードでは、RTO / RPO をある程度短く保ちたい一方で、常時アクティブ-アクティブほどのコストはかけづらいケースが多いです。

そこで本記事では、以下のようなウォームスタンバイ構成を題材にします。

  • データ層:Aurora Global Database によるクロスリージョンレプリケーション

  • トラフィック制御:Route 53 ARC による 「安全ルール付きの手動スイッチ」

  • 運用:外形監視(Synthetics)で検知 → 人が判断 → Step Functions を手動実行


3. アーキテクチャ概要

今回構築するウォームスタンバイ構成の全体像は、次のとおりです。

本章では、後続の手順を理解しやすくするために、どのリージョンに、どの役割のコンポーネントがあるかを整理します。


3.1 リージョン構成と役割

本構成では、役割の異なる次の 3 つのリージョンを使用します。

  • 東京リージョン(ap-northeast-1)
    通常時にトラフィックを受けるプライマリ。ALB / ECS と Aurora Global Database のプライマリ DB クラスター(ライター)が稼働します。

  • 大阪リージョン(ap-northeast-3)
    障害時に切り替えるウォームスタンバイ。縮小キャパシティの ALB / ECS と、Aurora Global Database のセカンダリ DB クラスター(リーダー)が常時稼働します。

  • オレゴンリージョン(us-west-2)
    監視専用リージョン。Synthetics Canary を配置し、リージョン固有障害と監視側障害の切り分け(第三地点)に使います。


3.2 アプリケーションレイヤー

東京・大阪の両リージョンに、同一構成のアプリケーションスタックを配置します。

  • Application Load Balancer(ALB)

  • ECS on Fargate で動作する API アプリケーション
    (例:残高照会・取引・件数管理などの勘定系 API)

通常時は東京のみがトラフィックを受け、大阪はウォームスタンバイとして待機します。


3.3 データベースレイヤー

データベースには Aurora Global Database(PostgreSQL)を使用します。

  • 東京リージョン :プライマリ DB クラスター

  • 大阪リージョン:セカンダリ DB クラスター

各リージョンのECSタスクは、同一リージョンにある Aurora の「クラスターエンドポイント(タイプ:ライター)」へ接続します。

セカンダリクラスターではこのエンドポイントが ステータス inactive(書き込みを処理しない) と表示されます。これはセカンダリがライターを持たないためで、接続自体はできますが、書き込みはエラーになり、読み取りのみ実行可能です。

ただし、セカンダリだったリージョンがプライマリに昇格すると、同じエンドポイントがライターとして有効になり、書き込みも可能になります。

また、Aurora Global Database では、Global Database ライターエンドポイント(フェイルオーバー/スイッチオーバー後も常にプライマリのライターを指すエンドポイント)も利用できます。

本記事では、グローバルライターエンドポイントは使わず、東京は東京・大阪は大阪の接続先を参照する構成にします。


3.4 ステート管理(DR 制御用)

切り替え時にアプリの動作を安全に制御するため、DynamoDB グローバルテーブルを状態管理用ストアとして利用します。

このテーブルには、主に次のような情報を保持します。

  • 各リージョンの稼働状態(どのリージョンが Active か)

  • 切り替え中にアプリケーションの処理を制御するための 閉塞フラグ

この記事の例では、閉塞フラグ(is_closed)を主に扱い、Active の判定は「is_closed=false のリージョン」として扱います(= リクエストを受け付けてよい状態)。

切り替え中は Step Functions が is_closed を更新し、アプリ側はそれを参照して挙動を制御します。

なお、この DynamoDB テーブルは インフラ観点での切り替え判断やトリガーには使用しません。あくまで、

  • 切り替え中の二重書き込み防止

  • プライマリ/セカンダリのアプリケーション挙動制御

といった アプリケーション制御のための共有ステートとして利用する位置づけです。


3.5 トラフィック制御

外部からのアクセスは、Route 53 を通じて ALB にルーティングされます。

  • Route 53 パブリックホストゾーン
  • Route 53 Application Recovery Controller(ARC)
    • Routing Control
    • Safety Rules

ARC の Routing Control 状態に応じて、東京 ALB / 大阪 ALB どちらに流すかを制御します。


3.6 監視と切り替えの位置づけ

東京リージョンの API に対して、以下のリージョンから外形監視を行います。

  • 大阪リージョンの Canary
  • 監視リージョン(オレゴン)の Canary

2 地点の外形監視結果をもとに、オペレーターが判断して Step Functions を手動実行し、リージョン切り替えを行います。


4. 前提:DR 検証で利用したアプリケーションの構成

本記事は DR 構成の設計・切り替えが主題のため、ALB / ECS / Aurora / Secrets Manager などの単一リージョン構成は作成済みという前提で進めます。

4.1 ECS で利用した Dockerfile / server.js の概要

ECS で動かしているアプリは、シンプルな Node.js + Express 構成です。

  • Aurora PostgreSQL へは SSL(RDS グローバル CA バンドル)を使って接続
  • /accounts にアクセスすることで DB の内容を表示するだけのサンプル API
  • 画面上の文言で「東京/大阪」の切り替わりを判別

本検証では、東京用/大阪用でコンテナイメージを分け、トップページの表示文言で到達リージョンを判別できるようにしています。

※本番想定では、エラーメッセージの詳細をレスポンスに返さないなどの配慮が必要です(ここでは検証のためレスポンスに含めています)。

Dockerfile の内容

# シンプルな Node.js 実行環境
FROM node:18-alpine

WORKDIR /usr/src/app

# CA証明書ストア + curl を追加し、RDS の CA バンドル(グローバル)を取得
RUN apk add --no-cache ca-certificates curl \
  && update-ca-certificates \
  && curl -fsSL https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem \
     -o /usr/src/app/rds-ca-bundle.pem

# 依存関係だけ先にコピーして npm install
COPY package*.json ./
RUN npm install --only=production

# アプリ本体をコピー
COPY . .

# Express の待ち受けポート
EXPOSE 3000

CMD ["node", "server.js"]

server.js の内容

// server.js
const express = require('express');
const { Pool } = require('pg');
const fs = require('fs');

const app = express();

// RDS CA バンドルのパス
const caPath = process.env.RDS_CA_BUNDLE_PATH || '/usr/src/app/rds-ca-bundle.pem';

// ECS タスク定義から渡される環境変数で接続先を切り替えられるようにする
const pool = new Pool({
  host: process.env.PGHOST,
  port: Number(process.env.PGPORT) || 5432,
  user: process.env.PGUSER,
  password: process.env.PGPASSWORD,
  database: process.env.PGDATABASE,

  // SSL/TLS(CA検証あり)
  ssl: {
    ca: fs.readFileSync(caPath, 'utf8'),
    rejectUnauthorized: true,
  },
});

// シンプルなトップページ
app.get('/', (_req, res) => {
  res.send('<h1>Core Banking Sample Tokyo</h1><p><a href="/accounts">口座一覧を見る</a></p>');
});

// /accounts で Aurora PostgreSQL の accounts テーブルを HTML テーブル表示
app.get('/accounts', async (_req, res) => {
  try {
    const result = await pool.query(`
      SELECT
        account_number,
        holder_name,
        balance,
        updated_at
      FROM accounts
      ORDER BY id;
    `);

    const rows = result.rows;

    // すごくシンプルな HTML テーブルに整形
    const htmlRows = rows
      .map(
        (r) => `
        <tr>
          <td>${r.account_number}</td>
          <td>${r.holder_name}</td>
          <td style="text-align:right">${Number(r.balance).toLocaleString()}</td>
          <td>${r.updated_at}</td>
        </tr>`
      )
      .join('');

    const html = `
      <!doctype html>
      <html lang="ja">
      <head>
        <meta charset="utf-8" />
        <title>口座一覧</title>
        <style>
          table { border-collapse: collapse; }
          th, td { border: 1px solid #ccc; padding: 4px 8px; }
          th { background: #f5f5f5; }
        </style>
      </head>
      <body>
        <h1>口座一覧 (/accounts)</h1>
        <table>
          <thead>
            <tr>
              <th>口座番号</th>
              <th>名義</th>
              <th>残高</th>
              <th>更新日時</th>
            </tr>
          </thead>
          <tbody>
            ${htmlRows}
          </tbody>
        </table>
        <p><a href="/">TOPに戻る</a></p>
      </body>
      </html>
    `;
    res.send(html);
  } catch (err) {
    console.error('DB error', err);
    res.status(500).send('<h1>DB error</h1><pre>' + err.message + '</pre>');
  }
});

const port = process.env.PORT || 3000;
app.listen(port, () => {
  console.log(`listening on ${port}`);
});

アプリケーションコード(server.js)側では、

  • DB 接続情報はすべて 環境変数から取得

  • SSL 用の CA バンドルのパスも環境変数で上書き可能

という形にします。

4.2 ECS タスク定義での Secrets Manager 参照

DB 接続情報(ユーザー名・パスワードなど)は、 ECS タスク定義から Secrets Manager を参照する形で設定します。

タスク定義のコンテナ設定では、以下の環境変数を定義しました。

  • PGHOST
  • PGPORT
  • PGDATABASE
  • PGUSER
  • PGPASSWORD

このうち、PGDATABASE を除く項目については、値を直接記述するのではなく、Secrets Manager に保存したシークレットを参照します。

ECS から Secrets Manager の値を環境変数として取得するには、タスク定義の環境変数設定で 値のタイプを「ValueFrom」 に指定し、以下の形式でシークレットを参照します。

  • < Secrets Manager の ARN>:< シークレットのキー >::

参考:Amazon ECS 環境変数経由で Secrets Manager シークレットを伝達する

4.3 Secrets Manager の設計方針

Secrets Manager では、シークレット情報を 他リージョンへレプリケートすることができます。

ただし今回の構成では、上述のとおり「リージョンごとに接続先が異なる」ため、東京用/大阪用でシークレットを分ける設計にします。

ちなみに、Secrets Manager のシークレットは以下のような形式で登録します。

キー 登録する値(例:マスク済み)
username < Aurora に接続する DB ユーザ名 >
password < 上記ユーザーの DB パスワード >
engine postgres
host < 接続先のDB エンドポイント >
port 5432
dbClusterIdentifier < 対象 Aurora の DB クラスター識別子 >

5. 構築してみた:リージョン切り替えのためのセットアップ

本章では、リージョン切り替えに関わる DR リソースのみを対象に、AWS マネジメントコンソールから順に作成します。

5.1 DynamoDB の構築

リージョン切り替え時に利用する アプリケーション制御フラグを保持するため、DynamoDB テーブルを作成します。

テーブルは「リージョンごとに 1 レコードを持つ」前提のため、テーブル名とパーティションキーのみを指定し、その他の設定はすべてデフォルトのまま作成しました。

  • パーティションキーregion(String)

作成後、以下の 2 レコードを登録します。

  • 東京リージョン

    • region = ap-northeast-1
    • is_closed = false
  • 大阪リージョン

    • region = ap-northeast-3
    • is_closed = true


5.2 DynamoDB グローバルテーブルの作成

作成した DynamoDB テーブルに 大阪リージョンのレプリカを追加し、グローバルテーブル化します。

数分後、大阪リージョンにレプリカが作成されていることを確認できます。

5.3 Aurora Global Database の作成

ここでは、東京リージョンに作成済みの Aurora PostgreSQL クラスターを Aurora Global Database に拡張し、大阪リージョン側にセカンダリクラスター(クロスリージョンレプリカ)を追加します。

まず、RDS コンソールで対象クラスターを選択し、[アクション] → [AWS リージョンを追加]をクリックします。

なお、DB クラスターで Secrets Manager の統合を有効化している場合、Aurora Global Database は Secrets Manager 統合をサポートしていないので、グローバルデータベース作成時にエラーになります。

エラーが発生した場合は、 Secrets Manager 統合をオフにしてから進めます。

追加先リージョンには Asia Pacific(Osaka) を選択します。

あとは DB クラスターのクラスやサブネットグループなど、必要な項目を指定して作成を進めます(詳細パラメータは本記事では割愛)。

作成完了後、RDS コンソール上で 大阪リージョン側にセカンダリクラスターが追加されたことを確認できました。

5.4 大阪セカンダリの DB 接続情報を Secrets Manager に登録

Aurora Global Database を作成すると、大阪リージョン側(セカンダリクラスター)のライターエンドポイント(inactive)を確認できます。

この ライターエンドポイント(inactive)DB クラスター識別子を、大阪リージョン側 Secrets Manager の以下のキーに登録します。

  • host:< 大阪セカンダリの ライターエンドポイント >
  • dbClusterIdentifier:< 大阪側の DB クラスター識別子 >

そのほかの値(username、password、engine、port)は、東京リージョン側と同一の内容にします。

Aurora Global Database では、接続先として グローバルライターエンドポイントを使う選択肢があります。

これはフェイルオーバー/スイッチオーバー後もライター側へ追従するため、接続先の切り替え手間を減らせます

しかし、グローバルライターエンドポイントを使う場合、各リージョンのアプリ(ECS)から どちらのリージョンの DB にも到達できるネットワーク疎通が必要です(例:VPC ピアリング / Transit Gateway など)。

参考:グローバルライターエンドポイントの使用に関する考慮事項

今回は検証用に 東京 VPC と大阪 VPC を同一 CIDR で作成してしまい、VPC ピアリング等でリージョン間接続を組めませんでした。

そのため 東京は東京の DB エンドポイント/大阪は大阪の DB エンドポイントを参照する構成とし、Secrets Manager も東京用・大阪用で分けています。

5.5 Route 53 ARC のセットアップ

Application Recovery Controller(ARC)は、リージョン切り替えの「スイッチ」を安全に運用するための仕組みです。

ARC の ルーティングコントロール を使って「東京か大阪のどちらに向けるか」を切り替えられるようにし、安全ルール で「同時に ON できるのは 1 つだけ」という制約をかけます。

本記事で作成するリソースの関係は、次のとおりです。

それでは、順に作成していきます。

まず ARC コンソールを開き、左メニュー [クラスター] → [作成] からクラスターを作成します(クラスター名は任意)。

続けて、作成したクラスター内で [コントロールパネルを作成] をクリックします。
クラスター名に先ほどのクラスターを選び、任意のコントロールパネル名を入力して作成します。

次に、コントロールパネル内の [ルーティングコントロールを追加] から、東京/大阪向けのルーティングコントロールを 2 つ作成します。

東京リージョン用(rc-tokyo)。

大阪リージョン用(rc-osaka)。

続いて、コントロールパネル内の [安全ルールを追加] から安全ルールを作成します。

ルールタイプに アサーションルールを選び、任意の名前を入力したうえで、作成した 2 つのルーティングコントロール(rc-tokyo, rc-osaka)を対象に指定します。

rc-tokyo か rc-osaka のどちらか1つしかオンにできない条件になるように、キャプチャのとおり設定しました。

最後に、Route 53 レコードと紐づけるための ルーティングコントロールヘルスチェックを作成します。

東京用ヘルスチェック(rc-tokyo)

大阪用ヘルスチェック(rc-osaka)

5.6 Route 53 のALB へのルーティング用 A レコード作成

次に Route 53(ホストゾーン)で、同じ名前の A レコードを 2 本(プライマリ/セカンダリ)作成します。 ARC のヘルスチェックに応じて、東京 ALB(プライマリ)/ 大阪 ALB(セカンダリ)へ振り分ける設定です。

ARC の切り替えを主軸にしたいため、「ターゲットのヘルスを評価」は「いいえ」にします。

東京 ALB 用(プライマリ)

  • レコード名:任意
  • レコードタイプ:A
  • エイリアス:ON(ターゲットに 東京 ALB を選択)
  • ルーティングポリシー:フェイルオーバー
  • フェイルオーバーレコードタイプ:プライマリ
  • ヘルスチェック ID:ARC で作成した rc-tokyo 用ヘルスチェック
  • レコード ID:tokyo(任意の一意値)
  • ターゲットのヘルスを評価:いいえ

なお「ヘルスチェック」には、プルダウンから rc-tokyo 用のルーティングコントロールヘルスチェックを選択します。

大阪 ALB 用(セカンダリ)

Route 53 のフェイルオーバールーティングは、同じ名前 / 同じタイプのレコードを プライマリとセカンダリの 2 本で作るのが前提です。

東京と同じレコード名で作成しつつ、次の点だけ変更します。

  • フェイルオーバーレコードタイプ:セカンダリ
  • レコード ID:osaka
  • ヘルスチェック:rc-osaka 用のヘルスチェック

5.7 (大阪・オレゴンリージョン)Canary で Route 53 を監視する

大阪リージョンと監視リージョン(オレゴン)の CloudWatch Synthetics Canary から、Route 53 で公開している DNS 名 に対して HTTP リクエストを定期実行し、ユーザーが実際に到達する経路(Route 53 → ALB → ECS → DB)の正常性を監視します。

まずは 大阪リージョンの CloudWatch コンソールで、左メニューの Synthetics Canaries をクリックし、[Canary を作成] をクリックします。

テンプレートは 「ハートビートのモニタリング」 を選択します。

次に、Canary の 名前と、監視対象 URL(例:http:///accounts)を指定します。/accounts を監視対象にしておくと、ALB → ECS → DB まで到達できているかをまとめて確認できます。

続いて、Canary の実行スケジュールを設定します。

また、作成画面の CloudWatch アラーム(オプション)でアラーム設定を有効にしておくと、Canary のメトリクスを元に CloudWatch Alarm を自動作成できます。

作成後、CloudWatch のアラーム画面に移動すると、Alarm が自動生成されていることを確認できます。必要に応じて、評価期間やしきい値は後から調整します。

同様の手順で オレゴンリージョンにも Canary と Alarm を作成し、監視対象は同じく Route 53 の DNS 名(/accounts) を指定します。

5.8 Step Functions の作成

大阪リージョン(ap-northeast-3)に、切り替え(スイッチオーバー)を実行する Step Functions を作成します。

ステートマシンでは、切り替え中の二重更新を避けるために DynamoDB の閉塞フラグを更新しつつ、ARC の routing control を切り替え、Aurora Global Database のスイッチオーバー完了(available)を確認してから大阪側をオープンする流れにします。

{
  "Comment": "Switchover: Close Tokyo -> Switch ARC (Lambda) -> Switchover Aurora Global -> Wait -> Open Osaka",
  "StartAt": "Init",
  "States": {
    "Init": {
      "Type": "Pass",
      "Parameters": {
        "now.$": "$$.State.EnteredTime"
      },
      "Next": "CloseTokyo"
    },
    "CloseTokyo": {
      "Type": "Task",
      "Resource": "arn:aws:states:::aws-sdk:dynamodb:updateItem",
      "Parameters": {
        "TableName": "<DynamoDBテーブル名>",
        "Key": {
          "region": {
            "S": "ap-northeast-1"
          }
        },
        "UpdateExpression": "SET is_closed = :t, updated_at = :now",
        "ExpressionAttributeValues": {
          ":t": {
            "BOOL": true
          },
          ":now": {
            "S.$": "$.now"
          }
        }
      },
      "Retry": [
        {
          "ErrorEquals": [
            "DynamoDB.ProvisionedThroughputExceededException",
            "DynamoDB.ThrottlingException",
            "States.TaskFailed"
          ],
          "IntervalSeconds": 2,
          "BackoffRate": 2,
          "MaxAttempts": 6
        }
      ],
      "Catch": [
        {
          "ErrorEquals": [
            "States.ALL"
          ],
          "Next": "Fail"
        }
      ],
      "Next": "SwitchArc"
    },
    "SwitchArc": {
      "Type": "Task",
      "Resource": "arn:aws:states:::lambda:invoke",
      "Parameters": {
        "FunctionName": "arn:aws:lambda:ap-northeast-3:<AWSアカウントID>:function:<Lambda名>",
        "Payload": {
          "UpdateRoutingControlStateEntries": [
            {
              "RoutingControlArn": "arn:aws:route53-recovery-control::<AWSアカウントID>:controlpanel/<コントロールパネルID>/routingcontrol/<ルーティングコントロールID_東京>",
              "RoutingControlState": "Off"
            },
            {
              "RoutingControlArn": "arn:aws:route53-recovery-control::<AWSアカウントID>:controlpanel/<コントロールパネルID>/routingcontrol/<ルーティングコントロールID_大阪>",
              "RoutingControlState": "On"
            }
          ]
        }
      },
      "ResultPath": "$.arc",
      "Retry": [
        {
          "ErrorEquals": [
            "Lambda.ServiceException",
            "Lambda.AWSLambdaException",
            "Lambda.SdkClientException",
            "Lambda.TooManyRequestsException",
            "States.TaskFailed"
          ],
          "IntervalSeconds": 2,
          "BackoffRate": 2,
          "MaxAttempts": 6
        }
      ],
      "Catch": [
        {
          "ErrorEquals": [
            "States.ALL"
          ],
          "Next": "Fail"
        }
      ],
      "Next": "SwitchoverAurora"
    },
    "SwitchoverAurora": {
      "Type": "Task",
      "Resource": "arn:aws:states:::aws-sdk:rds:switchoverGlobalCluster",
      "Parameters": {
        "GlobalClusterIdentifier": "<Auroraグローバルクラスター識別子>",
        "TargetDbClusterIdentifier": "arn:aws:rds:ap-northeast-3:<AWSアカウントID>:cluster:<大阪DBクラスター識別子>"
      },
      "ResultPath": "$.rds",
      "Retry": [
        {
          "ErrorEquals": [
            "RDS.ThrottlingException",
            "RDS.ServiceUnavailableException",
            "States.TaskFailed"
          ],
          "IntervalSeconds": 5,
          "BackoffRate": 2,
          "MaxAttempts": 6
        }
      ],
      "Catch": [
        {
          "ErrorEquals": [
            "States.ALL"
          ],
          "Next": "Fail"
        }
      ],
      "Next": "WaitAurora"
    },
    "WaitAurora": {
      "Type": "Wait",
      "Seconds": 15,
      "Next": "DescribeGlobalCluster"
    },
    "DescribeGlobalCluster": {
      "Type": "Task",
      "Resource": "arn:aws:states:::aws-sdk:rds:describeGlobalClusters",
      "Parameters": {
        "GlobalClusterIdentifier": "<Auroraグローバルクラスター識別子>"
      },
      "ResultPath": "$.rdsDescribe",
      "Retry": [
        {
          "ErrorEquals": [
            "RDS.ThrottlingException",
            "RDS.ServiceUnavailableException",
            "States.TaskFailed"
          ],
          "IntervalSeconds": 5,
          "BackoffRate": 2,
          "MaxAttempts": 10
        }
      ],
      "Catch": [
        {
          "ErrorEquals": [
            "States.ALL"
          ],
          "Next": "Fail"
        }
      ],
      "Next": "CheckAuroraStatus"
    },
    "CheckAuroraStatus": {
      "Type": "Choice",
      "Choices": [
        {
          "Variable": "$.rdsDescribe.GlobalClusters[0].Status",
          "StringEquals": "available",
          "Next": "OpenOsaka"
        }
      ],
      "Default": "WaitAurora"
    },
    "OpenOsaka": {
      "Type": "Task",
      "Resource": "arn:aws:states:::aws-sdk:dynamodb:updateItem",
      "Parameters": {
        "TableName": "<DynamoDBテーブル名>",
        "Key": {
          "region": {
            "S": "ap-northeast-3"
          }
        },
        "UpdateExpression": "SET is_closed = :f, updated_at = :now",
        "ExpressionAttributeValues": {
          ":f": {
            "BOOL": false
          },
          ":now": {
            "S.$": "$$.State.EnteredTime"
          }
        }
      },
      "Retry": [
        {
          "ErrorEquals": [
            "DynamoDB.ProvisionedThroughputExceededException",
            "DynamoDB.ThrottlingException",
            "States.TaskFailed"
          ],
          "IntervalSeconds": 2,
          "BackoffRate": 2,
          "MaxAttempts": 6
        }
      ],
      "Catch": [
        {
          "ErrorEquals": [
            "States.ALL"
          ],
          "Next": "Fail"
        }
      ],
      "End": true
    },
    "Fail": {
      "Type": "Fail",
      "Cause": "Switchover workflow failed"
    }
  }
}

グラフビューにすると、こんな感じです。

ARC の routing control 更新は Recovery Cluster API(データプレーン)を呼び出します。

この API は クラスターのリージョナルエンドポイント(用意された複数リージョンのうちいずれか)を指定して呼び出す必要があります。

そのため本記事では、Step Functions の AWS SDK 統合だけで完結させるのではなく、Lambda 側で endpoint_url を指定する構成にしました。

Recovery Cluster API のエンドポイントが用意されるリージョンは、 2026/01/21 時点では次の 5 つです。

変更される可能性があるため、最新は公式ドキュメント/マネジメントコンソールで確認してください。

  • us-east-1(米国東部・バージニア北部)
  • eu-west-1(欧州・アイルランド)
  • us-west-2(米国西部・オレゴン)
  • ap-northeast-1(アジアパシフィック・東京)
  • ap-southeast-2(アジアパシフィック・シドニー)

切り替え用 Lambda は次のようなシンプルな実装にしています(複数エンドポイントを順に試し、成功したら終了)。

import os
import boto3


def lambda_handler(event, context):
    entries = event.get("UpdateRoutingControlStateEntries")
    if not entries:
        raise ValueError("UpdateRoutingControlStateEntries is required")

    api_region = os.getenv("ARC_API_REGION")
    endpoints = [e.strip() for e in os.getenv("ARC_CLUSTER_ENDPOINTS", "").split(",") if e.strip()]
    if not endpoints:
        raise ValueError("ARC_CLUSTER_ENDPOINTS is required (comma-separated endpoints)")

    last_err = None
    for endpoint_url in endpoints:
        try:
            client = boto3.client(
                "route53-recovery-cluster",
                region_name=api_region,
                endpoint_url=endpoint_url
            )
            resp = client.update_routing_control_states(
                UpdateRoutingControlStateEntries=entries
            )
            return {"ok": True, "endpoint_used": endpoint_url, "response": resp}
        except Exception as e:
            last_err = e

    raise last_err

環境変数は次を設定します。

  • ARC_API_REGION:us-west-2
  • ARC_CLUSTER_ENDPOINTS:上記リージョンのクラスターエンドポイント URL

ARC_CLUSTER_ENDPOINTS は、ARC コンソールで作成したクラスター画面から確認できます。


6. 切り替えてみた:スイッチオーバーの挙動

ここでは、前章までに作成した構成を使って 東京 → 大阪へのスイッチオーバーを実行し、切り替え前後の挙動を確認します。

今回の検証では、構成の動作確認を優先し、実際の障害注入や Canary / Alarm の発報は行わず、Step Functions を手動実行します。

一方で、運用では CloudWatch Synthetics Canary を大阪リージョンとオレゴンリージョンの両方で実行し、両地点の結果をもとにオペレーターが判断して Step Functions を実行する流れを想定しています。

6.1 今回の検証手順

今回の検証は、次の流れで行いました。

  1. (事前確認)Route 53 の DNS 名へアクセスし、東京に向いていることを確認
  2. (切り替え実行)大阪リージョンの Step Functions を手動実行
  3. (切り替え確認)DNS/HTTP 応答が大阪に切り替わったことを確認
  4. (切り替え確認)Aurora の状態が大阪に切り替わったことを確認
  5. (切り替え確認)DynamoDB の閉塞フラグが想定どおり更新されることを確認

6.2 切り替え前:東京に向いていることを確認

スイッチオーバー前に、5.6 で作成した Route 53 の ALB へのルーティング用 A レコードへアクセスします。
レスポンスには Core Banking Sample Tokyo が表示され、東京リージョン側にルーティングされていることが分かります。


6.3 Step Functions を手動実行

Step Functions を手動で実行します。

実行が成功しました。

今回の検証では およそ 1 分程度で完了しました。

6.4 切り替え後:大阪に向いていることを確認

スイッチオーバー後に同じ DNS 名へアクセスすると、Core Banking Sample Osaka が表示され、大阪リージョン側へ切り替わったことが確認できます。
(DNS キャッシュの影響を受ける場合があるため、反映までの時間は TTL やクライアントの挙動に依存します)

6.5 Aurora / DynamoDB の状態確認

  • Aurora Global Database(ライターの移動)
    大阪リージョン側がプライマリ(ライター)になっていることを確認できます。
    Step Functions 側では switching-over が完了して available になるまで待機してから後続へ進むため、切り替え途中の状態で処理が進みにくい構成にします。

  • DynamoDB(閉塞フラグ)
    東京(ap-northeast-1)の is_closed が true になっており、切り替えに合わせてステートが更新されることを確認できます。
    updated_at に更新時刻も記録されます。


7. まとめ

本記事では、BLEA for FSI の考え方を参考にしつつ、ウォームスタンバイ構成の DR をコンソール操作のみで組み立て、Step Functions を手動実行して切り替えの一連の動作を確認しました。

今回の記事で確認したのは、主に次の 3 点です。

  • Aurora Global Database のスイッチオーバーを Step Functions から実行でき、状態遷移(switching-over → available)も待ち合わせできる
  • Route 53 ARC の routing control を切り替えることで、Route 53 のフェイルオーバールーティングを通じてアクセス先を東京→大阪へ切り替えられる
  • 切り替えに合わせて DynamoDB(グローバルテーブル)のフラグ更新も行え、切り替えフローの中でアプリ制御用のステートを更新できる

なお今回は検証をシンプルにするため、障害注入や Canary / Alarm の発報までは行わず、Step Functions を手動実行します。

ただし運用では、大阪リージョンとオレゴンリージョンの Canary で検知し、両方の結果をもとにオペレーターが判断して切り替える流れを想定しています。

「ウォームスタンバイの DR は、作って終わりではなく、切り替え・戻し・監視・運用判断まで含めて初めて“使える設計”になる」——その入口として、今回の手作業検証が何かの参考になれば幸いです。