Yappli Tech Blog

株式会社ヤプリの開発メンバーによるブログです。最新の技術情報からチーム・働き方に関するテーマまで、日々の熱い想いを持って発信していきます。

GoでS3 Batch Operationsを用いたS3間大量コピーを実現する

サーバーサイドエンジニアの森谷です。

業務の中でS3からS3への大量オブジェクトコピーを行う必要が生じたのですが、これが思いの外簡単には実現できず、またGoから利用している記事もあまりなかったため今回はその実装などについて備忘録的にまとめようと思います。

S3 Batch Operationsとは

S3 Batch Operationsとは、任意のS3オブジェクトのリストに対して一括でバッチ処理を行うことができる機能です。

docs.aws.amazon.com

サポートされている操作は

  • オブジェクトのコピー
  • オブジェクトの復元
  • オブジェクトタグの置換・削除
  • Lambdaの呼び出し
  • ACLの置換
  • etc...

などがあります。

今回はオブジェクトのコピーをGoから実行する方法を解説していきます。

S3 Batch Operations自体の使い方

S3 Batch Operations自体の使い方については、以下の記事で大変詳しく解説されているためここでは説明を割愛させていただきます。

dev.classmethod.jp

実行の流れに沿って簡単に概要をまとめます。

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 周りの権限が必要です。

docs.aws.amazon.com

まとめ

各要素の解説については公式ドキュメントや有名どころのテックブログ等に記事がありましたが、全体の体系を把握するのには手間取りました。

またジョブ作成部分の実装もなかなか難航し、HTTPステータスコード程度の情報しか返してくれないため、マニフェストの指定が悪いのか、どこかの権限が足りていないのか、あるいはジョブのその時のステータスと命令ステータスが噛み合っていないのか、デバッグには苦労しました。
(「AWSぜんぜんわからない。俺たちは雰囲気でIAMを利用している」な理解度だったこともあり、SREチームには大変お世話になりました。)

こうした問題の切り分けのために、一度S3コンソール上でジョブ作成を成功させておき、それをGoから DescribeJob して適切なフィールドの中身を確認するのもおすすめです。