【AWS】Lambda + API Gateway + DynamoDBで酒豪判定アプリを開発してみる【前編】

## はじめに

叡智に富んだ読者の皆様、御機嫌よう。
フルスタックエンジニアにメガ進化したいすぎちゃんです！

ぶっちゃけ、リアルすぎちゃんの懐事情は険しくなってきつつありますw（仕事しろ）

そんな爪に火を灯すような生活を送る中でWeb開発を試みると、
どうしてもコスパとタイパを気にせざるを得ません。

先月、AWSの[Aurora Serverless](https://aws.amazon.com/jp/rds/aurora/serverless/)をDBとしてポートフォリオを作ろうと試みたのですが、何もしなくても月に2500円もぶっ飛んじゃうので泣く泣く閉鎖することになりました :cry: （お金に余裕があればまた公開するかも）


https://aws.amazon.com/jp/rds/aurora/serverless/



さて、今回の記事ではコスパとタイパの両立ができる**サーバレスアーキテクチャ**を元にアプリケーションの作り方をレクチャーしたいと思います！

:::note info
**「サーバレス」とは**
サーバー周りの構築・運用を一切気にする必要がないサービス形態のこと。もちろん月々のサーバー代のことも気にしなくて良い。**従量課金制が一般的で、料金体系は柔軟で使った分だけ支払えばOK**。

:::



今回は[東大式スクリーニングテスト](https://www.google.com/search?q=%E6%9D%B1%E5%A4%A7%E5%BC%8F+%E3%82%A2%E3%83%AB%E3%82%B3%E3%83%BC%E3%83%AB%E3%83%86%E3%82%B9%E3%83%88&oq=&gs_lcrp=EgZjaHJvbWUqBggDEEUYOzIGCAAQRRg9MgkIARBFGDkYgAQyBwgCEAAYgAQyBggDEEUYOzIHCAQQABiABDIHCAUQABiABDIGCAYQRRg8MgYIBxBFGD3SAQgyMzUxajBqNKgCALACAQ&sourceid=chrome&ie=UTF-8)を元に「酒豪判定アプリ」のハンズオンを発信していきます！

アプリケーションの全機能の作成過程を一つの記事にまとめると長くなっちゃうので、以下のように複数の記事に分けて解説していきます！

**本ハンズオンの構成**


:::note info
1. DynamoDBに置いている質問項目を画面に表示しよう！⇦here
2. 結果を送って酒豪か下戸かを判定してもらおう！
3. 酒豪度を他の人と比較しよう！
:::

このシリーズでは、AWSのサービスを利用してサーバーレスなアプリケーションを構築する方法を学んでいきます。以下の手順で進めていく予定ですので、ぜひ参考にしてください！


## 使用技術

### フロントエンド
- node: v20.18.0
- npm: 10.8.2
- vue.ts: @vue/cli 5.0.8

環境作成はこちらの記事を参考にしてください :bow: 

https://qiita.com/Masa79/items/dfa48b8a096a742f5f0a

### インフラ
- DynamoDB：Key-Value型のデータベースのこと。RDSと違いスキーマの定義を予めしておく必要がない
- Lambda：Web上で関数を実行できるサービス
- Amazon API Gateway：REST API, WebSocket APIなどを簡単に実行&管理ができるサービス。それぞれのAPIに対してテストも可能



## 1. DynamoDBの設定

### 1-1. 基本設定
![スクリーンショット 2024-10-15 17.07.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401561/41a287dd-46f5-cb90-c6f6-c95bc8e39a01.png)

テーブル名は`questionnaire_items`, パーティションキーは`q_num`(データ型は**数値**)に設定してください！

その他はデフォルトのままで構いません。

### 1-2. データの投入

「返された項目」の右上にある「項目を作成」をクリックしてください。

そうすると以下のような画面が表示されます。

![スクリーンショット 2024-10-15 17.15.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401561/28fccecc-3842-fc2d-50a3-65ff93be301a.png)

以下が`questionnaire_items`テーブルに設定するべき属性です。

`statement`:お酒を飲んだ時の症状
`choice_1`:「いつも出る」の点数
`choice_2`:「時々出る」の点数
`choice_3`:「出ない」の点数

項目の登録を設問の数だけ繰り返します。


![スクリーンショット 2024-10-14 19.20.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401561/88210ac5-c7f8-a33c-2bda-f16b78dee0ce.png)

一つ目の項目の設定が終わればチェックボックスをクリックし、右上の「アクション」>「項目の複製」をタップしてください。

そうすると一から属性を定義する必要がなく、値を入力するだけで項目の設定が完了します。


## 2. Lambdaの設定

### 2-1. 基本設定

Lambdaの一覧画面の右上にあるオレンジ色の「関数を作成」をタップしてください。
関数の作成画面の最上部に表示されている３つのオプションの中から「**設計図の使用**」を選択して下さい。

![スクリーンショット 2024-10-15 17.52.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401561/ac05f5cc-7f85-a8b0-9b6a-48bd3280cae9.png)

「**設計図の使用**」を選択すると設計図名、関数名の設定欄が表示されます。


![スクリーンショット 2024-10-14 19.47.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401561/ceab4523-8d49-f547-21f6-81dcd8ed6602.png)

設計図名のドロップダウンメニューでは「**Create a microservice that interacts with a DDB table**」を選択してください。今回は比較的シンプルなpython3.10でLambdaのコードを書いていきます。



![スクリーンショット 2024-10-14 19.47.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401561/243b78ab-addb-512e-ffd4-673f401ffb1f.png)


![スクリーンショット 2024-10-14 19.49.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401561/b976ac9d-3ff2-69e9-3f8e-5321207b2bbc.png)

実行ロールのブロックでは「**AWSポリシーテンプレートから新しいロールを作成**」を選択してください！

ロール名はわかりやすいように`DynamoDbReadAccess`に設定して、ポリシーテンプレートは「**シンプルなマイクロサービスのアクセス権限**」を選択しましょう。

:::note warn
上記の手順を踏まないと、Lambdaを実行する際にDynamoへのアクセスが許可されず正しいコードを実行してもデータを取得できません。
:::

### 2-2. 関数の作成

```lambda_function.py
import boto3
import json

print('Loading function')
dynamo = boto3.client('dynamodb')


def respond(err, res=None):
    return {
        'statusCode': '400' if err else '200',
        'body': err.message if err else json.dumps(res),
        'headers': {
            'Content-Type': 'application/json',
        },
    }


def lambda_handler(event, context):
    try:
        # DynamoDBクライアントを作成
        dynamodb = boto3.resource('dynamodb')
        table = dynamodb.Table('questionnaire_items')
        
        # 全てのレコードを取得
        response = table.scan()

        items = response.get('Items', [])
        
         # 'q_num'で並べ替え（昇順）
        sorted_items = sorted(items, key=lambda x: x['q_num'])
        
        # もしデータが無い場合
        if not items:
            return {
                'statusCode': 404,
                'body': 'No data found'
            }

        # データを返す
        return {
            'statusCode': 200,
            'body':  sorted_items
        }
    except Exception as e:
        # エラーが発生した場合はエラーログを出力
        print(f'Error processing data: {e}')
        return {
            'statusCode': 500,
            'body': f'Error processing data: {str(e)}'
        }

```

上記のコードを用いればDynamoDBに追加した項目一覧を取得することができます！

boto3.resource('dynamodb'): DynamoDBクライアントを作成する

dynamodb.Table('questionnaire_items'): `questionnaire_items`テーブルを指定

table.scan(): `questionnaire_items`テーブルに登録されたデータを全て取得

:::note alert
`table.scan()`を実務で使うのはあまりお勧めできません。データ数が多くなればなるほど料金が嵩んでしまうからです。
:::





### 2-3. 疎通確認

![スクリーンショット 2024-10-16 11.30.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401561/59ad7be9-6609-41f6-598d-6e24009f9f58.png)

2-2のコードをlambda_function.pyに貼り付けたら、コードが正常に動作するか確認しましょう！
青色に点滅する「Test」ボタンをタップしてください。（途中、ウィンドウが何かしら出てくると思いますが、今回は何も設定しなくても大丈夫です！）



![スクリーンショット 2024-10-14 19.59.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401561/33b21ba8-eced-faeb-2b6e-e76c0d252192.png)

Execution resultに上記の画像のようにstatusCodeが200のレスポンスが表示されていれば、DynamoDBからデータが取得できてます！

## 3. API Gatewayの設定

### 3-1. APIの定義

API Gatewayの設定画面に遷移すると、APIタイプの選択が求められます。

- HTTP API
- WebSocket API
- REST API
- REST API プライベート

今回はVPC外からのアクセスが必要であること、ゲームのようにリアルタイム性も必要ないため**REST API**を使っていきたいと思います。

![スクリーンショット 2024-10-14 20.04.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401561/30d39015-ac7d-7603-c9cf-5584db16d903.png)

:::note info
**「REST API」とは？**

> REST API は、REST アーキテクチャの制約に従って、RESTful Web サービスとの対話を可能にする アプリケーション・プログラミング・インタフェース (API または Web API) です。REST (Representational State Transfer) は、コンピュータ・サイエンティストの Roy Fielding によって作成された API の構築方法を定義する仕様であり、REST 用に設計された REST API (または RESTful API) は軽量で高速であるため、IoT (モノのインターネット)、モバイル・アプリケーション開発、サーバーレス・コンピューティングなどの先進的なコンテキストに最適です。


引用：[Red Hat](https://www.redhat.com/ja/topics/api/what-is-a-rest-api)
:::

選択するとREST APIの作成画面に遷移します。

![スクリーンショット 2024-10-14 20.05.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401561/4458f94e-e555-42cb-9937-b1a08c65e6d4.png)


ラジオボタンでは**新しいAPI**を選択し、API名は`questionnaire_items`と設定してください。APIエンドポイントタイプはデフォルトのままで大丈夫です。

REST APIの作成に成功すれば、以下の画像のようなリソースの設定画面に遷移します。

![スクリーンショット 2024-10-14 20.06.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401561/930c545f-45fc-74bd-ed49-c4aada9f5bae.png)


### 3-2. Lambda関数との紐付け

次に3-1で作成したAPI定義とlambda関数の紐付けについて解説します。
「メソッド」ブロックの右上に位置する「**メソッドを作成**」をクリックしてください。

![スクリーンショット 2024-10-15 18.04.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401561/852742c1-9db6-8a78-172e-fc04583be322.png)

クリックするとメソッド作成画面が表示されます。

![スクリーンショット 2024-10-14 20.07.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401561/eedb7a40-647a-1ff5-d315-b9ee0b6dd2f4.png)


今回はアンケート項目の一覧を取得するのでメソッドタイプは**GET**に設定して下さい。
統合タイプは**Lambda関数**に設定したのち、赤枠で囲っているドロップダウンメニューにて**2で作成したLambda関数**を選択してください！

紐付けに成功すれば、Lambdaのメソッド詳細画面が以下のように表示されるはずです :smile:

![スクリーンショット 2024-10-15 18.00.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401561/63bfc4a8-0020-d81a-da90-982841e1bccc.png)



### スロットリングの設定

![スクリーンショット 2024-10-15 15.27.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401561/7466ee92-d38d-b905-a652-720ac03d4180.png)


「いたずらでF5ボタンを連打されたり、悪意をもった誰かにDos攻撃を受けて高額請求がきちゃった…」という痛ましいことにならないよう、**スロットリング**の設定も念のため行ってください！

:::note info
**「スロットリング」とは**
システムやアプリケーションに対して**過剰なリクエストや処理の負荷を制限する仕組み**のことです。過剰な負荷がかかると、サーバーがダウンしたり、全体のパフォーマンスが低下したりする可能性があるため、それを防ぐために使われます。
:::


添付してる画像とは異なりますが、レート数は**1**、バースト数を**100**に設定しておけば、運悪く誰かに攻撃を喰らって請求書をみた時に卒倒してしまう事態は防げるだろうと思います！安全管理大事！

https://qiita.com/teradonburi/items/a382a17e1e0245b7d831#api-gateway%E3%81%AE%E6%96%99%E9%87%91

:::note warn
実際に東京オリンピックの公式サイトは**4.5億回**の攻撃を受けたという[報告](https://www.dtc.co.jp/blog/ddos_aegis#66725ed6ed6e2c26c06071f4-1719290885723)がありました。
API Gatewayでは**100万リクエストあたり3.5ドル発生する**（2024年10月現在のレートに換算すると500円）ので、これの450倍分の料金が発生することを考えると恐ろしいですね…。

(ざっと計算してみましたが20万円請求されるみたいです…吐血)

:::

### APIのデプロイ


APIのデプロイに成功すれば、下記のようなステージ詳細画面にアクセスできます。

![スクリーンショット 2024-10-15 18.15.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401561/f2fa7c7e-bcef-0673-1ec7-30d65c1c15be.png)

APIエンドポイントが発行されているので、それをコピーして試しにブラウザのアドレスバーにペーストしてみましょう！

```
https://<ユニークなAPI識別子>.execute-api.us-east-1.amazonaws.com/questionnaire_items
```

上手くいけばブラウザに以下のように表示されるはずです！

![スクリーンショット 2024-10-15 18.20.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401561/917d502b-0450-39f4-fd2e-c283be3c56ad.png)

成功したお方はおめでとうございます！
これでサーバレスなマイクロアーキテクチャの構築に成功しました :clap: 





## フロントエンドへのデータ読み込み

### vue.js
それでは
[**Questionnaire.vue**](https://github.com/sugizakikun/alcohol-tolerant-test-v2/blob/develop/src/views/Questionnaire.vue)
```Questionnaire.vue
<template>
  <div class="home" style="text-align:left; max-width:720px; margin:auto;">
    <div class="inputGroup mb-6">
      <div v-for="(qstItem, idx) in questionnaireItems" :key="idx" class="pb-4">
        <div class="questionHeader pb-2" style="text-align: left">
          {{qstItem.statement}}
        </div>
        <div class="inputAnswer row">
          <label class="answer col-4 px-2" :for="idx+'_a'">
            <input class="answer-option" type="radio" :name="'q-'+idx" value=1 :id="idx+'_a'" v-model="userAnswers[idx]">
            <span class="answer-text">いつも出る</span>
          </label>
          <label class="answer col-4 px-2" :for="idx+'_b'">
            <input class="answer-option" type="radio" :name="'q-'+idx" value=2 :id="idx+'_b'" v-model="userAnswers[idx]">
            <span class="answer-text">時々出る</span>
          </label>
          <label class="answer col-4 px-2" :for="idx+'_c'">
            <input class="answer-option" type="radio" :name="'q-'+idx" value=3 :id="idx+'_c'" v-model="userAnswers[idx]">
            <span class="answer-text">出ない</span>
          </label>
        </div>
      </div>
    </div>

    <button @click="submit">submit</button>
  </div>
</template>

<script lang="ts">
import { Options, Vue } from 'vue-class-component'
import axios from 'axios'

export default class Questionnaire extends Vue {
  public questionnaireItems: [] = []
  public userAnswers: [] = []

  public getQuestionnaireItems () {
    return axios.get('https://xxxxxxxx.execute-api.us-east-1.amazonaws.com/questionnaire_items').then((res) => {
      this.questionnaireItems = res.data.body
    })
  }

  <!---省略--->

  mounted () {
    this.getQuestionnaireItems()
  }
}
</script>

```


![スクリーンショット 2024-10-15 15.24.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401561/ff395b3f-9ca8-052b-69e3-44e2c2a5292e.png)

### CORSエラーの解消

API Gatewayで特に何も設定していなければVPC外からの接続を拒否してしまい、ローカル端末からのリクエストができなくなってしまいます。

そこでCORSの有効化を行えば、VPC外に存在する端末からアクセスが可能になります！

APIのリソース画面に移動し、**リソースの詳細**の右側に位置する「**CORSを有効化する**」をタップしてください。
![スクリーンショット 2024-10-15 18.34.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401561/e1de1230-dba3-34b6-ea90-809185c78854.png)

タップすると以下のような設定画面が表示されます。

![スクリーンショット 2024-10-15 18.35.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401561/b1428533-c5b2-fb91-e604-61432bb4878d.png)

ゲートウェイのレスポンスでは全てのチェックボックスにチェックを入れ、
Access-Control-Allow-Methodsでは「**GET**」に必ずチェックを入れてください。

その他の設定はデフォルトままにしておき画面下部の「保存」ボタンをタップします。

「保存」ボタンをタップすると新規でOPTIONSメソッドが追加されているのを確認できます。

![スクリーンショット 2024-10-15 18.36.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401561/3d1f9c70-8ed9-1792-cd2e-688c30b75d3f.png)

その後、画面右上の「**APIをデプロイ**」を選択し同じステージに再度デプロイすれば、端末からのAPIへのアクセスが許可されるようになります。

再び読み込みをかけたところ無事にDynamoDBから取得したデータが画面に表示されました！

![スクリーンショット 2024-10-15 16.26.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401561/5d8029c9-40b9-149a-473b-b8669d90a4b3.png)


お疲れ様でした！ :clap: 


## つぶやき
初めてTypeScriptを使ってみたけどEslintさんがドラマに出てくる姑みたいに細かい…
お小言は愛の鞭と受け取っておこう…（吐血）