AWSのアカウント間で定期的にS3オブジェクトをコピー

# はじめに

今回やりたいことは、
「**あるAWSアカウントのS3バケットにあるオブジェクトを、異なるAWSアカウントのS3バケットに、毎日特定の時刻にコピーする**」
内容はすごく単純だけど、実際の中身はS3だけでなくIAM周りやEventBridgeなど複数要素が必要になり勉強になったのでまとめます。

ポイントは、
- コピー処理：コピーするプログラムをどう書く？
- 権限周り：異なるアカウントのS3オブジェクトにどうやってアクセスする？
- 定期実行：どうやって定期実行させる？

なおこの記事は、作業手順をまとめたものであり各要素技術について詳細に説明するものではありません（AssumeRoleは[この記事](https://dev.classmethod.jp/articles/iam-role-passrole-assumerole/)が個人的に面白かったです）。とはいえ、ここに記述する手順を踏んで実際に手を動化してみると色々勉強になりますのでトライしてみてください。

# 実現方法
今回はLambda関数を用いた方法について言及する。コピー先にLambda関数を作成し、コピー元にアクセスし対象オブジェクトをコピーするプログラムを記述、EventBridgeで作成したイベントルールをトリガーにLambda関数を実行するという流れです（他にもやり方はたくさんあります）。

# 前提
今回の例は、コピー元S3に毎日作成されるDBダンプファイルをコピー先に毎日コピーするというもの。
使用する各値は以下の通り。
項目|値
-- | --
コピー元（SRC）アカウントID | 111122223333
コピー先（DST）アカウントID | 999988887777
コピー元S3バケット名 | src-bucket
コピー先S3バケット名 | dst-bucket
Lambda関数名 | dst-copy-db-dump-file
IAMロール（コピー先で作成）名 | dst-role-copy-db-dump-file

# 手順
## 全体フロー
1. コピー先からコピー元S3にアクセスするための準備
    - コピー先にIAMロールを作成
    - コピー元アカウントのS3バケットポリシーを設定（唯一コピー元アカウントでの作業）
1. Lambda関数を作成
    - AssumeRole
    - コピー元にアクセス
    - コピー
1. EventBridgeでルールを作成
    - EventBridgeにルールを作成
    - Lambda関数の実行トリガーに設定

## 1. コピー先からコピー元S3にアクセスするための準備

#### 【コピー先３にIAMロールを作成】

Lambda関数からコピー元S3にアクセスするために、コピー元S3にアクセスできるIAMロールを作成する（ARN:`dst-role-copy-db-dump-file`）。Lambda関数ではこのロールをAssumeRoleして一時的にコピー元S3にアクセスできる権限を取得しアクセスする。
```
{
    "Version": "2012-10-17",
    "Statement": [
       {
            "Sid": "Statement1",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:sts::999988887777:assumed-role/dst-copy-db-dump-file-role-upnocnaz/dst-copy-db-dump-file"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}
```

#### 【コピー元アカウントのS3バケットポリシーを設定】
※コピー元アカウントでの作業
コピー対象のオブジェクトがあるバケットのバケットポリシーを次のようにする。Principalには先ほどコピー先で作成したIAMロールのARNを設定。
```
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "SyncDevStg",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::999888777:role/dst-role-copy-db-dump-file"
            },
            "Action": [
                "s3:PutObject",
                "s3:GetObject",
                "s3:ListBucket"
            ],
            "Resource": [
                "arn:aws:s3:::src-bucket/*",
                "arn:aws:s3:::src-bucket"
            ]
        }
    ]
}
```

## 2. Lambda関数を作成

- コピー先で作成したコピー元S3にアクセスするためのIAMロールをAssumeRole
- コピー元から最新（ファイル名が日時としているのでファイル名降順で先頭）のDBダンプファイルを取得
- コピー先S3にコピー

```Python:Python
import json
import boto3

IAM_ROLE_ARN = "arn:aws:iam::999988887777:role/stg-dmenu-money-dump-exporter"  # 後ほど作成
IAM_ROLE_SESSION_NAME = "dmenu-money-fetch-dump"    # 任意の値
REGION_NAME = "ap-northeast-1"
SRC_BUCKET = "src-bucket"
SRC_PREFIX = "dir123/"
DST_BUCKET = "dst-bucket"
DST_KEY = "dir123/latest-dump.sql.gz"

def sts_assume_role():
    try:
        sts_client = boto3.client('sts')
        response = sts_client.assume_role(
            RoleArn=IAM_ROLE_ARN,
            RoleSessionName=IAM_ROLE_SESSION_NAME
        )
        client = boto3.client(
            "s3",
            aws_access_key_id=response['Credentials']['AccessKeyId'],
            aws_secret_access_key=response['Credentials']['SecretAccessKey'],
            aws_session_token=response['Credentials']['SessionToken'],
            region_name=REGION_NAME
        )
    except Exception as e:
        raise e

    return client

def latest_dump_file(s3_client):
    try:
        objs = s3_client.list_objects(Bucket=SRC_BUCKET, Prefix=SRC_PREFIX)
    except Exception as e:
        raise e

    files = [content["Key"] for content in objs["Contents"]]
    if len(files) == 0:
        return ""

    files.sort(reverse=True)
    return files[0]

def copy_dump_file():
    try:
        s3 = sts_assume_role()
        latest = latest_dump_file(s3)
        print("latest = ", latest)

        s3.copy_object(
            Bucket=DST_BUCKET,
            Key=DST_KEY,
            CopySource={"Bucket": SRC_BUCKET, "Key": latest}
        )
        return latest
    except Exception as e:
        raise e

def lambda_handler(event, context):
    try:
        file = copy_dump_file()
        return {
            "statusCode": 200,
            "body": json.dumps("Copied file=" + file)
        }

    except Exception as e:
        print(e)
        return {
            "statusCode": 500,
            "body": json.dumps("Error occured...")
        }

```

## 3. EventBridgeでルールを作成
新規ルールを作成し、スケジュールパターンを毎朝4時（JST）とする。設定する際はUTCなので注意。
```
cron(0, 19, *, *, ?, *)
```
最後にLambda関数のトリガーに作成したルールをセットする。

# まとめ
いかがだったでしょうか。「S3間でオブジェクトをコピーする」というすごく単純な作業でも、種々の設定が必要でそれぞれが関係していてそんなに単純でもなかったと思います。個人的には、これまで曖昧にしかわかっていなかったIAMロールやAssumeRoleなどが理解できたので、やはり手を動かしてナンボだなと思いました。
今後も、小さな仕事でも直向きに取り組んで、スキル向上を促進していきたいと思います。