[初心者]LambdaでS3にファイルをアップロードした時に処理させる[Python]

全くLambda使ったことない方向けでS3とLambdaを使って超簡単な処理の書き方と設定の仕方を画像付きで書きます。

初心者向けですが、Lambdaって何？というと、雑に一言でいうとWeb上でちょっとした処理をさせたいとかいう時に個人レベルならタダで簡単にEC2とか立てないでも使える便利サービスくらいに思っておくとよいかと。
初めは難しそうに聞こえますが、簡単な処理だけならLambdaだけで十分というか、こっちの方が良いということも多いはずですので触ってみても面白いと思います

## 何をするのか？
画像をアップロードされた時に、サイズを小さくするとか、ファイル形式を変えるとか分析に使えるようにしたい、ファイルをアップロードしたときにtxtデータとかだったら整形して特定の形式に変換したい、通知送りたい、などなど色々させたい処理があると思います。

今回はS3に画像ファイルをアップロードされた時に特定の処理をさせるLambda関数を作ってみます。

Lambdaで使用するのはPythonですがNodeでもRubyでもJavaでも読み替えられるレベルです。どうしてもわからなかったら、ChatGPTにでも書き換えてもらってください。

注意：画像をつけますが、AWSはUIがよく変わるので完全には一致しなくなることが多いです。

無料期間なら無料だと思います。
ただ、Lambdaは無料期間すぎても相当使わないと有料にはならないと思いますが、大量に処理させたり、長時間処理させるとコストかかるかもしれません。

### メモ
年に数回使うことあるけど、その度どうするんだっけ？となるから書きました。
作りながらメモのように書いていって最後に補足して作成したので抜けとかあるかもしれない。あしからず


## S3のBucket作成
バケットを作成で作ってください。

![スクリーンショット 2025-02-07 19.24.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260345/1feee725-d16a-fca2-cff2-3895280e6e2d.png)

バケット名はユニークでないといけないのでどの人ともかぶらなそうな名前つけてください。
名前以外は何もいじらないで作成で大丈夫です。公開するバケットなら後で設定変えてください。今回はサンプルなのでこのままやります。

このバケットにファイルをアップロードすることにします。

## Lambda関数作成
関数を作成を押して関数を作成
![スクリーンショット 2025-02-07 19.26.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260345/ba3d7540-3e70-7635-e1c3-7296e10c4b3b.png)

バージョンなどは好みで変更してください。
![スクリーンショット 2025-02-07 19.41.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260345/060afb01-be97-3f96-3d71-3aa6551b6364.png)

アーキテクチャは変更する必要ないです。ただ、ライブラリインストールする時にここ間違うとビルド失敗するとかあるかもしれません。とりあえず変更しないでいいです。


作成したらこんな画面になると思います。Lambdaで処理させたいコードを書きます。
![スクリーンショット 2025-02-07 19.44.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260345/3da2ef70-746b-22c8-04bb-c9132c0093a6.png)


### Lambda 関数画面からトリガーを追加する方法
アップロードされた時に発火するトリガーを設定します。S3からもできるのですが、Lambdaで設定します。


トリガー追加してください。
![スクリーンショット 2025-02-07 19.50.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260345/98142da4-707b-bc15-0e75-91f1a324b12d.png)

S3を選択して最初に作ったバケットを選択。
イベントタイプはサンプルなので全てのオブジェクト作成イベントにしました。
POST, PUT, DELETEとか絞った方がいいですが。
![スクリーンショット 2025-02-07 19.51.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260345/c9498e68-5f7a-eeb2-a2ae-5c89a3ca1657.png)

プレフィックスとサフィックスがあります。
これは便利で特定の名前とかディレクトリのみ発火させることができます。

今回の場合だと特定の拡張子だけ処理させたいとかに使えるのですが、今回は練習なのでLambdaの処理の中で見ることにします。

最後の再起呼び出しの脅しにチェック入れてOKです。今回ファイルは作らないので大丈夫です。

:::note warn
この脅しは何いっているかというと、ファイルがアップロードされたらLambdaでファイルを生成するみたいなLambdaを作ったとすると、設定によっては、Lambdaが一生止まらないで発火し続けるからコストがやばいことなるよ〜ということです。
ファイルを生成するようなコードを書く時は気をつけましょう。
:::

こんな感じになったらOK
![スクリーンショット 2025-02-07 22.47.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260345/2edb474a-d6bd-ef51-0686-eff88442a5c5.png)

## Lambda関数を修正して
ファイルアップロードした時に発火する処理を書きます。
とりあえずprintだけ追加してこんな感じにしました。
ファイルをアップロードするとcloud watchにログが表示されるはずです。

```func.py
import json

def lambda_handler(event, context):
    print('ファイルアップロードしました')
    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }
```

デプロイボタンがあるので押します。

![スクリーンショット 2025-02-07 23.08.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260345/972ad15e-2e5f-55fd-a8d0-e0af3342f94c.png)

### S3にファイルアップロード
ファイルをアップロードしてください。
![スクリーンショット 2025-02-07 23.12.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260345/d9c8f73c-9e13-3c3a-f894-740224020809.png)

適当に生成した猫の画像をアップロードしました。中身に意味はないです。
![スクリーンショット 2025-02-07 23.14.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260345/851a259c-ef4a-5a53-e1f1-40db79b87410.png)

すでにCloudWatchにログが保存される設定になっているはずなのでLambdaのモニタリング→CloudWatch ログ みたいなのを押してください。
![スクリーンショット 2025-02-07 23.16.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260345/e0328ab8-0a46-03dc-ebad-6dc7a8fbb370.png)

最近のデータがあると思います。
![スクリーンショット 2025-02-07 23.16.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260345/7983cfae-4ccf-cf98-f634-18b6c666e2f7.png)
開いてみると`print`で書いたファイルアップロードしましたが表示されています。
![スクリーンショット 2025-02-07 23.19.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260345/b892728e-20b8-8c83-9a46-86ad7aae274f.png)

もう完了ですが、

## コードを少し修正

```S3ImageUploadHandler.py
import json

def lambda_handler(event, context):
    # 受け取ったイベント全体を出力（デバッグ用）
    print("Received event: " + json.dumps(event))
    
    # イベント内のレコードをループ処理
    for record in event.get('Records', []):
        # イベントが S3 からのものであることを確認
        if record.get('eventSource') == "aws:s3":
            bucket = record['s3']['bucket']['name']
            key = record['s3']['object']['key']
            
            # ファイル名が画像ファイルかどうか（例: jpg, jpeg, png, gif）をチェック
            if key.lower().endswith(('.jpg', '.jpeg', '.png', '.gif')):
                print(f"画像ファイルがアップロードされました: Bucket={bucket}, Key={key}")
            else:
                print(f"アップロードされたファイルは画像ではありません: Bucket={bucket}, Key={key}")
    
    # 正常終了を返す
    return {
        'statusCode': 200,
        'body': json.dumps('Lambda function executed successfully!')
    }
```

コードを変更したらデプロイをまた押してください。

内容的はファイルの拡張子見てるだけです。
画像ファイルと画像ファイル以外をアップロードしてCloudWatchに表示されるログが変わるの確認してみてください。


sample.pdfというファイルをアップロードした場合はCloudWatchに以下のような表示がされると思います。

```
アップロードされたファイルは画像ではありません: Bucket=sample-lambda-function-123, Key=sample.pdf
```


### 軽くコードについて
handlerの引数ですが、`event`はLambda 関数をトリガーしたイベントに関する詳細な情報を保持しています。
S3、SNS、API Gateway など、イベントの種類に応じて構造が異なる。今回の場合だと`event['file_key']`とか`event['bucket_name']`などS3のアップロードされたことによって起こったイベントの内容が入っています。

`context`は関数実行時の環境情報やメタデータを提供しています。実行時間の管理やログ出力、トラブルシューティングに役立つ情報が含まれています。

このサンプル程度のレベルでは使わないので気になった方は公式のドキュメント読んでください。


https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/python-handler.html

例えば、画像ファイルの大きさを圧縮してS3の特定の場所に別名で保存するとかにイベントの中身から画像を取得して圧縮、その後アップロードするようなコードがかけます

## 注意
格安だといっても超大量にリクエストしたり、重い処理をLambdaにさせるべきではないです。やる人いないとは思いますが、pandasなど使ってゴリゴリ分析させるとかさせない方が無難です。


## サンプルコード
検証中に使ったコード貼ります。

### S3からファイルを取得
boto3はデフォルトで使える。AWSのサービスを使うのに使う。
もし使う場合は、S3からの取得、配置、削除には権限を付与しないとできないので後で示すポシリーをつけてください。

```sample.py
import boto3
s3_client = boto3.client('s3')

def lambda_handler(event, context):
    bucket_name = 'sample-lambda-function-123'
    key_name = 'sample.jpeg'
    response = s3_client.get_object(Bucket=bucket_name, Key=key_name)
    # こらへんに何か変換処理入れる

    return {
        'statusCode': 200,
    }
```

### ファイルをアップロード
ファイルをアップロードさせる。
注意⚠️：何も制約やディレクトリも絞らないで、トリガーで同じバケットに生成するコード書くと無限にsample.txtを生成し続けることなると思うので注意。

- 発火させるバケットと違うバケットにファイルを生成させる
- ディレクトリ切る。トリガーの条件をprefixで対象を絞る
- suffixで拡張子やファイル名を絞る。コード上でも絞っても良い

```sample2.py
def lambda_handler(event, context):
    bucket_name = 'sample-s3-hogehoge'
    key_name = 'sample.txt'
    file_contents = '123456789あいうえお'
    response = s3_client.put_object(Body=file_contents, Bucket=bucket_name, Key=key_name)

    return {
        'statusCode': 200,
    }
```

画像をアップロードする時は、こんな感じでbyteにしてアップロードできます。


```
    # 画像をupload
    s3_client.put_object(
        Bucket=bucket_name,
        Key=new_key,
        Body=image_bytes
    )
```

### S3からファイルを取得して何か処理をしてS3にアップロードする
ファイルを取得してから、何か処理をしてから他のバケットにアップロードさせる。

- バケットを2つ用意
    - ファイルアップロード用のバケット
    - ファイル出力用のバケット
- ファイルアップロード用のバケットにファイルをアップロードすると以下のLambdaをトリガーさせる想定

```sample3.py
def lambda_handler(event, context):
    get_bucket_name = event['Records'][0]['s3']['bucket']['name']
    get_key_name = event['Records'][0]['s3']['object']['key']
    print(get_bucket_name)
    print(get_key_name)

    response = s3_client.get_object(Bucket=get_bucket_name, Key=get_key_name)
    base_name, _ = os.path.splitext(get_key_name)
    new_key = f'{base_name}_2.jpeg'
    print(f'before: {get_key_name} -> after: {new_key}')
    body: bytes = response['Body'].read()
    # バイトデータを読み込み
    image_bytes = func_hoge(body, 'jpeg')  # 変換処理など何か処理をする

    put_bucket_name = 'sample-s3-hogehoge'

    # 画像をupload
    s3_client.put_object(
        Bucket=put_bucket_name,
        Key=new_key,
        Body=image_bytes
    )

    return {
        'statusCode': 200,
    }
```

func_hoge()を作って画像の変換コードなど書いてやるといいと思います。
再度書きますが、トリガー付きで同じバケットに出力する時は気をつけてください。

### ポリシーをアタッチ
S3からファイルを取得したり、アップロードしたりするのには権限が必要です。
本当は権限は絞らないといけないのですが、サンプルなのでS3のフルアクセスをつけます

ポリシーをアタッチ
検索で「S3」などと打てば候補出るので`AmazonS3FullAccess`をつけます。
![スクリーンショット 2025-02-08 2.39.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260345/8257f06a-a9bc-8d14-754c-67af079bcbd1.png)

このポリシーがないとファイル取得やファイルアップロードが失敗すると思います。
![スクリーンショット 2025-02-08 2.42.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260345/3df14c73-3a85-d826-d04a-8356236e5f12.png)


## コードの手動実行方法
画面の横にテストボタンがあります。eventなどを使わない処理ならこのテストボタンでOK
テストタブの中にある「テスト」ボタンと一緒です。そちらでは、引数として受け取るeventの中身をシミュレートできるので、毎回ファイルをアップロードする必要なくなるので使ってみてください。

![スクリーンショット 2025-02-08 10.28.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260345/4dd9c0ea-f948-b2cc-9d42-89a75f4239fe.png)