クロスアカウントのS3へ署名付きURLでアップロードする

別のAWSアカウントに存在するS3に対するPUT用の署名付きURL(Presigned URL)を発行し、ファイルのアップロードを行う。その際に、オブジェクト所有権を引き継ぐ設定を行う。
2021.11.19

データアナリティクス事業本部、池田です。
クロスアカウントS3署名付きURL でファイルをアップロードする仕組みを作る必要があったのですが、 オブジェクト所有権の辺りで少し苦労したのでブログにします。

オブジェクト所有権については以下の弊社ブログが分かりやすかったです。

[アップデート] オブジェクト所有権でもう悩まない!S3 バケット所有者がアップロード時に自動的にオブジェクト所有権を引き継げるようになりました。

PUTでファイルをアップロードする

今回はAWS Lambda上のPython(boto3)で署名付きURLを発行します。 アップロードは何でも良いのですが、 curlコマンド で簡単に確認してみます。

↓Lambdaのコード(Python 3.9)はこんな感じです。

import boto3
from botocore.client import Config

def lambda_handler(event, context):
    S3_BUCKET_NAME = "{バケット名}"
    FILE_NAME = "{パスとファイル名}"

    s3 = boto3.client("s3", config=Config(signature_version="s3v4"))
    url = s3.generate_presigned_url(ClientMethod="put_object",
                                    Params={"Bucket": S3_BUCKET_NAME,
                                            "Key": FILE_NAME,
                                            "ACL": "bucket-owner-full-control"},
                                    ExpiresIn=3600,
                                    HttpMethod="PUT")
    return url

↑結果としてURLが取得できるので、
↓それを使ってファイルをアップロードします。

put_url='{取得したURL}'
target_file='{パスとアップロードするファイル名}'

curl -X PUT \
-H 'x-amz-acl:bucket-owner-full-control' \
--upload-file $target_file \
$put_url

Pythonの方の12行目で、ACLを指定してURLを発行しています。 ( generate_presigned_url()ドキュメント を見ても「Params (dict) -- The parameters normally passed to ClientMethod.」 としか書いていなかったので手探りの実装でした…)

アップロードの際はヘッダーに x-amz-acl を加えてリクエストを実行します。
ちなみにこのヘッダーが無いと以下のようなエラーになりました。
<Error><Code>SignatureDoesNotMatch</Code><Message>The request signature we calculated does not match the signature you provided. Check your key and signing method.</Message>…</Error>

POSTでファイルをアップロードする

generate_presigned_url() を使ったPUTでのアップロードはよくある実装かと思うのですが、 ファイルのアップロードはPOSTのメソッドでもできるようです。 ( generate_presigned_post()

↓こんな感じの実装になるようです。

import boto3
from botocore.client import Config

def lambda_handler(event, context):
    S3_BUCKET_NAME = "{バケット名}"
    FILE_NAME = "{パスとファイル名}"

    s3 = boto3.client("s3", config=Config(signature_version="s3v4"))
    res = s3.generate_presigned_post(Bucket=S3_BUCKET_NAME,
                                     Key=FILE_NAME,
                                     Fields={"acl": "bucket-owner-full-control"},
                                     Conditions=[
                                         {"acl": "bucket-owner-full-control"}
                                     ],
                                     ExpiresIn=3600)
    return res

戻り値としては、リクエストで使用するパラメータ(url・fields)が辞書形式で手に入ります。

↓そのパラメータを使用してリクエストを作成します。

curl -v -X POST \
-F acl={取得したfieldsを設定} \
-F key={取得したfieldsを設定} \
-F x-amz-algorithm={取得したfieldsを設定} \
-F x-amz-credential={取得したfieldsを設定} \
-F x-amz-date={取得したfieldsを設定} \
-F x-amz-security-token={取得したfieldsを設定} \
-F policy={取得したfieldsを設定} \
-F x-amz-signature={取得したfieldsを設定} \
-F file=@{アップロードするファイル名} \
{取得したurlを設定}

(fileは一番最後に設定しないとエラーになりました。)

おわりに

いろいろ試行錯誤して、最終的にアップロードできました。
初めはPOSTでないとできないかと思っていたのですが、PUTでもできてしまったので両方掲載しました。 (けっきょくPOSTのURLの利用ケースがどういう場合なのか分からないまま終わってしまいました…)

関連情報/参考にさせていただいたページ