S3 Object Lambdaを試す
Posted On 2025-01-03
TerraformとAWSのS3 Object Lambdaを連携させることで、S3バケットのオブジェクトを動的に加工しながら取得する仕組みを構築します。JSONデータのnameキーをLambdaでマスクする例を示し、追加のデータ複製なしにセキュアなデータ取得を実現する手順を解説します。
目次
はじめに
- DVAやDEAの勉強していたときに、「PIIが入っているバケットに対して特定のキーをマスクして取得したいときは、S3 Object Lambdaが便利だよ」ってあったので試してみた
S3 Object Lambdaとは
S3 Object Lambda を使用したオブジェクトの変換より図を引用。
S3の前段にObject Lambdaアクセスポイントを作り、その背後にLambdaを動かして、S3の構造は維持しつつ追加のデータの複製なしで、オブジェクトの内容を動的に編集して取得するというもの。
作るもの
どの程度使えるのかよくわからないので、サクッとChatGPTにTerraformを生成させて作ってみる。以下のようなディレクトリ構造。
.
├── main.tf
└── s3_object_lambda.py
Terraformのコード
# 1. S3バケットの作成
resource "aws_s3_bucket" "my_bucket" {
bucket = "my-s3-object-lambda-bucket-123456" # ユニークな名前に変更してください
force_destroy = true
}
# 2. JSONファイルのアップロード
resource "aws_s3_object" "object1" {
bucket = aws_s3_bucket.my_bucket.id
key = "object1.json"
content_type = "application/json"
content = jsonencode({
name = "Alice"
age = 30
})
}
resource "aws_s3_object" "object2" {
bucket = aws_s3_bucket.my_bucket.id
key = "object2.json"
content_type = "application/json"
content = jsonencode({
name = "Bob"
age = 25
})
}
# 3. Lambda用のIAMロールの作成
resource "aws_iam_role" "lambda_role" {
name = "s3_object_lambda_role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "lambda.amazonaws.com"
}
}]
})
}
# 4. Lambda用のIAMポリシーの作成
resource "aws_iam_role_policy" "lambda_policy" {
name = "s3_object_lambda_policy"
role = aws_iam_role.lambda_role.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = [
"s3:GetObject",
"s3:ListBucket"
]
Effect = "Allow"
Resource = [
aws_s3_bucket.my_bucket.arn,
"${aws_s3_bucket.my_bucket.arn}/*"
]
},
{
Action = [
"s3-object-lambda:WriteGetObjectResponse"
]
Effect = "Allow"
Resource = "*"
},
{
Action = [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
]
Effect = "Allow"
Resource = "arn:aws:logs:*:*:*"
}
]
})
}
# 5. Lambda関数のZIPアーカイブを作成
data "archive_file" "lambda_zip" {
type = "zip"
source_file = "s3_object_lambda.py"
output_path = ".cache/s3_object_lambda.zip"
}
# 6. Lambda関数の作成
resource "aws_lambda_function" "mask_function" {
function_name = "s3-object-lambda-mask"
role = aws_iam_role.lambda_role.arn
handler = "s3_object_lambda.handler"
runtime = "python3.12"
filename = data.archive_file.lambda_zip.output_path
source_code_hash = filebase64sha256(data.archive_file.lambda_zip.output_path)
memory_size = 128
timeout = 30
}
# 7. サポート用のS3アクセスポイントの作成
resource "aws_s3_access_point" "supporting_ap" {
bucket = aws_s3_bucket.my_bucket.id
name = "supporting-ap"
}
# 8. S3 Object Lambda Access Pointの作成
resource "aws_s3control_object_lambda_access_point" "object_lambda_ap" {
name = "my-object-lambda-ap"
configuration {
supporting_access_point = aws_s3_access_point.supporting_ap.arn
transformation_configuration {
actions = ["GetObject"]
content_transformation {
aws_lambda {
function_arn = aws_lambda_function.mask_function.arn
}
}
}
}
}
# 9. Lambda関数をS3 Object Lambdaから呼び出すための権限を付与
resource "aws_lambda_permission" "allow_s3_object_lambda" {
statement_id = "AllowS3ObjectLambdaInvoke"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.mask_function.function_name
principal = "s3-object-lambda.amazonaws.com"
source_arn = aws_s3control_object_lambda_access_point.object_lambda_ap.arn
}
以下メモ
- Lambdaを作る部分は普通と一緒だが、追加に
s3-object-lambda:WriteGetObjectResponse
のポリシーが必要。これは変換したオブジェクトをS3に返す際に必要になる - S3用のアクセスポイント
aws_s3_access_point
とaws_s3control_object_lambda_access_point
を作る。本来必要なのはObject Lambdaアクセスポイントだが、これを作成するのにS3のアクセスポイントが必要なため。 - EventBridgeなどと同様に、S3 Object LambdaからLambdaが呼び出されることを許可する
Lambdaの画面から、設定→アクセス権限と見ると、「リソースベースのポリシーステートメント」に追加されている。
Lambda
import json
import urllib.request
import boto3
def handler(event, context):
s3 = boto3.client("s3")
# getObjectContext から必要な情報を取得する
request_route = event["getObjectContext"]["outputRoute"]
request_token = event["getObjectContext"]["outputToken"]
# getObjectContextから情報を取得
get_object_context = event['getObjectContext']
input_s3_url = get_object_context['inputS3Url']
# S3オブジェクトを取得
with urllib.request.urlopen(input_s3_url) as response:
data = response.read().decode('utf-8')
# JSONデータをロード
json_data = json.loads(data)
# 'name'をマスク
if 'name' in json_data:
json_data['name'] = '***MASKED***'
# JSONデータを文字列に変換し、バイナリに変換
json_bytes = json.dumps(json_data).encode("utf-8")
# s3.write_get_object_response でレスポンスを返す
s3.write_get_object_response(
Body=json_bytes,
RequestRoute=request_route,
RequestToken=request_token,
ContentType="application/json"
)
# handler の return はログ用でエラーにならないなら何でもOK
return {"statusCode": 200}
以下のような点が注意である。
- 変換後のオブジェクトを
s3.write_get_object_response
で返す必要がある。これがないとAPIを叩いたときにエラーになる- この際にRequestRouteとRequestTokenが必要で、
event["getObjectContext"]
から取得できる。 - 返すときはバイナリ
- この際にRequestRouteとRequestTokenが必要で、
- URLベースからアクセスしているが、ポリシーがなければエラーになるのでこのTerraformからは、S3バケットを外部公開しているわけではない
取得してみる
Object Lambdaのアクセスポイントから取得する必要がある。普通にS3のマネジメントコンソールからアクセスしたときに勝手に上書きしてくれる便利機能ではない。
ここからアクセスすると、以下のように名前をマスクされる(本来はここはBobになっている)
{"age": 25, "name": "***MASKED***"}
AWS CLIからaws cp ...
で便利に取得できる機能はなく、プログラムベースで取得するときは、低レベルのAPIを使う必要がある。以下のようなコマンドでダウンロードできる。
aws s3api get-object --bucket arn:aws:s3-object-lambda:<リージョン>:<アカウントID>:accesspoint/my-object-lambda-ap --key object1.json output.json
結果はoutput.jsonに以下のように記録される。
{"age": 30, "name": "***MASKED***"}
所感
- 思ったより地味な機能だなという印象
- 似たようなアクセスポイントがあってハマった。権限も知らんかった。
- S3で静的ページをホストしているときは便利かもしれない?
Shikoan's ML Blogの中の人が運営しているサークル「じゅ~しぃ~すくりぷと」の本のご案内
技術書コーナー
北海道の駅巡りコーナー