インフラエンジニアがやってみた AWSでサーバーレス Webアプリケーション開発

# はじめに
以下のような構成でサーバーレスアプリケーションの個人開発を行いました。

![001.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369361/b136655a-21ea-b731-8d8d-aca54b2a4826.png)

様々なハマりどころを乗り越えて、ようやくおおよそ動くようになりました。
解らないことだらけですが、楽しかったです。
AWSを触りたいけど、ハマっちゃって先に進めない方に向けて私がハマったところを残します。

私の技術領域は主にインフラ領域(サーバーエンジニア)ですので、あまり開発の知識はありません。
特にフロントエンドは苦手意識があります。
時代遅れのITインフラを抱えてばかりでしたので、AWSも触ったのは初めてです。
サーバーレス、すごいですね！

全体を通して様々なサイトの情報を参考にさせて頂きました。
実際の手順等の細かな内容については参考URLのほうが俄然優秀ですので、そちらをご参照ください。

# 触った順序
以下の順番で開発を進めました。

* AWS Lambda
* DynamoDB
* API Gateway
* Amazon S3

それぞれの作業内容とハマりどころ示します。

# AWS Lambda
## やったこと
まずはHello Worldを作るところから開始しました。
Lambdaは、クラウド上のIDEで開発を行うのですが、以下がやりづらいなぁ、と感じました。

* 入力補完機能が弱い
* バージョン管理が…？

### 入力補完機能について
入力補完機能については私の使い方が悪いのかも知れませんが、全然便利じゃありませんでした。
言語はPython 3.8を使っていました。(Node.jsを選ぶ勇気はありませんでした…)
特にtry句を使用した際に、毎回修正が必要でとてもめんどくさかったです。

```python:try句の入力補完(誤)
try:
    # TODO: write code...
except Exception, e:
    raise e
```
なんとこれ、Syntax Errorになります…。
Python 3.8で変わったのかは調べてませんが、正しくは以下のように書きます。

```python:try句(正)
try:
    # TODO: write code...
except Exception as e:
    raise e
```

### バージョン管理機能について
バージョン管理自体はできるようなのですが、バージョンごとにエンドポイントが変わってしまうようです。
FaaSとして外部からの多量のアクセスを受けるという特性上、アリなのかな…？と思いました。
個人開発なのでバージョン管理していませんでしたが、「あー、前のバージョンの時、どうやって書いてたっけー(涙)」っていう場面が結構ありました。

## ハマりどころ
前のバージョンに戻したいとき、書き方を知りたい時が結構あるので、ちゃんとバージョン管理したほうがいいと思います。
特に一番最初にHello Worldのサンプルが出てきた時の回りくどいreturn句の書き方は、後々必須になります。

```python:Lambdaのサンプルreturn句
return {
    'statusCode': 200,
    'body': json.dumps('Hello from Lambda!')
}
```
`json.dumps()`マジ神。

# DynamoDB
## やったこと
モデルを一つ作って、Lambda経由でCRUD操作を実装しました。
NoSQLを初めて触ったのですが、boto3というライブラリで結構便利に色々出来て良い感じでした。

## ハマりどころ
良い感じとか言いながら、結構ハマりどころが多かったです。

* IAMのロール設定
* DynamoDBの独自文化
* `scan`と`query`の違い
* Lambda内から直接DynamoDB触っていいのか問題

### IAMのロール設定
LambdaからDynamoDBにアクセスできるようにロールの設定を忘れずに。
特に気にしてなければLambda作成時に専用のロールも作られているので、そこにDynamoDBへのポリシーをアタッチしましょう。
FullAccessを付けたんだけど、きっとtableとかで絞るんだと思います。

### DynamoDBの独自文化
DynamoDBというよりNoSQLの独自文化かもしれません。
インデックスの使い方が結構違うので、何も考えずにRDBのように使うと全然インデックス効きません。

でも、RDBと違って、行ごとのcolumnを同じにする必要がないのはすごく便利でした。
あとからcolumnを追加しても、既存のデータには影響を与えずに追加できるので、DBの進化具合に驚かされました。

### `scan`と`query`の違い
データを取得する方法に`scan`と`query`というものがあるのですが、scanは全表走査でqueryは条件付けたアクセスです。
全表走査すると、遅いし、お金もかかるようですので基本的には`query`を使ったほうがいいです。
ですが、`query`だと、オプション指定しないと主キーしか見てくれないので、全表走査せずに行を絞るのにハマりました。

結果、以下のような感じで実現できます。

```python:queryで主キー以外を使う
table.query(
    IndexName='genre_id-index',
    KeyConditionExpression=Key('genre_id').eq('001')
)
```

主キー以外のインデックスをsecondary indexなどと呼ぶようですが、それを使いたいときは↑のように`IndexName`オプションを指定しないと機能しません。

プチハマりどころとして、Key()メソッドは以下のように`boto3`のimportとは別でimportしないと使えないので注意です。

```python:Keyメソッドがないって怒られるとき
import boto3
from boto3.dynamodb.conditions import Key
```

### Lambda内から直接DynamoDB触っていいのか問題
LambdaのPythonコードでゴリゴリにDynamoDBを直接触っているので、LambdaがDynamoDBに依存してしまっています。
DBの選択肢を広げるために、LambdaはもっとI/Oから切り離したほうがいいと思います。

DynamoDBは対象に入っていないみたいですが、RDSはDB Proxyなる機能がプレビューで追加されているので、そちらの機能を使うといいかも知れません。
次善策として、Lambda内にDBへのインターフェースとなる関数を追加するといいかも知れません。
私はそこまではやっていません。

# API Gateway
## やったこと
Lambdaにくっつけた。

## ハマりどころ
提供されている概念はシンプルなんですけどハマりました。

* プロキシ統合使わないとマッピングで死ぬ
* Lambdaのテストと、API Gatewayのテスト、どちらもうまくいかせるのが大変
* APIのデプロイを忘れる

### プロキシ統合使わないとマッピングで死ぬ
プロキシ統合が何のことかいまいちよくわかっていませんが、この機能を使わないと、パラメータのマッピングを自身で定義する必要があります。
POSTでいっぱいパラメータが飛んでくる場合や、頻繁にパラメータが変更される場合、これいじってるだけの毎日に嫌気が差しそうだと思いました。

### Lambdaのテストと、API Gatewayのテスト、どちらもうまくいかせるのが大変
Lambdaのテストが順調に回るので良い感じだと思っていたら、API Gatewayからのテストが上手くいかない場合があります。
Lambdaで変更をちゃんと保存しているか確認しましょう。

Lambda経由とAPI Gateway経由でパラメータの渡され方が違い、求められるreturnも違うので、双方の要件を満たす必要があります。

|           | Lambdaのテスト | API Gatewayのテスト    |
|-----------|------------------|------------------|
| request   | Test Eventの設定でRequest Headerから、Request Bodyまで何でもかんでもいじれる。 | プロキシ統合を経由してパラメーターが生成されるため、API Gatewayのルールに合わせる必要がある。 |
| response | statusCodeなんて見てない。Syntax Errorとか例外起きなければSuccess！！ | statusCodeちゃんとみてるし、API Gatewayのルールに沿ったresponseを返す必要がある。 |

このような関係ですので、基本的にLambdaのテストが自由すぎます。
API Gatewayの基準に合わせてLambdaのテストを書きましょう。

#### responseの問題
response(return)の問題は、前述のLambdaの`json.dumps()`を参考に乗り越えてください！
以下のような形でreturnすれば、API Gatewayも喜んでくれます。

```python:APIGatewayが欲しがっているreturn
return {
    'isBase64Encoded': False,
    'statusCode': 200,
    'headers': {},
    'body': json.dumps(response)
}
```

#### requestの問題
API Gateway経由でパラメータを受け取る場合は以下のようになると思います。

* GETの時 - `event['queryStringParameters']`
* POSTの時 - `event['body']`

さらに厄介なのが、LambdaとAPI GatewayでPOST時のパラメータの型が違うことです。

|   | Lambda | API Gateway |
|---|--------|-------------|
| 型 | dict  | json        |

このせいで、双方のテストで型エラーが起きて悩まされました。
正攻法のやり方じゃないと思いますが、どちらもdictに変換する関数で無理やり解決しました。

```python:dictへ変換する関数
# Lambdaから直接テストした時と、API Gateway経由でテストした時でeventのタイプが違う
#   Lambdaから直接  : dict
#   API Gateway経由 : json
# どちらの型でもpayload変数で処理できるようにする
def load_payload(body):
    try:
        payload = json.loads(body)
    except Exception:
        payload = body

    return payload
```

POST、DELETE、PATCHなどなど、POST系のメソッドでパラメータを処理したい手前で呼ぶようにしました。

```python:POST、DELETE、PATCHなどなどで呼ぶ
    payload = load_payload(event['body'])
```

### APIのデプロイを忘れる
プロキシ統合を使ってる場合、あんまりいじらないと思います。
逆にいじらないからAPI変更したときのデプロイを忘れます…。

# Amazon S3
## やったこと
フロントエンドのコンテンツアップロードした。
CDNでbootstrapとjQuery使って、API Gatewayへアクセス！

## ハマりどころ

* 公開忘れる
* jQueryのバッティング
* CORS

### 公開忘れる
どれもこれも初めて触るもんで、こんなところでもミスを…。
アップロードして、のりこめー＾＾したらわけわかんないエラー出ました。
公開されてないことが原因でした。

### jQueryのバッティング
これは5分ぐらいで気づけて良かったです。
`$.ajax()`がないよ、みたいに言われました。
headでGoogleのjQuery読んで、body下部でBootstrapのjQuery読んでたので、下部のjQueryを消して解決しました。
どこかが動かなくなってるかも知れません。

### CORS
Amazon S3のハマりどころなのかって感じですが、CORSで激ハマりしました。
AWS側の手順の通り、CORSを有効化したのですが、それ以外の手順がわからずハマりました。

Lambdaのreturnで適切なパラメータを返してあげる必要がありました。

以下、サンプルです。

```python:CORSに必要なreturn
return {
    'isBase64Encoded': False,
    'statusCode': 200,
    'headers': {
        'Access-Control-Allow-Headers': 'Content-Type',
        'Access-Control-Allow-Origin':  'https://hogehoge.s3.us-east-2.amazonaws.com',
        'Access-Control-Allow-Methods': 'OPTIONS,GET'
    },
    'body': json.dumps(response)
```

`Access-Control-Allow-Origin`に呼び出し元(今回は`$.ajax()`を実行しているS3)のoriginを指定する必要があります。
originが何かわからなくても大丈夫！参考URLのCORSのページが超わかりやすいです！

`Access-Control-Allow-Origin`の値が、`*`であればどこのサイトからでもリクエストを受け付けます。
S3のoriginだけ登録した状態でlocalでWebサーバーを起動してテストしてみたところ、以下のエラーが出ました。

```javascript:javascriptのコンソールに出たエラー
Access to XMLHttpRequest at 'https://hogehoge.execute-api.us-east-2.amazonaws.com/default/test'
from origin 'http://127.0.0.1:5500' has been blocked by CORS policy:
The 'Access-Control-Allow-Origin' header has a value 'https://hogehoge.s3.us-east-2.amazonaws.com'
that is not equal to the supplied origin.
```

そこから、`Access-Control-Allow-Origin`を`http://127.0.0.1:5500`と`*`に変更したところ、APIアクセスできました！
ちょっとわかったような気がします。

`Access-Control-Allow-Origin`の末尾に`/`を入れてしまって、アクセスできないことで15分ぐらい悩んだので、完全一致じゃないとダメなことにお気を付けください。


`Access-Control-Allow-Methods`も使う`Http Method`に合わせて書き換えてください。
`isBase64Encoded`は特に意味なく`False`にしてます。環境に合わせて変えてください。

# 最後に
わからないことだらけですが楽しかったです。
サーバーレスによってインフラエンジニアは要らなくなる…？
今後どうなるのか全く分かりませんが、すべて新しい概念だったのでとても勉強になりました。

まだ、CORS経由のPOST関連の処理が出来ていないので、引き続き触ってみます！

# 参考URL
大変勉強になります！

* [AWS Lambda + API Gateway + S3 で簡易Webアプリケーションを作成](https://www.datastadium.co.jp/engineer/e-reports/5245)
* [AWS Lambdaバージョン管理のススメ](https://qiita.com/quotto/items/4c364074edc69cb67d70)
* [API Gateway + Lambda + DynamoDB](https://qiita.com/leomaro7/items/314a80b6d91f9e6b4060)
* [【初心者向け】DynamoDBを理解する〜Pythonを用いた取得パターン特化編〜](https://qiita.com/UpAllNight/items/a15367ca883ad4588c05)
* [Lambda calling Lambda - how to access the payload in the second?](https://stackoverflow.com/questions/55422499/lambda-calling-lambda-how-to-access-the-payload-in-the-second)
* [なんとなく CORS がわかる...はもう終わりにする。](https://qiita.com/att55/items/2154a8aad8bf1409db2b)