【AWS】LambdaでJSONファイルを「S3へ保存/S3から取得」してみる

# はじめに
先日、Lambdaを用いて、S3へ推論結果を格納したり、S3から推論結果を取得したりする処理を実装する機会がありました。
今後似たような処理を実装する際にすぐに思い出すことができるよう全体の流れを整理したいと思い、記事に残すことにしました。

# シナリオ
API GatewayへのリクエストをトリガーにLambda関数を実行し以下の処理を行う。

* A - S3へJSONファイルを保存する
* B - S3からJSONファイルを取得する

# A - S3へJSONファイルを保存
![スクリーンショット 2021-12-01 23.28.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/646903/46a940cd-982d-6a8f-5cb3-b9bee501fe72.png)
## 1. S3のバケットを準備する
1. 「バケットを作成」をクリック
1. バケット名を入力。今回は `sample-json-test`とする。

## 2. Lamda関数の作成
今回はリクエストを送る際に発行されるrequest_idをそのままファイル名にしてJSONファイルを保存します。

1. Lambdaのトップページから「一から作成」をクリック
2. 関数名を`upload-json-to-s3`、ランタイムを`Python 3.8`に設定し「関数の作成」をクリック
3. 以下のコードを書き、「Deploy」をクリック

```python
import json
import boto3

s3 = boto3.resource('s3')
# バケット名を指定
BUCKET_NAME = "sample-json-test"

def lambda_handler(event, context):
    # request_idを取得
    request_id = context.aws_request_id

    # バケット名,オブジェクト名を指定
    bucket = s3.Bucket(BUCKET_NAME)
    object_key_name = f"{request_id}.json"
    
    # オブジェクトを生成
    obj = bucket.Object(object_key_name)
    
    # 対象のバケットにjsonデータをアップロード
    json_data = event
    r = obj.put(Body = json.dumps(json_data))
    
    return {
        'request_id': request_id,
        'statusCode': 200,
    }
```
### テストの実行
4. 「Test」をクリックしイベント名を適当に入力する
3. 「作成」をクリック
4. 「テスト」のタブをクリックし、内容を確認後、「テスト」をクリック
5. レスポンス内容の中に`"errorMessage": "An error occurred (AccessDenied) when calling the PutObject operation: Access Denied"`という記載があり、S3にアクセスするための権限がないことが分かる。

### ポリシーの付与
1. 「設定」タブをクリックし該当するロール名をクリック
1. 「ポリシーをアタッチします」をクリックし、`AmazonS3FullAccess`を選択した後、「ポリシーのアタッチ」をクリック

※ `AmazonS3FullAccess`は全てのバケットに全てのアクション（ダウンロードやアップロード、バケットの作成等の操作の種類のこと）でアクセス可能なポリシーなので、本来はセキュリティの観点からアクセス制限をかけたポリシーを選択（自作）する必要があります。

### 再度テストの実行
1. 「テスト」をクリック
1. レスポンスとして`"statusCode": 200`が返ってくることを確認

### 指定したS3バケットを見に行く
以下のようにrequest_idをファイル名としたJSONファイルが格納されていることが確認できます。
![スクリーンショット 2021-12-05 21.40.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/646903/19dbfcba-25bc-389a-9944-993159dfd512.png)

## 2. API Gatewayの設定
RESTAPIのメソッドを定義した後APIキーを作成しデプロイを行う
### メソッドの作成
1. 「APIを作成」 -> REST API　を選択し「構築」 をクリック
1. APIの名前（今回は`operation-of-s3`とする）を入力し、「APIの作成」をクリック
1. 「アクション」 -> 「メソッドの作成」 -> 「POST」を選択
1. Lambda関数に先ほど作成した`upload-json-to-s3`を入力し、「保存」 ボタンをクリック

### APIキーの作成、設定
現時点ではAPIが完全にパブリックに公開されている状態なのでAPIキーを設定する。

1. サイドバーの「APIキー」 -> 「アクション」 -> 「APIキーの作成」 -> 名前を入力 -> 「保存」
1. リソース画面の「リクエストメソッド」をクリック
1. APIキーの必要性を false から true に変更する

### 使用量プランの作成
1. サイドバーの「使用料プラン」 -> 「作成」
1. 名前を入力し, スロットリング、クォータを設定する（今回はどちらもチェックなし）
1. 「APIステージの追加」で先ほど作成したAPIを選択 -> チェックボタン
1. 「APIキー」タブをクリックし、先ほど作成したAPIキーを選択 -> チェックボタン

### デプロイ
1. 「アクション」 -> 「APIのデプロイ」
1. デプロイされるステージを選択（今回は`dev`を新規作成）
1. 「デプロイ」をクリック

# B - S3からJSONファイルを取得
![スクリーンショット 2021-12-01 23.38.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/646903/8a01232e-f833-d7ab-be3e-8881841c77f7.png)

## Lambda関数の作成
GETリクエストのボディに含まれるrequest_idをもとに、S3から対象のJSONファイルを取得し辞書型に変換した値を返すよう関数を作成します。

1. 関数名を`get-json-from-s3`, ランタイムを`Python 3.8`に設定
1. 以下のコードを書き、「Deploy」をクリック

```python
import json
import boto3


s3 = boto3.resource('s3')
# バケット名を指定
BUCKET_NAME = 'sample-json-test'

def lambda_handler(event, context):
    # request_idを取得
    request_id = event["request_id"]
    
    # バケット名,オブジェクト名を指定
    bucket = s3.Bucket(BUCKET_NAME)
    object_key_name = '{}.json'.format(request_id)

    # オブジェクトを生成
    obj = bucket.Object(object_key_name)
    
    try:
        # 対象のjsonを取得し中身を取り出す
        response = obj.get()    
        body = response['Body'].read()
        
        # json -> 辞書型へ変換
        json_data = json.loads(body.decode('utf-8'))
    except:
        print('指定したファイルは存在しません')
        json_data = {}

    return json_data
```

### ポリシーの付与
`AmazonS3ReadOnlyAccess`のポリシーをロールに付与する


## 2. APIGatewayの作成
### メソッドの作成
1. 先ほど作成した`operation-of-s3`を選択
1. 「アクション」 -> 「メソッドの作成」 -> 「GET」を選択
1. Lambda関数に先ほど作成した`get-json-to-s3`を入力し、「保存」 ボタンをクリック

* 先述した内容を参考に以下を行う
  * APIキーの必要性を false から true に変更
  * デプロイ

# 3. 実際にAPIを叩いてみる
## A - JSONファイルをS3に格納
以下の辞書型データをS3にJSONファイルとして格納する

```
{
  "name":"yamada",
  "age": 30
}
```
* curlコマンドでエンドポイントへPOSTリクエストを行う

```bash
$ curl -H "x-api-key: {APIキー}" -X POST "{APIエンドポイント}" -d "{\"name\":\"yamada\", \"age\":30}"
```
* レスポンスを確認する

```plaintext
{"request_id": "9c3c6107-ef1f-454d-bb15-2dc7b4de73f4", "statusCode": 200}
```
* S3のバケットにリクエストIDをファイル名としたJSONファイルの存在を確認する
![スクリーンショット 2021-12-05 23.42.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/646903/26eb682d-327f-02fd-dc43-24735aed3783.png)

## B - S3からJSONファイルを取得
取得したリクエストIDを保持させてエンドポイントへGETリクエストを行う

```bash
$ curl -H "x-api-key: {APIキー}" -X GET "{APIエンドポイント}" -d "{\"request_id\":\"9c3c6107-ef1f-454d-bb15-2dc7b4de73f4\"}"
```

* レスポンスを確認する

```
{"name": "yamada", "age": 30}
```
# 感想
* シンプルな構成ですが、この流れを把握することができれば画像を加工して保存/取得したりと自分の実現したい処理を色々と作成できると思いました。


# 参考
https://qiita.com/USI/items/448c328fad44a3e06daf#%E4%BE%8B3-api%E3%82%AD%E3%83%BC%E3%82%92%E6%8C%81%E3%81%9F%E3%81%9B%E3%81%A6put

https://qiita.com/baikichiz/items/ed787c5c79059213401e