はじめに
3/19(金)に「Amazon S3 Object Lambda」というS3の新機能がGAになりました。
これは上記公式サイトにも
「S3 から取得したデータをアプリケーションに返す前に独自のコードを追加して処理できる新機能」
とあるように、例えばS3バケットのキーをgetObjectする際に、あらかじめ何かの処理(フィルタリングやマスキングなど)を実施した値になっているため、getObject()を呼び出すLambdaでの変換処理が不要になります。
というわけで、今回はこのS3OLについて試してみた話です。
TL;DR
- S3OLの概要
- TypeScriptで実装してみた
- ハマった点など
S3OLの仕組み
概要としては、こんな感じです。
- S3OLアクセスポイント経由でS3バケットにアクセスすると、S3OLにて変換処理をした値を取得できる
- S3OLアクセスポイントを経由しなければ、元の値(生値)を取得できる
- 1つのS3バケットについて、S3OLアクセスポイント(≒変換処理)を複数設定できる
ユースケース
ユースケースとしては、下記のようなケースです。
- 元の情報を編集して返す必要がある。
- (例) 特定の情報をフィルタ/マスクする、画像のサイズを変える...など
- 複数の処理(≒Lambda)で、上記のような処理を行う(=共通化)
- モジュール化/レイヤー化より、管理は簡単かも
参考サイト
- Amazon S3 Object Lambdaの紹介 – コードを利用して S3 から取得するデータを処理 | Amazon Web Services ブログ (AWS公式ブログ)
- 【アップデート】S3に対するGetリクエストのレスポンスをLambdaで加工するS3 Object Lambdaが利用可能になりました | DevelopersIO (クラスメソッドさんのブログ)
実装してみる
というわけで、さっそく実装します。(今回はTypeScriptで実装)
前提条件として、aws-sdkを最新バージョンに更新しておきます。(最新バージョンじゃないとS3OLをサポートしていないので。Ver2.874.0でサポートを確認)
まずは「参考サイト」のAWS公式ブログに載っているコードを元に、TypeScriptで下記コードを書きました。(AWS公式ブログはpythonですが、全く問題なく理解できる内容だと思います。)
※元となる情報ですが、今回も「ドルアーガの塔 宝物リスト」のjsonを使用しています。(内容はこのブログの最後に載せています)
今回は「上記リスト(tower_of_druaga.json)から、階数(Floor)が素数の階の情報のみ取得する」という処理を実装しています。
// s3_object_lambda.ts import axios from 'axios' import 'source-map-support/register'; import * as AWS from 'aws-sdk'; import S3 from 'aws-sdk/clients/s3'; // S3OLのレスポンスの定義 interface IS3ObjectLambdaResponse { statusCode: number } // ObjectContextの定義 interface IObjectContext { inputS3Url: string, outputRoute: string, outputToken: string } // フロアの詳細(Detail)の定義 interface IDetail { Condition: string, Effect: string, Memo: string, Name: string }; // 各フロアの情報の定義 interface IFloor { Type: string, Floor: number, Detail: IDetail[] } // ハンドラ関数 export async function handler(event:any): Promise<IS3ObjectLambdaResponse>{ console.log(`[event] ${JSON.stringify(event)}`); const context:IObjectContext = event.getObjectContext; const route:string = context.outputRoute; const token:string = context.outputToken; const url:string = context.inputS3Url const res:any = await axios.get(url); console.log(`[resData] ${JSON.stringify(res.data)}`); const originalContent:IFloor[] = res.data; // 変換処理を実施する。 const filteredContent:string = filter(originalContent); const s3:S3 = new AWS.S3(); const param:S3.WriteGetObjectResponseRequest = { RequestRoute: route, RequestToken: token, Body: filteredContent }; const result = await s3.writeGetObjectResponse(param).promise(); const response:IS3ObjectLambdaResponse = { statusCode: 200 } return response; } // 階数(Floor)が素数の階の情報のみ抜き出す処理。 // 素数の判定は、今回は手抜きしてます。 function filter(_content:IFloor[]):string { // const items:IFloor[] = _content const filteredItem:IFloor[] = _content.filter(x => [2,3,5,7].findIndex(y => y === x.Floor) !== -1); console.info(`[filteredItem] ${JSON.stringify(filteredItem)}`) return JSON.stringify(filteredItem); }
キーになるのは、「event.getObjectContext」で取得できる以下3つの値です。
キー名 | 説明 | 備考 |
---|---|---|
outputRoute | S3OLからLambdaに渡されるルーティングトークン | writeGetObjectResponseで使用 |
outputToken | S3OLからLambdaに渡される認証(マッピング)トークン | 同上 |
inputS3Url | S3から元のキー内容を取得するためのURL |
これらをevent引数から取得したら、あとはソースにある通り、
- inputS3Urlにgetリクエストを投げて、そのレスポンスから元のキー内容を取得
- 「元のキー情報(=ファイル名)」はgetObjectの引数「key」で設定される
- 変換処理を実施し、元のキー情報から変換後の値を取得
- 上のソースなら「filter」関数
- writeGetObjectResponse関数で、getObjectの戻り値を変換後の値に更新
- このLambda自体のレスポンスを返却する。(statusCode:200)
という処理を実施する感じです。
あとは上記Lambdaを、AWSに新規Lambda関数として作成しておいてください。
- コンソール直でも、CloudFormation(以下Cfn)経由でもどちらでもOKです。
- Cfnの場合、定義は通常のLambda関数と全く同じでOKです。
- もちろんServerless Frameworkも
また、上記S3OLのLambda関数を呼び出すLambda(s3_object_lambda_base.ts)も実装しました。
(詳細は省略しますが、レスポンスのBodyとして、「raw」にtower_of_druaga.jsonのそのままの値を、「filter」に先述の「階数(Floor)が素数の階のみの情報」を格納します。)
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; import 'source-map-support/register'; import { Context } from 'vm'; import * as AWS from 'aws-sdk'; import S3, { GetObjectRequest, GetObjectOutput } from 'aws-sdk/clients/s3'; interface IDetail { Condition: string, Effect: string, Memo: string, Name: string }; interface IFloor { Type: string, Floor: number, Detail: IDetail[] } export async function handler(event:APIGatewayProxyEvent, _context:Context):Promise<APIGatewayProxyResult> { console.log(`[event] ${JSON.stringify(event)}`); const s3:S3 = new AWS.S3(); const param_raw:GetObjectRequest = { Bucket: 'suzukima-s3-object-lambda-test', Key: 'tower_of_druaga.json' }; const param_filtered:GetObjectRequest = { Bucket: '<S3OLアクセスポイントのARN>', Key: 'tower_of_druaga.json' }; const result:IFloor[] = await Promise.all([s3getObject(s3, param_raw), s3getObject(s3, param_filtered)]); const response:APIGatewayProxyResult = { statusCode: 200, body: JSON.stringify({ raw: result[0], filtered: result[1] }) } return response; } async function s3getObject(_s3:S3, _param:GetObjectRequest):Promise<IFloor> { const res:GetObjectOutput = await _s3.getObject(_param).promise(); console.info(`[res] ${JSON.stringify(res)}`); return JSON.parse(res.Body.toString()); }
設定する(コンソール上で)
ということで、まずはS3OLをコンソール上で作成します。
といってもS3OLはS3アクセスポイントを必要とするので、まずはS3アクセスポイントを作成します。
コンソールから[S3]→[アクセスポイント]→[アクセスポイントの作成]と選択します。
キー名 | 説明 | 備考 |
---|---|---|
アクセスポイント名 | 一意な名前を指定 | |
バケット名 | 対象のS3バケットを指定 | 「S3の参照」から選択可能 |
ネットワークオリジン | VPCかインターネットか。VPCならVPCIDも指定する。 | 今回は「インターネット」を指定 |
次に、S3OLのアクセスポイントを作成します。 コンソールから[S3]→[オブジェクトLambdaアクセスポイント]→[オブジェクトLambdaアクセスポイントの作成]と選択します。
今回は下記項目のみ設定しました。(それ以外はデフォルトのまま)
キー名 | 説明 | 備考 |
---|---|---|
オブジェクトLambdaアクセスポイント名 | 一意な名前を指定 | |
サポートするアクセスポイント | 対象のS3アクセスポイントのARNを指定 | さっき作ったS3アクセスポイントのARN |
Lambda関数の呼び出し | 呼び出すLambda関数を名前で指定するか、ARNで指定するか | 今回は「アカウントの関数から選択」を指定 |
Lambda関数(のARN) | 「Lambda関数の呼び出し」の値に従い、関数名か関数のARNを指定 |
ここまで出来たら、動作を確認してみましょう。(今回はコンソール上で動作確認を行います)
動作確認は、コンソールから[S3]→[オブジェクトLambdaアクセスポイント]から上で作成したS3OLアクセスポイントを選択し、「オブジェクト」タブでキーをチェックして、「アクション」から「開く」を実施することで実施できます。
ハマった点など
「writeGetObjectResponse関数がない)」といわれる
これは参考サイトの クラスメソッドさんの記事にある通り、「AWS側のaws-sdkランタイムのバージョンが最新ではない」ことが原因です。
対策ですが、上記の記事通りに最新版のaws-sdkをLambdaレイヤーとして登録すればOKです。(Laambdレイヤーの説明は、今回は省略します)
結果が「Forbidden」になる
自分はこれにかなりハマりました。
結果としては、S3バケット&S3OLアクセスポイントへのポリシー設定で、下記のActionを許可すればOKです。
- S3バケット:s3:GetObject
- S3OLアクセスポイント:s3-object-lambda:WriteGetObjectResponse
ただ、僕が勘違いしてたのは、
- WriteGetObjectResponseを(S3OLアクセスポイントではなく)S3バケットに対して設定していた
- WriteGetObjectResponseを「s3:WriteGetObjectResponse」と勘違いしていた
点で、ここの調査にだいぶ時間を費やしてしまいました。(分かってしまえばなんてことはないんですけどね...)
結果
というわけで、最終的にs3_object_lambda_base.ts(AWS上ではjsだけど)を呼び出した結果が下の通り。
「raw」が「tower_of_druaga.json」そのままの値なのに対し、「filter」の方はちゃんとFloorが素数の情報のみになっています。
なので、S3OLが正しく動いている、と考えてよさそうです。
HTTP/1.1 200 OK (中略) { "raw": [ { "Type": "treasure", "Floor": 1, "Detail": [ { "Condition": "グリーンスライムを3匹倒す", "Effect": "壁を宝箱を取る前後1回ずつ壊せる", "Memo": "", "Name": "カッパーマトック" } ] }, { "Type": "treasure", "Floor": 2, "Detail": [ { "Condition": "ブラックスライムを2匹倒す", "Effect": "足が速くなる", "Memo": "", "Name": "ジェットブーツ" } ] }, { "Type": "treasure", "Floor": 3, "Detail": [ { "Condition": "ブルーナイトのどちらかを倒す", "Effect": "ミスしても残機が減らない(1回だけ)", "Memo": "正確な条件は、「ブルーナイトのうち、先にフロアに出現した方を倒す」", "Name": "ポーション・オブ・ヒーリング" } ] }, { "Type": "treasure", "Floor": 4, "Detail": [ { "Condition": "扉を通過する", "Effect": "フロア開始時、鍵がある方向を向くと音が鳴る", "Memo": "宝箱を出すより先に鍵を取ってしまうと出ない", "Name": "チャイム" } ] }, { "Type": "treasure", "Floor": 5, "Detail": [ { "Condition": "メイジの呪文を歩きながら盾で3回受ける", "Effect": "攻撃力UP", "Memo": "18階のドラゴンスレイヤーを取るのに必要", "Name": "ホワイトソード" } ] }, { "Type": "treasure", "Floor": 6, "Detail": [ { "Condition": "最上段に上がった後、下がる", "Effect": "10階までゴーストが見えるようになる", "Memo": "", "Name": "キャンドル" } ] }, { "Type": "treasure", "Floor": 7, "Detail": [ { "Condition": "カッパーマトックをなくす", "Effect": "宝箱を取る前に1回、取った後2回使える", "Memo": "最大で宝箱を取る前に4回、取った後に5回まで使えるが、Effectの回数より多く使うと1/3の確率で壊れる", "Name": "シルバーマトック" } ] }, { "Type": "treasure", "Floor": 8, "Detail": [ { "Condition": "ステート時点からX軸,Y軸共にずれた位置で剣を振る", "Effect": "体力が上がる(その階のみ)", "Memo": "", "Name": "ポーション・オブ・パワー" } ] }, { "Type": "treasure", "Floor": 9, "Detail": [ { "Condition": " 最上段の右から8列目と左から8列目の両地点を通過する。", "Effect": "体力が下がる(その階のみ)", "Memo": "", "Name": "ポーション・オブ・エナジー・ドレイン" } ] }, { "Type": "treasure", "Floor": 10, "Detail": [ { "Condition": "レッドスライムの放つ呪文を盾で受ける。", "Effect": "26階の宝箱(ハイパーガントレット)を取るのに必要。", "Memo": "レッドスライムの気分次第。", "Name": "ガントレット" } ] } ], "filtered": [ { "Type": "treasure", "Floor": 2, "Detail": [ { "Condition": "ブラックスライムを2匹倒す", "Effect": "足が速くなる", "Memo": "", "Name": "ジェットブーツ" } ] }, { "Type": "treasure", "Floor": 3, "Detail": [ { "Condition": "ブルーナイトのどちらかを倒す", "Effect": "ミスしても残機が減らない(1回だけ)", "Memo": "正確な条件は、「ブルーナイトのうち、先にフロアに出現した方を倒す」", "Name": "ポーション・オブ・ヒーリング" } ] }, { "Type": "treasure", "Floor": 5, "Detail": [ { "Condition": "メイジの呪文を歩きながら盾で3回受ける", "Effect": "攻撃力UP", "Memo": "18階のドラゴンスレイヤーを取るのに必要", "Name": "ホワイトソード" } ] }, { "Type": "treasure", "Floor": 7, "Detail": [ { "Condition": "カッパーマトックをなくす", "Effect": "宝箱を取る前に1回、取った後2回使える", "Memo": "最大で宝箱を取る前に4回、取った後に5回まで使えるが、Effectの回数より多く使うと1/3の確率で壊れる", "Name": "シルバーマトック" } ] } ] }
まとめ
というわけで、S3OLの紹介&実際に動かしてみました。
今までモジュールなどで行っていましたが、これを使うことで、確かに便利なケースは出てきそうですね。 (例えば公式ブログにあった画像サイズ変換とか、あどクラスメソッドさんの記事にもあった文字コード変換とか)
なお、今回はS3OLの設定をコンソール上で行いましたが、その2ではこれはCloudFormation(実際はServerless Frameworkだけど)から定義・作成する方法を書こうと思っています。(serverless.tsの内容は、その際に公開します。)
告知
先日行われた「JAWS DAYS 2021 re:Connect」における、私のセッションのアーカイブ動画が公開されたしたので、よろしければご参照ください。 www.youtube.com
また、他の方の動画も公開されているので、見逃したという人も、この機会にぜひご覧ください。
それでは、今回はこの辺で
参考資料:ドルアーガの塔 宝物リスト
過去のServerless Meetup Japan VirtualやQiita Advent Calendar 2020などで使用している「ドルアーガの塔 宝物リスト」の内容になります。
(「ドルアーガの塔」がわからない人は、ググってみると幸せになれるかもしれません。)
内容としては、下記情報を格納したオブジェクトの配列になります。(実際は60階分あるけど、今回は10階まで用意)
キー名 | キー名(Detail内) | 説明 | 備考 |
---|---|---|---|
Type | 内容の種類 | 「treasure」固定。パーティションキー | |
Floor | 階数(1~60) | ソートキー | |
Detail | 下記4項目が格納されたオブジェクトの配列 | 配列なのは、45階だけ宝物が2つあるから | |
Condition | 宝物の出現条件 | ||
Effect | 宝物の効果。 | ||
Memo | 一言メモ | ||
Name | 宝物のアイテム名 | 一部微妙に違ってるかも |
[ { "Type": "treasure", "Floor": 1, "Detail": [ { "Condition": "グリーンスライムを3匹倒す", "Effect": "壁を宝箱を取る前後1回ずつ壊せる", "Memo": "", "Name": "カッパーマトック" } ] }, { "Type": "treasure", "Floor": 2, "Detail": [ { "Condition": "ブラックスライムを2匹倒す", "Effect": "足が速くなる", "Memo": "", "Name": "ジェットブーツ" } ] }, { "Type": "treasure", "Floor": 3, "Detail": [ { "Condition": "ブルーナイトのどちらかを倒す", "Effect": "ミスしても残機が減らない(1回だけ)", "Memo": "正確な条件は、「ブルーナイトのうち、先にフロアに出現した方を倒す」", "Name": "ポーション・オブ・ヒーリング" } ] }, { "Type": "treasure", "Floor": 4, "Detail": [ { "Condition": "扉を通過する", "Effect": "フロア開始時、鍵がある方向を向くと音が鳴る", "Memo": "宝箱を出すより先に鍵を取ってしまうと出ない", "Name": "チャイム" } ] }, { "Type": "treasure", "Floor": 5, "Detail": [ { "Condition": "メイジの呪文を歩きながら盾で3回受ける", "Effect": "攻撃力UP", "Memo": "18階のドラゴンスレイヤーを取るのに必要", "Name": "ホワイトソード" } ] }, { "Type": "treasure", "Floor": 6, "Detail": [ { "Condition": "最上段に上がった後、下がる", "Effect": "10階までゴーストが見えるようになる", "Memo": "", "Name": "キャンドル" } ] }, { "Type": "treasure", "Floor": 7, "Detail": [ { "Condition": "カッパーマトックをなくす", "Effect": "宝箱を取る前に1回、取った後2回使える", "Memo": "最大で宝箱を取る前に4回、取った後に5回まで使えるが、Effectの回数より多く使うと1/3の確率で壊れる", "Name": "シルバーマトック" } ] }, { "Type": "treasure", "Floor": 8, "Detail": [ { "Condition": "ステート時点からX軸,Y軸共にずれた位置で剣を振る", "Effect": "体力が上がる(その階のみ)", "Memo": "", "Name": "ポーション・オブ・パワー" } ] }, { "Type": "treasure", "Floor": 9, "Detail": [ { "Condition": " 最上段の右から8列目と左から8列目の両地点を通過する。", "Effect": "体力が下がる(その階のみ)", "Memo": "", "Name": "ポーション・オブ・エナジー・ドレイン" } ] }, { "Type": "treasure", "Floor": 10, "Detail": [ { "Condition": "レッドスライムの放つ呪文を盾で受ける。", "Effect": "26階の宝箱(ハイパーガントレット)を取るのに必要。", "Memo": "レッドスライムの気分次第。", "Name": "ガントレット" } ] } ]