Amazon Aurora DSQL を使ってみる (4) アプリも含めたマルチリージョン冗長構成イメージ

# 1. はじめに
- これまでの記事「[Amazon Aurora DSQL を使ってみる (1) 基本的な操作](https://qiita.com/mksamba/items/ddf3d819adcf92c199f2)」、「[Amazon Aurora DSQL を使ってみる (2) リージョン間同期の遅延確認](https://qiita.com/mksamba/items/f2fcbd8b0a502d890f1d)」、「[Amazon Aurora DSQL を使ってみる (3) 楽観的同時実行制御(OCC)の確認](https://qiita.com/mksamba/items/f6c01f6a9227cc525809)」にて、基本的な操作方法、リージョン間同期の遅延速度、楽観的同時実行制御の挙動について検証した。
- Aurora DSQLの気になるポイントとして、「システム全体をマルチリージョンで冗長化したい場合、結局どういう風に使えばいいのか？」がある。
- いろいろ設計のやり方があるのかもしれないが、今回はDynamodb Global Tableのような感じで、複数リージョンでAct/Actでの書き込み処理を行うような想定要件で、フロントのアプリっぽいものも含めて動作イメージを確認する。

# 2. やったこと
- Aurora DSQLをus-east-1/us-east-2のマルチリージョンで作成し、検証用のテーブルを作成する。
- us-east-1/us-east-2それぞれにEC2インスタンスにおいて、Flaskアプリ(メモ帳的なもの)からリージョン内のAurora DSQLエンドポイントに接続し、Webから入力されたコメントをAurora DSQLのテーブルに保存する。
- Flaskアプリを立てた、各リージョンのEC2インスタンスのEIPをRoute53のAレコードに登録する。通常時は50%/50%の加重ルーティングとし、ラウンドロビンで両方への読み書きができることを確認する。また、障害やメンテナンス時は加重ルーティングの値を変更し、片寄せができることを確認する。

# 3. 構成図

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/189302/647afc9e-3629-4958-90ae-8c81864ba370.png)

# 4. 手順

## 4.1 Aurora DSQL 検証用テーブル作成

- 以下の列を持つ検証用テーブルを作成する。
    - comment： WEBから入力されたコメント
    - time： コメントが入力された日付／時刻
    - region： コメントが書き込まれたリージョン

``` create-table.sql
postgres=> CREATE TABLE comment_list (comment VARCHAR(50) NOT NULL, time TIMESTAMP NOT NULL, region VARCHAR(50) NOT NULL);
```

- テーブルは以下のようになる。

| comment | time | region |
| --- | --- | ---
| hello | 2025-02-25 13:15:17 | us-east-1 |
| bye | 2025-02-25 13:14:10 | us-east-1 |
| thanks | 2025-02-25 13:10:10 | us-east-2 |


## 4.2 Flaskアプリ(メモ帳的なもの)の作成

### 必要資材のインストール
- Amazon Linux 2023に、以下の必要資材をインストールする。
    - Flask： WEBサイト構築のため
    - psycopg3： DSQL(PostgreSQL)への接続のため
    - boto3： DSQLのパスワード(トークン)の取得のため
- FlaskからのSQL接続用に、Flask-SQLAlchemyを使いたかったが、DSQL接続時の必須設定となっている「sslmode=require」の対応方法がよく分からず今回は断念して、psycopg3でそのままSQL文を実行している。

```
[ec2-user@ip-10-0-0-65 ~]$ sudo dnf install pip
[ec2-user@ip-10-0-0-65 ~]$ pip install flask
[ec2-user@ip-10-0-0-65 ~]$ pip install "psycopg[binary]>=3”
[ec2-user@ip-10-0-0-65 ~]$ pip install boto3
```
### WEBサイト構成ファイルの作成

- 以下の2つのファイルを作成する。
    - myapp.py： メインロジック
    - templates/memo-post.html： htmlのテンプレート
- おおまかな仕様としては以下。
    - Flaskサーバにユーザが「http:\//x.x.x.x:5000/」でhttpアクセス(GET)すると、コメントの入力画面が表示される。ユーザがコメントを入力して送信ボタンを押すと、同じパスにhttpアクセス(POST)が行われる。
    - FlaskサーバがPOSTリクエストを受けると、入力されたコメントを取得し、「コメント、処理した時刻、自分のリージョン」の3つをAurora DSQLのテーブルへINSERTする。INSERTした後、テーブルをSELECTして全レコードを取得し、memo-post.html(テンプレート)にレコードの内容を埋め込んでhtmlとして出力する。
    - us-east-1のFlaskサーバを用いて書き込まれたコメントはus-east-1のAurora DSQLからus-east-2のAurora DSQLへ同期されるため、us-east-2側のFlaskサーバでもすぐに参照できる。

``` myapp.py
from flask import Flask, render_template, request
import psycopg
import boto3
from datetime import datetime

# DSQLのエンドポイントとリージョンは、自リージョンのものをそれぞれ指定
cluster_endpoint = "xxxxxxxxxxxxxxxxxxxxxxxx.dsql.us-east-1.on.aws" 
region = 'us-east-1'
client = boto3.client("dsql", region_name=region)
password_token = client.generate_db_connect_admin_auth_token(cluster_endpoint, region)

conn = psycopg.connect(
    dbname='postgres',
    user='admin',
    password=password_token,
    host=cluster_endpoint,
    sslmode='require'
)

conn.set_autocommit(True)
cur = conn.cursor()

app = Flask(__name__)

@app.route('/', methods=['GET', 'POST'])
def index():
    if request.method == 'GET':
        cur.execute("SELECT * FROM comment_list ORDER BY time DESC")
        results = cur.fetchall()
        formatted_results = []
        for row in results:
            formatted_time = row[1].strftime('%Y-%m-%d %H:%M:%S')
            formatted_results.append((row[0], formatted_time, row[2]))
        return render_template('memo-post.html', comments=formatted_results)
     
    if request.method == 'POST':
        comment = request.form['data1']
        itime = datetime.now()
        cur.execute("INSERT INTO comment_list(comment, time, region) VALUES(%s, %s, %s)",(comment, itime, region))
        cur.execute("SELECT * FROM comment_list ORDER BY time DESC")
        results = cur.fetchall()
        formatted_results = []
        for row in results:
            formatted_time = row[1].strftime('%Y-%m-%d %H:%M:%S')
            formatted_results.append((row[0], formatted_time, row[2]))
        return render_template('memo-post.html', comments=formatted_results)       

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)
```

``` memo-post.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title></title>
</head>
<body>
<h1>DSQL マルチリージョンメモ帳 us-east-1</h1>
<form action="/" method="POST">
        <input name="data1"></input>
        <button type="submit">送信</button>
    </form>
</body>

<h1>Comments</h1>
<ul>
    {% for comment in comments %}
        <li>{{ comment[0] }} - {{ comment[1] }} - {{ comment[2] }}</li>
    {% endfor %}
</ul>
</html>
```

## 4.3 Route53レコード登録
- 所持しているパブリックドメイン(ここでは xxxxx.netとする)に、Aレコードを登録する。今回は以下のように登録する。
- 通常時は2つのFlaskサーバのIPアドレスが半々に返却される。加重の設定値を変更することで片寄せなどを行うことができる。

| レコード名 | タイプ | ルーティングポリシー | 差別化要因 | 値
| --- | --- | --- | --- | --- |
| memo | A | 加重 | 50 | xx.xx.xx.242 (us-east-1のインスタンス)
| memo | A | 加重 | 50 | yy.yy.yy.149 (us-east-2のインスタンス)

## 4.4 Flaskアプリからの読み書きの確認

### 通常時の動作
- http:\//memo.xxxxx.net:5000 にアクセスすると、us-east-1/us-east-2のどちらかのFlaskサーバに半々の割合で接続される。(以下の画面はus-east-1側に接続した場合)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/189302/72c637d9-38f6-4b65-a7d6-fc7fb5f229a5.png)

- コメント欄に「hello」と入力し送信すると、コメント、入力時刻、書き込んだリージョン、の3つの情報がINSERTされ保存され、その後SELECTした結果がComments欄に表示される。今回はus-east-1側で書き込まれていることが分かる。

 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/189302/d996a91f-340b-4b10-9375-68f26fdc76e7.png)

- しばらくして同じURL(http:\//memo.xxxxx.net:5000)に再度アクセス(GET)すると、アクセス先がus-east-2側のFlaskサーバとなっている。GETアクセス時にSELECT文が実行されるが、先ほどus-east-1側でINSERTされた内容がus-east-2側に同期されてきているため、us-east-2側でもus-east-1側で登録したコメントが表示される。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/189302/ec5ef72b-f56e-4fe5-a966-69a29f8e176e.png)

- コメント欄に「bye」と入力し送信(POST)すると、コメント、入力時刻、書き込んだリージョン、の3つの情報がINSERTされ保存され、その後SELECTした結果がComments欄に表示される。レコードは両方のリージョン間で双方向同期されており、最初にus-east-1側で「hello」が、次にus-east-2側で「bye」が書き込まれたことが分かる。（Comments欄は新しいものを上に表示）


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/189302/759eb991-7985-4331-9af8-2fbe0e582331.png)

### 片系障害時の動作
- 片方のリージョンのFlaskサーバやAurora DSQLに障害が発生した場合、加重の値を変更して、トラフィックを片寄せする。今回は以下のように値を変更する。

| レコード名 | タイプ | ルーティングポリシー | 差別化要因 | 値
| --- | --- | --- | --- | --- |
| memo | A | 加重 | 0 | xx.xx.xx.242 (us-east-1のインスタンス)
| memo | A | 加重 | 100 | yy.yy.yy.149 (us-east-2のインスタンス)

- レコードの設定変更反映後は、us-east-2側のIPアドレスのみが返却されるため、片寄せを行うことができる。

# 5. 所感
- しょぼいWEBアプリだが、一応アプリケーションも含めた使い方イメージが説明できるようになった。
- Aurora DSQLの接続では必須となっている「sslmode=require」の対応方法が分からず、SQLAlchemyのようなORマッパーを使うことができず、今回はSQLを直打ちしてしまった。今後やるときは改善したい。

# 6. 参考サイト/記事
- Flaskの基本的な使い方やDB接続の手順確認のため、主に以下のサイトを参考とさせて頂いた。

https://qiita.com/Bashi50/items/30065e8f54f7e8038323

https://hoshi-it-o.com/python-flask-foundamentals/