Lambdaでお手軽サーバーレス入門（Lambdaのイベント from API Gateway編）

前回：[Lambdaでお手軽サーバーレス入門（API Gateway設定編）](https://qiita.com/kakken1988/items/bcf052116b2148818930) 

こちらでGETとPOSTを定義しましたが、これに耐えうるようにLambdaもちょっと修正が必要です。
今回は、受け取れるイベントと、返すべきオブジェクトについて書いてみます。

# Lambdaの1つ目の引数 `event`

初めて書いたLambda関数は以下のような内容でした。

```python
import json

def lambda_handler(event, context):
    # TODO implement
    return {
        "statusCode": 200,
        "body": json.dumps(event)
    }
```

では、このeventには何が入ってくるのか、見てみます。
（API GatewayからLambda Proxy Integrationで呼び出される場合です。）

```json
{
  "resource": "/hello-world",
  "path": "/hello-world",
  "httpMethod": "GET",
  "headers": {
    "X-header-bar": "sample-value2",
    "X-header-foo": "sample-value1"
  },
  "multiValueHeaders": {
    "X-header-bar": [
      "sample-value2"
    ],
    "X-header-foo": [
      "sample-value1"
    ]
  },
  "queryStringParameters": {
    "param1": "value1",
    "param2": "value2"
  },
  "multiValueQueryStringParameters": {
    "param1": [
      "value1"
    ],
    "param2": [
      "value2"
    ]
  },
  "pathParameters": null,
  "stageVariables": null,
  "requestContext": {
    "path": "/hello-world",
    "accountId": "199811994466",
    "resourceId": "40afgo",
    "stage": "test-invoke-stage",
    "requestId": "2c67ea73-c3fd-11e8-b124-73dfe8b888d1",
    "identity": {
      "cognitoIdentityPoolId": null,
      "cognitoIdentityId": null,
      "apiKey": "test-invoke-api-key",
      "cognitoAuthenticationType": null,
      "userArn": "arn:aws:iam::199811994466:user/ogawa@adasiaholdings.com",
      "apiKeyId": "test-invoke-api-key-id",
      "userAgent": "aws-internal/3 aws-sdk-java/1.11.398 Linux/4.9.119-0.1.ac.277.71.329.metal1.x86_64 OpenJDK_64-Bit_Server_VM/25.181-b13 java/1.8.0_181",
      "accountId": "199811994466",
      "caller": "AIDAJLEACL7U2KXWOMEDE",
      "sourceIp": "test-invoke-source-ip",
      "accessKey": "ASIAS5BNZCNRO4CEID4F",
      "cognitoAuthenticationProvider": null,
      "user": "AIDAJLEACL7U2KXWOMEDE"
    },
    "resourcePath": "/hello-world",
    "httpMethod": "GET",
    "extendedRequestId": "N_U-FHO9tjMFXhQ=",
    "apiId": "kmd0f95tsb"
  },
  "body": null,
  "isBase64Encoded": false
}
```

## かいつまんで紹介

* resource  
  API Gatewayで定義したリソースのフルパスです。  
  pathとはちょっと違います。（後述）
* path  
  API Gatewayで定義したリソースのフルパスです。  
  resourceとはちょっと違います。（後述）
* httpMethod  
  GETやらPOSTやら。
* headersおよびmultiValueHeaders  
  クライアントから渡されたヘッダです。指定があれば、`dict`で取得できます。なければnullです。
* queryStringParametersおよびmultiValueQueryStringParameters  
  クライアントから渡されたクエリストリングです。指定があれば、`dict`で取得できます。なければnullです。
* pathParameters  
  resourceで変数を指定した場合に、変数名と実際の値の`dict`が取れます。なければnullです。
* stageVariables  
  ステージ変数。API Gatewayをデプロイすると取得できます。  
  ※今回はまだDeployしていないのでnullです。
* requestContext  
  いろいろデータが取れます。
  いつか認証周りを取り上げたときにはここを使います。
* body  
  POSTやPATCHのときの本文です。GETでは必然的にnullになります。
* isBase64Encoded  
  入力がバイナリの場合、bodyにはBase64エンコードされた状態で格納されます。そのときにtrueになります。

## resourceとpathの違い（そしてpathParametersとの関連）

これを確かめるために、API Gatewayで次のようなリソースを定義してテストしてみました。

![apigw13.png](https://qiita-image-store.s3.amazonaws.com/0/143240/63e23ba3-f3a8-135c-c5d4-c47b7e7cb049.png)

Resource Pathには`{hogehoge}`のように、波括弧で囲った名前を指定しました。
この下にGETメソッドを定義して、呼び出してみました。

※一部抜粋しています。

```json
{
  "resource": "/hello-world/{id}",
  "path": "/hello-world/123",
  "httpMethod": "GET",
  "pathParameters": {
    "id": "123"
  },
  "body": null,
  "isBase64Encoded": false
}
```

ここでresourceとpathに違いが出ました。
つまり、 **resourceは定義しているものそのもの**、**pathは値を当てはめたもの**が渡されます。

そして、pathParametersにはidと、指定した値（ここでは123）がペアで入っています。
値は必ずstr（文字列）で入ります。

もしLambda関数内で、リソースによって処理を振り分けたい場合は、resourceとpathParametersを見て切り分けができます。

# 戻り値

以上のようなイベントを受け取って、Lambda関数では好きに処理ができますが、何かしら応答を返してあげる必要があります。
Handlerで指定された関数のreturnで返せばOKなのですが、いくつかルールがあります。

## 超最低限の例

```python
def lambda_handler(event, context):
    return {
        'statusCode': 204
    }
```

最低限、`statusCode`を返せばOKです。
もう少し読みやすくしたければ

```python
from http import HTTPStatus

def lambda_handler(event, context):
    return {
        'statusCode': HTTPStatus.NO_CONTENT
    }
```
こうすると、あれ、204ってなんだっけ？がなくなりますね。

## bodyも返したい

```python
import json
from http import HTTPStatus

def lambda_handler(event, context):
    return {
        'statusCode': HTTPStatus.OK,
        'body': json.dumps({
            'message': 'Hello from Lambda & API Gateway'
        })
    }
```

## Headerも返したい

```python
import json
from http import HTTPStatus

def lambda_handler(event, context):
    return {
        'statusCode': HTTPStatus.OK,
        'headers': {
            'Content-Type': 'application/json'
        },
        'body': json.dumps({
            'message': 'Hello from Lambda & API Gateway'
        })
    }
```

## バイナリを返したい

`isBase64Encoded` がミソです。

```python
import base64
from http import HTTPStatus

def lambda_handler(event, context):
    body = b'何かしらバイナリ（PDFとか）'

    return {
        'statusCode': HTTPStatus.OK,
        'headers': {
            'Content-Type': 'application/pdf',
            'Content-Disposition': 'attachment; filename="sample.pdf"'
        },
        'body': base64.b64encode(body).decode('utf-8'),
        'isBase64Encoded': true
    }
```

# サンプルソース
最初に作ったLambda関数と、API Gateway定義は以下のように変えます。
※サンプルなのでかなり適当に書きました。すみません。

```python:lambda_function.py
import json
from http import HTTPStatus

USERS = {
    'railgun': {
        'name': 'Mikoto Misaka',
        'age': 14,
        'city': 'Gakuen-toshi',
        'country': 'Japan'
    },
    'imagine-breaker': {
        'name': 'Touma Kamijo',
        'age': 15,
        'city': 'Gakuen-toshi',
        'country': 'Japan'
    },
    'index': {
        'name': 'Index-Librorum-Prohibitorum',
        'age': 14,
        'city': 'London?',
        'country': 'United Kingdom'
    }
}

def lambda_handler(event: dict, context):
    http_method = event['httpMethod']
    resource = event['resource']
    
    response_status = None
    
    if http_method == 'GET':
        response_body = {
            'method': 'get information',
            'data': None
        }
        if resource == '/hello-world':
            response_body['data'] = USERS
            response_status = HTTPStatus.OK

        elif resource == '/hello-world/{id}':
            response_body['data'] = USERS.get(event['pathParameters']['id'])
            if not response_body['data']:
                response_status = HTTPStatus.NOT_FOUND
            else:
                response_status = HTTPStatus.OK
    
    elif http_method == 'POST':
        response_body = {
            'method': 'insert information',
            'data': json.loads(event['body']) if event['body'] else None
        }
        response_status = HTTPStatus.CREATED
        
        # Do nothing (it's sample)
    
    return {
        'statusCode': response_status,
        'body': json.dumps(response_body)
    }
```

レールガン、やっぱりかっこいいっすね。

# 次回予告
次回は、これで実際に外部から呼び出してみます。
次回：[Lambdaでお手軽サーバーレス入門（API Gatewayデプロイ編）](https://qiita.com/kakken1988/items/c864e8eecccdf877c612)