S3の紐付けURLを発行するLambda

## 🎯 目的

- S3にファイルをアップロード・取得するための **Presigned URL を Lambda 経由で動的に生成する API** を構築することが目的です。
- この仕組みを使えば、クライアントがS3に**直接アクセス**できるようになり、サーバー側でファイルを保持する必要がなくなります。

## 🔧 背景と要件

このAPIでは以下の要件を満たす必要があります：

- **外部からAPI Gateway経由でリクエスト**される
- アップロード後の **ファイル名はクエリパラメータで指定**、または自動生成
- Presigned URL（PUT）と、取得用URL（GET）を生成
- 必要な情報（バケット名など）は環境変数で管理

## ❗ 注意点・前提条件

このLambda関数をデプロイするにあたり、以下の点に注意する必要があります：

- 環境変数 `BUCKET_NAME` を必ず設定すること
- 必要な IAM ポリシー（`s3:PutObject`, `s3:GetObject`）をLambdaロールに付与
- LambdaはAPI Gatewayと連携して外部から呼び出される
- S3バケットとLambdaが同じリージョンにあることを推奨
- **S3に直接アップロードする場合、Lambda経由のBase64エンコードではなくPresigned URLを使う理由**
    - API Gateway経由でファイルを直接送信する場合、***サイズ上限（最大10MB）***が存在
    - 一方、Presigned URLであれば、**S3が許容する最大サイズ（5GB〜）までアップロード可能**
    - そのため、大きなファイルを扱うにはこの方法が不可欠

## ✅ 解決方法（処理の流れ）

- Lambdaハンドラーはクエリパラメータから `file_name` を取得
    - 無ければ `UUID + タイムスタンプ` で自動生成
- `boto3` を使って **Presigned URL（PUT/GET）** を生成
- `https://{bucket}.s3.{region}.amazonaws.com/...` 形式で **public URL** も構築
- それら3つのURLをJSONで返却

## 💡 工夫したこと

- Lambdaのデプロイ環境に依存せず柔軟に対応できるよう、バケット名やリージョンは**環境変数経由で取得**
- Presigned URLには有効期限（**3600秒 = 1時間**）を明示的に設定
- ファイル名がクエリで未指定の場合でも衝突しないよう **UUID＋タイムスタンプ** によるユニーク命名を導入
- エラーハンドリングを簡潔に整理（環境変数が無い場合とその他の例外を区別）

## 🧱 アーキテクチャ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4055742/d2770d45-6791-42a8-8be1-c5be2f8e9306.png)

- API Gateway で HTTP リクエストを受け付け
- Lambda 関数が起動し、Presigned URLを生成
- クライアントがそのURLを使ってS3に直接アップロード・取得

### コースコード

```python
import json
import boto3
import os
import uuid
from datetime import datetime

# S3クライアントを作成
s3 = boto3.client('s3')

def lambda_handler(event, context):
    try:
        # 環境変数からバケット名を取得
        bucket_name = os.environ['BUCKET_NAME']

        # クエリパラメータからファイル名を取得、無ければ自動生成
        query_params = event.get('queryStringParameters') or {}
        file_name = query_params.get('file_name', None)
        
        # file_nameがない場合は自動生成
        if not file_name:
            timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
            file_name = f"{timestamp}_{uuid.uuid4().hex}.jpg"

        # presigned URLを生成（PUTメソッド用、アップロード後にpublicアクセス可能なACLを付与）
        presigned_url = s3.generate_presigned_url(
            'put_object',
            Params={
                'Bucket': bucket_name,
                'Key': file_name
            },
            ExpiresIn=3600  # URLの有効期限（秒）＝1時間
        )

        # Presigned URLを生成（GETメソッド用、画像アクセス用）
        presigned_get_url = s3.generate_presigned_url(
            'get_object',
            Params={
                'Bucket': bucket_name,
                'Key': file_name
            },
            ExpiresIn=3600  # 1時間
        )

        # アップロード後のpublicアクセス用URLを生成（固定形式）
        region = os.environ.get("AWS_REGION", "us-east-1")
        public_url = f"https://{bucket_name}.s3.{region}.amazonaws.com/{file_name}"

        # presigned URLとpublic URLをレスポンスとして返却
        return {
            'statusCode': 200,
            'body': json.dumps({
                'presigned_put_url': presigned_url,
                'presigned_get_url': presigned_get_url,
                'public_url': public_url
            })
        }

    except KeyError as e:
        # 環境変数が不足している場合のエラーハンドリング
        return {
            'statusCode': 500,
            'body': json.dumps({'error': f'環境変数が不足しています: {str(e)}'})
        }

    except Exception as e:
        # その他の例外処理
        return {
            'statusCode': 500,
            'body': json.dumps({'error': str(e)})
        }

```

## 📌 まとめ

Presigned URLは、S3と外部クライアントをセキュアに接続するシンプルで強力な手段です。

今回のLambda実装により、以下のような利点が得られます：

- バックエンドサーバーを通さずにファイルを処理できるため、**スケーラブル**かつ**低コスト**
- アップロード対象や有効期限などを柔軟に制御可能
- Lambda + API Gateway構成で、**サーバーレスに外部公開**も可能