【アップデート】S3に対するGetリクエストのレスポンスをLambdaで加工するS3 Object Lambdaが利用可能になりました

機密データの動的マスク等を実現するS3 Object Lambdaが利用可能になりました!!
2021.03.20

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

CX事業本部@大阪の岩田です。2021/3/18付けのアップデートでS3に対するGetリクエストのレスポンスをLambdaで加工するS3 Object Lambdaという機能が利用できるようになりました。

S3 Object Lambdaとは?

S3に対するGETリクエストをトリガーにLambdaを起動し、レスポンスを自由に加工できる機能です。例えば以下のようなユースケースが想定されています。

  • 行のフィルタリング
  • 画像の動的なリサイズ
  • 機密データのマスク

この機能を利用するには、S3アクセスポイントをラップした「オブジェクト Lambda アクセスポイント」が必要になります。クライアントが「オブジェクト Lambda アクセスポイント」経由でオブジェクトのGETを試行するとLambdaが起動し、Lambdaによってカスタマイズされたレスポンスが返却されるという流れになります。

S3アクセスポイントについては以下の記事も参考にしてください

やってみる

実際にS3 Object Lambdaの機能を試してみます。

Lambda Functionの作成

まずはオブジェクトLambdaアクセスポイントに設定するLambda Functionを作成しておきましょう。Lambda実行ロールに s3-object-lambda:WriteGetObjectResponse の権限が必要になるので、権限付与を忘れないよう注意して下さい。

コードは後ほど編集していくので、現段階ではLambdaを作成するだけでOKです。

オブジェクトLambdaアクセスポイントの作成

続いてオブジェクトLambdaアクセスポイントを作成します。マネコンから「オブジェクトLambda アクセスポイントの作成」を選択します。

オブジェクトLambdaアクセスポイントにはS3アクセスポイントが必要です。「アクセスポイントの作成」からS3アクセスポイントを作成しましょう ※既存のアクセスポイントがあればそれを流用して頂いても構いません

適当なS3バケットと紐付けたアクセスポイントを作成します。

作成したアクセスポイントのARNを「サポートするアクセスポイント」に指定し、「Lambda関数のARN」に事前に作成したLambdaのARNを設定します。

その他の設定はデフォルトのまま進めます。

作成完了です!

Lambdaレイヤーの準備

ここからは実際にLambdaのコードを書きながら動作確認していきます。公式ドキュメントにいくつかサンプルコードが紹介されているのですが、現時点ではJavaのサンプルコードしか見つかりませんでした。

https://docs.aws.amazon.com/AmazonS3/latest/userguide/olap-writing-lambda.html#olap-getobject-response

今回はPython3.8のLambdaで試そうと思うのですが、Lambda実行環境にバンドルされているboto3はバージョンが古く、S3クライアントのwrite_get_object_responseが利用できません。以下のブログ等を参考に、最新版のboto3を導入したLambdaレイヤーを用意して下さい。

Lambda で Aurora Serverless の Data API 使えました!そう、Lambda Layer があればね

なお、現時点での最新のboto3はバージョン1.17.33でした。

レスポンスを書き換えて403エラーを返却してみる

レイヤーの準備ができたら実際にコードを書いていきましょう。まずはステータスコードを403に上書きする処理を実装してみます。S3 Object Lambdaで利用するLambda FunctionはS3のAPIWriteGetObjectResponseのレスポンスを返却する必要があります。

PythonのLambdaではboto3のwrite_get_object_responseを利用し、引数の指定でレスポンスを組み立てていくことになります。今回は以下のようなコードを書いてみました。

import boto3

s3_client = boto3.client('s3')

def lambda_handler(event, context):

    get_obj_context = event['getObjectContext']
    
    return s3_client.write_get_object_response(
        RequestRoute=get_obj_context['outputRoute'],
        RequestToken=get_obj_context['outputToken'],
        StatusCode=403,
        ErrorCode='MissingToken',
        ErrorMessage='some error message'
    )

write_get_object_responseの引数の指定は以下のとおりです。

  • RequestRoute
    • ルーティング用のトークン
  • RequestToken
    • WriteGetObjectResponseとユーザーのリクエストをマッピングするためのトークンを指定します
  • StatusCode
    • クライアントに返却するHTTPステータスコードを指定します
  • ErrorCode
    • クライアントに返却するエラーコードを指定します
  • ErrorMessage
    • クライアントに返却するエラーメッセージを指定します。

ポイントはRequestRouteRequestTokenです。これらの引数は指定が必須となっており、指定すべき値はLambdaのイベントデータから渡されます。Lambdaのイベントデータは以下のような構造となっています。

{
    "xAmzRequestId": "requestId",
    "getObjectContext": {
        "inputS3Url": "https://my-s3-ap-111122223333.s3-accesspoint.us-east-1.amazonaws.com/example?X-Amz-Security-Token=<snip>",
        "outputRoute": "io-use1-001",
        "outputToken": "OutputToken"
    },
    "configuration": {
        "accessPointArn": "arn:aws:s3-object-lambda:us-east-1:111122223333:accesspoint/example-object-lambda-ap",
        "supportingAccessPointArn": "arn:aws:s3:us-east-1:111122223333:accesspoint/example-ap",
        "payload": "{}"
    },
    "userRequest": {
        "url": "https://object-lambda-111122223333.s3-object-lambda.us-east-1.amazonaws.com/example",
        "headers": {
            "Host": "object-lambda-111122223333.s3-object-lambda.us-east-1.amazonaws.com",
            "Accept-Encoding": "identity",
            "X-Amz-Content-SHA256": "e3b0c44298fc1example"
        }
    },
    "userIdentity": {
        "type": "AssumedRole",
        "principalId": "principalId",
        "arn": "arn:aws:sts::111122223333:assumed-role/Admin/example",
        "accountId": "111122223333",
        "accessKeyId": "accessKeyId",
        "sessionContext": {
            "attributes": {
                "mfaAuthenticated": "false",
                "creationDate": "Wed Mar 10 23:41:52 UTC 2021"
            },
            "sessionIssuer": {
                "type": "Role",
                "principalId": "principalId",
                "arn": "arn:aws:iam::111122223333:role/Admin",
                "accountId": "111122223333",
                "userName": "Admin"
            }
        }
    },
    "protocolVersion": "1.00"

https://docs.aws.amazon.com/AmazonS3/latest/userguide/olap-writing-lambda.html

Lambdaのコードが準備できたらテストしてみます。

マネコンの「オブジェクト Lambda アクセスポイント」から適当なオブジェクトのGETを試行すると...

403エラーが返却されました。エラーコードやエラーメッセージがLambdaの指定通りになっていることが分かります。

レスポンスの文字コードを書き換えてみる

別のパターンも試してみましょう。今度はS3上に配置したUTF-8のテキストファイルをCP932に変換して返却する処理を実装してみます。実装はこんな感じになりました。

import io
import urllib

import boto3

s3_client = boto3.client('s3')

def lambda_handler(event, context):

    get_obj_context = event['getObjectContext']    
    signed_url = get_obj_context['inputS3Url']
    with urllib.request.urlopen(signed_url) as res:
        raw_body = res.read().decode('utf-8')
    
    new_body =  io.BytesIO()
    new_body.write(raw_body.encode('cp932'))
    new_body.seek(0)
    
    return s3_client.write_get_object_response(
        RequestRoute=get_obj_context['outputRoute'],
        RequestToken=get_obj_context['outputToken'],
        Body=new_body
    )

イベントデータのgetObjectContext.inputS3UrlにはS3アクセスポイントからオブジェクトをGETするためのPresigned-URLが取得できます。このURLを利用して

with urllib.request.urlopen(signed_url) as res:
	raw_body = res.read().decode('utf-8')

の部分で生データのテキストファイルの中身を取得します。続いて

new_body =  io.BytesIO()
new_body.write(raw_body.encode('cp932'))
new_body.seek(0)

の部分でテキストデータをCP932に変換しバイナリストリームに書き込みます。生成したバイナリストリームをwrite_get_object_responseの名前付き引数Bodyに渡すことで、クライアントにはCP932のテキストデータが返却されます。

Lambdaの準備ができたので動作確認してみましょう。まずは文字コードUTF-8で以下のテキストファイルを用意します。

クラスメソッド株式会社
CX事業本部
岩田 智哉

このテキストファイルをS3バケットにアップ

 aws s3 cp japanese.txt s3://<S3バケット>

オブジェクトLambdaアクセスポイント経由でjapanese.txtをDLします。

aws s3api get-object  --bucket arn:aws:s3-object-lambda:ap-northeast-1:<AWSアカウントID>:accesspoint/<アクセスポイント名>  --key japanese.txt cp932.txt

DLしたファイルをcatで確認してみましょう

$ cat cp932
�N���X���\�b�h�������
CX���Ɩ{��
��c�@�q��

文字コードがCP932なのでターミナルには文字化けして表示されます。

nkfで文字コードを推測してみます

$ nkf --guess cp932
Shift_JIS (LF)

ちゃんとCP932に変換できてそうですね。

UTF-8に変換してターミナルに出力してみましょう

nkf -w cp932
クラスメソッド株式会社
CX事業本部
岩田 智哉

表示できました!

まとめ

S3 Object Lambdaを利用してS3からのレスポンスをオンデマンドでレスポンスを加工できるようになりました。クライアントの属性によってレスポンスを微調整したいようなユースケースでは便利に使えそうです。Lambdaでレスポンスを加工するという特性から、速度やコスト面では多少なりともデメリットがあるので、その点には注意が必要です。例えば今回紹介したような文字コードの変換などは事前にCP932に変換したファイルもS3に配置しておき、クライアント側で適切なオブジェクトをGETしてもらうという方法も考えられます。コストや性能面での要件と管理コストを考慮して適切な方式を選択するようにしましょう。