AWS DataSync によるクロスアカウントでの S3 のデータ移行

# はじめに

クロスアカウントの S3 バケット間でデータを移行する場合いくつか選択肢がありますが、
最近 [AWS DataSync](https://aws.amazon.com/jp/datasync/) がクロスアカウントの移行に対応したようなので試しました。
比較的に容易に、かつ柔軟性のある設定でデータの移行ができました。

[公式ブログ](https://aws.amazon.com/jp/blogs/storage/how-to-use-aws-datasync-to-migrate-data-between-amazon-s3-buckets/)の内容に沿って行っていますが、[曖昧な箇所があり分かりづらい](https://stackoverflow.com/questions/68821694/aws-datasync-s3-s3-cross-account-confused-about-destination-role-account)ため日本語のドキュメントを残します。

# 送信先 S3 バケットの作成

移行対象の送信元 S3 バケットはすでにあるとして、送信先アカウントでバケットを作成します。
以下、 AWS CloudFormation のコードを示しながら進めます。

```yaml:送信先バケット
S3BucketDataSyncDist:
  Type: 'AWS::S3::Bucket'
  DeletionPolicy: 'Retain'
  Properties:
    AccessControl: 'Private'
    BucketEncryption:
      ServerSideEncryptionConfiguration:
        - ServerSideEncryptionByDefault:
            SSEAlgorithm: 'AES256'
    BucketName: 'datasync-dist'
    VersioningConfiguration:
      Status: 'Enabled'
    PublicAccessBlockConfiguration:
      BlockPublicAcls: True
      BlockPublicPolicy: True
      IgnorePublicAcls: True
      RestrictPublicBuckets: True
```

# IAM ロールの作成

**送信先アカウント**で DataSync が送信元、送信先それぞれの Location にアクセスするための IAM ロールを作成します。
実際の移行を行う DataSync の Task も送信先アカウントで実行することになります。

`${S3BucketDataSyncSource}` には送信元バケット名を渡してください。

```yaml:送信元LocationのIAMロール
IAMRoleDataSyncSourceLocation:
  Type: 'AWS::IAM::Role'
  Properties:
    AssumeRolePolicyDocument:
      Version: '2012-10-17'
      Statement:
        - Effect: 'Allow'
          Principal:
            Service: 'datasync.amazonaws.com'
          Action: 'sts:AssumeRole'
    RoleName: 'datasync-source-location-role'
    Policies:
      - PolicyDocument:
          Statement:
            - Action:
                - 's3:GetBucketLocation'
                - 's3:ListBucket'
                - 's3:ListBucketMultipartUploads'
              Effect: 'Allow'
              Resource: !Sub 'arn:aws:s3:::${S3BucketDataSyncSource}'
            - Action:
                - 's3:AbortMultipartUpload'
                - 's3:DeleteObject'
                - 's3:GetObject'
                - 's3:ListMultipartUploadParts'
                - 's3:PutObjectTagging'
                - 's3:GetObjectTagging'
                - 's3:PutObject'
              Effect: 'Allow'
              Resource: !Sub 'arn:aws:s3:::${S3BucketDataSyncSource}/*'
        PolicyName: 'datasync-source-location-policy'
```

```yaml:送信先LocationのIAMロール
IAMRoleDataSyncDistLocation:
  Type: 'AWS::IAM::Role'
  Properties:
    AssumeRolePolicyDocument:
      Version: '2012-10-17'
      Statement:
        - Effect: 'Allow'
          Principal:
            Service: 'datasync.amazonaws.com'
          Action: 'sts:AssumeRole'
    RoleName: 'datasync-dist-location-role'
    Policies:
      - PolicyDocument:
          Statement:
            - Action:
                - 's3:GetBucketLocation'
                - 's3:ListBucket'
                - 's3:ListBucketMultipartUploads'
              Effect: 'Allow'
              Resource: !GetAtt S3BucketDataSyncDist.Arn
            - Action:
                - 's3:AbortMultipartUpload'
                - 's3:DeleteObject'
                - 's3:GetObject'
                - 's3:ListMultipartUploadParts'
                - 's3:PutObjectTagging'
                - 's3:GetObjectTagging'
                - 's3:PutObject'
              Effect: 'Allow'
              Resource: !Sub ${S3BucketDataSyncDist.Arn}/*
        PolicyName: 'datasync-dist-location-policy'
```

# 送信元バケットのバケットポリシーの追加

**送信元アカウント**で送信元バケットのバケットポリシーに、作成した送信元 Location の IAM ロール(`datasync-source-location-role`)と、
このあと `aws datasync create-location-s3` を実行する送信先アカウントのユーザ(`DataSyncDistAccountUserArn`)に対して権限を与えます。

YAML 内の `S3BucketDataSyncSource` は送信元バケットのリソースです。
AWS CLI を実行する際のユーザの ARN は `aws sts get-caller-identity` で確認することができます。

```yaml:送信元バケットのバケットポリシー
S3BucketPolicyDataSyncSource:
  Type: AWS::S3::BucketPolicy
  Properties:
    Bucket: !Ref S3BucketDataSyncSource
    PolicyDocument:
      Statement:
        - Effect: Allow
          Principal:
            AWS:
              - !Sub 'arn:aws:iam::${DataSyncDistAccountId}:role/datasync-source-location-role'
              - !Ref DataSyncDistAccountUserArn
          Action:
            - 's3:GetBucketLocation'
            - 's3:ListBucket'
            - 's3:ListBucketMultipartUploads'
            - 's3:AbortMultipartUpload'
            - 's3:DeleteObject'
            - 's3:GetObject'
            - 's3:ListMultipartUploadParts'
            - 's3:PutObject'
            - 's3:GetObjectTagging'
            - 's3:PutObjectTagging'
          Resource:
            - !GetAtt S3BucketDataSyncSource.Arn
            - !Sub ${S3BucketDataSyncSource.Arn}/*
```

# DataSync Location の作成

**送信先アカウント**で Location を作成します。
CloudFormation に `AWS::DataSync::LocationS3` リソースがありますが、
クロスアカウントの作成に対応していないようで、 `Invalid request provided: Please provide a bucket in the us-east-1 region where DataSync is currently used.` といったエラーとなるため、
送信元 Location のみ AWS CLI から作成します。

AWS CLI で送信先アカウントに対して以下のコマンドで送信元 Location を作成します。

```sh:送信元Locationの作成　
aws datasync create-location-s3 --s3-bucket-arn arn:aws:s3:::[送信元バケット名] --s3-config '{"BucketAccessRoleArn":"arn:aws:iam::[送信先アカウントID]:role/datasync-source-location-role"}'
```

この時、コマンドを実行するユーザはバケットポリシで許可したユーザである必要があります。

送信先 Location は問題なく CloudFormation で作成することができます。

```yaml:送信先Location
DataSyncLocationS3DistLocation:
  Type: AWS::DataSync::LocationS3
  Properties:
    S3BucketArn: !GetAtt S3BucketDataSyncDist.Arn
    S3Config:
      BucketAccessRoleArn: !GetAtt IAMRoleDataSyncDistLocation.Arn
```

S3 のレプリケーションを使うと Prefix によって送信元、送信先で同じフォルダ間でしか移行ができないのに対し、
DataSync は Location で `Subdirectory` を指定することができるため、
送信元、送信先でそれぞれ別々のフォルダを指定することができるところが良いです。

# ロググループの作成(オプション)

**送信先アカウント**で DataSync の Task 実行時にログを吐くためのロググループとリソースポリシーを作成します。ログが必要ないなら不要です。

```yaml
LogsLogGroupDataSync:
  Type: AWS::Logs::LogGroup
  Properties:
    LogGroupName: datasync
    RetentionInDays: 14

LogsResourcePolicy:
  Type: AWS::Logs::ResourcePolicy
  Properties:
    PolicyName: datasync-log-resource-policy
    PolicyDocument: "{\"Statement\": [{\"Sid\": \"DataSyncLogsToCloudWatchLogs\", \"Effect\": \"Allow\", \"Action\": [\"logs:PutLogEvents\", \"logs:CreateLogStream\"], \"Principal\": {\"Service\": \"datasync.amazonaws.com\"}, \"Resource\": \"*\"}], \"Version\": \"2012-10-17\"}"
```

`PolicyDocument` が String しか受け付けてくれませんが、 `AWS::IAM::ManagedPolicy` のように JSON で受け付けてくれると良いですね。

# DataSync Task の作成

**送信先アカウント**で DataSync の Task を作成します。
YAML の `DataSyncSourceLocationArn` は `aws datasync create-location-s3` の結果で返される `LocationArn` です。
`aws datasync list-locations` でも確認することができます。

`AWS::DataSync::Task` の細かな仕様は[公式ドキュメント](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-datasync-task.html)を参照してください。　

```yaml
DataSyncTask:
  Type: AWS::DataSync::Task
  Properties:
    SourceLocationArn: !Ref DataSyncSourceLocationArn
    DestinationLocationArn: !GetAtt DataSyncLocationS3DistLocation.LocationArn
    Excludes:
      - FilterType: SIMPLE_PATTERN
        Value: /logs
    Schedule:
      ScheduleExpression: 'rate(1 hour)'
    Options:
      OverwriteMode: NEVER
      TransferMode: CHANGED
      VerifyMode: ONLY_FILES_TRANSFERRED
      LogLevel: BASIC
    CloudWatchLogGroupArn: !Select [0, !Split [':*', !GetAtt LogsLogGroupDataSync.Arn]]
```

# おわりに

S3 のレプリケーションだと同じフォルダ間でしか移行できなかったり、
バッチオペレーションだと定期実行ができなかったりと、不都合があった部分が DataSync によってうまく解決できました。

ただし、もちろん料金は発生するためきちんと見積もりをしてから実行するのが良いでしょう。
https://aws.amazon.com/jp/datasync/pricing/