Amazon S3 conditional writesを試してみた

# 背景・目的
最近、下記のリリースが発表されたので、条件付き書き込みを簡単に試してみました。

https://aws.amazon.com/about-aws/whats-new/2024/08/amazon-s3-conditional-writes/


# まとめ
下記に特徴をまとめます

| 特徴| 説明
|:--|:--|
|概要|・Condition requestを行うとS3オペレーションに前提条件を追加できる<BR>・Read、Writeリクエストにヘッダーを追加し、条件が満たされない場合、失敗する
|料金|条件付きリクエストには、追加料金はかからない<BR>・失敗したリクエストを含めて該当するリクエストに対して既存の料金がかかる|
|条件付き読み込み|条件付き読み取りを使用すると、 読み取りリクエストに追加のヘッダーを使用して、S3オペレーションに前提条件を追加できる。<BR>前提条件に合わないしない場合は、失敗する|
|条件付き読み取りが使用できるAPI|・GetObject<BR>・HeadObject<BR>・CopyObject|
|GetObjectで使用できるヘッダー|・If-Match<BR>・If-Modified-Since<BR>・If-None-Match<BR>・If-Unmodified-Since|
|HeadObjectで使用できるヘッダー|同上|
|CopyObjectで使用できるヘッダー|・x-amz-copy-source-if-match<BR>・x-amz-copy-source-if-modified-since<BR>・x-amz-copy-source-if-none-match<BR>・x-amz-copy-source-if-unmodified-since|
|条件付き書き込み|条件付き書き込みを使用すると、書き込みリクエストに、追加のヘッダーを使用して、S3オペレーションに前提条件を追加できる
|条件追記書き込みの前提|・条件付き書き込みを実行するには、s3:PutObject権限が必要<BR>条件付き書き込みを使用するには、HTTPS(TLS)か、AWS 署名バージョン4のリクエストに署名が必要|
|条件付き書き込みが使用できるAPI|・PutObject<BR>・CompleteMultipartUpload|
|PutObjectで使用できるヘッダー|If-None-Match|
|CompleteMultipartUploadで使用できるヘッダー|同上|


# 概要
下記の記事を基に整理します。

https://docs.aws.amazon.com/AmazonS3/latest/userguide/conditional-requests.html

## 条件付きリクエスト
> Conditional requests allow you to add preconditions to your S3 operation. When using conditional requests you add headers to your READ and WRITE requests. These headers specify conditions that, if not met, will result in the S3 operation failing.

> There is no additional charge for conditional requests. You are only charged existing rates for the applicable requests, including for failed requests. For information about Amazon S3 features and pricing, see Amazon S3 pricing.


- Condition requestを行うとS3オペレーションに前提条件を追加できる
- Read、Writeリクエストにヘッダーを追加する
- 条件が満たされない場合、失敗する
- 条件付きリクエストには、追加料金はかからない
- 失敗したリクエストを含めて該当するリクエストに対して既存の料金がかかる

### Conditional reads
> With conditional reads you can use additional headers to your read requests in order to add preconditions to your S3 operation. If these preconditions are not met the request will fail.

> The following S3 APIs support using conditional reads:
> - GetObject
> - HeadObject
> - CopyObject

- 条件付き読み取りを使用すると、 読み取りリクエストに追加のヘッダーを使用して、S3オペレーションに前提条件を追加できる
- 前提条件に合わないしない場合は、失敗する
- 条件付き読み取りをサポートしているAPIは、下記の通り
    - GetObject
    - HeadObject
    - CopyObject


> You can use the following headers to return an object dependent on the entity tag (ETag) or last modified date. For more information about object metadata such as ETags and Last-Modified see, System-defined object metadata.

- 下記のヘッダーを使用して、Entity Tag（ETag）か最終変更日に依存するエンティティを使用することができる

> GetObject
> - If-Match — Return the object only if its ETag matches the one provided.
> - If-Modified-Since — Return the object only if it has been modified since the time specified.
> - If-None-Match — Return the object only if its ETag does not matches the one provided.
> - If-Unmodified-Since — Return the object only if it has not been modified since the time specified.


> HeadObject
> - If-Match — Return the object only if its ETag matches the one provided.
> - If-Modified-Since — Return the object only if it has been modified since the time specified.
> - If-None-Match — Return the object only if its ETag does not matches the one provided.
> - If-Unmodified-Since — Return the object only if it has not been modified since the time specified.


- GetObjectとHeadObjectは、下記のヘッダーが利用できる

|ヘッダー |説明 |
|:--|:--|
|If-Match|指定したETagがマッチしたオブジェクトのみ返す|
|If-Modified-Since|指定された時間以降に変更されたオブジェクトのみ返す|
|If-None-Match|指定したETagにマッチしないオブジェクトを返す|
|If-Unmodified-Since|指定した時間以降に変更されていないオブジェクトを返す|


> CopyObject
> - x-amz-copy-source-if-match — Copies the source object only if its ETag matches the one provided.
> - x-amz-copy-source-if-modified-since — Copies the source object only if it has been modified since the time specified.
> - x-amz-copy-source-if-none-match — Copies the source object only if its ETag does not matches the one provided.
> - x-amz-copy-source-if-unmodified-since — Copies the source object only if it has not been modified since the time specified.

- CopyObjectは、下記のヘッダーが利用できる

|ヘッダー |説明 |
|:--|:--|
|x-amz-copy-source-if-match|指定したETagにマッチしたオブジェクトのみコピーする|
|x-amz-copy-source-if-modified-since|指定された時間以降に変更されたオブジェクトのみ返す|
|x-amz-copy-source-if-none-match|指定したETagにマッチしないオブジェクトを返す|
|x-amz-copy-source-if-unmodified-since|指定した時間以降に変更されていないオブジェクトを返す|

### 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.

- 条件付き書き込みを使用すると、書き込みリクエストに、追加のヘッダーを使用して、S3オペレーションに前提条件を追加できる

> 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.

- 条件付き書き込みを実行するには、s3:PutObject権限が必要。これにより呼び出し元はオブジェクトの存在を確認できる
- AWS SDKでは署名付きURLを使用した条件付き書き込みが使用できる

> Note
To use conditional writes, you must make the requests over HTTPS (TLS) or use AWS Signature Version 4 to sign the request.

- 条件付き書き込みを使用するには、HTTPS(TLS)か、AWS 署名バージョン4のリクエストに署名が必要


> The following S3 APIs support using conditional writes:
> - PutObject
> - CompleteMultipartUpload

- 条件付き書き込みでは、下記のS3 APIがサポートされている
    - PutObject
    - CompleteMultipartUpload

> PutObject
If-None-Match — Upload the object only if no existing object with the same key name already exists in the specified bucket. Expects the * (asterisk) value.

- PutObject では、下記のヘッダーが利用できる

|ヘッダー |説明 |
|:--|:--|
|If-None-Match|指定したバケットに同一キーのオブジェクトがない場合のみ、アップロードできる<BR>アスタリスクの指定が必要|

> CompleteMultipartUpload
If-None-Match — Complete the upload only if no existing object with the same key name already exists in the specified bucket. Expects the * (asterisk) value.

- CompleteMultipartUploadでは、下記のヘッダーが利用できる
|ヘッダー |説明 |
|:--|:--|
|If-None-Match|指定したバケットに同一キーのオブジェクトがない場合のみ、アップロードを完了できる<BR>アスタリスクの指定が必要|

# 実践
条件付き書き込みを簡単に試してみます。

## 前提
事前に、S3バケットとファイルを作成しておきます

### バケットの作成
1. AWSにサインインし、S3に移動します
1. S3バケットを作成します

### ファイルの作成
1. 適当にファイルを作成します
    ```
    $ date > date1.txt
    $ cat date1.txt 
    2024年 8月24日 土曜日 10時35分41秒 JST
    $
    ```

## 通常の書き込み
まずは、通常の書き込みで挙動を確認しておきます。

1. バケットにファイルが存在しないことを事前に確認します。ありません
    ```
    $ BucketName=XXXXX
    $ aws s3 ls ${BucketName}
    $
    ```
1. まずは、最初のファイルをアップロードします
    ```
    $ aws s3api put-object --bucket ${BucketName} --key date1.txt --body date1.txt         
    {
        "ETag": "\"XXXXXXXXXXXXXXXXXXXXXXXX\"",
        "ServerSideEncryption": "AES256"
    }
    $
    ```
1. 確認します。アップロードされていました
    ```
    $ aws s3 ls ${BucketName}
    2024-08-24 10:44:42         48 date1.txt
    $
    ```
1. 再度、同一ファイルをアップロードします
    ```
    ＄ aws s3api put-object --bucket ${BucketName} --key date1.txt --body date1.txt              
    {
        "ETag": "\"XXXXXXXXXXXXXXXXXXXXXXXX\"",
        "ServerSideEncryption": "AES256"
    }
    ＄ 
    ```
1. 確認します。上書きされていました。（タイムスタンプが変わっていました）
    ```
    $ aws s3 ls ${BucketName}
    2024-08-24 10:49:46         48 date1.txt
    $
    ```

## 条件つき書き込み（更新）
すでにファイルが置かれた状況で同一ファイルをアップロードします

1. 下記のコマンドを実行します。エラーになりました
    ```
    $ aws s3api put-object --bucket ${BucketName} --key date1.txt --body date1.txt --if-none-match "*"
    
    An error occurred (PreconditionFailed) when calling the PutObject operation: At least one of the pre-conditions you specified did not hold
    $ 
    ```
1. ファイルは更新されていません。（想定通りです）
    ```
    $ aws s3 ls ${BucketName}
    2024-08-24 10:49:46         48 date1.txt
    $
    ```

## 条件つき書き込み（新規）
条件付きで、新規にアップロードした際の挙動も見ておきます。


1. ファイルをコピーします
    ```
    $ ls -l date1.txt 
    -rw-r--r--  1 XXXX  XXXX  48  8 24 10:35 date1.txt
    $ cp date1.txt date2.txt
    $ ls -l date*
    -rw-r--r--  1 XXXX  XXXX  48  8 24 10:35 date1.txt
    -rw-r--r--  1 XXXX  XXXX  48  8 24 11:00 date2.txt
    $
    ```

1. 新規のファイルを条件付きでアップロードします。成功しました。問題ありません
    ```
    $ aws s3api put-object --bucket ${BucketName} --key date2.txt --body date2.txt --if-none-match "*"
    {
        "ETag": "\"YYYYYYYYYY\"",
        "ServerSideEncryption": "AES256"
    }
    $
    ```
1. 問題なくアップロードされていました
    ```
    $ aws s3 ls ${BucketName}                                                                         
    2024-08-24 10:49:46         48 date1.txt
    2024-08-24 11:02:37         48 date2.txt
    $ 
    ```

# 考察
今回は、条件付き書き込みを試してみました。
同一ファイルを一度しか書き込みたくない場合などに利用できそうです。


# 参考
https://aws.amazon.com/about-aws/whats-new/2024/08/amazon-s3-conditional-writes/

https://docs.aws.amazon.com/AmazonS3/latest/userguide/conditional-requests.html

https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingMetadata.html#SysMetadata

https://awscli.amazonaws.com/v2/documentation/api/latest/reference/s3api/put-object.html