【AWS】Lambda ~ Python / S3トリガー ~

■ はじめに

https://dk521123.hatenablog.com/entry/2017/04/05/235618
https://dk521123.hatenablog.com/entry/2021/10/07/103317

の続き。

 (よくありがちな) AWS Lambdaで、
S3バケット内のファイルを監視し、
そのファイルを別の場所にデプロイする、、、みたいなことをやってみる

(本当は、Serverless Framework で実装したかったが
時間もないので、ひとまず、手動でデプロイする形で、、、)
 => (追記 2024/05/26)Serverless Framework v4 が、
  有償になったため、別フレームワークを考える

Serverless Framework以外のフレームワーク
https://dk521123.hatenablog.com/entry/2024/05/26/001612

目次

【1】今回やること
 1)その他事項(Version)
【2】Tips
 1)S3バケットおよびパスの取得
 2)メモ:Python3.12 上の注意点
【3】使用上の注意
 1)S3イベント再作成する場合
【4】構築手順例
 0)前提条件
 1)Lambda関数をAWS上に登録する
 2)トリガーの追加する
 3)【任意】環境変数の追加する
 4)処理したいプログラムを実装する
 5)動作確認
【5】サンプル

【1】今回やること

[1] S3バケットのファイルを監視し、ファイルが更新された場合にLamda起動
[2] そのファイルを、REST API経由でアップロードする

1)その他事項(Version)

* Python: Python3.12

【2】Tips

1)S3バケットおよびパスの取得

import urllib.parse

def lambda_handler(event, context):
    # ★引数 event から以下のように取得
    s3_bucket_name = event["Records"][0]["s3"]["bucket"]["name"]
    s3_key = urllib.parse.unquote_plus(event["Records"][0]["s3"]["object"]["key"], encoding="utf-8")
    logger.info(f"Event fired from s3 [s3://{s3_bucket_name}/{s3_key}]")

2)メモ:Python3.12 上の注意点

(AWS Lambda とは、直接関係ないが、作ってて築いたので、メモ)

* いくつかのライブラリが廃止されている
 => 例えば、以下の関連記事で扱った distutilsパッケージ とか。

Python ~ 基本編 / bool ~
https://dk521123.hatenablog.com/entry/2021/10/02/000000

【3】使用上の注意

1)S3イベント再作成する場合

間違ったなどでS3イベント再作成する場合、
エラー「Configuration is ambiguously defined」が表示される可能性がある。
なお、対象方法については、以下の関連記事を参照のこと

Lambda のトラブルシュート
https://dk521123.hatenablog.com/entry/2017/12/16/231714

【4】構築手順例

0)前提条件

* S3バケット自体は作成済
* AWS Lambda に使うロール/VPCは作成済

1)Lambda関数をAWS上に登録する

[1] AWSマネージメントコンソールにログインし、
 「Lambda」のページにアクセスする
[2] 画面右上の「Create function」ボタン押下
[3] 以下を入力し、画面右下の「Create function」ボタン押下
~~~~~~~
 * 「Author from scratch」を選択(デフォルトで選択されている)

Base information
 * Function name : 任意(今回は「demo-s3-trigger」)
 * Runtime : Python 3.12 (他にも「3.8」~「3.11」が選択可能)
 * Architecture : x86_64(デフォルトで選択されている)
~~~~~~~

※ 後は、既存のロールやVPCなどを選択する
※ 今回は、「Test Event登録およびTest 実行」は省略
 => 行いたい場合は、以下の関連記事を参照のこと

Lambda ~ Python / 入門編 ~
https://dk521123.hatenablog.com/entry/2021/10/07/103317

2)トリガーの追加する

「1)-[3]」の流れのまま...

[1] 「Add trigger」ボタン押下
[2] Add triggerの設定ページにおいて、以下を「s3」と入力すると
 「s3」アイコンがでてくるので選択
[3] 選択すると、以下の項目が現れるので、適宜入力し「Add」ボタン押下
 * Bucket: 対象のS3バケット名(ここでは「your-s3-bucket」)
 * Event types : 任意のイベント(ここでは、デフォルトの「All object create events」)
 * Prefix: 任意(ここでは「images/」)
 * Suffix: 任意(ここでは「.png」)
 * Recursive invocation: 内容(I acknowledge that...)を確認し、チェック

3)【任意】環境変数の追加する

[1] [Configuration]-[Environment variables]を選択
[2] 「Edit」ボタン押下
[3] 「Add environment variables」ボタン押下
[4] Key/Valueを入力し「Save」ボタン押下

 * Key: 任意 (今回は「TARGET_URL」,「DRY_RUN」)
 * Value: 任意(今回は「http://your-rest-api-host:9092」,「false」)

4)処理したいプログラムを実装する

[1] 以下「【4】サンプル」を参考に実装
[2] 「Deploy」ボタン押下

5)動作確認

[1] 該当S3バケットのパスにファイルを置く
[2] Lambda の画面で、
 [Monitor]-[View CloudWatch logs]を選択し、ログを確認できる

【5】サンプル

import os
import logging
import json
import urllib.parse
import boto3

logger = logging.getLogger()
logger.setLevel(logging.INFO)

logger.info("Loading function")

s3_client = boto3.client("s3")

def lambda_handler(event, context):
    # Step1: Get the object from the event and show its content type
    s3_bucket_name = event["Records"][0]["s3"]["bucket"]["name"]
    s3_key = urllib.parse.unquote_plus(event["Records"][0]["s3"]["object"]["key"], encoding="utf-8")
    logger.info(f"Event fired from s3 [s3://{s3_bucket_name}/{s3_key}]")

    try:
        # Step2: Get contents
        response = s3_client.get_object(Bucket=s3_bucket_name, Key=s3_key)
        target_content = response ["Body"].read()

        # Step3-1: Create request to upload a file
        url = os.getenv("TARGET_URL", "http://your-rest-api-host:9092")
        req = urllib.request.Request(
            url,
            target_content ,
            method="POST",
            headers={"Content-Type": "application/octet-stream"},
        )

        # Step3-2: Send the request and print response
        is_dry_run = os.getenv("DRY_RUN", "false").lower() == "true"
        if (is_dry_run ):
            logger.info("just dry run...")
        else:
            logger.info("Uploading...")
            with urllib.request.urlopen(req) as res:
                logger.info(json.loads(res.read()))
     
    except Exception as ex:
        logger.error(ex)
        raise ex

    logger.info("Done...")
    return {
        "statusCode": 200,
        "body": { "result": "The process is successful!!" }
    }

参考文献

よくありがちなので、ほぼそのまんまの例があった。

公式ドキュメント
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/with-s3-example.html
その他一般サイト
https://qiita.com/hp-Genqiita/items/676a830105cce4a82039

関連記事

Lambda ~ 基礎知識編 ~
https://dk521123.hatenablog.com/entry/2017/04/05/235618
Lambda ~ Python / 入門編 ~
https://dk521123.hatenablog.com/entry/2021/10/07/103317
Lambda ~ Python / 外部モジュール追加 ~
https://dk521123.hatenablog.com/entry/2024/05/25/005456
Lambda のトラブルシュート
https://dk521123.hatenablog.com/entry/2017/12/16/231714
Serverless Framework ~ 環境設定編 ~
https://dk521123.hatenablog.com/entry/2023/11/02/000200
Serverless Framework ~ 入門編 ~
https://dk521123.hatenablog.com/entry/2023/11/03/234825
Serverless Framework ~ offline ~
https://dk521123.hatenablog.com/entry/2023/11/09/004458
Serverless Framework以外のフレームワーク
https://dk521123.hatenablog.com/entry/2024/05/26/001612
Python ~ 基本編 / urllib ~
https://dk521123.hatenablog.com/entry/2022/08/05/000000
Python ~ Requestsライブラリ ~
https://dk521123.hatenablog.com/entry/2024/05/23/162229
Python ~ 基本編 / bool ~
https://dk521123.hatenablog.com/entry/2021/10/02/000000
Python ~ 基本編 / ファイル読込・書込 ~
https://dk521123.hatenablog.com/entry/2019/10/07/000000
Python ~ 基本編 / 正規表現
https://dk521123.hatenablog.com/entry/2019/09/01/000000
Terraform ~ AWS Lambda / あれこれ ~
https://dk521123.hatenablog.com/entry/2024/05/31/005406