はじめに
アノテーション の中野です。
S3内オブジェクト削除をAWS SDK for JavaScript v3のDeleteObjectsCommand
を使って行っていました。
しかし、ある日を境にファイル削除でエラーが発生するようになったため、詳細を確認して対応しました。
背景
以前にローンチしたプロダクトで運用がしばらく経つとユーザー数も増えていきました。
このプロダクトでは、S3オブジェクトをLambdaから削除する処理を、深夜帯に動く日時バッチとして実装していました。
しかし、ユーザー数が増加していくことで、S3内のオブジェクト数も比例して増加しました。
結果的に、日時バッチとして削除する対象のオブジェクトが当初想定していたよりも多く、DeleteObjectsCommand
の制約に引っかかってしまいました。
結論
- MalformedXMLエラーは、1000件以上のオブジェクト削除すると発生するエラー
- AWS SDKの
DeleteObjectsCommand
は1000件ずつしか削除できないのが仕様 DeleteObjectsCommand
で1000件を超えるオブジェクト削除する場合は、1000件ごとに分割して削除を実行する
エラー内容
1000件を超えるオブジェクトを削除する際に、以下のようなエラーが発生します。
▼エラーメッセージ
MalformedXML: The XML you provided was not well-formed or did not validate against our published schema
エラー原因
サンプルのコードとして、以下のdeleteAll関数を例に説明します。
この関数の仕様として、引数に削除対象のファイルのpathをもったリストを渡して、関数内でリストに対してDeleteObjectsCommand
を実行します。
このとき、pathが1000件以内だとDeleteObjectsCommand
の処理は成功します。
しかし、1000件を超える場合は上述のエラーが発生します。
import {
S3Client,
DeleteObjectsCommand,
} from "@aws-sdk/client-s3"
const client = new S3Client({
region: "ap-northeast-1",
})
export interface ObjectProps {
path: string
}
const deleteAll = async (objects: ObjectProps[]): Promise<void> {
const objects = object.map((obj) => ({
Key: obj.path,
}))
if (objects.length === 0) return
const bucketParams = {
Bucket: bucketName,
Delete: {
Objects: objects,
},
})
await client.send(new DeleteObjectsCommand(bucketParams))
}
修正したコード
エラーを解消するためには、1000件ごとにDeleteObjectsCommand
を実行すれば成功します。
import {
S3Client,
DeleteObjectsCommand,
} from "@aws-sdk/client-s3"
const client = new S3Client({
region: "ap-northeast-1",
})
export interface ObjectProps {
path: string
}
const deleteObjectsPerThousand = async (
targetObjects: {
Key: string
}[]
): Promise<void> {
const bucketParams = {
Bucket: bucketName,
Delete: {
Objects: targetObjects,
},
})
await client.send(new DeleteObjectsCommand(bucketParams))
}
const deleteAll = async (objects: ObjectProps[]): Promise<void> {
const objects = object.map((obj) => ({
Key: obj.path,
}))
if (objects.length === 0) return
// 1000件ごとに分割して配列にいれる
const totalObjects = []
while (objects.length > 0) {
const objectChunk = objects.splice(0, 1000)
totalObjects.push(objectChunk)
}
await Promise.all(
totalObjects.map((deleteObject) =>
deleteObjectsPerThousand(deleteObject)
)
)
}
上記コードの37行目でspliceを使ってオブジェクトを1000件ごとに配列に分割しています。
その後、deleteObject
を引数にしてdeleteObjectsPerThousand
関数を並列に呼び出して削除処理を実行します。
注意点としてPromiss.allを使っていますが、削除対象のオブジェクトの並列処理が数件の場合はよいですが、多すぎるとS3のレートリミットに引っかかる可能性があります。
ただし、今回の仕様では日時バッチが深夜帯に動くことや、オブジェクトの順番に依存性がないことを踏まえて並行で削除する処理としました。
[補足] ListObjectV2で取得した1000件以上の全ファイル削除
今回の例は、日時バッチ処理で発生したエラーの解消について紹介しました。
調査している段階で、ListObjectsV2Command
を利用して1000件以内のオブジェクトを削除する方法はありましたが、1000件を超える場合のスクリプトは見つかりませんでした。
そのため、ListObjectsV2Command
で取得したS3内の全オブジェクトを削除する方法が、同じようにできないか調べてみました。
すると、paginateListObjectsV2
という関数を見つけました。
paginateListObjectsV2
を利用することで、1000件ごとにページネーション取得したオブジェクトをループしながら削除できます。
ListObjectsV2Command
も1000件までしかオブジェクトを取得できないという制約があるため、この方法を利用すれば、S3内のオブジェクトの全削除はできそうです。
deleteObjectsPerThousand.ts
import {
S3Client,
DeleteObjectsCommand,
paginateListObjectsV2,
} from "@aws-sdk/client-s3"
const client = new S3Client({
region: "ap-northeast-1",
})
const BUCKET_NAME = process.env.BUCKET_NAME || ""
const deleteObjectsPerThousand = async (bucketName: string): Promise<void> => {
if (bucketName == null || bucketName == "") return
for await (const data of paginateListObjectsV2(
{ client, pageSize: 1000 },
{ Bucket: bucketName }
)) {
const s3Objects = data.Contents?.map(({ Key }) => ({ Key }))
const bucketParams = { Bucket: bucketName, Delete: { Objects: s3Objects } }
try {
console.log(data)
await client.send(new DeleteObjectsCommand(bucketParams))
} catch (err) {
console.error(err)
}
}
}
deleteObjectsPerThousand(BUCKET_NAME)
$ npx ts-node ./deleteObjectsPerThousand.ts
さいごに
この記事が誰かのお役に立ちますように
参考情報
- MalformedXML: The XML you provided was not well-formed or did not validate against our published schema - Stack Overflow
- DeleteObjectCommand | @aws-sdk/client-s3
- node.js - aws-sdk S3:listObjectsV2ですべてのキーをリストする最良の方法 - コードログ
アノテーション株式会社について
アノテーション株式会社は、クラスメソッド社のグループ企業として「オペレーション・エクセレンス」を担える企業を目指してチャレンジを続けています。「らしく働く、らしく生きる」のスローガンを掲げ、様々な背景をもつ多様なメンバーが自由度の高い働き方を通してお客様へサービスを提供し続けてきました。現在当社では一緒に会社を盛り上げていただけるメンバーを募集中です。少しでもご興味あれば、アノテーション株式会社 WEB サイトをご覧ください。