aws s3 sync で差分同期する際に大量にHeadObjectが叩かれていないか確認してみた
差分同期をする際に差分を検出するために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
を複数回実行している
- 1,000件以上のチェックをする際は
やってみた
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
の読み書きのデータイベントを取得するようにしていました。
DuckDBでCloudTrailのイベントを確認します。
DuckDBの実行は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 │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
PutObject
とListObjects
が実行されていますね。
実行回数やリクエストパラメーターから各ファイルを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":""} │
└─────────────────────┴─────────────┴─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
はい、PutObject
とListObjects
をそれぞれ1回しかAPIは叩かれていないですね。
試しに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.
では、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秒ほどで完了しました。
実行中のターミナルは以下のように進捗を表示してくれていました。
この時の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
を叩く際に必要な権限は紹介されており、結果はある程度見えていますが、大量オブジェクトがある場合の差分同期にフォーカスはされていなかったので改めての確認でした。
結論、差分チェック処理で大量API課金の可能性は低そうです。500万ファイルを日次でチェックする場合は 0.0555 USDです。このぐらいであれば許容できるでしょう。
この記事が誰かの助けになれば幸いです。
以上、クラウド事業本部 コンサルティング部の のんピ(@non____97)でした!