S3メタデータからAthena UDFを使用して署名付きURLを生成してみた

S3メタデータからAthena UDFを使用して署名付きURLを生成してみた

S3 MetadataからAthenaでLambda UDFを使用して署名付きURLの生成まで試してみました。メタデータ付与について、ユーザー定義メタデータとオブジェクトタグの比較もしています。
Clock Icon2025.04.04

こんにちは、データ事業本部の渡部です。

今回はS3バケットに保存した画像ファイルのメタデータが集積されたS3メタデータテーブルから、AthenaとUDF(ユーザー定義関数)を使用して署名付きURLをクエリ結果として出力してみます。

S3メタデータとは

S3メタデータはS3オブジェクトのメタデータを自動的にテーブルへ収集する、re:Invent2024で発表された新機能となります。
画像などの膨大になりがちな非構造データ群から欲しいファイルを、S3メタデータテーブルから検索して、容易に特定することを可能とします。

取得できるメタデータとしては、自動的に付与されるシステム定義メタデータとオブジェクトをS3に入れる時に任意に設定できるユーザー定義メタデータの2種類があります。
今回はユーザー定義メタデータでデータをフィルタ検索して、S3メタデータテーブルにあるバケットとオブジェクトキーから、署名付きURLを取得してみます。

システム定義メタデータユーザー定義メタデータについては以下をご参照ください。
https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingMetadata.html#SysMetadata

なおS3メタデータと同じような機能として、S3インベントリがありますが、こちらは日次or週次でのメタデータ一覧化、かつメタデータについてもユーザー定義メタデータは一覧に載りません。
対してS3メタデータはニアリアルタイムでメタデータが更新され、取得可能なメタデータの種類も多いため、より素早く具体的な検索が可能です。

今回の構成

やりたいことのイメージとしては以下となります。

貼り付けた画像_2025_04_03_8_06

やってみる

S3の準備

まずはS3バケット・S3テーブルバケット・S3メタデータ統合を作成・設定します。

# S3バケットの作成
aws s3 mb s3://cm-watanabe-metadata \
    --region us-east-1 

# S3メタデータテーブル用のS3テーブルバケットの作成
aws s3tables create-table-bucket \
    --name cm-watanabe-table-bucket \
    --region us-east-1 \

# S3メタデータ統合の設定
aws s3api create-bucket-metadata-table-configuration \
    --bucket cm-watanabe-metadata \
    --region us-east-1 \
    --metadata-table-configuration '{
        "S3TablesDestination": {
            "TableBucketArn": "arn:aws:s3tables:us-east-1:<account-id>:bucket/cm-watanabe-table-bucket",
            "TableName": "cm_watanabe_meta_table"
        }
    }'    

今回はAWS CLIで作成しましたが、もちろんマネジメントコンソールでも作成可能です。
その手順については以下に詳しく載っていますので、ご確認ください。

https://dev.classmethod.jp/articles/amazon-s3-metadata-open-to-close/

S3バケットにメタデータとともにファイル配置

この時点ではS3メタデータテーブルにクエリをしても何も表示されません。バケットにファイルを格納していないので、当たり前ですね。
というわけで、ファイルをローカルPCからS3バケットへ配置します。

何のファイルを配置すべきか小一時間悩んだのですが、弊社公式マスコットキャラクターである「くらにゃん」を3枚配置しました。
その際、ユーザー定義メタデータとしてはlocationというKeyを用意し、それぞれの画像に{hibiya,osaka,fukuoka}というValueを同時に渡しました。

# ファイル配置
aws s3 cp ./clanyan1.jpg s3://cm-watanabe-metadata \
    --metadata '{"location":"hibiya"}' \

aws s3 cp ./clanyan2.jpg s3://cm-watanabe-metadata \
    --metadata '{"location":"osaka"}' \

aws s3 cp ./clanyan3.jpg s3://cm-watanabe-metadata \
    --metadata '{"location":"fukuoka"}' \

ところでユーザー定義メタデータとオブジェクトタグどちらにメタデータを付与すればいいの?

少し話は逸れるのですが、
上記ではユーザー定義メタデータに対してメタデータを付与しましたが、S3のオブジェクトにはタグも付与することが可能です。
こちらもKey-Valueで値を渡せて、かつメタデータテーブルから取得することができるのですが、一体どちらをメタデータとして使用すればいいのでしょうか?

以下に比較をしてみました。

評価観点 ユーザー定義メタデータ オブジェクトタグ
メタデータ更新 アップロード後はメタデータ更新できない
変更するにはオブジェクトを上書きする必要がある
オブジェクト上書きすることなく、タグを柔軟に更新、追加、削除できる
サイズ/数の制限 keyとvalueのペアの合計サイズに制限あり(約2KB) オブジェクトに対しては最大10個のタグが付与可能。各タグのkeyは ≤ 128文字、valueは ≤ 256文字
文字の制限 特別な規定はない keyとvalueは、アルファベット(a-z, A-Z)、数字(0-9)、_./=+-:@のみを含む(正規表現:^([\p{L}\p{Z}\p{N}_.:/=+\-]*)$
=+&?、およびASCII以外の文字はサポートされていない
JSONのサポート HTTPヘッダーの制限を超えない場合、JSON文字列を保存できる JSONの直接保存はサポートされていない
ユースケース 検索用メタデータの管理:オブジェクトにメタデータを付与して、バージョン、作成日、および作成者を特定する。 ・ライフサイクル管理:オブジェクトタグをつけることで、オブジェクト単位でのライフサイクル管理が可能
・コスト管理:オブジェクトをプロジェクトやユーザーグループに分類するためにタグを使用し、コストの追跡と分析を容易にする。
※オブジェクトタグはAWSのコスト分析ツールで簡単に使用され、詳細なレポートを作成する。

https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/UsingMetadata.html

ユースケースにまとめを載せているのですが、私の個人的な意見だと、
オブジェクトタグは全社的に中央で管理したいライフサイクル管理だったり、コスト管理に使用すべきで、
今回のような検索用メタデータとしてはユーザー定義メタデータを使用すべきかなと思います。

オブジェクトタグにしかできないことをオブジェクトタグにやらせる、なんていったってオブジェクトタグは10個しか使用できませんので。という理由です。

Athena UDFたるLambdaのデプロイ

閑話休題。
Athenaのユーザー定義関数(UDF)として使用するLambdaをデプロイします。
今回はSAMを準備したので、そちらを以下に共有します。

SAMのディレクトリ構成はざっくりと以下です。

.
├── README.md
├── athena_udf_lambda
│   ├── __init__.py
│   ├── app.py
│   └── requirements.txt
├── samconfig.toml
├── template.yaml

template.yaml

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  athena-udf

Globals:
  Function:
    Timeout: 300
Parameters:
  PythonVersion:
    Type: String
    Description: Python version
    AllowedValues:
      - "3.9"
      - "3.10"
      - "3.11"
      - "3.12"
    ConstraintDescription: Must be one of the allowed values.

Resources:
  AthenaUDFFunction:
    Type: AWS::Serverless::Function 
    Properties:
      CodeUri: athena_udf_lambda/
      FunctionName: athena_s3_presigned_url_udf
      Handler: app.lambda_handler
      Runtime: !Sub "python${PythonVersion}"
      MemorySize: 256
      Architectures:
        - x86_64
      Policies:
        - AmazonS3ReadOnlyAccess
Outputs:
  AthenaUDFFunction:
    Description: "Athena UDF Function ARN"
    Value: !GetAtt AthenaUDFFunction.Arn

samconfig.toml

# More information about the configuration file can be found here:
# https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-config.html
version = 0.1

[default.global.parameters]
stack_name = "athena-udf"

[default.build.parameters]
cached = true
parallel = true

[default.validate.parameters]
lint = true

[default.deploy.parameters]
capabilities = "CAPABILITY_IAM"
confirm_changeset = true
resolve_s3 = true
s3_prefix = "athena-udf"
region = "us-east-1"
image_repositories = []

[default.package.parameters]
resolve_s3 = true

[default.sync.parameters]
watch = true

[default.local_start_api.parameters]
warm_containers = "EAGER"

[default.local_start_lambda.parameters]
warm_containers = "EAGER"

app.py

import boto3
import athena_udf
from botocore.exceptions import ClientError

# S3クライアントを初期化してAmazon S3とやり取りします
s3_client = boto3.client('s3')

class PresignedUrlUDF(athena_udf.BaseAthenaUDF):
    """
    PresignedUrlUDFクラスはBaseAthenaUDFから継承され、AWS Athenaのクエリを処理し、
    S3オブジェクトに対する事前署名付きURLを返します。

    主なメソッド:
        - handle_athena_record: Athenaクエリの入力を受け取り、事前署名付きURLを生成して返します。
    """

    @staticmethod
    def handle_athena_record(input_schema, output_schema, arguments):
        """
        Athena UDFのレコードを処理し、S3オブジェクトの事前署名付きURLを生成します。

        引数:
            input_schema (list): UDFの入力スキーマ(このロジックでは使用されません)。
            output_schema (list): UDFの出力スキーマ(このロジックでは使用されません)。
            arguments (list): UDFに渡される引数のリスト。引数は以下の通り:
                - arguments[0]: bucket_name (str) - S3バケットの名前。
                - arguments[1]: object_key (str) - バケット内のオブジェクトのキー。
                - arguments[2] (オプション): expiration (int) - URLの有効期限(秒単位)。デフォルトは3600秒(1時間)。

        戻り値:
            str: S3オブジェクトの事前署名付きURL。

        例外:
            ValueError: bucket_name または object_key が不足している場合に発生します。
        """

        # argumentsからbucket_nameとobject_keyを取得
        bucket_name = arguments[0]
        object_key = arguments[1]
        expiration = int(arguments[2]) if len(
            arguments) > 2 else 3600  # デフォルトは1時間

        # bucket_nameとobject_keyが空でないことを確認
        if not bucket_name or not object_key:
            raise ValueError(
                "Both bucket_name and object_key must be provided.")

        # presigned URLを生成
        presigned_url = generate_presigned_url(
            bucket_name, object_key, expiration)

        return presigned_url

def generate_presigned_url(bucket_name, object_key, expiration):
    """
    S3オブジェクトのダウンロード用に事前署名付きURLを生成します。

    引数:
        bucket_name (str): S3バケットの名前。
        object_key (str): バケット内のオブジェクトのキー。
        expiration (int): URLの有効期限(秒単位)。

    戻り値:
        str: S3オブジェクトの事前署名付きURL。

    例外:
        ValueError: 事前署名付きURLの生成中にエラーが発生した場合。
    """
    try:
        # S3からオブジェクトをダウンロードするためのpresigned URLを生成
        url = s3_client.generate_presigned_url(
            'get_object',
            Params={'Bucket': bucket_name, 'Key': object_key},
            ExpiresIn=expiration
        )
        return url
    except ClientError as e:
        raise ValueError(f"Error generating presigned URL: {str(e)}")

# UDFの`lambda_handler`関数にlambda_handlerを割り当て
lambda_handler = PresignedUrlUDF().lambda_handler

requirements.txt

athena_udf

SAMのビルド・デプロイをしました。

sam build --parameter-overrides PythonVersion=3.11
sam deploy --parameter-overrides PythonVersion=3.11

Lake Formationでのアクセス制御

メタデータテーブルが格納されるS3 TablesはLake Formationでのアクセス許可をしなければ、アクセスできないため、アクセス許可をします。
Lake FormationのData PermissionでCatalog・Database・Tableに対してアクセス許可設定をしました。

貼り付けた画像_2025_04_04_8_02

Athenaでクエリ

まずはメタデータテーブルに対して、全件SELECTをします。

貼り付けた画像_2025_04_04_8_07

色々ファイルを何回か放り込んだ後なので、レコードが多くなってしまっていますが、ここではclanyan.{1,2,3}.jpgがメタデータとして出力されていることを見てください。
以下は抜粋結果です。user_metadataに拠点のメタデータが格納されています。

# bucket key user_metadata
3 cm-watanabe-metadata clanyan1.jpg {location=hibiya}
4 cm-watanabe-metadata clanyan3.jpg {location=fukuoka}
7 cm-watanabe-metadata clanyan2.jpg {location=osaka}

メタデータテーブルのテーブルスキーマは以下をご参考ください。
https://docs.aws.amazon.com/AmazonS3/latest/userguide/metadata-tables-schema.html

続けて、location = hibiyaのレコードだけ抽出してみました。

SELECT * FROM "cm_watanabe_meta_table" WHERE user_metadata['location']='hibiya'

貼り付けた画像_2025_04_04_8_24

ここから本題のUDFを使用してAthenaで署名付きURLを取得します。
SQLにUDFをかませて返り値とともに出力します。
以下のSQL、上から3行が関数の宣言で、4行以降がSELECT文となっています。

USING EXTERNAL FUNCTION s3_presigned_url(bucket VARCHAR, key VARCHAR)
RETURNS VARCHAR
LAMBDA 'arn:aws:lambda:us-east-1:<account-id>:function:athena_s3_presigned_url_udf'
SELECT 
    bucket,
    key,
    s3_presigned_url(bucket, key) as presigned_url
FROM "aws_s3_metadata"."cm_watanabe_meta_table"
WHERE user_metadata['location'] = 'hibiya'

署名付きURLが取得できました。

貼り付けた画像_2025_04_04_10_02

署名付きURLをブラウザで開いてみると、無事くらにゃん1が表示されました。

貼り付けた画像_2025_04_04_9_35

さいごに

S3メタデータを使用して、オブジェクトの検索から署名付きURLの発行までしてみました。
このクエリ結果をもとに、より検索からデータのアクセスまで短時間で可能となります。

注意点としては署名付きURLの期限は払い出した元のクレデンシャルの有効期限に依存するということです。
今回の署名付きURLを払い出したLambdaのIAM Roleのクレデンシャルはデフォルトの1時間のため、1時間が経つとアクセス不可能となります。
もっと長い有効期限を持たせたい場合は、クレデンシャルの期限を伸ばすか(最長12時間)、アクセスキー・シークレットアクセスキーを用いた払い出しにするか(最長7日)を検討ください。

それでは、ありがとうございました。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.