署名付きURLでS3にファイルをアップロード/ダウンロードする

署名付きURLでS3にファイルをアップロード/ダウンロードする

署名付きURLでS3にファイルをアップロード/ダウンロードする

S3が提供する機能の1つに、署名付きURLがあります。

署名付き URL を使用して、オプションでオブジェクトを共有したり、顧客/ユーザーが AWS セキュリティ認証情報またはアクセス許可なしでオブジェクトをバケットにアップロードしたりできます。

署名付き URL の使用

今回は署名付きURLを使用して、ファイルをアップロード/ダウンロードする方法を確認します。

構築する環境

Diagram of Upload/Download files to/from S3 with Presigned URL.

S3バケットを作成します。
ここにオブジェクトをアップロード/ダウンロードします。

2つのLambda関数を作成します。
どちらの関数も署名付きURLを作成する働きを行います。
一方はオブジェクトのアップロード用、もう一方はダウンロード用です。
なお関数のランタイム環境はPython3.8とします。

CloudFormationテンプレートファイル

上記の構成をCloudFormationで構築します。
以下のURLにCloudFormationテンプレートを配置しています。

https://github.com/awstut-an-r/awstut-fa/tree/main/106

テンプレートファイルのポイント解説

S3バケット

Resources:
  Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Ref Prefix
      AccessControl: Private
Code language: YAML (yaml)

署名付きURLを作成する上で、S3バケット側は特別な設定は不要です。

ACL設定を「Private」とすることで、原則、他者によるバケットへのアクセスを拒否します。

Lambda関数1

ファイルアップロード用の署名付きURLを作成するための関数です。

Resources:
  Function1:
    Type: AWS::Lambda::Function
    Properties:
      Environment:
        Variables:
          BUCKET_NAME: !Ref S3BucketName
          EXPIRE: !Ref S3PresignedUrlExpire
          OBJECT_KEY: !Ref S3ObjectKey
          REGION: !Ref AWS::Region
      Code:
        ZipFile: |
          import boto3
          import os

          bucket_name = os.environ['BUCKET_NAME']
          expire = os.environ['EXPIRE']
          object_key = os.environ['OBJECT_KEY']
          region = os.environ['REGION']

          client_method = 'put_object'
          http_method = 'PUT'
          params = {
            'Bucket': bucket_name,
            'Key': object_key
          }

          s3_client = boto3.client('s3', region_name=region)

          def lambda_handler(event, context):
            url = s3_client.generate_presigned_url(
              ClientMethod=client_method,
              HttpMethod=http_method,
              Params=params,
              ExpiresIn=expire
            )
            return url
      FunctionName: !Sub "${Prefix}-function1"
      Handler: !Ref Handler
      Runtime: !Ref Runtime
      Role: !GetAtt FunctionRole1.Arn
      Timeout: !Ref Timeout
Code language: YAML (yaml)

今回はインライン形式でLambda関数を定義します。

CloudFormationを使用して、Lambda関数を作成する方法については、以下のページをご確認ください。

https://awstut.com/2022/02/02/3-patterns-for-creating-lambda-with-cloudformation

署名付きURLの作成は、generate_presigned_urlメソッドを使用します。

引数を確認します。
アップロード用のURLを作成する場合、ClientMethodの値は「put_object」を、HttpMethodの値は「PUT」を、Paramsにアップロード先のS3バケットやオブジェクト名を指定します。
ExpiresInに期限切れまでの時間を秒単位で指定します。
今回は3600秒(1時間)とします。

作成するURLの設定をまとめますと、S3バケット「fa-106」に、オブジェクト名を「sample.txt」として、ファイルをアップロードするためのURLを、1時間限定で作成するということになります。

以下が本関数用のIAMロールです。

Resources:
  FunctionRole1:
    Type: AWS::IAM::Role
    DeletionPolicy: Delete
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Action: sts:AssumeRole
            Principal:
              Service:
                - lambda.amazonaws.com
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: !Sub "${Prefix}-S3PutObjectPolicy"
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - s3:PutObject
                Resource:
                  - !Sub "arn:aws:s3:::${S3BucketName}/*"
Code language: YAML (yaml)

署名付きURLを作成する上で、ポイントは権限です。
AWS公式では、以下の通り言及されています。

オブジェクトをアップロードするための署名付き URL を受け取った場合、オブジェクトをアップロードすることができるのは、署名付き URL の作成者がそのオブジェクトをアップロードするのに必要な権限を持っている場合のみです。

オブジェクトをアップロードするための署名付き URL の生成

つまりIAMロールという形で、Lambda関数にアップロード用の権限を与える必要があります。

今回は対象のS3バケットに対して、「s3:PutObject」アクションを許可するように設定します。

Lambda関数2

ファイルダウンロード用の署名付きURLを作成するための関数です。

Resources:
  Function2:
    Type: AWS::Lambda::Function
    Properties:
      Environment:
        Variables:
          BUCKET_NAME: !Ref S3BucketName
          EXPIRE: !Ref S3PresignedUrlExpire
          OBJECT_KEY: !Ref S3ObjectKey
          REGION: !Ref AWS::Region
      Code:
        ZipFile: |
          import boto3
          import os

          bucket_name = os.environ['BUCKET_NAME']
          expire = os.environ['EXPIRE']
          object_key = os.environ['OBJECT_KEY']
          region = os.environ['REGION']

          client_method = 'get_object'
          http_method = 'GET'
          params = {
            'Bucket': bucket_name,
            'Key': object_key
          }

          s3_client = boto3.client('s3', region_name=region)

          def lambda_handler(event, context):
            url = s3_client.generate_presigned_url(
              ClientMethod=client_method,
              HttpMethod=http_method,
              Params=params,
              ExpiresIn=expire
            )
            return url
      FunctionName: !Sub "${Prefix}-function2"
      Handler: !Ref Handler
      Runtime: !Ref Runtime
      Role: !GetAtt FunctionRole2.Arn
      Timeout: !Ref Timeout
Code language: YAML (yaml)

設定は先述の関数とほとんど同様です。
ClientMethodに「get_object」を、HttpMethodに「GET」とすることで、ダウンロード用のURLを作成します。

作成するURLの設定をまとめますと、S3バケット「fa-106」に配置されているオブジェクト名「sample.txt」を、ダウンロードするためのURLを、1時間限定で作成するということになります。

以下が本関数用のIAMロールです。

  FunctionRole2:
    Type: AWS::IAM::Role
    DeletionPolicy: Delete
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Action: sts:AssumeRole
            Principal:
              Service:
                - lambda.amazonaws.com
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: !Sub "${Prefix}-S3GetObjectPolicy"
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - s3:GetObject
                Resource:
                  - !Sub "arn:aws:s3:::${S3BucketName}/*"
Code language: YAML (yaml)

今度はアップロード用の権限を、Lambda関数に与える必要があります。
S3バケットに対して、「s3:GetObject」アクションを許可するように設定します。

環境構築

CloudFormationを使用して、本環境を構築し、実際の挙動を確認します。

CloudFormationスタックを作成し、スタック内のリソースを確認する

CloudFormationスタックを作成します。
スタックの作成および各スタックの確認方法については、以下のページをご確認ください。

https://awstut.com/2021/12/02/cloudformation-nested-stacks

各スタックのリソースを確認した結果、今回作成された主要リソースの情報は以下の通りです。

  • S3バケット:fa-106
  • Lambda関数1(アップロード用URL作成):fa-106-function1
  • Lambda関数2(ダウンロード用URL作成):fa-106-function2

AWS Management Consoleから各リソースを確認します。

Lambda関数を確認します。

Detail of Lambda 1.
Detail of Lambda 2.

確かに2つの関数が作成されています。
アップロード/ダウンロード用の署名付きURLを作成します。

動作確認

準備が整いましたので、署名付きURLを使って、S3バケットにファイルをアップロード/ダウンロードします。

アップロード用署名付きURL

まずアップロード用の署名付きURLを確認します。

関数1を実行します。

Detail of Lambda 3.

関数を実行した結果、署名付きURLが返ってきました。

curlコマンドを使って、このURLに対してファイルのアップロードを試みます。

$ echo "hogehoge" > hogehoge.txt

$ curl -X PUT --upload-file hogehoge.txt "https://fa-106.s3.amazonaws.com/sample.txt?AWSAccessKeyId=[AWSAccessKeyId]&Signature=[Signature]&x-amz-security-token=[x-amz-security-token]"Code language: PHP (php)

S3バケットを確認します。

Detail of S3 1.

hogehoge.txtというファイルが、sample.txtという名前に変更された上で、オブジェクトとして設置されました。

このようにアップロード用署名付きURLを使用することによって、S3バケットにオブジェクトをアップロードすることができます。

ダウンロード用署名付きURL

次にダウンロード用の署名付きURLを確認します。

関数2を実行します。

Detail of Lambda 4.

関数を実行した結果、署名付きURLが返ってきました。

curlコマンドを使って、このURLに対してファイルのダウンロードを試みます。

$ curl "https://fa-106.s3.amazonaws.com/sample.txt?AWSAccessKeyId=[AWSAccessKeyId]&Signature=[Signature]&x-amz-security-token=[x-amz-security-token]"
hogehogeCode language: JavaScript (javascript)

sample.txtのファイルの中身が出力されました。

このようにダウンロード用署名付きURLを使用することによって、S3バケットからオブジェクトをダウンロードすることができます。

(参考)エラーメッセージ

リダイレクト

S3バケットが作成されて間もない場合、署名付きURLにアクセスした時に、以下のようなメッセージが表示される場合があります。

<?xml version="1.0" encoding="UTF-8"?>
<Error>
  <Code>TemporaryRedirect</Code>
  <Message>Please re-send this request to the specified temporary endpoint. Continue to use the original request endpoint for future requests.</Message>
  <Endpoint>fa-106.s3-ap-northeast-1.amazonaws.com</Endpoint>
  <Bucket>fa-106</Bucket>
</Error>
Code language: HTML, XML (xml)

この理由について、AWS公式では以下の通り説明しています。

Amazon S3 バケットを作成した後、バケット名がすべての AWS リージョンに伝達するまでに最長で 24 時間かかる場合があります。その間に、お使いのバケットと同じリージョンにないリージョンのエンドポイントにリクエストすると、307 Temporary Redirect レスポンスが返される場合があります。

Amazon S3 から HTTP 307 Temporary Redirect レスポンスが返されるのはなぜですか?

期限切れ

署名付きURLを作成時に設定した期限を超過した場合、以下のようなメッセージが表示されます。

<?xml version="1.0" encoding="UTF-8"?>
<Error>
  <Code>AccessDenied</Code>
  <Message>Request has expired</Message>
  <Expires>2022-12-21T12:30:33Z</Expires>
  <ServerTime>2022-12-22T12:55:32Z</ServerTime>
</Error>
Code language: HTML, XML (xml)

まとめ

署名付きURLを使用して、ファイルをアップロード/ダウンロードする方法を確認しました。