AWS Lambdaを用いたサーバーレスアーキテクチャでWebAPIの構築 #1

## はじめに
この記事は[サーバーレスアーキテクチャで翻訳webAPI](https://pages.awscloud.com/JAPAN-event-OE-Hands-on-for-Beginners-Serverless-1-2022-confirmation_422.html)を構築するを参考に記事にしました。

## 構築するアーキテクチャ
以下の図ようなアーキテクチャを構築する。流れとしてはAPI Gatewayがリクエストを受け付け、lambdaを呼び出す。そしてlambdaから翻訳サービスであるAWS Translateを呼び出し結果を取得し、API lambdaに返します。その結果をレスポンスとしてユーザに返す。DynamoDBはその履歴を取得するために利用する。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2040882/d0cc68dc-f18d-4efa-7fec-5eeb08491759.png)

## なぜサーバーレスアーキテクチャなのか？
サーバーを立ててしまうとそのメンテナスや維持管理に労力やコストが発生してしまう。サーバー立てるだけで以下のような作業が発生してしまう。
- サーバーのセットアップ
- ミドルウェアやランタイムの設定
- セキュリティーパッチの適用
- 耐障害性を確保するアーキテクチャの検討

一方でサーバーレスアーキテクチャの特徴として以下がある。
- インフラのプロビジョニングや管理が不要
- 自動でスケール
- 高い可用性
- 使った分だけの支払い

このような特徴からサーバーレスアーキテクチャが注目を集めている。

## AWS Lambdaのイベントソースと呼び出しタイプ
ラムダは他のサービスから呼び出されることが多いサービスになる。その際、AWS Lambdaは呼び出し元のリソースによって呼び出され方が変わる。呼び出しタイプには「非同期呼び出し」と「同期呼び出し」がある。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2040882/3083a9e9-175c-448d-46a4-953c2a960f62.png)


## Lambda　Functionのライフサイクル
Lambdaは呼び出されるとコンテナ上でプログラムを実行する。１つのリクエストで同時に実行できるのは１つのリクエストまで
コンテナは再利用されるが利用可能なコンテナがない場合コールドスタートになる。

- コールドスタート時の処理
コンテナ生成→デプロイ→ランタイム起動・初期化→メソッド起動

- ウォームスタートの場合
メソッド起動

## AWS LambdaをAWS Translateと連携する

作業の流れ
Python SDKのドキュメントをみながらTranslateの呼び出し方を実装する。
↓
IAMロールを修正する。
↓
レスポンスとしてJSON形式で英語訳を返すようにする。
↓
Lambda関数をテスト実行する。

### Python SDKドキュメント
このサイト↓よりSDKの使い方を見る。APIレファレンスをクリックし、「Translate」と検索する。
[Python SDK](https://aws.amazon.com/jp/sdk-for-python/)

赤枠のインポート文とclinetインスタンスをlambdaに記述する。
また、今回使用するメソッドは「translate_text()」なので青枠をクリックし、詳細を確認する。

![スクリーンショット 2022-07-24 12.43.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2040882/7fc8e68f-fb13-150d-4a37-15b328650af0.png)

クリックして移動すると「translate_text」の使い方が開いてある。赤枠をlambdaに記述する。


![スクリーンショット 2022-07-24 12.47.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2040882/48fad3f4-f8d5-88c5-dd52-226133f62f5b.png)

また、「returns」を見ると翻訳された言葉が「TranslatedText」で帰ってくることがわかる。

![スクリーンショット 2022-07-24 12.50.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2040882/545b6634-72f0-042d-bd94-739e23da6eeb.png)

以上をまとめると、lambdaファンクションは以下のように書ける。

```python:tranlate-function-h4b
import json
import boto3

translate = boto3.client('translate')

def lambda_handler(event, context):
    
    input_text='おやすみ'
    response = translate.translate_text(
        Text=input_text,
        SourceLanguageCode='ja',
        TargetLanguageCode='en',
    )

    output_text=response.get('TranslatedText')

    return {
        'statusCode': 200,
        'body': json.dumps(
            {
                'output_text':output_text
            })
    }

```

### IAMロールを修正する。
AWS Lambdaの今回の関数から設定タブ→アクセス権限→実行ロールのリンクをクリック。
TranslateFullAccessポリシーをアタッチする。
※本来は最小限のポリシーをアタッチする。


## AWS API Gateway の特徴
AWS API Gatewayはサーバーをプロビジョニング・管理することなくAPIを作成・管理できるマネージドサービスである。可用性の担保やスケーリング、APIキーの管理といったAPI開発で必要なことをAPI Gatewayに任せることが出来る。RESTAPIとWebSocketに対応。

## AWS API Gatewayを単体で使ってみる
練習としてMockデータを返すAPIを作成する。
流れとしては以下である。

- 新規にAPIを作成する。
↓
- sampleリソースを作成しGETメソッドを作成
↓
- 統合タイプとしてMockを選択
↓
- devステージにデプロイする。
↓
- API実行

### 新規にAPIを作成する
マネジメントコンソールからAPIの作成→RESTを選択→新しいAPIを選択し、API名をtranslate-api-h4bとする。（エンドタイプはリージョン）→APIの作成をクリック。

### sampleリソースを作成しGETメソッドを作成 
アクションタブからリソースの作成をクリックし、リソース名をsampleにし、リソースの作成をクリック。
それからsampleリソースが選ばれている状態から、メソッドの作成をクリックし、プルダウンからGETを選択する。（横のチェックマークを押すのを忘れずに。）

### 統合タイプとしてMockを選択
統合タイプをMockにする。（Mockは決まったレスポンスを返す。）
今回は固定のjsonを返すので統合レスポンスをクリック→プルダウンを押し、マッピングテンプレートのプルダウンを押す。→appication/jsonとし、以下のコードを記述する。

```json:taranslate-api
{
    "statusCode": 200,
    "body":{
        {
        "report_id": 5,
        "report_title":"Hello,World"
        },
         {
        "report_id": 7,
        "report_title":"こんにちは"
        }
    }

}
```   
メソッドの画面でテストという文字をクリックすると先ほどのjsonが出力として出てくる。


### devステージにデプロイする
アクションボタンからAPIのデプロイを選択→デプロイされるステージを[新しいステージ]にし名前を「dev」にする。→デプロイ

無事にデプロイできているのか確認する。ステージの項目からsample-h4bのGETメソッドをクリックし、URLの呼び出しをクリックすると先ほどの結果が出力されている。devステージのURLの呼び出しをクリックしても出力されていないので注意が必要。


![スクリーンショット 2022-07-24 16.16.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2040882/28f338d8-165a-79e3-9210-d2c10d5dc4f4.png)

このようなMockはフロントエンドの開発者とバックエンドの開発者が別れている時に、フロントエンドのエンジニア向けにモックを用意しておくことでインターフェースレベルの認識齟齬（そご）を減らすことが出来る。

## APIGatewayとLambdaを組み合わせる
入力した日本語を英語に翻訳するAPIを作成する。
流れとしては以下である。

/translateメソッドを作りGETメソッドを作成する。
↓
統合タイプとしてLambdaファンクションを選択する。
↓
プロキシ統合を設定し、
Input/Outputをパススルーする。
↓
メソッドリクエストでクエリパラメータの設定をする。
↓
Lambda関数を修正する。
↓
Lambda関数をテスト実行する。
↓
APIをデプロイ

### /translateメソッドを作りGETメソッドを作成する
まず、先ほど作ったリソースの/sample-h4bをリソースの削除で削除する。→リソースの作成で/translateを作成し、メソッドをGETにする。（右のチェックマークを忘れずに。）

先ほどのGETメソッドのセットアップは以下のようにする。
Lambdaプロキシ統合を使って、Gatewayからのinput/Outputをパススルーするため使用にチェックを忘れない。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2040882/33da0fae-e9fe-d7e6-b6a6-516843a1accc.png)



プロキシ統合を設定したため、下画像の統合レスポンスが設定できないようになっていることが確認できる。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2040882/31208ef8-760f-63f9-e5c6-02f75f2523d3.png)

### メソッドリクエストでクエリパラメータの設定をする。
メソッドリクエストからURLクエリ文字列パラメータ→クエリ文字列の追加をクリックし、「input_text」と入力→必須にチェックをする。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2040882/0c43c647-ecda-4779-60d2-9c92c81b7867.png)



### Lambda関数を修正する
プロキシ統合設定をしているおり、APIGateway側のルールに従って、レスポンスを返す必要がある。
例えばブラウザで「APIGateway レスポンス」と検索すると以下のようなページが出てくる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2040882/1f333f53-451c-8485-8709-f57b5d133541.png)

赤枠のコードを拝借し、Lambda関数に書き加える。

また、APIGatewayのクエリパラメータで上がってくる文字列にインプットするようにラムダ関数を変更する。

Lambda関数のTESTの横のプルダウンを押し、configure test eventをクリック→下の画像のように入力する。
queryStringParametersの"foo"を"input_text"に変更。

![スクリーンショット 2022-07-25 9.21.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2040882/4f11628e-3256-e403-5c53-eb7d6a84432e.png)

またLambda関数のコードからinput_textにevent["queryStringParameters"]["input_text"]を代入する必要がある。


```python:変更前
import json
import boto3

translate = boto3.client('translate')



def lambda_handler(event, context):
    
    input_text='おやすみ'
    
    response = translate.translate_text(
        Text=input_text,
        SourceLanguageCode='ja',
        TargetLanguageCode='en',
    )

    output_text=response.get('TranslatedText')

    
    return {
        'statusCode': 200,
        headers: { 
        },
        'body': json.dumps(
            {
                'output_text':output_text
            }),
        'isBase64Encoded":False
    }

```


```python:変更後
import json
import boto3

translate = boto3.client('translate')



def lambda_handler(event, context):
    
    input_text=event["queryStringParameters"]["input_text"]
    
    response = translate.translate_text(
        Text=input_text,
        SourceLanguageCode='ja',
        TargetLanguageCode='en',
    )

    output_text=response.get('TranslatedText')

    
    return {
        'statusCode': 200,
        headers: { 
        },
        'body': json.dumps(
            {
                'output_text':output_text
            }),
        'isBase64Encoded":False
    }

```

### APIをデプロイ
APIGatewayのマネジメントコンソールに戻って、devステージにデプロイする。
GETメソッドのURL呼び出しをクリックすると、{"message": "Internal server error"}と出るが、
URLにクエリパラメータを追加（一番後ろに?input_text=こんばんは）すれば、うまく翻訳される。


## DynamoDBの概要
### DynamoDBの特徴
- フルーマネージド型のNoSQLデータベースサービス
- 3つのAvailability Zoneに保存されるので信頼性が高い
- 性能要件に応じてテーブルごとにスループットキャパシティーを定義する
　 キャパシティーのAuto Scaling,オンデマンドキャパシティといった設定も可能
- ストレージの容量制限がない。



## APIGW＋LambdaにDyanamoDBを組み合わせる
翻訳履歴をDynamoDBにストアするように修正する。
流れとして
Python SDKドキュメントを見ながらDynamoDBにストアするようにLambda関数を修正する。
↓
IAMロールの設定をする。
↓
API叩いてみる。


### DynamoDBにストアするようにLambda関数を修正
まず、さきほどと同様に「AWS Python SDK」などとググる。するとドキュメントが出てくるので、DynamoDBの部分を参照する。
今回はテーブルの操作をするため、赤枠のtableをクリック。

![スクリーンショット 2022-07-26 18.24.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2040882/e4a0fc89-9387-9add-39b8-901fc50beb9a.png)


以下のコードがサイトにあるため、これを自分の使いやすいように変更して記述する。

```python:サイトのコード
import boto3

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('name')
```


```python:Lambda関数に記述したコード
dynamodb_translate_history_tbl = boto3.resource('dynamodb').Table('translate-history')
```


今回使うのは「put_item()」なので赤枠をクリック。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2040882/d78c5173-09e9-de37-5cbb-54ef7029f8fe.png)
以下の画像からtableに対して.put_itemすれば良いのがわかる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2040882/186675a3-5f6a-bb93-8ecc-11230d58d1aa.png)


```python:Lambda関数変更後
import json
import boto3
import datetime
translate = boto3.client('translate')

dynamodb_translate_history_tbl = boto3.resource('dynamodb').Table('translate-history')


def lambda_handler(event, context):
    
    input_text=event["queryStringParameters"]["input_text"]
    
    response = translate.translate_text(
        Text=input_text,
        SourceLanguageCode='ja',
        TargetLanguageCode='en',
    )

    output_text=response.get('TranslatedText')
    
    dynamodb_translate_history_tbl.put_item(
        Item={
        'timestamp':datetime.datetime.now().strftime("%Y%m%d%H%M%S"),
        'input_text':input_text,
        'output_text':output_text
    }
        
        )

    
    return {
        'statusCode': 200,
        'headers': {},
        'body': json.dumps(
            {
                'output_text':output_text
            }),
        'isBase64Encoded':False
    }
```

### IAMロールの設定をする
Lambdaの設定タブ→アクセス権限から紐付いているロールがわかる。そのロールをクリックする。
→許可を追加のプルダウンからポリシーをアタッチ→検索ボックスにDynamoDBと入力し、DynamoDBFullAccessを選択する。
これにより、LambdaからDynamoDBにアクセスできるようになる。

先ほど作ったテストイベント「APICall」を実行すると、以下のようにDynamoDBにプットされていることがわかる。（星がついている項目は手入力で作成した。）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2040882/ff3dc338-5bfc-3c29-99cd-0abd53a8e2c4.png)



### APIを叩いてみる

APIGWのマネジメントコンソールからGETメソッドを開き、URLの呼び出しをクリック。
→URLの最後に「?input_text=こんばんは」と入力すると、{"output_text": "Good evening"}が変えてくる。

また、DynamoDBを確認すると以下のようにプットされており、正しく動いていることが確認できる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2040882/76b9491d-c66d-917e-b2a9-c92b958af581.png)