前回:Lambdaでお手軽サーバーレス入門(API Gateway設定編)
こちらでGETとPOSTを定義しましたが、これに耐えうるようにLambdaもちょっと修正が必要です。
今回は、受け取れるイベントと、返すべきオブジェクトについて書いてみます。
Lambdaの1つ目の引数 event
初めて書いたLambda関数は以下のような内容でした。
import json
def lambda_handler(event, context):
    # TODO implement
    return {
        "statusCode": 200,
        "body": json.dumps(event)
    }
では、このeventには何が入ってくるのか、見てみます。
(API GatewayからLambda Proxy Integrationで呼び出される場合です。)
{
  "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で次のようなリソースを定義してテストしてみました。
Resource Pathには{hogehoge}のように、波括弧で囲った名前を指定しました。
この下にGETメソッドを定義して、呼び出してみました。
※一部抜粋しています。
{
  "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なのですが、いくつかルールがあります。
超最低限の例
def lambda_handler(event, context):
    return {
        'statusCode': 204
    }
最低限、statusCodeを返せばOKです。
もう少し読みやすくしたければ
from http import HTTPStatus
def lambda_handler(event, context):
    return {
        'statusCode': HTTPStatus.NO_CONTENT
    }
こうすると、あれ、204ってなんだっけ?がなくなりますね。
bodyも返したい
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も返したい
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 がミソです。
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定義は以下のように変えます。
※サンプルなのでかなり適当に書きました。すみません。
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デプロイ編)

