0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【AWS DR連載 第2回】Step Functions × Lambdaで異種DB(PostgreSQL / Oracle)の復旧を並列・自動化する(第1フェーズ)

0
Last updated at Posted at 2026-07-28

はじめに

※ 本記事に登場するシステム名、リソース名、IPアドレスなどはすべて仮称およびダミーデータであり、実際の運用環境を示すものではありません。

連載第1回では、社会インフラシステムにおけるマルチリージョンDRの全体設計思想を記述しました。
第2回となる本記事では、DR切り替えの心臓部である「第1フェーズ:待機系リソースのプロビジョニングと環境有効化」の具体的実装について記述します。
本記事もどなたかのお役に立てれば幸いです...!

本フェーズでは、復旧方式の全く異なる2つのDBを AWS Step Functions で完全に可視化・オーケストレーションしました。

  • PostgreSQL: リードレプリカの「Promote(昇格)」
  • Oracle: S3バックアップからの「PITRリストア」

全体処理フロー(Step Functions ステートマシン)

Step Functionsの処理構造を示すMermaid図と、実運用のASL(Amazon States Language)定義です。


ステートマシン定義 (ASL: Amazon States Language)

要となるStep Functionsの定義(抜粋)です。Parallel 内で RetryCatch を階層的に構成し、エラー時には失敗通知Lambdaへ遷移させました。

{
  "Comment": "DR Recovery Flow for Tokyo to Osaka with Final Success Notification",
  "TimeoutSeconds": 3600,
  "StartAt": "Parallel_DB_Recovery",
  "States": {
    "Parallel_DB_Recovery": {
      "Type": "Parallel",
      "Next": "Infra_Ops_And_Notify",
      "Catch": [
        {
          "ErrorEquals": ["States.ALL"],
          "Next": "Global_Fail_Handler",
          "ResultPath": "$.ErrorDetails"
        }
      ],
      "Branches": [
        {
          "StartAt": "Initiate_Postgres",
          "States": {
            "Initiate_Postgres": {
              "Type": "Task",
              "Resource": "arn:aws:lambda:ap-northeast-3:123456789012:function:app-Osaka-DB-Provisioning",
              "Parameters": { "db_type": "postgres" },
              "Next": "Wait_Postgres"
            },
            "Wait_Postgres": { "Type": "Wait", "Seconds": 60, "Next": "Check_Postgres" },
            "Check_Postgres": {
              "Type": "Task",
              "Resource": "arn:aws:lambda:ap-northeast-3:123456789012:function:app-Osaka-DB-Poller",
              "Next": "Is_Postgres_Ready"
            },
            "Is_Postgres_Ready": {
              "Type": "Choice",
              "Choices": [ { "Variable": "$.is_ready", "BooleanEquals": true, "Next": "Modify_Postgres" } ],
              "Default": "Wait_Postgres"
            },
            "Modify_Postgres": {
              "Type": "Task",
              "Resource": "arn:aws:lambda:ap-northeast-3:123456789012:function:app-Osaka-DB-Configurator",
              "Parameters": { "db_id.$": "$.db_id", "db_type": "postgres" },
              "End": true
            }
          }
        },
        {
          "StartAt": "Initiate_Oracle",
          "States": {
            "Initiate_Oracle": {
              "Type": "Task",
              "Resource": "arn:aws:lambda:ap-northeast-3:123456789012:function:app-Osaka-DB-Provisioning",
              "Parameters": { "db_type": "oracle" },
              "Next": "Wait_Oracle"
            },
            "Wait_Oracle": { "Type": "Wait", "Seconds": 60, "Next": "Check_Oracle" },
            "Check_Oracle": {
              "Type": "Task",
              "Resource": "arn:aws:lambda:ap-northeast-3:123456789012:function:app-Osaka-DB-Poller",
              "Next": "Is_Oracle_Ready"
            },
            "Is_Oracle_Ready": {
              "Type": "Choice",
              "Choices": [ { "Variable": "$.is_ready", "BooleanEquals": true, "Next": "Modify_Oracle" } ],
              "Default": "Wait_Oracle"
            },
            "Modify_Oracle": {
              "Type": "Task",
              "Resource": "arn:aws:lambda:ap-northeast-3:123456789012:function:app-Osaka-DB-Configurator",
              "Parameters": { "db_id.$": "$.db_id", "db_type": "oracle" },
              "End": true
            }
          }
        }
      ]
    },
    "Infra_Ops_And_Notify": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:ap-northeast-3:123456789012:function:app-Osaka-Env-Activator",
      "Parameters": { "mode": "infra_ops" },
      "Next": "Wait_Final_Oracle"
    },
    "Wait_Final_Oracle": { "Type": "Wait", "Seconds": 60, "Next": "Check_Final_Oracle" },
    "Check_Final_Oracle": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:ap-northeast-3:123456789012:function:app-Osaka-DB-Poller",
      "Parameters": { "db_id": "app-db-ora-osaka-prd", "db_type": "oracle" },
      "Next": "Is_Final_Oracle_Ready"
    },
    "Is_Final_Oracle_Ready": {
      "Type": "Choice",
      "Choices": [ { "Variable": "$.is_ready", "BooleanEquals": true, "Next": "Notify_Final_Success" } ],
      "Default": "Wait_Final_Oracle"
    },
    "Notify_Final_Success": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:ap-northeast-3:123456789012:function:app-Osaka-Env-Activator",
      "Parameters": { "mode": "notify_success" },
      "Next": "Final_Success"
    },
    "Final_Success": { "Type": "Succeed" }
  }
}

💡 実装のポイント:昇格時のインスタンススケールアップ

ステートマシン内の Modify_Postgres(app-Osaka-DB-Configurator)では、単なる設定変更だけでなくインスタンスクラスのスケールアップも実行しています。
平常時、大阪側のリードレプリカはコスト削減のために最小スペックで待機させています。このフェーズでDBの昇格(Promote)が完了した直後に、Lambdaから ModifyDBInstance を叩いて本番トラフィックに耐えうるインスタンスクラスへと変更し、新本番環境としてのスペックを確保させました。


主要Lambda関数の実装とポイント

1. DB-Provisioning (非同期でのリストア/昇格実行)

Lambdaの15分制限に引っかからないよう、この関数は「API呼び出し」のみを行い即座にレスポンスを返します。

import boto3
import logging
import datetime

logger = logging.getLogger()
logger.setLevel(logging.INFO)

REGION = 'ap-northeast-3'
rds = boto3.client('rds', region_name=REGION)

PG_REPLICA_ID = 'app-db-pg-osaka-prd'
ORACLE_SOURCE_ID = 'app-db-ora-prd'        # 東京のソースID
ORACLE_TARGET_ID = 'app-db-ora-osaka-prd'  # 大阪で復元するID

def lambda_handler(event, context):
    db_type = event.get('db_type')
    
    if db_type == 'postgres':
        # PostgreSQLリードレプリカの昇格 (Promote)
        logger.info(f"Promoting PostgreSQL: {PG_REPLICA_ID}")
        try:
            rds.promote_read_replica(DBInstanceIdentifier=PG_REPLICA_ID)
        except Exception as e:
            if "InvalidDBInstanceState" in str(e):
                logger.info("Postgres is likely already available or promoting.")
            else:
                raise e
        return {"db_id": PG_REPLICA_ID, "status": "promoting", "db_type": "postgres"}

    elif db_type == 'oracle':
        # OracleのPITRリストア開始
        logger.info(f"Searching automated backups in {REGION} for: {ORACLE_SOURCE_ID}")
        response = rds.describe_db_instance_automated_backups(
            Filters=[{'Name': 'db-instance-id', 'Values': [ORACLE_SOURCE_ID]}]
        )
        backups = response['DBInstanceAutomatedBackups']
        latest_backup = sorted(backups, key=lambda x: x.get('RestoreWindowLatestTime'), reverse=True)[0]
        
        rds.restore_db_instance_to_point_in_time(
            TargetDBInstanceIdentifier=ORACLE_TARGET_ID,
            SourceDBInstanceAutomatedBackupsArn=latest_backup['DBInstanceAutomatedBackupsArn'],
            UseLatestRestorableTime=True,
            DBInstanceClass='db.m5.xlarge',
            DBSubnetGroupName='sng-app-db-osaka-prd',
            VpcSecurityGroupIds=['sg-1234567890abcdef0'],
            Engine='oracle-se2',
            StorageType='gp3',
            Iops=12000,
            StorageThroughput=500
        )
        return {"db_id": ORACLE_TARGET_ID, "status": "restoring", "db_type": "oracle"}

2. DB-Poller (状態確認)

Step Functionsの Wait (60秒) と組み合わせてポーリングを行います。

import boto3

REGION = 'ap-northeast-3'
rds = boto3.client('rds', region_name=REGION)

FAILED_STATUSES = ['failed', 'incompatible-network', 'incompatible-parameters', 'incompatible-restore']

def lambda_handler(event, context):
    db_id = event.get('db_id')
    db_type = event.get('db_type')

    try:
        response = rds.describe_db_instances(DBInstanceIdentifier=db_id)
        status = response['DBInstances'][0]['DBInstanceStatus']

        if status == 'available':
            return {"db_id": db_id, "db_type": db_type, "is_ready": True}
        elif status in FAILED_STATUSES:
            raise Exception(f"Restore failed with status: {status}")
        else:
            return {"db_id": db_id, "db_type": db_type, "is_ready": False}
            
    except rds.exceptions.DBInstanceNotFoundFault:
        return {"db_id": db_id, "db_type": db_type, "is_ready": False}

3. Failure-Notify (進捗を可視化した失敗通知)

どこまで正常に終了してどこでコケたかを可視化するメッセージを作成し、SNSで送信します。

import boto3
import json

SNS_TOPIC_ARN = 'arn:aws:sns:ap-northeast-3:123456789012:app-DR-Notification-MAIL'
sns = boto3.client('sns', region_name='ap-northeast-3')

STEPS_ORDER = [
    "app-Osaka-DB-Provisioning",
    "app-Osaka-DB-Poller",
    "app-Osaka-DB-Configurator",
    "app-Osaka-Env-Activator"
]

def lambda_handler(event, context):
    failed_step = event.get('FailedStep', 'Unknown')
    error_raw = event.get('Cause', 'Unknown Error')

    msg_lines = ["DR切替「第一フェーズ」の実行結果:<失敗>\n", "詳細情報:"]
    failed_index = STEPS_ORDER.index(failed_step) if failed_step in STEPS_ORDER else -1

    for i, step_name in enumerate(STEPS_ORDER):
        if i < failed_index:
            msg_lines.append(f"[正常終了] {step_name}")
        elif i == failed_index:
            msg_lines.append(f"[異常終了] {step_name}")
            msg_lines.append(f"  [Error] {error_raw[:500]}")
        else:
            msg_lines.append(f"[未実施] {step_name}")

    sns.publish(
        TopicArn=SNS_TOPIC_ARN,
        Subject="【重要】DR切替 第一フェーズ:失敗",
        Message="\n".join(msg_lines)
    )


まとめと次回予告

Step Functionsの「Parallel」と「Waitによるポーリングループ」を組み合わせることで、実行時間が読めない複数DBの復旧処理をタイムアウトなく・安全に全自動化することができました。

最終回となる次回は、「第3回:DNSルーティング切り替えと旧環境クリーンアップ・切り戻し準備(第2〜4フェーズ)」について記述します!


連載インデックス

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?