1
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?

【ECS×RDS】コンテナを止めないDB切り替え:RDS Proxy×B/Gデプロイ

1
Posted at

1. はじめに

コンテナ(ECS Fargate)などで動くアプリケーションを運用していると、DB のメンテナンス(バージョンアップ・フェイルオーバー)のたびに「アプリへの影響をどう最小化するか」という問題が浮上します。

AWS マネージドサービスの進化で Blue/Green デプロイ自体の作成は簡単になりました。しかしスイッチオーバー時の瞬断による影響は、アプリ側での切り替え対応が必要になります。

特に ECS(Fargate)では、DB 接続エラーが未処理のまま Python プロセスが終了すると ECS タスクが STOPPED 状態になります。

本記事では、RDS Proxy を挟む ことで、ECS コンテナアプリが B/G スイッチオーバーを最小限のコードで乗り越えられるかを検証します。

前回記事(RDS Proxy 経由で Aurora PostgreSQL の Blue/Green デプロイをやってみた)では Proxy + B/G の基本動作と数秒のダウンタイムを確認しました。本記事はその発展形として、実際に ECS Fargate タスクとして動かすコンテナアプリの視点から「Proxy の恩恵がどう見えるか」を検証します。

今回の検証ゴール

# 検証項目
1 Proxy なし(直接接続)でスイッチオーバーを実行すると、ECS タスクにどのような影響が出るか
2 Proxy 経由では、同じリトライコードでどう結果が変わるか
3 Proxy 経由での復旧に必要な最小限の実装は何か

結論(先出し)

  • Proxy なし(以下NoProxy、クラスタエンドポイントへの直接接続): スイッチオーバー時に接続エラー → 再接続を試みるも 30 秒タイムアウトで失敗 → コンテナ終了(STOPPED)
  • Proxy あり(以下Proxy): 同じく接続エラーが発生するが、約 1 秒で再接続成功、タスクは RUNNING のまま継続
  • 必要なコード: 1 回の再接続 のみ
  • ポイント: Proxy の真価は「切断を防ぐ」ではなく「切断後の再接続が約 1 秒で完了する」ことにあります。直接接続では DNS 更新などを待つ間にアプリが 30 秒フリーズして落ちますが、Proxy エンドポイントは切り替え後も即座に応答できる状態を維持するため再接続が高速です

2. 検証環境

項目
コンテナ基盤 Amazon ECS (Fargate)
DB Amazon Aurora PostgreSQL 13.23(Blue)→ 17.9(Green)
Proxy Amazon RDS Proxy(bgdb-proxy
ログ出力先 Amazon CloudWatch Logs(NoProxy: /ecs/db-monitor-direct・Proxy: /ecs/db-monitor

3. 構成と検証アプリ

3.1. 全体構成

3.2. 検証アプリ(app.py)

1 秒ごとに SELECT NOW(), pg_postmaster_start_time() を実行し、レスポンスタイムと DB インスタンスの起動時刻を標準出力します。ECS タスクの stdout は CloudWatch Logs に自動転送されます。

  • NOW(): トランザクション開始時刻
  • pg_postmaster_start_time(): PostgreSQL サーバープロセスが起動した時刻。スイッチオーバーで接続先のインスタンスが切り替わるとこの値が変化するため、Blue → Green への切替検出に利用可能
app.py
import os
import time

import psycopg2


def connect() -> psycopg2.extensions.connection:
    conn = psycopg2.connect(
        host=os.environ["DB_HOST"],
        port=int(os.environ.get("DB_PORT", "5432")),
        dbname=os.environ["DB_NAME"],
        user=os.environ["DB_USER"],
        password=os.environ["DB_PASSWORD"],
        connect_timeout=30,
    )
    return conn


def main() -> None:
    conn = connect()
    print(f"接続先: {os.environ['DB_HOST']}", flush=True)
    print("クエリ発行を開始します(Ctrl-C / タスク停止で終了)", flush=True)
    print("-" * 60, flush=True)

    while True:
        t0 = time.monotonic()
        try:
            with conn.cursor() as cur:
                cur.execute("SELECT NOW(), pg_postmaster_start_time()")
                ts, started = cur.fetchone()
            conn.commit()  # トランザクションを明示的に閉じる(RDS Proxy のピン留め回避)
            elapsed_ms = int((time.monotonic() - t0) * 1000)
            print(
                f"[{ts.strftime('%H:%M:%S')}] {elapsed_ms:5d}ms"
                f" | started={started.strftime('%H:%M:%S')}",
                flush=True,
            )
        except psycopg2.OperationalError as e:
            elapsed_ms = int((time.monotonic() - t0) * 1000)
            print(f"[{time.strftime('%H:%M:%S')}] {elapsed_ms:5d}ms | ERROR: {e}", flush=True)
            print(f"[{time.strftime('%H:%M:%S')}] 再接続を試みます...", flush=True)
            conn = connect()
        time.sleep(1)


if __name__ == "__main__":
    main()

3.3. Dockerfile

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
# -u: unbuffered モード(CloudWatch Logs へのリアルタイム出力のため)
CMD ["python", "-u", "app.py"]

3.4. タスク定義

検証①と②の違いは DB_HOST 環境変数と CloudWatch Logs グループだけです。family を分けることで、2 つのタスクを ECS 上で同時に識別・監視できます。

検証①用(task-definition-direct.json

{
  "family": "db-monitor-direct",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "256",
  "memory": "512",
  "executionRoleArn": "arn:aws:iam::xxxxxxxxxxxx:role/ecsTaskExecutionRole",
  "containerDefinitions": [{
    "name": "app",
    "image": "your-dockerhub-username/db-monitor:latest",
    "essential": true,
    "environment": [
      {"name": "DB_HOST", "value": "bgdb-cluster.cluster-xxxxxxxxxx.ap-northeast-1.rds.amazonaws.com"},
      {"name": "DB_PORT", "value": "5432"},
      {"name": "DB_NAME",  "value": "bgdb"},
      {"name": "DB_USER",  "value": "postgres"},
      {"name": "DB_PASSWORD", "value": "xxxxxxxx"}
    ],
    "logConfiguration": {
      "logDriver": "awslogs",
      "options": {
        "awslogs-group": "/ecs/db-monitor-direct",
        "awslogs-region": "ap-northeast-1",
        "awslogs-stream-prefix": "ecs"
      }
    }
  }]
}

検証②用(task-definition-proxy.json

{
  "family": "db-monitor",
  ...(DB_HOST  awslogs-group のみ異なる)...
  "environment": [
    {"name": "DB_HOST", "value": "bgdb-proxy.proxy-xxxxxxxxxx.ap-northeast-1.rds.amazonaws.com"},
    ...
  ],
  "logConfiguration": {
    "logDriver": "awslogs",
    "options": {
      "awslogs-group": "/ecs/db-monitor",
      ...
    }
  }

本番環境では DB_PASSWORD を Secrets Manager などで管理してください
タスク定義に平文で書くと AWS コンソール・CLI から誰でも参照できます。今回は検証用途のため平文環境変数を使っています。


4. 検証:同時並走でスイッチオーバー

検証①と②を同時に起動し、1 回のスイッチオーバーで両者の挙動を比較します。

ターミナル A:検証①タスク起動 & ログ監視(NoProxy)
ターミナル B:検証②タスク起動 & ログ監視(Proxy)
ターミナル C:B/G スイッチオーバー実行

4.1. 【ターミナル A、B】検証①、検証②タスクを起動してログ監視を開始する

ターミナルA、ターミナルB それぞれでタスクを起動し、ログ監視を行います。
1 秒おきにログが流れ始めたことを確認します。

4.2. 【ターミナル C】B/G スイッチオーバーを実行する

両ターミナルのログが流れていることを確認してから実行します。

AWSコンソール → RDS → Blue/Green deployments → 対象を選択 → "切り替え" をクリック。


5. 結果

5.1. 検証①(NoProxy)の CloudWatch Logs

スイッチオーバー時刻は 01:25:37 UTC(10:25:37 JST)です。

2026-05-08T01:25:34      1ms | started=00:46:37  ← Blue に接続中
2026-05-08T01:25:35      1ms | started=00:46:37
2026-05-08T01:25:36      2ms | started=00:46:37
2026-05-08T01:25:37      0ms | ERROR: SSL SYSCALL error: EOF detected
2026-05-08T01:25:37 再接続を試みます...
2026-05-08T01:26:07 Traceback (most recent call last):
2026-05-08T01:26:07   File "/app/app.py", line 30, in main
2026-05-08T01:26:07     cur.execute("SELECT NOW(), pg_postmaster_start_time()")
2026-05-08T01:26:07 psycopg2.OperationalError: SSL SYSCALL error: EOF detected
2026-05-08T01:26:07 During handling of the above exception, another exception occurred:
2026-05-08T01:26:07 Traceback (most recent call last):
2026-05-08T01:26:07   File "/app/app.py", line 48, in <module>
2026-05-08T01:26:07     main()
2026-05-08T01:26:07   File "/app/app.py", line 43, in main
2026-05-08T01:26:07     conn = connect()
2026-05-08T01:26:07   File "/app/app.py", line 8, in connect
2026-05-08T01:26:07     conn = psycopg2.connect(
2026-05-08T01:26:07 psycopg2.OperationalError: connection to server at
2026-05-08T01:26:07   "bgdb.cluster-xxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com"
2026-05-08T01:26:07 (xx.xx.xx.xx), port 5432 failed: timeout expired

観察結果:

  • 01:25:37: SSL SYSCALL error: EOF detected が発生(line 30cur.execute(...) のクエリ実行行)。elapsed は 0ms で即時検出されています。スイッチオーバーにより、クラスタエンドポイントの旧 Writer 側 TCP セッションがサーバーから切断されました
  • 即座に再接続を試みますが、クラスタエンドポイントの DNS レコードが新 Writer の IP に伝播しきっていない、あるいはルーティングが完了していないため、TCP 接続の確立でハングします
  • connect_timeout=30(30 秒)が経過した 01:26:07 に timeout expired で再接続失敗(line 43except 内の conn = connect()line 8connect() 関数内の psycopg2.connect(...)
  • この 2 つ目の例外は except ブロック内で発生しているためキャッチされず、Python プロセスが終了
  • ECS タスクは STOPPED 状態になり、CloudWatch Logs のストリームはそこで途切れます

5.2. 検証②(Proxy)の CloudWatch Logs

Proxy のスイッチオーバー検出時刻は 01:25:38 UTC(10:25:38 JST)です。

2026-05-08T01:25:33     8ms | started=00:46:37  ← Blue に接続中
2026-05-08T01:25:34     7ms | started=00:46:37
2026-05-08T01:25:35     8ms | started=00:46:37
2026-05-08T01:25:38  2158ms | ERROR: SSL connection has been closed unexpectedly
2026-05-08T01:25:38 再接続を試みます...
2026-05-08T01:25:39   130ms | started=01:21:24  ← Green に切り替わった
2026-05-08T01:25:40    20ms | started=01:21:24
2026-05-08T01:25:41     7ms | started=01:21:24

観察結果:

  • 01:25:35: 最後の正常クエリ(8ms、started=00:46:37 の Blue)
  • 01:25:38: SSL connection has been closed unexpectedly が発生。elapsed が 2158ms と大きい点に注目です。スイッチオーバーにより接続が切れたあと、cur.execute() が約 2 秒ハングしてから例外として検出されています。Proxy 経由でも B/G スイッチオーバーのような大規模な切り替えでは、進行中のクエリへの影響は避けられません1
  • 即座に connect() を呼び、130ms で接続成功。エラーは 1 回のみ
  • started00:46:37(Blue)→ 01:21:24(Green)に変わっており、新しいライターへの接続が確認できます
  • エラー検出から復旧まで約 1 秒(130ms)、最後の正常クエリから次の正常クエリまでは約 4 秒
  • ECS タスクは RUNNING のまま継続し、CloudWatch Logs のストリームも途切れていません

NoProxy-NoError-task.png
(実行中が切り替え後も継続されている)

5.3. 比較サマリー

時刻(UTC) NoProxy(検証①) Proxy(検証②)
01:25:33〜35 正常クエリ(started=00:46:37・Blue) 正常クエリ(started=00:46:37・Blue)
01:25:37 ERROR: SSL SYSCALL EOF(elapsed=0ms) (クエリ実行中・ハング開始)
01:25:37〜 再接続を試行中(connect() 待ち) クエリがハング状態
01:25:38 connect() 試行中… 2158ms ハング後 ERROR、再接続開始
01:25:39 connect() 試行中… 130ms で復旧 started=01:21:24(Green)→ 以降正常クエリ継続
01:26:07 30 秒タイムアウト → クラッシュ(STOPPED) 正常クエリ継続中

復旧時間: NoProxy = 回復不能(connect_timeout=30 秒で失敗)/ Proxy = エラー検出から約 1 秒(130ms)
(最後の正常クエリから次の正常クエリまで: NoProxy 回復不能 / Proxy 約 4 秒)


6. 考察

6.1. Proxy の役割

比較観点 NoProxy(検証①) Proxy(検証②)
切断時のエラー SSL SYSCALL EOF(即時 0ms) SSL connection has been closed unexpectedly(2158ms ハング後に検出)
再接続の成否 失敗(30 秒タイムアウト後にクラッシュ) 成功(130ms で復旧)
再接続の挙動 クラスタエンドポイントの新 Writer への切替(名前解決の更新など)の完了を待つ必要がある Proxy エンドポイントの IP は変化せず、Proxy がバックエンド接続を管理するためクライアントは即座に再接続できる
復旧時間 回復不能(ECS タスクが STOPPED) 約 1 秒
ECS タスクへの影響 STOPPED(Python プロセスが 30 秒タイムアウトで終了) RUNNING のまま継続
必要なアプリ側コード 再接続を書いても無意味 1 回の再接続 のみ

理想:RDS Proxy は Multi-AZ フェイルオーバー時にクライアント TCP 接続を維持したままスタンバイへ切り替える仕組みを持っています。条件が揃えば透過的な切り替えが可能です。

現実:Blue/Green スイッチオーバーはクラスタ(リソース)自体が入れ替わる大規模な処理であり、通常のフェイルオーバーより重い操作です。今回の検証では、セッション状態を持たないシンプルなコードでも Proxy 経由で切断が発生しました。B/G 切り替えにおける Proxy の接続維持はベストエフォートと考えるのが実態に即していると言えます。

運用上の結論:「切断される前提で 1 回のリトライを書いておく」が現実解です。2 つの接続形態の差は「切断された後の挙動」にあると考えています。

Proxy 経由では、Proxy エンドポイントは切り替え後も即座に応答できる状態を維持しています。クライアントが再接続を試みると、Proxy は新しいバックエンド(Green(新Blue))との接続を 130ms で確立しました。DNS 更新待ちが発生しないことが高速再接続の理由と考えています。1 回のリトライを書いておけば、Proxy 経由なら確実に復旧できます。

進行中のクエリへの影響: Proxy は 2 秒ハング、NoProxy は即時エラー
今回の検証では興味深い差異が観察されました。スイッチオーバー時に実行中だったクエリの挙動が異なります。

  • NoProxy: SSL SYSCALL error: EOF detected、elapsed 0ms → クラスタエンドポイントへの接続が瞬時に切断
  • Proxy 経由: SSL connection has been closed unexpectedly、elapsed 2158mscur.execute() が約 2 秒ハングしてから例外として検出

NoProxy の方がエラー検出は速いですが、再接続できないため意味がありません。Proxy 側の 2 秒ハングは Proxy がバックエンドの切り替えを検出・対応している時間と考えられます。この間アプリは「フリーズ」しているように見えますが、エラーとして検出された後は約 1 秒(130ms)で復旧します。connect_timeout=30 の範囲内であればアプリは生き残ります。

6.2. 運用の自由度向上

今回の検証では、同じリトライコードを持つアプリにおいて、Proxyがある事で再接続が容易に実施可能である事が確認できました。これは Proxy の存在がアプリのリトライ実装の「有効性」を担保しているとも言えます。

約 1 秒で自動復旧するなら DB メンテナンスを日中のトラフィックがある時間帯でも実施しやすくなります。CloudWatch Logs に ERROR が数行残ることはありますが、アプリの停止やタスクの STOPPED は起きません。

6.3. connect_timeout の設定が鍵

今回の検証から、connect_timeout の設定がとくに重要です。

  • NoProxy: connect_timeout=30 を設定しても、DNS の更新などが完了するまでの間は接続できないため 30 秒間タイムアウト待ちになります。30 秒未満に設定するとさらに早く落ちます
  • Proxy: 実測では connect_timeout=30 で再接続は約 1 秒で成功しました。極端に短い値(数百 ms 単位)に設定しない限りは問題ないと考えられます

タイムアウト設定のガイドライン(psycopg2 の場合)

  • connect_timeout: 接続タイムアウト。今回の実測(Proxy 経由で 1 秒で接続成功)から、余裕を持って数秒以上を推奨
  • ORM(SQLAlchemy・Django ORM 等)を使う場合は、ORM 側のタイムアウト設定も同様に確認が必要

7. まとめ

# 検証項目 結論
1 Proxy なしでスイッチオーバーすると ECS タスクにどう影響するか 接続エラー後、再接続試行も 30 秒タイムアウトで失敗 → コンテナ終了(STOPPED)
2 Proxy 経由では同じリトライコードでどう変わるか 接続は切断されるが、約 1 秒で再接続成功。ECS タスクは RUNNING のまま継続
3 必要なアプリ側の実装は何か 1 回の再接続 のみ(必要に応じての実装になりますが・・)

RDS Proxy の強みは「切断後の再接続が速い」ことです。B/G スイッチオーバー時の切断自体は Proxy 経由でも直接接続でも発生します。決定的な違いは再接続の結果です。Proxy エンドポイントはスイッチオーバー後も即座に応答できる状態を維持するため再接続は約 1 秒で完了します。直接接続では DNS 更新などを待つ間にアプリが 30 秒フリーズして落ちます。この差がコンテナの STOPPED / RUNNING の違いを生みます。

前回記事では Proxy + B/G の基本動作(数秒ダウンタイム)を確認しました。本記事ではその一歩先として「同じリトライコードでも、Proxy 経由か直接かで結果が異なる」という実態を数字で示せたと思います。

参考

  1. https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/UserGuide/rds-proxy.howitworks.html#rds-proxy-failover

1
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
1
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?