AWSで同窓会募集フォームを作る

# 書かないこと
コーディング内容については記載しません。(専門ではないため)

# 作るきっかけ
大学で軽音サークルに入っており、下記の2機能を持ったものを作りたかった。
- 各日程に参加できるか否かを回答するもの
- バンドの募集フォーム
    - 各々、やりたいバンドを書いていって、やりたいバンドの自分のパートが空いていれば、好きにエントリーする
    - ただ、参加したいバンドのほかパートのメンバと、参加可能日程があっていないといけないので、現在エントリーしているメンバをエントリー時に確認できる

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3759447/5e4f1dce-6db0-4ea4-b465-2e7a604d6b63.png)


当初はGoogle スプレッドシートで共有しようとしていたが、スマホで記入しにくいという問題点があった。
- スマホアプリを入れるか、PC表示にしないと入力ができない
- 10日程くらい候補日があり、スマホからだといちいち10セル入力しないといけない

ということで、Web化することに。

# インフラ構成
AWSでS3+Lambda+RDSで作るか、Google CloudのApp EngineでFlaskで作るか悩みましたが、Google Cloud CloudSQLに無料利用枠がないため、AWSで作ることを決定。
下記の構成としました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3759447/49cd8aad-ebf4-4cbb-9162-7818a2ce2957.png)
が、後々、RDS Proxyの課金がかさんできてしまい、Lambda→RDSは直接接続としました。
部員も30名程度しかおらず、スパイクする恐れは皆無と判断しました。特に構成変更後も障害は起きていないです。


### CloudFront
-  基本的にはS3に、/api/*はAPI Gatewayに向けています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3759447/045d603d-2c55-4cf4-ab1d-4b60dd1eca1f.png)
### API Gateway
- /part,/bandにそれぞれGET,POSTのメソッドを作っています。
    - それぞれ、Lambdaに飛ばしています(非Lambda統合)
    - partは参加状況。GETで参加状況一覧を取得、POSTで参加状況を登録・更新します。
    - bandはバンド募集。GETでバンドエントリー状況取得、POSTで登録・更新です。

### S3
- 特に仕組みはないです。下記の構成です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3759447/9948915a-dcee-457b-8f46-f3f182c0fdfd.png)
- OACは有効化しています。

### RDS
- band、participateテーブルを作成しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3759447/8a79dfa7-5e6e-4ac5-ad60-417b3e17ec1d.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3759447/e717150c-5930-49e4-86a9-4cc5ec46bea1.png)
- bandの各メンバは、participateのIDを見ています。
- participateのdayMMDDは、1が参加、2が検討中、3が不参加としています。
- participateのid=-1 , 0にはそれぞれ、「不要」と「募集中」を割り当てています。
    - バンド募集時の初期値として「募集中」を表示し、不要パートに「不要」を選択できるようにするため
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3759447/c712bf89-076c-4f89-8251-ee2479517c0d.png)

### Lambda
こちらも特に特殊なことはないです。


## フロントエンドのコーディング
当方インフラエンジニアのため、ほとんど書けません。
なのでChatGPTに書いてもらいました。(動かなかったので、少し手直しはしました)

最終的に、下記が実現できていればOKです。
- partページ
    - ページ読み込み時に、/partにGETリクエストを送り、参加者状況を取得し、ページに表示する
    - 新規登録を確定したとき、/partにPOSTリクエストを送る

- bandページ
    - ページ読み込み時に、/bandにGETリクエストを送り、エントリー状況と参加者状況を取得し、ページに表示する
    - バンドのエントリー時、/bandにPOSTリクエストを送る


## バックエンドのコーディング
こちらは少しは書きました。
LambdaからPostgreSQLに接続するためのひな型として、psycopg2を使おうと思ったのですが、どうもうまくいかず、結局、RDSのパスワードローテを基に接続関数を作成して使いました。
https://github.com/aws-samples/aws-secrets-manager-rotation-lambdas/blob/master/SecretsManagerRDSPostgreSQLRotationSingleUser/lambda_function.py
SecretsManagerでRDSのシークレットを作成して、ローテーションを有効化すると勝手にLambdaが作られるので、それをzipでDLして、関数をいじって、Lambdaにアップロードして利用しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3759447/ae360e74-76b0-4305-bb0d-d9be42d12bf2.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3759447/853fab79-b391-48db-b4b0-c7accfda0fab.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3759447/350bdaa2-0d75-4d80-9a7f-1a08371ccb29.png)

/api/partにGETメソッドに紐づく関数は下記のようになりました(RDS接続部分は省略、シークレット情報はSecretsManagerから取得)
```python:get_part.py
def get_part(event, context):
    conn = get_connection(get_secret_dict())
    if conn:
        result = ""
        
        try:
            with conn.cursor() as cur:
                cur.execute("SELECT * FROM participate;")
                result = cur.fetchall()
                conn.commit()
        finally:
            conn.close()

        return {
        "statusCode": 200,
        "headers": {
            'Access-Control-Allow-Origin': 'https://xxxxxxxxxxxxxxx.cloudfront.net',
            'Content-Type': 'application/json'
        },
        "body": result
    }
    else:
        raise ValueError("Unable to log into database")
```
なお、POSTメソッドの関数は、更新も新規登録も同じものを利用しているため、参加者名が一致するレコードがあればUPDATE、なければINSERTとしています。
```python:post_part.py(一部)
            with conn.cursor() as cur:
                query = """
                    SELECT id FROM participate WHERE name=%s;
                """
                cur.execute(query, [data['name']])
                result = cur.fetchone()
                exist_id = result[0] if result else None 

                if exist_id is None:
                    query = """
                        INSERT INTO participate (name, DAYmmdd, ...(省略) , DAYmmdd)
                        VALUES (%s, %s, ...(省略) , %s);
                    """
                    cur.execute(query, [data['name']] + list(map(int, data['statuses'])))
                    conn.commit()
                else:
                    query = """
                        UPDATE participate 
                        SET DAYmmdd = %s, ...(省略) , DAYmmdd = %s
                        WHERE id = %s;
                    """
                    cur.execute(query, list(map(int, data['statuses'])) + [exist_id])
                    conn.commit()

```

## 作ってみて
- アプリを作る実務経験がなかったのでやや不安だったが、1晩で作ることができてよかった
- ChatGPTのおかげが大きいが、やっぱりバグはあるので、自分で直すことができるくらいのスキルは持っていないといけないと痛感した
- 自前のアカウントでやると、コストが怖い。毎日コンソールでコストを見ていた。
    - RDSが今のところ無料利用枠なので、VPC費用のみ掛かっている。来年からはお金がかかるな・・・