週刊SleepNel新聞

SleepNel所属のぽうひろが日々の個人開発で気になったことを綴ります。

AWS S3 Presigned URLについてまとめてみる(Python, Kotlinの実装例つき)

みなさん、こんにちは。ぽうひろです。

この記事は、クロスマート・テックアドカレ5日目の記事です。
qiita.com

前日は、私の【エンジニアポエム】目指すテックリード像には近づけたのか?
という記事でした。まだみてない方はチェックしてみてください。一応、会社でちゃんと振り返ってえらいと褒められました。
sleepnel.hatenablog.com

今回は技術の話をします。みなさんご存知、AWS S3ですが、私も一応仕事で使った経験はありました。
が、この会社にきて初めて凄腕SREの方にPresigned URL というものを教えていただき、サービスに導入しました。
めちゃめちゃいい仕組みです。
もしかしたらまだ知らないエンジニアの方もいらっしゃるかもしれないので、今回こちらについて紹介したいと思います。


S3 Presigned URLとは

Presigned URLは日本語で言うと「事前署名付きURL」と言う翻訳になります。
S3にファイルをアップロードしたりダウンロードしたりするには、権限(許可みたいなもの)が必要になるのですが、それが付与され、かつこの場所に置いてね〜とS3から置き場所の事前予約を取ったURLというイメージになります。
システムでよく使う方法としては、画面のアップロード画面からファイルをバイナリとしてバックエンドAPIへPOSTし、バックエンドAPI内でその受け取ったバイナリをS3へアップロード(S3へのアクセスするための権限は設定されている)する といった構図をよく目にすると思います。

従来の方法を図にするとこんな感じですね

ユーザがアップロードしたファイルは最終的にサーバサイドがS3にアップロードしている

Presigned URLを使ってアップロードする方法だと、下図のように、一旦フロントからサーバサイドにPresignedURLの発行を依頼します。
(今回はサーバサイド経由でS3にPresigned URL発行を依頼していますが、フロント側でもフロント用のS3 SDKと認証ファイルを保持していればS3にPresigned URL発行を依頼することもできます。)
サーバサイドはS3からPresignedURLを発行しフロントに返します。フロントはそのURLに対してファイルアップロードするという手順になります。
URLというくらいなので、S3のファイルアップロードを受け付ける専用のURLになっています。(ダウンロードについてもPresigned URLが発行可能です。)

弊社ではS3ダイレクトアップロードと呼んでいます

Presigned URLを使わない場合となにが違うの? と言いますと、実際にS3にアップロードするのが「サーバーサイド」になるのか「フロント(Web画面か)」の違いになります。
じゃあユーザにとってはそんなに変わらないよね?と思うかもしれませんが、ユーザだけでなくシステム提供側にもメリットがある方式です。ここからは、Presigned URLのメリットを紹介していきます。

Presigned URLを使うメリット

処理をシンプル化

Presigned URLを利用すれば、フロント(ブラウザやアプリ)から直接S3にデータをアップロードまたはダウンロードでき、バックエンドサーバーを経由する必要がなくなります。S3とのやり取りはクライアント側で完結できるため、システム全体の処理がシンプルになります。

バックエンドサーバーリソース消費削減

バックエンドを経由せずにデータを直接S3とやり取りできるため、バックエンドサーバーリソースの消費が抑えられます。

ネットワークトラフィック量の軽減

従来の方法だと、バックエンドサーバーがクライアントからのリクエストを受け取り、S3バケットとの間でデータ転送を行う場合、サーバーのネットワーク帯域幅を多く消費します。Presigned URLを利用すれば、クライアントが直接S3にデータをアップロード/ダウンロードするため、バックエンド側にトラフィックが発生しません。

スケールしやすいアーキテクチャ:

大量のファイルを扱うアプリケーション(動画ストリーミング、画像アップロード/ダウンロードなど)では、サーバーが中継を行うとトラフィック増加に伴いリソースのスケールが必要になります。Presigned URLを使えば、S3自体がスケールするため、インフラの運用コストが削減されます。

柔軟なセキュリティ制御

Presigned URLは特定の操作(GET、PUTなど)に限定的なアクセス権を付与できます。そのため、IAMポリシーやバケットポリシーで広範囲にアクセスを許可する必要がありません。
また、Presigned URLには有効期限が設定できるので、期間限定一時的な共有リンクを作るみたいなこともでき、柔軟なセキュリティ制御を行うこともできます。

高速なデータ転送

エンドユーザーのクライアントはPresigned URLを使用して、直接S3バケットと通信します。この通信はAWSの最適化されたグローバルインフラを活用して行われるため、データの転送速度が速いです。(アクセルモードというpresignedURLも作ることができます!)
AWS内での通信は非常に高速かつ安定しているため、エンドユーザーもその恩恵を受けられます。また、地理的に分散したエッジロケーションから効率的にデータを配信可能です。

実装の仕方

たくさんメリットがあることを感じていただいたと思いますので、具体的な実装方法を軽く紹介したいと思います。

Pythonの場合

PythonでS3を扱う場合、AWSから出ている便利なSDK boto3というライブラリを利用します。

1. boto3がインストールされていない場合は以下でインストールします:

pip install boto3

2. AWS認証情報の設定:

環境変数、~/.aws/credentialsファイル、またはIAMロールを使用してAWS認証情報を設定します。
必要な権限:
s3:PutObject(アップロード用)
s3:GetObject(ダウンロード用)

3. boto3でPresigned URLを生成

import boto3
from botocore.exceptions import NoCredentialsError

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

def generate_presigned_url(bucket_name, object_key, method, expiration=3600):
    """
    S3のPresigned URLを生成する関数
    :param bucket_name: S3バケット名
    :param object_key: オブジェクトのキー(ファイルパス)
    :param method: 操作('get_object'または'put_object')
    :param expiration: 有効期限(秒)
    :return: Presigned URL(文字列)
    """
    try:
        presigned_url = s3_client.generate_presigned_url(
            ClientMethod=method,
            Params={'Bucket': bucket_name, 'Key': object_key},
            ExpiresIn=expiration
        )
        return presigned_url
    except NoCredentialsError:
        print("AWS認証情報が見つかりません。")
        return None

# 使用例
bucket_name = "your-bucket-name"
object_key = "your-file-path/your-file.txt"

# アップロード用URLを生成
upload_url = generate_presigned_url(bucket_name, object_key, 'put_object')
print(f"Upload URL: {upload_url}")

# ダウンロード用URLを生成
download_url = generate_presigned_url(bucket_name, object_key, 'get_object')
print(f"Download URL: {download_url}")

Kotlin(Java)の場合

1. Gradleに依存関係を追加 プロジェクトのbuild.gradle.ktsに以下を追加します。

implementation("software.amazon.awssdk:s3:2.20.0")

※バージョンは最新を確認してください。

2. AWS認証情報の設定
Pythonの時と同じです。
環境変数、~/.aws/credentialsファイル、またはIAMロールを使用します。

3. Presigned URLを生成
以下のコードで、Presigned URLを生成します。

import software.amazon.awssdk.auth.credentials.ProfileCredentialsProvider
import software.amazon.awssdk.regions.Region
import software.amazon.awssdk.services.s3.S3Client
import software.amazon.awssdk.services.s3.presigner.S3Presigner
import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest
import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest
import software.amazon.awssdk.services.s3.model.GetObjectRequest
import software.amazon.awssdk.services.s3.model.PutObjectRequest
import java.time.Duration

fun generatePresignedUrl(bucketName: String, objectKey: String, method: String, expirationMinutes: Long): String {
    // S3Presignerを作成
    val presigner = S3Presigner.builder()
        .region(Region.US_EAST_1) // リージョンを指定
        .credentialsProvider(ProfileCredentialsProvider.create()) // 認証情報プロバイダー
        .build()

    return when (method.lowercase()) {
        "get" -> {
            // ダウンロード用Presigned URLを生成
            val getRequest = GetObjectRequest.builder()
                .bucket(bucketName)
                .key(objectKey)
                .build()

            val presignRequest = GetObjectPresignRequest.builder()
                .signatureDuration(Duration.ofMinutes(expirationMinutes))
                .getObjectRequest(getRequest)
                .build()

            presigner.presignGetObject(presignRequest).url().toString()
        }
        "put" -> {
            // アップロード用Presigned URLを生成
            val putRequest = PutObjectRequest.builder()
                .bucket(bucketName)
                .key(objectKey)
                .build()

            val presignRequest = PutObjectPresignRequest.builder()
                .signatureDuration(Duration.ofMinutes(expirationMinutes))
                .putObjectRequest(putRequest)
                .build()

            presigner.presignPutObject(presignRequest).url().toString()
        }
        else -> throw IllegalArgumentException("Invalid method: $method")
    }
}

fun main() {
    val bucketName = "your-bucket-name"
    val objectKey = "your-folder-path/your-file.txt"

    // アップロード用Presigned URLを生成
    val uploadUrl = generatePresignedUrl(bucketName, objectKey, "put", 60)
    println("Upload URL: $uploadUrl")

    // ダウンロード用Presigned URLを生成
    val downloadUrl = generatePresignedUrl(bucketName, objectKey, "get", 60)
    println("Download URL: $downloadUrl")
}

まとめ

S3を使ったシステムを構築することはよくあることだと思いますが、Presigned URLを使うことで、さらにシンプルで効率的な構成が可能になります。
セキュリティを確保しつつ、直接S3とやり取りできるため、大容量データもスムーズに扱え、エンドユーザーにも開発側にも嬉しい仕組みです。
システムの負担を軽減しコスト削減にもつながるので、特に多くのデータやユーザーを扱うサービスには非常に有益です。
まだご存知なかった方は、ぜひこの機会に導入を検討してみてください。きっとシステム全体のクオリティを向上させるはずです。

最後までお読みいただき、ありがとうございました!!

次のアドベントカレンダーの記事は、UIデザイナーおしまさんの「PM向けにfigmaの「オートレイアウト」と仲良くなるための勉強会をした話」という記事です。

見逃すな〜!!

qiita.com