S3に直接アップロード可能なPre-Signed URLをLambdaで作ってみる

S3にはPre-Signed URLという機能があり、S3のオブジェクトへの限定的なアクセスを提供することが可能です。
弊社ブログでもいくつか紹介されています。

あらかじめ限定的なアクセスに絞っておくことで、一時的に認証なしで直接S3へファイルをアップロードすることができるところが便利そうです。

このPre-Signed URLをLambdaで作ってみたのでご紹介いたします。

AWS構成概要

今回、こんな構成で作っています。

ユーザーのアクセスをきっかけにLambdaが起動し、STSを利用して一時的なCredential情報を発行します。

一時的なCredential情報を用いて、ファイルアップロード用のS3のPre-Signed URLを発行しています。

発行したPre-Signed URLをユーザーに返し、これを利用してユーザーがS3へ直接ファイルをアップロードするといった仕組みです。

PythonでS3のPre-Signed URLを発行する

以下は、python3のLambdaでPre-Signed URLを発行するサンプルコードです。
動作させるためには以下の環境変数を適切に設定する必要があります。

  • REGION_NAME: リージョン名を設定
  • S3_BUCKET_NAME: ファイルをアップロードしたいS3バケット名を設定
  • IAM_ROLE_ARN: AssumeRoleで上記S3の操作を許可したIAM RoleのARNを設定
  • DURATION_SECONDS: Pre-Signed URLの有効期限を秒で設定
# -- coding: utf-8 --

import boto3
from boto3.session import Session
from botocore.client import Config
import uuid
import os

REGION_NAME = os.environ['REGION_NAME']
S3_BUCKET_NAME = os.environ['S3_BUCKET_NAME']
IAM_ROLE_ARN = os.environ['IAM_ROLE_ARN']
DURATION_SECONDS = int(os.environ['DURATION_SECONDS'])

def lambda_handler(event, context):
    print('Event: {}'.format(event))
    
    FILE_NAME = str(uuid.uuid4())

    print(FILE_NAME)
    
    client = boto3.client('sts')

    # AssumeRoleで一時的なCredential情報を発行
    response = client.assume_role(RoleArn=IAM_ROLE_ARN,
                                  RoleSessionName=FILE_NAME,
                                  DurationSeconds=DURATION_SECONDS)

    print(response)
    
    session = Session(aws_access_key_id=response['Credentials']['AccessKeyId'],
                      aws_secret_access_key=response['Credentials']['SecretAccessKey'],
                      aws_session_token=response['Credentials']['SessionToken'],
                      region_name=REGION_NAME)

    s3 = session.client('s3', config=Config(signature_version='s3v4'))
    
    url = s3.generate_presigned_url(ClientMethod = 'put_object', 
                                    Params = {'Bucket' : S3_BUCKET_NAME, 'Key' : FILE_NAME}, 
                                    ExpiresIn = DURATION_SECONDS, 
                                    HttpMethod = 'PUT')

    print(url)

    return {
       'statusCode': 200,
       'statusDescription': '200 OK',
       'isBase64Encoded': False,
       'headers': {
           'Content-Type': 'text/html; charset=utf-8'
        },
        'body': '{}\n'.format(url)
    }

(※2019/05/31追記)

以下記事を参考に、AWS 署名バージョン 4 を使用してPre-Signed URLを生成するよう、サンプルコードを修正しました。

S3 バケットの署名付き URL が、指定した有効期限より前に失効する


このコードをLambdaでテスト実行すると下図のようにURLが取得できます。

ALBをフロントに置けば、curlコマンドでアクセスしてこんな感じでURLが取得できます。

S3のPre-Signed URLで画像ファイルをアップロードする

S3のPre-Signed URLはPUTでアップロードができるので、curlコマンドを使って画像をアップロードしてみます。
こんな感じでコマンドを打ちます。

$ url=$(curl my-alb-xxxxxxxxxx.us-west-2.elb.amazonaws.com)

$ curl -X PUT --upload-file my-image.png $url

そしてS3を確認すると、ファイルがアップロードできていることがわかります。

S3からファイルをダウンロードして、拡張子を元々のpngに変更して、プレビューで確認してみると、アップロードした画像が問題なく開けています。

(※2019/05/31追記)

有効期限が過ぎた後、同じ様に画像をcurlコマンドでアップロードしようとしてみます。

$ curl -X PUT --upload-file my-image.png $url
<?xml version="1.0" encoding="UTF-8"?>
<Error><Code>AccessDenied</Code><Message>Request has expired</Message><X-Amz-Expires>300</X-Amz-Expires><Expires>2019-05-31T08:11:29Z</Expires><ServerTime>2019-05-31T08:19:53Z</ServerTime><RequestId>XXXXXXXXXXXXXXXX</RequestId><HostId>XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX</HostId></Error>% 

AccessDeniedでエラーになって、有効期限が切れた後はアップロードができないことがわかります。

おわりに

S3のPre-Signed URLを使用すると、S3へ直接ファイルをアップロードすることができるので、いろいろできそうです。
S3へのファイルアップロードは、認証を考えるとサーバーサイドからのアップロードをする必要があると思っていましたが、 一時的に権限を付与してクライアントサイドから直接ファイルをアップロードするといったこともできそうです。
状況に合わせて使い分けていきたいと思います。