aws s3 sync で差分同期する際に大量にHeadObjectが叩かれていないか確認してみた

aws s3 sync で差分同期する際に大量にHeadObjectが叩かれていないか確認してみた

差分チェック処理で大量API課金の可能性は低そう
Clock Icon2025.05.11

差分同期をする際に差分を検出するためにHeadObjectを叩いていないか気になる

こんにちは、のんピ(@non____97)です。

皆さんはaws s3 syncで差分同期をする際に差分を検出するためにHeadObjectを叩いていないか気になったことはありますか? 私はあります。

ローカル端末とS3バケット間、S3バケットとS3バケット間のオブジェクトの同期のためにs3 syncはよく叩くと思います。

s3 syncは差分同期をしてくれるため、送信元と送信先を比較して差分があったオブジェクトのみを転送してくれます。

では、差分を検出するためにはどのようにして情報を取得しているのでしょうか?

例えば、HeadObjectを一オブジェクトごとに叩いているのでしょうか?

その場合、大量のオブジェクトがあるとかなり時間がかかりそうですし、S3のAPIリクエストは従量課金なのでコストも気になります。

東京リージョンの場合、GETやSELECTなどのリクエストは 0.00037 USD/1,000です。

中規模なファイルサーバーだと500万ファイル程度はあるでしょう。仮にHeadObjectで差分チェックをしている場合、S3への差分同期のため日次でs3 syncを使う場合の料金は500万 × 31日 / 1,000 × 0.00037 = 55.5 USDです。

コストインパクトとしてはそこまで大きくはないかもしれないですが、料金が安いには越したことはないです。

ということで、実際に差分同期をする際にどのようなAPIを叩いているのか確認してみます。

いきなりまとめ

  • s3 syncの差分検出にはListObjectsV2が実行されている
    • 1,000件以上のチェックをする際はListObjectsV2を複数回実行している

やってみた

30ファイル程度の初回同期

30ファイル程度の初回同期を行います。

> aws s3 sync ./ s3://non-97-s3-sync-test/
upload: lib/.DS_Store to s3://non-97-s3-sync-test/lib/.DS_Store
upload: lib/src/cf2/normalize-webp-cache-key/index.js to s3://non-97-s3-sync-test/lib/src/cf2/normalize-webp-cache-key/index.js
upload: lib/src/cf2/rewrite-to-webp/index.js to s3://non-97-s3-sync-test/lib/src/cf2/rewrite-to-webp/index.js
upload: lib/construct/certificate-construct.ts to s3://non-97-s3-sync-test/lib/construct/certificate-construct.ts
upload: ./.DS_Store to s3://non-97-s3-sync-test/.DS_Store
upload: lib/src/cf2/directory-index/index.js to s3://non-97-s3-sync-test/lib/src/cf2/directory-index/index.js
upload: lib/construct/bucket-construct.ts to s3://non-97-s3-sync-test/lib/construct/bucket-construct.ts
upload: lib/construct/waf-construct.ts to s3://non-97-s3-sync-test/lib/construct/waf-construct.ts
upload: lib/src/.DS_Store to s3://non-97-s3-sync-test/lib/src/.DS_Store
upload: lib/construct/contents-delivery-construct.ts to s3://non-97-s3-sync-test/lib/construct/contents-delivery-construct.ts
upload: lib/construct/log-analytics-construct.ts to s3://non-97-s3-sync-test/lib/construct/log-analytics-construct.ts
upload: lib/src/contents/dir/index.html to s3://non-97-s3-sync-test/lib/src/contents/dir/index.html
upload: lib/src/contents/error.html to s3://non-97-s3-sync-test/lib/src/contents/error.html
upload: lib/src/contents/dir/test.html to s3://non-97-s3-sync-test/lib/src/contents/dir/test.html
upload: lib/src/contents/index.html to s3://non-97-s3-sync-test/lib/src/contents/index.html
upload: lib/src/contents/dir/non__97_dir.png.webp to s3://non-97-s3-sync-test/lib/src/contents/dir/non__97_dir.png.webp
upload: lib/src/contents/favicon.ico to s3://non-97-s3-sync-test/lib/src/contents/favicon.ico
upload: lib/src/contents/non__97.png.webp to s3://non-97-s3-sync-test/lib/src/contents/non__97.png.webp
upload: lib/src/contents/non__97.png to s3://non-97-s3-sync-test/lib/src/contents/non__97.png
upload: lib/src/contents/test.html to s3://non-97-s3-sync-test/lib/src/contents/test.html
upload: lib/src/contents/non__97.jpeg to s3://non-97-s3-sync-test/lib/src/contents/non__97.jpeg
upload: lib/src/lambda/move-cloudfront-access-log/index.ts to s3://non-97-s3-sync-test/lib/src/lambda/move-cloudfront-access-log/index.ts
upload: lib/src/lambda/directory-index/index.ts to s3://non-97-s3-sync-test/lib/src/lambda/directory-index/index.ts
upload: lib/src/contents/non__97_2.png to s3://non-97-s3-sync-test/lib/src/contents/non__97_2.png
upload: lib/src/lambda/rewrite-to-webp/index.ts to s3://non-97-s3-sync-test/lib/src/lambda/rewrite-to-webp/index.ts
upload: lib/construct/hosted-zone-construct.ts to s3://non-97-s3-sync-test/lib/construct/hosted-zone-construct.ts
upload: lib/website-stack.ts to s3://non-97-s3-sync-test/lib/website-stack.ts
upload: ./package.json to s3://non-97-s3-sync-test/package.json
upload: test/website.test.ts to s3://non-97-s3-sync-test/test/website.test.ts
upload: ./tsconfig.json to s3://non-97-s3-sync-test/tsconfig.json
upload: lib/src/lambda/tsconfig.json to s3://non-97-s3-sync-test/lib/src/lambda/tsconfig.json

それでは、こちらの操作をした際のCloudTrailのイベントを確認します。

事前にCloudTrailの証跡で、non-97-s3-sync-testの読み書きのデータイベントを取得するようにしていました。

2.CloudTrail証跡.png

DuckDBでCloudTrailのイベントを確認します。

DuckDBの実行はCloudShellで行いました。お作法は以下記事が参考になります。

https://dev.classmethod.jp/articles/duckdb-s3-sql-from-aws-cloudshell/

まず、何回も直接S3バケットに出力されたオブジェクトにクエリするのは気が引けるので、使用するカラムを選択してParquetファイルに出力します。

~ $ duckdb
v1.2.2 7c039464e4
Enter ".help" for usage hints.
Connected to a transient in-memory database.
Use ".open FILENAME" to reopen on a persistent database.

# S3にアクセスするために [httpfs](https://duckdb.org/docs/stable/extensions/httpfs/overview.html) をロード
D INSTALL httpfs;
D LOAD httpfs;

# S3にアクセスするための認証情報の読み込み
# 参考 : https://dev.classmethod.jp/articles/duckdb-s3-authentication-methods/
D CREATE SECRET (
    TYPE S3,
    PROVIDER CREDENTIAL_CHAIN
  );
┌─────────┐
│ Success │
│ boolean │
├─────────┤
│ true    │
└─────────┘

# S3バケットに出力されたCloudTrailのイベントログをParquet形式に変換
# オブジェクトサイズが大きい場合以下のようなエラーが出力されたため、32MBまでのオブジェクトを扱えるように指定
#  "maximum_object_size" of 16777216 bytes exceeded while reading file "s3://cm-members-cloudtrail-<AWSアカウントID>/AWSLogs/<AWSアカウントID>/CloudTrail/ap-northeast-1/2025/05/10/<AWSアカウントID>_CloudTrail_ap-northeast-1_20250510T0705Z_Xn5WoZl0ZZiJu09o.json.gz" (>19566032 bytes).
 Try increasing "maximum_object_size".
D COPY (
    WITH cloudtrail_event AS (
      SELECT 
        strptime(json_extract_string(r, '$.eventTime'), '%xT%XZ') as eventTime,
        json_extract_string(r, '$.eventSource') AS eventSource,
        json_extract_string(r, '$.eventName') AS eventName,
        r.requestParameters::JSON AS requestParameters
      FROM (
        SELECT unnest(Records) AS r
        FROM read_json(
          's3://cm-members-cloudtrail-<AWSアカウントID>/AWSLogs/<AWSアカウントID>/CloudTrail/ap-northeast-1/2025/05/10/*.json.gz',
          maximum_depth=2,
          sample_size=-1,
          maximum_object_size=33554432
        )
      ) AS records
    )
    SELECT 
      eventTime,
      eventSource,
      eventName,
      requestParameters,
    FROM cloudtrail_event
  ) TO 'filtered_cloudtrail.parquet' (FORMAT PARQUET);
100% ▕████████████████████████████████████████████████████████████▏ 

# 変換したParquetに対してクエリをし、意図した形になっているか確認
D SELECT * FROM filtered_cloudtrail.parquet LIMIT 10;
┌─────────────────────┬────────────────────┬──────────────┬─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│      eventTime      │    eventSource     │  eventName   │                                                                    requestParameters                                                                    │
│      timestamp      │      varchar       │   varchar    │                                                                          json                                                                           │
├─────────────────────┼────────────────────┼──────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 2025-05-09 23:53:01 │ s3.amazonaws.com   │ GetBucketAcl │ {"bucketName":"cm-members-cloudtrail-<AWSアカウントID>","Host":"cm-members-cloudtrail-<AWSアカウントID>.s3.ap-northeast-1.amazonaws.com","acl":""}                │
│ 2025-05-09 23:53:02 │ s3.amazonaws.com   │ GetBucketAcl │ {"bucketName":"cm-members-cloudtrail-<AWSアカウントID>","Host":"cm-members-cloudtrail-<AWSアカウントID>.s3.ap-northeast-1.amazonaws.com","acl":""}                │
│ 2025-05-09 23:53:35 │ s3.amazonaws.com   │ GetBucketAcl │ {"bucketName":"cm-members-cloudtrail-<AWSアカウントID>","Host":"cm-members-cloudtrail-<AWSアカウントID>.s3.ap-northeast-1.amazonaws.com","acl":""}                │
│ 2025-05-09 23:55:15 │ logs.amazonaws.com │ StartQuery   │ {"queryLanguage":"CWLI","logGroupName":"/aws/appsignals/ec2","startTime":1746834300,"endTime":1746834900,"queryString":"fields @message | stats count…  │
│ 2025-05-09 23:55:14 │ logs.amazonaws.com │ StartQuery   │ {"queryLanguage":"CWLI","logGroupName":"/aws/appsignals/eks","startTime":1746834300,"endTime":1746834900,"queryString":"fields @message | stats count…  │
│ 2025-05-09 23:55:15 │ logs.amazonaws.com │ StartQuery   │ {"queryLanguage":"CWLI","logGroupName":"/aws/appsignals/k8s","startTime":1746834300,"endTime":1746834900,"queryString":"fields @message | stats count…  │
│ 2025-05-09 23:55:15 │ logs.amazonaws.com │ StartQuery   │ {"queryLanguage":"CWLI","logGroupName":"/aws/appsignals/generic","startTime":1746834300,"endTime":1746834900,"queryString":"fields @message | stats c…  │
│ 2025-05-09 23:55:14 │ logs.amazonaws.com │ StartQuery   │ {"queryLanguage":"CWLI","logGroupName":"/aws/application-signals/data","startTime":1746834300,"endTime":1746834900,"queryString":"fields @message | f…  │
│ 2025-05-09 23:59:36 │ sts.amazonaws.com  │ AssumeRole   │ {"roleArn":"arn:aws:iam::<AWSアカウントID>:role/cm-config-role-all-regions","roleSessionName":"ConfigResourceCompositionSession","durationSeconds":900}      │
│ 2025-05-09 23:59:36 │ sts.amazonaws.com  │ AssumeRole   │ {"roleArn":"arn:aws:iam::<AWSアカウントID>:role/cm-config-role-all-regions","roleSessionName":"ConfigResourceCompositionSession","durationSeconds":900}      │
├─────────────────────┴────────────────────┴──────────────┴─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 10 rows                                                                                                                                                                                                 4 columns │
└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

意図した形になっていますね。

それでは、初回同期をしたタイミングで実行されたS3のAPIを抽出します。

D SELECT 
    eventTime,
    eventName,
    requestParameters
  FROM filtered_cloudtrail.parquet 
  WHERE
    eventSource = 's3.amazonaws.com' AND
    eventTime > '2025-05-10 06:17:00' AND
    eventTime < '2025-05-10 06:18:00'
  ;
┌─────────────────────┬─────────────┬──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│      eventTime      │  eventName  │                                                                      requestParameters                                                                       │
│      timestamp      │   varchar   │                                                                             json                                                                             │
├─────────────────────┼─────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 2025-05-10 06:17:20 │ PutObject   │ {"bucketName":"non-97-s3-sync-test","Host":"non-97-s3-sync-test.s3.ap-northeast-1.amazonaws.com","key":"lib/src/contents/dir/non__97_dir.png.webp"}          │
│ 2025-05-10 06:17:20 │ PutObject   │ {"bucketName":"non-97-s3-sync-test","Host":"non-97-s3-sync-test.s3.ap-northeast-1.amazonaws.com","key":"lib/construct/certificate-construct.ts"}             │
│ 2025-05-10 06:17:20 │ PutObject   │ {"bucketName":"non-97-s3-sync-test","Host":"non-97-s3-sync-test.s3.ap-northeast-1.amazonaws.com","key":"lib/src/lambda/rewrite-to-webp/index.ts"}            │
│ 2025-05-10 06:17:20 │ PutObject   │ {"bucketName":"non-97-s3-sync-test","Host":"non-97-s3-sync-test.s3.ap-northeast-1.amazonaws.com","key":".DS_Store"}                                          │
│ 2025-05-10 06:17:20 │ PutObject   │ {"bucketName":"non-97-s3-sync-test","Host":"non-97-s3-sync-test.s3.ap-northeast-1.amazonaws.com","key":"lib/src/contents/dir/test.html"}                     │
│ 2025-05-10 06:17:20 │ PutObject   │ {"bucketName":"non-97-s3-sync-test","Host":"non-97-s3-sync-test.s3.ap-northeast-1.amazonaws.com","key":"lib/src/lambda/directory-index/index.ts"}            │
│ 2025-05-10 06:17:20 │ PutObject   │ {"bucketName":"non-97-s3-sync-test","Host":"non-97-s3-sync-test.s3.ap-northeast-1.amazonaws.com","key":"lib/construct/hosted-zone-construct.ts"}             │
│ 2025-05-10 06:17:20 │ PutObject   │ {"bucketName":"non-97-s3-sync-test","Host":"non-97-s3-sync-test.s3.ap-northeast-1.amazonaws.com","key":"lib/website-stack.ts"}                               │
│ 2025-05-10 06:17:20 │ PutObject   │ {"bucketName":"non-97-s3-sync-test","Host":"non-97-s3-sync-test.s3.ap-northeast-1.amazonaws.com","key":"lib/src/contents/non__97.png.webp"}                  │
│ 2025-05-10 06:17:20 │ PutObject   │ {"bucketName":"non-97-s3-sync-test","Host":"non-97-s3-sync-test.s3.ap-northeast-1.amazonaws.com","key":"lib/construct/log-analytics-construct.ts"}           │
│ 2025-05-10 06:17:20 │ PutObject   │ {"bucketName":"non-97-s3-sync-test","Host":"non-97-s3-sync-test.s3.ap-northeast-1.amazonaws.com","key":"tsconfig.json"}                                      │
│ 2025-05-10 06:17:20 │ PutObject   │ {"bucketName":"non-97-s3-sync-test","Host":"non-97-s3-sync-test.s3.ap-northeast-1.amazonaws.com","key":"lib/src/contents/non__97.jpeg"}                      │
│ 2025-05-10 06:17:20 │ PutObject   │ {"bucketName":"non-97-s3-sync-test","Host":"non-97-s3-sync-test.s3.ap-northeast-1.amazonaws.com","key":"lib/src/.DS_Store"}                                  │
│ 2025-05-10 06:17:20 │ PutObject   │ {"bucketName":"non-97-s3-sync-test","Host":"non-97-s3-sync-test.s3.ap-northeast-1.amazonaws.com","key":"lib/src/cf2/rewrite-to-webp/index.js"}               │
│ 2025-05-10 06:17:20 │ PutObject   │ {"bucketName":"non-97-s3-sync-test","Host":"non-97-s3-sync-test.s3.ap-northeast-1.amazonaws.com","key":"lib/src/contents/dir/index.html"}                    │
│ 2025-05-10 06:17:20 │ PutObject   │ {"bucketName":"non-97-s3-sync-test","Host":"non-97-s3-sync-test.s3.ap-northeast-1.amazonaws.com","key":"lib/src/contents/non__97_2.png"}                     │
│ 2025-05-10 06:17:20 │ PutObject   │ {"bucketName":"non-97-s3-sync-test","Host":"non-97-s3-sync-test.s3.ap-northeast-1.amazonaws.com","key":"lib/construct/waf-construct.ts"}                     │
│ 2025-05-10 06:17:20 │ PutObject   │ {"bucketName":"non-97-s3-sync-test","Host":"non-97-s3-sync-test.s3.ap-northeast-1.amazonaws.com","key":"lib/src/contents/index.html"}                        │
│ 2025-05-10 06:17:20 │ PutObject   │ {"bucketName":"non-97-s3-sync-test","Host":"non-97-s3-sync-test.s3.ap-northeast-1.amazonaws.com","key":"lib/src/lambda/move-cloudfront-access-log/index.ts"} │
│ 2025-05-10 06:17:20 │ PutObject   │ {"bucketName":"non-97-s3-sync-test","Host":"non-97-s3-sync-test.s3.ap-northeast-1.amazonaws.com","key":"lib/construct/bucket-construct.ts"}                  │
│ 2025-05-10 06:17:20 │ PutObject   │ {"bucketName":"non-97-s3-sync-test","Host":"non-97-s3-sync-test.s3.ap-northeast-1.amazonaws.com","key":"lib/src/lambda/tsconfig.json"}                       │
│ 2025-05-10 06:17:20 │ PutObject   │ {"bucketName":"non-97-s3-sync-test","Host":"non-97-s3-sync-test.s3.ap-northeast-1.amazonaws.com","key":"lib/src/contents/favicon.ico"}                       │
│ 2025-05-10 06:17:20 │ PutObject   │ {"bucketName":"non-97-s3-sync-test","Host":"non-97-s3-sync-test.s3.ap-northeast-1.amazonaws.com","key":"package.json"}                                       │
│ 2025-05-10 06:17:20 │ PutObject   │ {"bucketName":"non-97-s3-sync-test","Host":"non-97-s3-sync-test.s3.ap-northeast-1.amazonaws.com","key":"lib/src/contents/non__97.png"}                       │
│ 2025-05-10 06:17:20 │ PutObject   │ {"bucketName":"non-97-s3-sync-test","Host":"non-97-s3-sync-test.s3.ap-northeast-1.amazonaws.com","key":"lib/construct/contents-delivery-construct.ts"}       │
│ 2025-05-10 06:17:20 │ PutObject   │ {"bucketName":"non-97-s3-sync-test","Host":"non-97-s3-sync-test.s3.ap-northeast-1.amazonaws.com","key":"lib/src/contents/error.html"}                        │
│ 2025-05-10 06:17:20 │ PutObject   │ {"bucketName":"non-97-s3-sync-test","Host":"non-97-s3-sync-test.s3.ap-northeast-1.amazonaws.com","key":"lib/src/contents/test.html"}                         │
│ 2025-05-10 06:17:20 │ PutObject   │ {"bucketName":"non-97-s3-sync-test","Host":"non-97-s3-sync-test.s3.ap-northeast-1.amazonaws.com","key":"lib/src/cf2/directory-index/index.js"}               │
│ 2025-05-10 06:17:20 │ PutObject   │ {"bucketName":"non-97-s3-sync-test","Host":"non-97-s3-sync-test.s3.ap-northeast-1.amazonaws.com","key":"test/website.test.ts"}                               │
│ 2025-05-10 06:17:20 │ PutObject   │ {"bucketName":"non-97-s3-sync-test","Host":"non-97-s3-sync-test.s3.ap-northeast-1.amazonaws.com","key":"lib/.DS_Store"}                                      │
│ 2025-05-10 06:17:20 │ PutObject   │ {"bucketName":"non-97-s3-sync-test","Host":"non-97-s3-sync-test.s3.ap-northeast-1.amazonaws.com","key":"lib/src/cf2/normalize-webp-cache-key/index.js"}      │
│ 2025-05-10 06:17:20 │ ListObjects │ {"list-type":"2","bucketName":"non-97-s3-sync-test","Host":"non-97-s3-sync-test.s3.ap-northeast-1.amazonaws.com","encoding-type":"url","prefix":""}          │
├─────────────────────┴─────────────┴──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 32 rows                                                                                                                                                                                3 columns │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

PutObjectListObjectsが実行されていますね。

実行回数やリクエストパラメーターから各ファイルをS3バケットにPUTした回数だけPutObjectが実行されたことが分かります。

一方でListObjectsは一回のみです。

特にHeadObjectは実行されていませんね。

ということはListObjectsで差分チェックをしていそうです。

30ファイル程度の差分同期

それでは差分同期の場合です。

>  aws s3 sync ./ s3://non-97-s3-sync-test/
upload: ./.DS_Store to s3://non-97-s3-sync-test/.DS_Store

1ファイルだけ同期されました。

この時実行されたS3のAPIを抽出します。

D SELECT 
    eventTime,
    eventName,
    requestParameters
  FROM filtered_cloudtrail.parquet 
  WHERE
    eventSource = 's3.amazonaws.com' AND
    eventTime > '2025-05-10 06:41:00' AND
    eventTime < '2025-05-10 06:42:00'
  ;
┌─────────────────────┬─────────────┬─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│      eventTime      │  eventName  │                                                                  requestParameters                                                                  │
│      timestamp      │   varchar   │                                                                        json                                                                         │
├─────────────────────┼─────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 2025-05-10 06:41:35 │ PutObject   │ {"bucketName":"non-97-s3-sync-test","Host":"non-97-s3-sync-test.s3.ap-northeast-1.amazonaws.com","key":".DS_Store"}                                 │
│ 2025-05-10 06:41:35 │ ListObjects │ {"list-type":"2","bucketName":"non-97-s3-sync-test","Host":"non-97-s3-sync-test.s3.ap-northeast-1.amazonaws.com","encoding-type":"url","prefix":""} │
└─────────────────────┴─────────────┴─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

はい、PutObjectListObjectsをそれぞれ1回しかAPIは叩かれていないですね。

試しにs3 sync時に--exact-timestampsも付与して実行します。以下記事でも紹介されているとおり、このオプションの付与を忘れて同期されていなかったのはあるあるだと思います。

https://dev.classmethod.jp/articles/s3-sync-exact-timestamps/

今回は差分はありませんでした。

>  aws s3 sync ./ s3://non-97-s3-sync-test/ --exact-timestamps

この時実行されたS3のAPIを抽出します。

D SELECT 
    eventTime,
    eventName,
    requestParameters
  FROM filtered_cloudtrail.parquet 
  WHERE
    eventSource = 's3.amazonaws.com' AND
    eventTime > '2025-05-10 06:53:00' AND
    eventTime < '2025-05-10 06:53:30'
  ;
┌─────────────────────┬─────────────┬─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│      eventTime      │  eventName  │                                                                  requestParameters                                                                  │
│      timestamp      │   varchar   │                                                                        json                                                                         │
├─────────────────────┼─────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 2025-05-10 06:53:05 │ ListObjects │ {"list-type":"2","bucketName":"non-97-s3-sync-test","Host":"non-97-s3-sync-test.s3.ap-northeast-1.amazonaws.com","encoding-type":"url","prefix":""} │
└─────────────────────┴─────────────┴─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

ListObjectsのみでした。ListObjectsで差分チェックをしていることで確定のようです。

ListObjectsの実際のログイベントは以下のとおりです。

{
  "Records": [
    {
      "eventVersion": "1.11",
      "userIdentity": {
        .
        .
        (中略)
        .
        .
      },
      "eventTime": "2025-05-10T06:53:05Z",
      "eventSource": "s3.amazonaws.com",
      "eventName": "ListObjects",
      "awsRegion": "ap-northeast-1",
      "sourceIPAddress": "104.28.243.105",
      "userAgent": "[aws-cli/2.27.5 md/awscrt#0.26.1 ua/2.1 os/macos#24.4.0 md/arch#arm64 lang/python#3.13.3 md/pyimpl#CPython m/G,C cfg/retry-mode#standard md/installer#source md/prompt#off md/command#s3.sync]",
      "requestParameters": {
        "list-type": "2",
        "bucketName": "non-97-s3-sync-test",
        "Host": "non-97-s3-sync-test.s3.ap-northeast-1.amazonaws.com",
        "encoding-type": "url",
        "prefix": ""
      },
      "responseElements": null,
      "additionalEventData": {
        "SignatureVersion": "SigV4",
        "CipherSuite": "TLS_AES_128_GCM_SHA256",
        "bytesTransferredIn": 0,
        "AuthenticationMethod": "AuthHeader",
        "x-amz-id-2": "+QWQQ0XpE4YNpQlqnKzurb/LuQeB6BYPXZgfU1UFuWQZ3cmnGcJFbhlf4B5nFsLobYAwYKuPvN0=",
        "bytesTransferredOut": 10005
      },
      "requestID": "NN7Y237H14BQR2VC",
      "eventID": "a6dc0c99-4006-43c4-8c6f-2d8c6ffb33d0",
      "readOnly": true,
      "resources": [
        {
          "accountId": "<AWSアカウントID>",
          "type": "AWS::S3::Bucket",
          "ARN": "arn:aws:s3:::non-97-s3-sync-test"
        },
        {
          "type": "AWS::S3::Object",
          "ARNPrefix": "arn:aws:s3:::non-97-s3-sync-test/"
        }
      ],
      "eventType": "AwsApiCall",
      "managementEvent": false,
      "recipientAccountId": "<AWSアカウントID>",
      "eventCategory": "Data",
      "tlsDetails": {
        "tlsVersion": "TLSv1.3",
        "cipherSuite": "TLS_AES_128_GCM_SHA256",
        "clientProvidedHostHeader": "non-97-s3-sync-test.s3.ap-northeast-1.amazonaws.com"
      }
    }
  ]
}

"list-type": "2"となっていることからListObjectsV2を叩いているようですね。

このAPIを実行した際は以下のようなレスポンスが返ってきます。

HTTP/1.1 200
x-amz-request-charged: RequestCharged
<?xml version="1.0" encoding="UTF-8"?>
<ListBucketResult>
   <IsTruncated>boolean</IsTruncated>
   <Contents>
      <ChecksumAlgorithm>string</ChecksumAlgorithm>
      ...
      <ChecksumType>string</ChecksumType>
      <ETag>string</ETag>
      <Key>string</Key>
      <LastModified>timestamp</LastModified>
      <Owner>
         <DisplayName>string</DisplayName>
         <ID>string</ID>
      </Owner>
      <RestoreStatus>
         <IsRestoreInProgress>boolean</IsRestoreInProgress>
         <RestoreExpiryDate>timestamp</RestoreExpiryDate>
      </RestoreStatus>
      <Size>long</Size>
      <StorageClass>string</StorageClass>
   </Contents>
   ...
   <Name>string</Name>
   <Prefix>string</Prefix>
   <Delimiter>string</Delimiter>
   <MaxKeys>integer</MaxKeys>
   <CommonPrefixes>
      <Prefix>string</Prefix>
   </CommonPrefixes>
   ...
   <EncodingType>string</EncodingType>
   <KeyCount>integer</KeyCount>
   <ContinuationToken>string</ContinuationToken>
   <NextContinuationToken>string</NextContinuationToken>
   <StartAfter>string</StartAfter>
</ListBucketResult>

差分チェックに使用する各オブジェクトの最終更新日時やサイズが含まれているため、都度HeadObjectを叩く必要はなく、ListObjectsV2で十分という訳ですね。

2万ファイル程度の差分同期

ListObjectsV2のドキュメントに一回のAPIリクエストで1,000件まで返すような説明がありました。

Returns some or all (up to 1,000) of the objects in a bucket with each request.

ListObjectsV2 - Amazon Simple Storage Service

では、1,000件以上ファイルがある場合はどのような挙動になるのでしょうか。

適当に2万ファイルほど用意しました。

>  find . -type f | wc -l
   20727

この状態で同期を行います。

>  aws s3 sync ./ s3://non-97-s3-sync-test/ --exact-timestamps
upload: ./.DS_Store to s3://non-97-s3-sync-test/.DS_Store
upload: node_modules/.bin/esparse to s3://non-97-s3-sync-test/node_modules/.bin/esparse
upload: node_modules/.bin/esvalidate to s3://non-97-s3-sync-test/node_modules/.bin/esvalidate
upload: node_modules/.bin/acorn to s3://non-97-s3-sync-test/node_modules/.bin/acorn
upload: node_modules/.bin/cdk to s3://non-97-s3-sync-test/node_modules/.bin/cdk
upload: node_modules/.bin/create-jest to s3://non-97-s3-sync-test/node_modules/.bin/create-jest
upload: node_modules/.bin/browserslist to s3://non-97-s3-sync-test/node_modules/.bin/browserslist
upload: node_modules/.bin/fxparser to s3://non-97-s3-sync-test/node_modules/.bin/fxparser
upload: node_modules/.DS_Store to s3://non-97-s3-sync-test/node_modules/.DS_Store
.
.
(中略)
.
.
upload: node_modules/yn/readme.md to s3://non-97-s3-sync-test/node_modules/yn/readme.md
upload: node_modules/yn/index.d.ts to s3://non-97-s3-sync-test/node_modules/yn/index.d.ts
upload: node_modules/yn/lenient.js to s3://non-97-s3-sync-test/node_modules/yn/lenient.js
upload: node_modules/yocto-queue/index.d.ts to s3://non-97-s3-sync-test/node_modules/yocto-queue/index.d.ts
upload: node_modules/yocto-queue/package.json to s3://non-97-s3-sync-test/node_modules/yocto-queue/package.json
upload: node_modules/yocto-queue/license to s3://non-97-s3-sync-test/node_modules/yocto-queue/license
upload: node_modules/yocto-queue/readme.md to s3://non-97-s3-sync-test/node_modules/yocto-queue/readme.md
upload: node_modules/yocto-queue/index.js to s3://non-97-s3-sync-test/node_modules/yocto-queue/index.js

おおよそ2分30秒ほどで完了しました。

実行中のターミナルは以下のように進捗を表示してくれていました。

1.大量のファイルをs3 sync.png

この時のS3のAPIごとの実行回数は以下のとおりです。

D SELECT 
    eventName,
    COUNT(*) AS event_count
  FROM filtered_cloudtrail.parquet 
  WHERE
    eventSource = 's3.amazonaws.com' AND
    eventTime > '2025-05-10 07:00:00' AND
    eventTime < '2025-05-10 07:03:00'
  GROUP BY eventName
  ;
┌─────────────────────────┬─────────────┐
│        eventName        │ event_count │
│         varchar         │    int64    │
├─────────────────────────┼─────────────┤
│ CreateMultipartUpload   │           8 │
│ UploadPart              │          17 │
│ PutObject               │       20724 │
│ ListObjects             │           1 │
│ CompleteMultipartUpload │           8 │
└─────────────────────────┴─────────────┘

ListObjectsは1回しか実行されていませんね。

PutObjectの実行回数については予想の範囲ですね。また、サイズが大きいファイルがいくつかあったのでマルチパートアップロード時のAPIも実行されていますね。

この状態で再度s3 syncを実行します。

> aws s3 sync ./ s3://non-97-s3-sync-test/ --exact-timestamps

実行時間はおおよそ15秒ほどでした。

2万件のファイルの差分チェックに15秒はなかなかの速度ではないでしょうか。

この時のS3のAPIごとの実行回数は以下のとおりです。

D SELECT 
    eventName,
    COUNT(*) AS event_count
  FROM filtered_cloudtrail.parquet 
  WHERE
    eventSource = 's3.amazonaws.com' AND
    eventTime > '2025-05-10 07:15:15' AND
    eventTime < '2025-05-10 07:15:45'
  GROUP BY eventName
  ;
┌─────────────┬─────────────┐
│  eventName  │ event_count │
│   varchar   │    int64    │
├─────────────┼─────────────┤
│ ListObjects │     21      │
└─────────────┴─────────────┘

D SELECT 
    eventTime,
    eventName,
    requestParameters
  FROM filtered_cloudtrail.parquet 
  WHERE
    eventSource = 's3.amazonaws.com' AND
    eventTime > '2025-05-10 07:15:15' AND
    eventTime < '2025-05-10 07:15:45'
  ORDER BY eventTime
  ;
┌─────────────────────┬─────────────┬───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│      eventTime      │  eventName  │                                                                               requestParameters                                                                               │
│      timestamp      │   varchar   │                                                                                     json                                                                                      │
├─────────────────────┼─────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 2025-05-10 07:15:25 │ ListObjects │ {"list-type":"2","bucketName":"non-97-s3-sync-test","continuation-token":"1/S5nT0x+8CLoFN22S+EQWllDOVL5bF4yH5Ef+zXgXqhYhssP4dD6w3fMxfP3iDNW5mSShM3DLBWgEGWalm3IlHhwjxPsesj8…  │
│ 2025-05-10 07:15:25 │ ListObjects │ {"list-type":"2","bucketName":"non-97-s3-sync-test","Host":"non-97-s3-sync-test.s3.ap-northeast-1.amazonaws.com","encoding-type":"url","prefix":""}                           │
│ 2025-05-10 07:15:26 │ ListObjects │ {"list-type":"2","bucketName":"non-97-s3-sync-test","continuation-token":"1MJYftU2fPkxC9iMujhvcWsB/stPmwbKzLzSEnH9U4iqjFe4sQn8mOTE3D2IeuD5WbRxfoAA8GEHQCwLgs2C8fBS23yuBE+wu…  │
│ 2025-05-10 07:15:26 │ ListObjects │ {"list-type":"2","bucketName":"non-97-s3-sync-test","continuation-token":"1/KZXDZtSCWIztvSRRw/9+50zOfKQpd4IbPwvFYhNFB7m0B3SCdD3fK1PeMQkjry+rD4h2OYWteclWwRGb2BKaexBRYdUgCfj…  │
│ 2025-05-10 07:15:26 │ ListObjects │ {"list-type":"2","bucketName":"non-97-s3-sync-test","continuation-token":"1HQXheO+KD5GhFY2X9dS8iEa8yyXs8zxuy2Ea2oephA1VNu2bPc8vdwhFmj0dFtBMRoozp3710xeRv4wnFqYdF6LcniSy4i05…  │
│ 2025-05-10 07:15:27 │ ListObjects │ {"list-type":"2","bucketName":"non-97-s3-sync-test","continuation-token":"1l2ILeiw/OPjV34jXVsK//BEFLj6LXWPlipFZMx6D61ZChIaBGZ9aeHfiiXOGZvZxFNf80nHHoLDpUInvhytzDqu+hYiFM6pE…  │
│ 2025-05-10 07:15:27 │ ListObjects │ {"list-type":"2","bucketName":"non-97-s3-sync-test","continuation-token":"1sINemfQ5TNvj0fUWy07hibTHooDZ9D01+YkVkdlbp3kj3ZIPP9UqSCdcOK8Bu5llTC6nSo0nIDKJHptbXwPVrsHD6rOm9wN4…  │
│ 2025-05-10 07:15:27 │ ListObjects │ {"list-type":"2","bucketName":"non-97-s3-sync-test","continuation-token":"1xThpUoLcz3kVpCcnSKOfKZ3rtuRuf+hrSDFFdjhjulG9d5J2ZHGNn8e3xtSu3n3sKDUABwfCFQm+woDWQaFPE8zHmSTk44ls…  │
│ 2025-05-10 07:15:28 │ ListObjects │ {"list-type":"2","bucketName":"non-97-s3-sync-test","continuation-token":"1WjFEI3jIzaAI5/toKwt8cAu1FNazsI9cdn+FelwYXeaye1OGuCo1wSrhjAbxtIerrtrLUj8kOZuOwZMMf17Bz7UQzN00q2tu…  │
│ 2025-05-10 07:15:28 │ ListObjects │ {"list-type":"2","bucketName":"non-97-s3-sync-test","continuation-token":"14YXIvaV4Tt88XbQuoKK8w7ugshMR+InNHbUtUsRZEZ+HG9c/u1FvgphFLUxiaJrwC1+TYB3I9q499RIrqzg+GJ6hLWFCzY55…  │
│ 2025-05-10 07:15:29 │ ListObjects │ {"list-type":"2","bucketName":"non-97-s3-sync-test","continuation-token":"18JPukm08y0CCvvy7yJL5Lnmd/WSswbKkNEuY67b4t6i1rEN0J1kEcYmYI3ZvOTn3LjfF3I42iNRXZGGyBXbKTPxkgyyU3sAC…  │
│ 2025-05-10 07:15:29 │ ListObjects │ {"list-type":"2","bucketName":"non-97-s3-sync-test","continuation-token":"1HWosC5xo95GZBzwIe69sBA4+FbkfilhVtxELoNCzpcjiMd3ayYMlHqZ1ykjUxYbIKr+6cc7721pIOUpvBJ9jG39Kaak/jnXA…  │
│ 2025-05-10 07:15:29 │ ListObjects │ {"list-type":"2","bucketName":"non-97-s3-sync-test","continuation-token":"11d7phxNwsxGJRxYQsmkxiwOe21Wuyb8lTesGo89IRP9xfBr35fQJz+DYYoLytO9aXkB7MRzMU76FkmqxyerxeZ/67eLbCZEZ…  │
│ 2025-05-10 07:15:30 │ ListObjects │ {"list-type":"2","bucketName":"non-97-s3-sync-test","continuation-token":"1hTFUxtWnjoyjsJvpqknWJqKWmhEFGcMXdM5oufKocfksv9cn5l6/gNaAIW344iHnLKCcqxitBZRe2s+eTZbkQ0/wd4Au9DOw…  │
│ 2025-05-10 07:15:30 │ ListObjects │ {"list-type":"2","bucketName":"non-97-s3-sync-test","continuation-token":"1aj1GJabwM8uFLttuU8+dY8VrGCh93PoO+J6sPobeKa8OOgAH0JPjdORC+xYQ+9Yaw5ApyTVK65EbCEfmtLaOIM5fC2njkvT5…  │
│ 2025-05-10 07:15:30 │ ListObjects │ {"list-type":"2","bucketName":"non-97-s3-sync-test","continuation-token":"1oIemmATqoHeK0X9OzTeK9l6o4hMmkO0lVdCLCxOQ3Xj5BLY4MxzoSYhp/jKzw1vxAv5qtetl/vMAEokkUB+Fa/1vQLVBgArZ…  │
│ 2025-05-10 07:15:31 │ ListObjects │ {"list-type":"2","bucketName":"non-97-s3-sync-test","continuation-token":"1meIjoqdHMc3ZJxXSMGM9B+POQpjU2TPRMPGAZ60yGOn7C5/cgyANtHrA4AVG1eeWchP6vGEs+bM7ogzAqSQoP1c9XzNXpyeg…  │
│ 2025-05-10 07:15:31 │ ListObjects │ {"list-type":"2","bucketName":"non-97-s3-sync-test","continuation-token":"1aheiWgz3nQ+m70+D+fAlde1o59uczz/Vwq419Oro5e+aARs6U2lTEzeJvQkmgEiEJwGkc7UxbqrxRYOSzo8gXdTJOYmpVWJ9…  │
│ 2025-05-10 07:15:31 │ ListObjects │ {"list-type":"2","bucketName":"non-97-s3-sync-test","continuation-token":"1Y6qzBSuLJESN1ALhN3wgkPAbzYlXEy6A2Op1glOdHFxtmI5otiwTQlkMe+D/1W/zBaRoQAMPMXjZ7AwJ6IsfZZtn8L8wP6v/…  │
│ 2025-05-10 07:15:32 │ ListObjects │ {"list-type":"2","bucketName":"non-97-s3-sync-test","continuation-token":"1d2f3WQUb+LX1lJSr0v6NuF7ylzMuABu/QPnFk8eyQyLbTpCJx3coPhU/oQEOLTPnB1VRIlxBHAH3DCqM4NcQOcaFRhWd205c…  │
│ 2025-05-10 07:15:32 │ ListObjects │ {"list-type":"2","bucketName":"non-97-s3-sync-test","continuation-token":"1qKLzYT0OKfdVcPFvxLl1seuCQEmwoZCTs8ZkIGr+0f0VabizC8AzAu1ZzgRwQbjlELbUIEw/AQgkWeVxHtkHn1ZLP9V0gs+4…  │
├─────────────────────┴─────────────┴───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 21 rows                                                                                                                                                                                                 3 columns │
└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

ListObjectsが21回実行されていました。

continuation-tokenがあることから前回取得時の途中から取得していることが分かります。

差分チェック処理で大量API課金の可能性は低そう

aws s3 syncで差分同期する際に大量にHeadObjectが叩かれていないか確認してみました。

以下記事で、s3 syncを叩く際に必要な権限は紹介されており、結果はある程度見えていますが、大量オブジェクトがある場合の差分同期にフォーカスはされていなかったので改めての確認でした。

https://dev.classmethod.jp/articles/what-permissions-are-needed-for-s3-sync/

結論、差分チェック処理で大量API課金の可能性は低そうです。500万ファイルを日次でチェックする場合は 0.0555 USDです。このぐらいであれば許容できるでしょう。

この記事が誰かの助けになれば幸いです。

以上、クラウド事業本部 コンサルティング部の のんピ(@non____97)でした!

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.