API Gateway ↔ Lambda ↔ Dynamo


# はじめに

[前回](https://qiita.com/narutaro/items/b553a26cc84f195f165f)のものに API Gateway を繋いでみます。こんな感じがゴール

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32540/acec4af0-fc22-1fd5-d56e-e9d1049d8933.png)

# API Gateway

## 基本

GUI がかなり直感的でなので頭使わずに使える感じです

1. エンドポイントのリソース `/project` 作って
2. リソースにメソッド `GET` `PUT` `POST` `DELETE` 作って
3. メソッド毎にLambda のコード `project` にマッピングするだけ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32540/a897911a-d0eb-85bc-6707-8ccbd1960bb3.png)

忘れがちなのが、APIの変更は「デプロイ」を押してデプロイしないといけないこと。これでとりあえず動くはず

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32540/22dede20-c774-0e59-a3e1-0a1448c309d7.png)

## APIキー

APIキーを使うときはちょっと面倒

1. リソースの各メソッド毎にAPIキーでの認証を必須にする
2. 「使用料プラン」を作成する（ここでAPIに対するレートリミットなどを設定）
3. 「ステージ」を作成する（開発用とか商用とかそういう意味のステージのことで、ここでは `dev_1` とした）
4. 「APIキー」「使用料プラン」「ステージ」を関連づける

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32540/735daa3f-5f46-2a39-62b5-7644920bc3e4.png)

変更の反映に数分かかることもあるようですが、これでAPIキーを付けてコールできるはず

```shell
curl -s -X GET -H "x-api-key: your-api-key-here" https://<project-id>.execute-api.us-east-2.amazonaws.com/dev_1/projects | jq
```

## Lambda 統合プロキシ

- `Lambda 統合プロキシ` にチェックを入れる
- メソッド（GET/PUT/POST/DELETE）をラムダ関数に紐づける[^1]

[^1]: 今回のようなケースでは、実はメソッドとして`ANY`を使うことでメソッド個別にラムダ関数に紐づける必要はなくなる

することで、HTTPリクエストに含まれる様々なパラメターがそのまま Lambda に渡される

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32540/10cbc011-18cb-0ced-300a-0f533246f2ae.png)

例えば、HTTPメソッドが `POST` なのか `GET` なのかが引き渡されるので Lambda 側でわかる. 

こんな風に読んだ時に

```shell
curl -s -X GET -H "x-api-key:your-api-key-here" https://<api-id>.execute-api.us-east-2.amazonaws.com/dev_1/projects?say=ciao -d '{"say": "hello"}'
```

Lambda に `event` として渡されるパラメータはこんな感じ

```json:event
{
  "resource": "/projects",
  "path": "/projects",
  "httpMethod": "GET",
  "headers": {
    "accept": "*/*",
    "content-type": "application/x-www-form-urlencoded",
    "Host": "<project-id>.execute-api.us-east-2.amazonaws.com",
    "User-Agent": "curl/7.64.1",
    "X-Amzn-Trace-Id": "Root=1-5fd44390-5f78ee5a289e7e3a0355bec2",
    "x-api-key": "your-api-key",
    "X-Forwarded-For": "***.**.**.**",
    "X-Forwarded-Port": "443",
    "X-Forwarded-Proto": "https"
  },
  "multiValueHeaders": {
    "accept": [
      "*/*"
    ],
    "content-type": [
      "application/x-www-form-urlencoded"
    ],
    "Host": [
      "<project-id>.execute-api.us-east-2.amazonaws.com"
    ],
    "User-Agent": [
      "curl/7.64.1"
    ],
    "X-Amzn-Trace-Id": [
      "Root=1-5fd44390-5f78ee5a289e7e3a0355bec2"
    ],
    "x-api-key": [
      "<your-api-key>"
    ],
    "X-Forwarded-For": [
      "***.**.**.**"
    ],
    "X-Forwarded-Port": [
      "443"
    ],
    "X-Forwarded-Proto": [
      "https"
    ]
  },
  "queryStringParameters": {
    "say": "ciao"
  },
  "multiValueQueryStringParameters": {
    "say": [
      "ciao"
    ]
  },
  "pathParameters": null,
  "stageVariables": null,
  "requestContext": {
    "resourceId": "<resource-id>",
    "resourcePath": "/projects",
    "httpMethod": "GET",
    "extendedRequestId": "Xa9-iG9kCYcFU8Q=",
    "requestTime": "12/Dec/2020:04:14:08 +0000",
    "path": "/dev_1/projects",
    "accountId": "<account-id>",
    "protocol": "HTTP/1.1",
    "stage": "dev_1",
    "domainPrefix": "<project-id>",
    "requestTimeEpoch": 1607746448145,
    "requestId": "06499aaf-9168-49d7-b4da-2a0a3ad5d4ad",
    "identity": {
      "cognitoIdentityPoolId": null,
      "cognitoIdentityId": null,
      "apiKey": "<your-api-key>",
      "principalOrgId": null,
      "cognitoAuthenticationType": null,
      "userArn": null,
      "apiKeyId": "<api-key-id>",
      "userAgent": "curl/7.64.1",
      "accountId": null,
      "caller": null,
      "sourceIp": "***.**.**.**",
      "accessKey": null,
      "cognitoAuthenticationProvider": null,
      "user": null
    }, 
    "domainName": "<project-id>.execute-api.us-east-2.amazonaws.com",
    "apiId": "<api-id>"
  },
  "body": "{\"say\": \"hello\"}",
  "isBase64Encoded": false
}
```
クライアントからのデータ `body` は `event['body']` でアクセスできるので Lambda 側の CRUD するコードを[前回](https://qiita.com/narutaro/items/b553a26cc84f195f165f)から少し変えて、こんな感じにしときます

```ruby

require 'json'
require 'aws-sdk-dynamodb'

def add_project(table, body)
  table.put_item({ item: body })  
end

def delete_project(table, body)
  params = { table_name: table, key: { 'project-id': body['project-id'] } }
  begin
    table.delete_item(params)
  rescue Aws::DynamoDB::Errors::ServiceError => error
    puts error.message
  end
end

def update_project(table, body)
  params = {
    table_name: table,
    key: { 'project-id': body['project-id'] },
    attribute_updates: {
      name: {
        value: body['name'],
        action: "PUT"
      }
    }
  }
  table.update_item(params)  
end

def list_project(table)
  scan_output = table.scan({ limit: 50, select: "ALL_ATTRIBUTES" })

  scan_output.items.each do |item|
    keys = item.keys
    keys.each do |k|
      puts "#{k}: #{item[k]}"
    end
  end
end

def lambda_handler(event:, context:)
  
  http_method = event['httpMethod'] # このへんを追加
  body = JSON.parse(event['body'])  # このへんを追加
  
  dynamodb = Aws::DynamoDB::Resource.new(region: 'us-east-2')
  table = dynamodb.table('project')
  
  case http_method
    when 'GET'    then list_project(table)
    when 'PUT'    then update_project(table, body)
    when 'POST'   then add_project(table, body)
    when 'DELETE' then delete_project(table, body)
    else 0
  end

  { statusCode: 200, body: JSON.generate(list_project(table)) }

end

```

例えばこんな感じでリクエスト投げると、HTTPのメソッドをみて CRUD 処理をする。ちなみに `jq` しとくと見やすいのでおすすめ

```shell
curl -s -X POST -d '{ "project-id":101, "name":"Cycling", "is-default":false }' -H "x-api-key: <your-api-key-here>" https://<project-id>.execute-api.us-east-2.amazonaws.com/dev_1/projects | jq
```

うむ、動いたぞ

# シリーズ

- [Lambda ↔ DynamoDB](https://qiita.com/narutaro/items/b553a26cc84f195f165f)
- [API Gateway ↔ Lambda ↔ DynamoDB](https://qiita.com/narutaro/items/57cdd9f5ec8634112b09)
- [Cognito ↔ API Gateway ↔ Lambda ↔ Dynamo](https://qiita.com/narutaro/items/adaf160d944b91791b59)
- [Amplify [Hosting] ↔ API Gateway [REST] ↔ Lambda](https://qiita.com/narutaro/items/7aba191c12d917d36758)
- [API GatewayでCORSを有効にするためにやったこと](https://zenn.dev/masaino/articles/dd9ee709799af0)
- [AmplifyのJavascript SDKでCognito認証からデータ取得まで](https://zenn.dev/articles/9c4586fe1fc179/edit)