Lambdaで6MBを超えるデータをReturnできなかったので、S3のPre-Signed URLを使った話

Lambdaにはレスポンス6MBの制限があります。大量のデータや履歴系のデータを扱う場合には注意しましょう。
2021.05.10

IoT機器からセンサーデータを収集しています。 そのセンサーデータをAPI Gateway(+Lambda)で扱おうとしたところ、Lambdaのレスポンス上限6MB(同期実行時)に引っかかりました。

IoTデータ収集とデータ収集APIの概要図

そこで、対策としてS3バケットのPre-Signed URLを使ってみました。

おすすめの方

  • LambdaでPre-Signed URLを使いたい方

適当なデータを作成する

サンプルデータ

下記のようなデータを作成します。

{
    "aaa": [
        [1619600735000, 11.1],
        [1619600737000, 22.1],
        [1619600739000, 33.1]
    ],
    "bbb": [
        [1619600735000, 11.2],
        [1619600737000, 22.2],
        [1619600739000, 33.2]
    ],
    "ccc": [
        [1619600735000, 11.3],
        [1619600737000, 22.3],
        [1619600739000, 33.3]
    ],
    "ddd": [
        [1619600735000, 11.4],
        [1619600737000, 22.4],
        [1619600739000, 33.4]
    ],
    "eee": [
        [1619600735000, 11.5],
        [1619600737000, 22.5],
        [1619600739000, 33.5]
    ]
}

データ作成用のプログラム

make_data.py

import json
import sys
from random import random

def make_data(number):
    aaa = []
    bbb = []
    ccc = []
    ddd = []
    eee = []

    # タイムスタンプは固定にしておく
    timestamp = 1619600735000

    for i in range(number):
        aaa.append([timestamp, round(random() * 100, 1)])
        bbb.append([timestamp, round(random() * 100, 1)])
        ccc.append([timestamp, round(random() * 100, 1)])
        ddd.append([timestamp, round(random() * 100, 1)])
        eee.append([timestamp, round(random() * 100, 1)])

    data = json.dumps({
        'aaa': aaa,
        'bbb': bbb,
        'ccc': ccc,
        'ddd': ddd,
        'eee': eee,
    })

    print(data)

if __name__ == '__main__':
    if len(sys.argv) != 2:
        print('please data count: $ python make_data.py 3')
    else:
        make_data(int(sys.argv[1]))

データを作成する

python make_data.py 100000 > data_100000.json

S3バケットに格納する

aws s3 cp data_100000.json s3://cm-fujii.genki-test

S3オブジェクトのファイルサイズ

6MBを超えていました。

  • data_100000.json: 10.9 [MB]

まずは、Lambdaが6MBを超えるデータをReturnできないことを確認する

sam init

sam init \
    --runtime python3.8 \
    --name Lambda-Pre-Signed-Test \
    --app-template hello-world \
    --package-type Zip

SAMテンプレート

template.yaml

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Lambda-Pre-Signed-Test

Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: hello_world/
      Handler: app.lambda_handler
      Runtime: python3.8
      Timeout: 10
      Policies:
        - arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess
      Events:
        HelloWorld:
          Type: Api
          Properties:
            Path: /hello
            Method: get

  HelloWorldFunctionLogGroup:
      Type: AWS::Logs::LogGroup
      Properties:
        LogGroupName: !Sub /aws/lambda/${HelloWorldFunction}

Outputs:
  HelloWorldApi:
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"

Lambdaコード

app.py

import json
import boto3

S3_BUCKET_NAME = 'cm-fujii.genki-test'
S3_OBJECT_KEY = 'data_100000.json'

s3 = boto3.resource('s3')

def lambda_handler(event, context):
    obj = s3.Object(S3_BUCKET_NAME, S3_OBJECT_KEY)
    data = obj.get()['Body'].read().decode('utf-8')

    print(obj)
    print(len(data) / 1024 / 1024)

    return {
        "statusCode": 200,
        "body": data,
    }

デプロイ

sam build

sam deploy \
    --template-file template.yaml \
    --stack-name Lambda-Pre-Signed-Test-Stack \
    --s3-bucket cm-fujii.genki-deploy \
    --capabilities CAPABILITY_NAMED_IAM \
    --no-fail-on-empty-changeset

APIエンドポイントを取得

$ aws cloudformation describe-stacks \
    --stack-name Lambda-Pre-Signed-Test-Stack \
    --query 'Stacks[].Outputs'
[
    [
        {
            "OutputKey": "HelloWorldApi",
            "OutputValue": "https://awvil3t6nc.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/",
        }
    ]
]

APIアクセスすると、Internal server error になる

$ curl https://awvil3t6nc.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/
{"message": "Internal server error"}

CloudWatch Logの様子

下記のメッセージが残っていました(抜粋)。

LAMBDA_RUNTIME Failed to post handler success response. Http response code: 413.

RuntimeError: Failed to post invocation response

Response payload size (11450971 bytes) exceeded maximum allowed payload size (6291556 bytes).
Function.ResponseSizeTooLarge

Lambdaの最大レスポンスサイズ(6MB)より多いデータは返せませんでした。

Pre-Signed URLを使う

Lambdaコード

Lambdaコードを書き換えて、Pre-Signed URLを返すようにします。

app.py

import json
import boto3

S3_BUCKET_NAME = 'cm-fujii.genki-test'
S3_OBJECT_KEY = 'data_100000.json'

s3 = boto3.client('s3')

def lambda_handler(event, context):
    url = s3.generate_presigned_url(
        'get_object',
        Params={
            'Bucket': S3_BUCKET_NAME,
            'Key': S3_OBJECT_KEY
        },
        ExpiresIn=3600
    )

    return {
        "statusCode": 200,
        "body": url,
    }

ExpiresInは1時間(3600秒)としましたが、6時間を超える値を設定する場合は、適切な認証情報を用いてください。

HTTPステータスは200を返すようにしていますが、303でも良さそうです。

デプロイ

sam build

sam deploy \
    --template-file template.yaml \
    --stack-name Lambda-Pre-Signed-Test-Stack \
    --s3-bucket cm-fujii.genki-deploy \
    --capabilities CAPABILITY_NAMED_IAM \
    --no-fail-on-empty-changeset

APIアクセスすると、Pre-Signed URLを取得できる

$ curl https://awvil3t6nc.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/
https://s3.ap-northeast-1.amazonaws.com/cm-fujii.genki-test/data_100000.json?AWSAccessKeyId\=xxxxx

\=部分は=だけです。(\=にしないと、本ブログ上で表示されませんでした)

S3オブジェクトを取得する

$ curl -o s3_object.json "https://s3.ap-northeast-1.amazonaws.com/cm-fujii.genki-test/data_100000.json?AWSAccessKeyId\=xxxxx"

バッチリ取得できました!!

$ ls -lh s3_object.json
***    11M  *** s3_object.json

参考