S3へのアップロード、Object上書き防止の仕組み作りが簡単になった（conditional writesの話）

# 概要
8月20日に、以下のタイトルでAWSから発表がありました。
**Amazon S3 now supports conditional writes**

https://aws.amazon.com/jp/about-aws/whats-new/2024/08/amazon-s3-conditional-writes/?utm_source=tldrdevops

今回の機能ですが、
オブジェクト作成前に同一オブジェクトの存在を確認し、存在する場合にはアップロードをさせないためのパラメータを、APIリクエストで設定できるようになった、という話です。

つまり、**オブジェクトの上書き防止の仕組みをこちらで用意しなくてよい**ということです！

**これまで**
1. `XXXX`というオブジェクトの存在確認のためのAPIリクエストを実行
2. 存在判定を実施
3. 存在しなければ書き込みAPI実行

**今後**
1. 書き込みAPI実行時のパラメータ追加のみ

### 対象となるAPI（2つ）
- PutObject（今回はこっちで動作確認を進めます）
- CompleteMultipartUpload

そしてこの機能は、AWS GovCloud (US) リージョンやAWS China リージョンを含むすべてのAWSリージョンで**追加料金なしで利用できる**とのことです。

ドキュメントを確認しながら、実際に動作を見ていきます！

### S3のドキュメントを見てみよう
https://docs.aws.amazon.com/AmazonS3/latest/userguide/conditional-requests.html

##### conditional writesを使うために
> **With conditional writes you can use additional headers to your write requests in order to add preconditions to your S3 operation.** This can prevent overwrites of existing data. Conditional writes will validate there is no existing object with the same key name already in your bucket.
To perform conditional writes you must have the s3:PutObject permission. This enables the caller to check for the presence of objects in the bucket. You may use conditional writes with presigned URLs with the AWS SDKs.

以下2つの作業だけやっておけばOKみたいです。
-  APIリクエスト時、conditional writesを適用するパラメーターを追加
- 操作対象のユーザに`s3:PutObject`の権限を付与

##### SDK、CLIどちらでも対応可能
コマンドでも出来るのはありがたい方もいるかもしれません。以下のように、`--if-none-match`というオプションで条件を設定すればよさそうです。
```
aws s3api put-object --bucket amzn-s3-demo-bucket --key dir-1/my_images.tar.bz2 --body my_images.tar.bz2 --if-none-match "*"
```
SDKの場合は、PutObjectのリクエストでパラメータを追加します。

https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObject.html

> Uploads the object only if the object key name does not already exist in the bucket specified. Otherwise, Amazon S3 returns a 412 Precondition Failed error.
If a conflicting operation occurs during the upload S3 returns a 409 ConditionalRequestConflict response. On a 409 failure you should retry the upload.
Expects the '*' (asterisk) character.

オブジェクト名が既に存在する状態でアップロードを試みた場合、HTTPコード：**412**を返すとのことです！

### 試してみた
対象のアカウントに対する権限付与については、説明は省きます。
##### CLIで確認
1. AWSのCLIを最新版に更新
    > $ aws --version
    aws-cli/2.17.36 Python/3.11.9 Windows/10 exe/AMD64

2. S3バケットへのアップロード（1回目）
    ```sh
    aws s3api put-object --bucket xxxxx --key sample.txt --body ./sample.txt --if-none-match "*"
    ```
    > {
        "ETag": "\"ab56d4d93b40713acc5af89285d4b786\"",
        "ServerSideEncryption": "AES256"
    }

    アップロードしました。
    ![](https://storage.googleapis.com/zenn-user-upload/9bff3f50e2e7-20240823.png)

3. S3バケットへのアップロード（2回目）
    コマンドは1回目と同じです。
    > An error occurred (PreconditionFailed) when calling the PutObject operation: At least one of the pre-conditions you specified did not hold
    
    Errorが発生しました！

   **`PreconditionFailed`と記載されているので、412エラーが発生したことになります。挙動としては想定通りです！**

### SDK(Python)で確認
1. `if-none-match`の設定方法を[公式ドキュメント](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3/client/put_object.html)で確認
2. コード作成
    ```python
    import boto3
    import traceback
    
    s3 = boto3.client('s3')
    
    # 必要な情報を設定
    bucket_name = 'xxxxx'
    key_name = 'sample2.txt'
    file_path = './sample2.txt'
    
    # ファイルを開いて、S3にアップロード
    with open(file_path, 'rb') as file_data:
        try:
            s3.put_object(Bucket=bucket_name, Key=key_name, Body=file_data, IfNoneMatch='*')
            print(f"File {file_path} has been uploaded to {bucket_name}/{key_name}")
        except:
            traceback.print_exc()
    ```
3. S3バケットへのアップロード（1回目）
    ```
    python main.py
    ```
    > File ./sample2.txt has been uploaded to xxxxx/sample2.txt

    ![](https://storage.googleapis.com/zenn-user-upload/1073fc152b7b-20240823.png)

4. S3バケットへのアップロード（2回目）
    再度プログラムを実行しました。
    > Traceback (most recent call last):
        raise error_class(parsed_response, operation_name)
    botocore.exceptions.ClientError: An error occurred (PreconditionFailed) when calling the PutObject operation: At least one of the pre-conditions you specified did not hold
    
    こちらも、**`PreconditionFailed`と記載されていたので、挙動としては想定通りです！**

### まとめ
簡単ではありますが、S3の新しい機能、conditional writesについてまとめてみました。

APIの引数を追加するだけで、「存在判定&重複時はエラーを返す」まで処理できるのは助かりますね。
今後S3のアップデート処理を実装する場合、今回のアップデートによって以下が期待できそうです。
- 実装がスリムに
- APIリクエスト数削減による処理時間短縮（大幅に、ではないと思いますが）