サーバーサイドエンジニアの森谷です。
業務の中でS3からS3への大量オブジェクトコピーを行う必要が生じたのですが、これが思いの外簡単には実現できず、またGoから利用している記事もあまりなかったため今回はその実装などについて備忘録的にまとめようと思います。
S3 Batch Operationsとは
S3 Batch Operationsとは、任意のS3オブジェクトのリストに対して一括でバッチ処理を行うことができる機能です。
サポートされている操作は
- オブジェクトのコピー
- オブジェクトの復元
- オブジェクトタグの置換・削除
- Lambdaの呼び出し
- ACLの置換
- etc...
などがあります。
今回はオブジェクトのコピーをGoから実行する方法を解説していきます。
S3 Batch Operations自体の使い方
S3 Batch Operations自体の使い方については、以下の記事で大変詳しく解説されているためここでは説明を割愛させていただきます。
実行の流れに沿って簡単に概要をまとめます。
- 対象オブジェクトの一覧が記載されたオブジェクトを作成し、S3の任意のバケット内に配置する
- 一覧オブジェクトには以下のどちらかの形式を使用
S3 Inventory
機能により生成されるmanifest.json並びにdataオブジェクト- csvファイル
- Bucket, Key (, VersionID) の一覧
- https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/batch-ops-create-job.html#specify-batchjob-manifest
- 一覧オブジェクトには以下のどちらかの形式を使用
- ジョブを作成
- ジョブ自体を作成するための権限と、各オペレーションに必要な権限が求められる
- 詳しくは後述
- ジョブを実行
- ジョブ作成とジョブ実行で権限が異なるため追加で設定
- 詳しくは後述
S3間コピーでBatch Operationsを使用するに至った経緯
Batch Operations云々といった話の前段として、そもそもGoでS3間の大量コピーを容易には実現できなかった背景があるため簡単に触れようと思います。
github.com/aws/aws-sdk-go/service/s3
パッケージを除くと、まずCopyObject関数を見つけることができます。
https://pkg.go.dev/github.com/aws/aws-sdk-go/service/s3#CopyObject
こちらはオブジェクト1つに対するコピーは提供しているのですが、複数オブジェクトをコピーしようとするとその数だけリクエストする必要が生じます。
「そもそもs3 syncコマンド的な関数は提供されていないのか?」と思って探してみると、こんなexampleが見つかります。
https://github.com/aws/aws-sdk-go/blob/main/example/service/s3/sync/sync.go
こちらは github.com/aws/aws-sdk-go/service/s3/s3manager
パッケージを使用しています。
BatchUploadObjectなどの構造体があり、「名前的にこの辺の諸々で実現できそうでは?」と一瞬期待をするのですが、こちらはGoからS3へのアップロードもしくはダウンロードしか行えず、無駄にGoを経由しなければなりません。
当然その分時間もかかるため、少量のオブジェクトならばこちらでも問題ないのかもしれませんが、コピーに数十秒、数分かかるようですと他の手法を探したくなります。
こういった経緯でBatch Operationsを使ってみることにしました。
s3control packageからBatch Operationsを操作する
今回は対象リストをcsvで管理し、既にS3にアップロードされているという前提で話を進めます。
ジョブの作成
まずジョブを作成するコードは以下になります。
package main import ( "fmt" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/s3control" ) func main() { sess, _ := session.NewSession() // Configの設定は省略。これがジョブ参照・作成・実行権限を持つ必要がある client := s3control.New(sess) input := &s3control.CreateJobInput{ AccountId: aws.String(""), // AWS Account ID Manifest: &s3control.JobManifest{ // 対象リストオブジェクトの設定 Location: &s3control.JobManifestLocation{ ETag: aws.String(""), // オブジェクトのETag ObjectArn: aws.String(""), // オブジェクトのArn。 `arn:aws:s3:::bucket/key` の形式 }, Spec: &s3control.JobManifestSpec{ Format: aws.String(s3control.JobManifestFormatS3batchOperationsCsv20180820), // フォーマットを指定する定数。今回はCSV(バージョンIDなし)を指定 Fields: []*string{aws.String("Bucket"), aws.String("Key")}, // CSVのフィールド }, }, Operation: &s3control.JobOperation{ // 操作の指定 S3PutObjectCopy: &s3control.S3CopyObjectOperation{ // 今回はオブジェクトのコピーを指定 AccessControlGrants: []*s3control.S3Grant{ // アクセスコントロールリストを設定 { Grantee: &s3control.S3Grantee{ Identifier: aws.String(""), // オブジェクト所有者のID TypeIdentifier: aws.String(s3control.S3GranteeTypeIdentifierId), // 識別方法を指定する定数。今回はID }, Permission: aws.String(s3control.S3PermissionFullControl), // 権限を指定する定数。今回は読み書き両方持たせる }, }, MetadataDirective: aws.String(s3control.S3MetadataDirectiveCopy), // メタデータの扱いを指定する定数。今回は既存メタデータのコピー StorageClass: aws.String(s3control.S3StorageClassStandard), // ストレージクラスを指定する定数。今回はスタンダード TargetResource: aws.String(""), // コピー先バケット。`arn:aws:s3:::bucket` の形式 TargetKeyPrefix: aws.String(""), // コピー先Prefix }, }, Priority: aws.Int64(10), // ジョブの優先度。S3コンソールからジョブを作成した場合のデフォルト値は10 Report: &s3control.JobReport{ // ジョブの実行後のレポート出力設定 Enabled: aws.Bool(true), // レポート出力は任意で実行可能。今回は実行 Bucket: aws.String(""), // 出力先バケット。`arn:aws:s3:::bucket` の形式 Prefix: aws.String(""), // 出力先Prefix Format: aws.String(s3control.JobReportFormatReportCsv20180820), // レポート形式を指定する定数 ReportScope: aws.String(s3control.JobReportScopeAllTasks), // レポートのスコープを指定する定数 }, RoleArn: aws.String(""), // バッチ操作に必要な権限を持つRoleを指定。今回は対象オブジェクトのコピー権限を持つ必要がある } out, _ := client.CreateJob(input) fmt.Println(out.JobId) // ジョブIDのみがCreateJobの結果として返ってくる }
はい。なかなかの項目数ですね。
個人的に感じたBatch Operationsを扱う最大の難関はこの設定項目の多さと権限周りの複雑さです。
項目はドキュメントを追うだけではなかなか理解が難しいので、一度S3コンソールからジョブを作成してみた後に、画面のどの項目がどのフィールドに対応しているのかを整理していくのが近道だと思います。
ジョブの確認
CreateJob
の結果はJobIDのみが返却されます。
これはジョブのステータスが刻々と変化するためで、ジョブの情報を取得したい場合は都度都度 client.DescribeJob
でJobIDを渡してリクエストすることになります。
job, _ := client.DescribeJob(&s3control.DescribeJobInput{
AccountId: aws.String(""),
JobId: out.JobId,
})
jobの主要な中身であるJobDescriptor構造体は次のとおりです。
https://pkg.go.dev/github.com/aws/aws-sdk-go/service/s3control#JobDescriptor
項目数が多いためStatusフィールドにのみ着目しますが、以下のenumで定義されています。
package s3control const ( JobStatusActive = "Active" JobStatusCancelled = "Cancelled" JobStatusCancelling = "Cancelling" JobStatusComplete = "Complete" JobStatusCompleting = "Completing" JobStatusFailed = "Failed" JobStatusFailing = "Failing" JobStatusNew = "New" JobStatusPaused = "Paused" JobStatusPausing = "Pausing" JobStatusPreparing = "Preparing" JobStatusReady = "Ready" JobStatusSuspended = "Suspended" )
CreateJobの直後、ステータスは Preparing
を返します。
ジョブ作成が完了すると Suspended
となり、ネクストアクションであるジョブの実行が可能となります。
ジョブの実行
ジョブを実行するコードは以下の通りです。
out2, _ := client.UpdateJobStatus(&s3control.UpdateJobStatusInput{
AccountId: aws.String(""),
JobId: out.JobId,
RequestedJobStatus: aws.String(s3control.JobStatusReady),
})
ジョブのステータスを Ready
に更新することがジョブを実行することとなります。
こちらは Active
, Completing
の状態を経て完了後に Completed
になります。
権限周りでハマったこと
すでに何度か触れてきたように、Batch Operationsの実行には
- ジョブに関する権限
- 操作に関する権限
の2種類の権限が必要となります。
今回のコードの例で言うと前者は s3:DescribeJob
, s3:CreateJob
, s3:UpdateJobStatus
の権限が必要です。
後者はオブジェクトコピーのため s3:PutObject
周りの権限が必要です。
まとめ
各要素の解説については公式ドキュメントや有名どころのテックブログ等に記事がありましたが、全体の体系を把握するのには手間取りました。
またジョブ作成部分の実装もなかなか難航し、HTTPステータスコード程度の情報しか返してくれないため、マニフェストの指定が悪いのか、どこかの権限が足りていないのか、あるいはジョブのその時のステータスと命令ステータスが噛み合っていないのか、デバッグには苦労しました。
(「AWSぜんぜんわからない。俺たちは雰囲気でIAMを利用している」な理解度だったこともあり、SREチームには大変お世話になりました。)
こうした問題の切り分けのために、一度S3コンソール上でジョブ作成を成功させておき、それをGoから DescribeJob
して適切なフィールドの中身を確認するのもおすすめです。