AWSクロスアカウントでS3を読み書きする方法と注意点などを2019年最後にまとめてみた

この記事は [マイナビ Advent Calendar 2019](https://qiita.com/advent-calendar/2019/mynavi) 20日目の記事となります。

こんにちは。皆さんあんまりクロスアカウント[^1]ってやっていないのでしょうか。
クロスアカウントのS3アクセスについて、Tipsが1箇所にまとまっておらず個人的に情報収集に少し手こずったのでS3クロスアカウント周りで知っている情報をこちらにまとめて今年2019年を納めたいと思います！
**※以下、「クロスアカウント」は長いのでCAと略したり略さなかったりしますのでご注意**

[^1]: 複数のAWSアカウント間で処理を行ったり、データ連携を行うこと

# やりたいこと
- 2つのAWSアカウント(`アカウントA`と`アカウントB`)がある状況で
- アカウントBに作成したS3バケット(`バケットB`)に、アカウントAのユーザ(`ユーザA`)でファイルを書き込む
- アカウントAでもアカウントBでもS3ファイルのReadができるようにする
- 更に、ちょっと急にピンポイントな話ですが、Glue(pyspark)によるクロスアカウントS3書き込みも行いたい、とする

# 前提
- 一部プログラム例がありますが、すべてTypeScript(Node.js)のコードになります。
- GlueはVersion0.9のPython2.7です。Glue1.0だと事情が多少異なる可能性アリです。
    - 実装検証していたのが２０１９年春～夏だったので泣く泣くPython2.7のGlueを使っていました。→直後に新バージョンが出た

# 本記事の要約
先に要約をまとめると以下のような感じです。

- S3クロスアカウントアクセスの方法
    - 方法は３つ「バケットポリシーを利用する方法」「AssumeRoleを利用する方法」「ACLを利用する方法」
    - 選択肢は実質２択
        - 少ない実装で、ほぼ権限設定だけで解決したい場合
            - →バケットポリシーを利用
        - リソースベースポリシーが使えないサービスも利用する、実装コストが多少上がってもOKな場合
            - →AssumeRoleを利用
- IAMポリシーとバケットポリシーの許可設定
    - 単一アカウント内アクセス：IAMポリシーかバケットポリシーのどちらかで許可すればアクセス可
    - クロスアカウントアクセス：IAMポリシーとバケットポリシーの両方で許可しなければアクセスできない
- S3クロスアカウントで出力されたファイルはバケットオーナー側でReadできない問題への対策
    - 通常のAWSライブラリを用いたプログラムでは
        - Writeする側でACLの`bucket-owner-full-control`を指定する
    - glueの場合
        - https://qiita.com/pioho07/items/f13cf2714d40cb6eefce
    - その他のサービス利用時
        - サービスによっては(Glueのように)特別な設定が必要なものがあるかもしれませんね・・

話自体は大体以上でおしまいなんですが、以下、可能なレベルで詳しく説明していきます。

# クロスアカウントでS3アクセスをする方法３パターン
まずはS3のクロスアカウントアクセスの方法の大枠について。
本家サイトに書かれていますが、以下３パターンになるそうです。[^2]

1. リソースベースのポリシーと AWS Identity and Access Management (IAM) ポリシーを用いるパターン
2. リソースベースのアクセスコントロールリスト (ACL) と IAM ポリシーを用いるパターン
3. クロスアカウント IAM ロールを用いるパターン

[^2]: https://aws.amazon.com/jp/premiumsupport/knowledge-center/cross-account-access-s3/

それぞれを比較をすると以下のような感じです。
※個人の主観が少し入っている気がします。→詳細は本家AWSのドキュメントを参照してください。

|No|概要|権限付与対象|CAによる読み込み処理の実装方法|CAによる書き込み処理の実装方法|
|:-:|:-:|:-|:-:|:-:|
|1|バケットポリシーとIAMポリシーで権限制御|<ul><li>IAMユーザ</li><li>IAMロール</li></ul>|非CA時の実装と同じ|非CA時とほぼ同じだがファイルオーナの切り替えが必要(後述)|
|2|ACLとIAMポリシーで権限制御|AWSアカウント|非CA時の実装と同じ|非CA時とほぼ同じだがファイルオーナの切り替えが必要(後述)|
|3|AssumeRoleで権限制御|IAMロール<br>(ユーザAからAssumeRoleされることを許可したロールBに対して権限付与する)|AssumeRole処理の実装が必要|←に同じ|

## 各CAアクセス方法のケース別判断フロー(主観アリアリ)
- CAでの書き込みアクセスは行わない場合(つまり読み込みのみ)
    - →バケットポリシーでやるのが簡単。プログラム側でCAの考慮は不要で権限設定だけでいける
- CAでの書き込みアクセスがあり得る場合
    - →バケットポリシー or AssumeRole
    - 一応本記事のファイルオーナの切り替え処理を行えばバケットポリシーでいけるが、他にもACL周りで問題出る可能性も否定できず。（たぶん大丈夫な気はするが…）
- CAでの書き込みアクセスが有り得る、リソースベースポリシー未対応サービスも処理する必要がある
    - →AssumeRoleでやる。少し実装が必要＆ロールの信頼関係設定などが必要
    - この条件時にバケットポリシーを選択しないほうが良い理由は [バケットポリシーによるアクセス時の注意点](#バケットポリシーによるクロスアカウントアクセス時の注意点) を参照
- かなり特殊なケースのみ
    - →ACLでやる。本家ドキュメント[^2]によるとACLよりもバケットポリシーが強く推奨されているように見えますので普通は選択しない
    - ACLを利用する必要のある特殊なケースについては以下に説明されています。
        - https://docs.aws.amazon.com/AmazonS3/latest/dev/access-policy-alternatives-guidelines.html#when-to-use-acl
        - このサイトの説明によると、やはりかなり特殊なケースです。通常は該当しないと思います。

## バケットポリシーによるクロスアカウントアクセス時の注意点
### バケットポリシーでCA書き込みした時にアカウントB側でファイルが読めない問題
実は、ユーザAからバケットBにファイルを書いた場合、そのファイルのオーナーはアカウントAになります。
そのため、なんと**バケットBの中のファイルにも関わらずアカウントBからはアクセスができない状況**となります！！
(こんなん普通ハマるわー)
下の記事にありますが、`bucket-owner-full-control`を指定してファイルを書き込むことで、アカウントBのユーザやロールでもアクセス可能にできます。
https://dev.classmethod.jp/cloud/aws/s3-cross-permission/

TypeScriptの(Node.jsの)`aws-sdk`ライブラリの`S3`を利用した場合の`bucket-owner-full-control`指定例を示します。

```typescript
    const client = new S3({apiVersion: '2006-03-01'});
    await client.putObject({
        Bucket: bucketName,
        Key: filePath,
        Body: data.Body,
        ContentType: "任意のContentType",
        ACL: "bucket-owner-full-control",     // ←ココ クロスアカウント書き込みなのでオーナー変更必要なのでACL指定
    }).promise();
```
#### もしかすると以下で解決？
この記事を書いていて見つけたのですが、もしかしたら以下のページに有る方法を使えばプログラムでファイル書き込みをする際に`bucket-owner-full-control`を明示しなくても良いのかもしれません。→未検証です。知っている方、教えて頂けますと助かります。
https://docs.aws.amazon.com/AmazonS3/latest/dev/amazon-s3-policy-keys.html#grant-putobject-conditionally-1


### リソースベースポリシーのサービスごとの対応状況
バケットポリシーとACLはリソースベースポリシーと呼ばれる方法となります。
一部のサービスではこのリソースベースポリシーが利用できません。もしS3だけでなく色々なサービスのオブジェクトをアクセスするようなプログラムでは、実装が多少面倒でもAssumeRoleした方がシンプルになるという判断もあるかと思います。
https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/reference_aws-services-that-work-with-iam.html

# バケットポリシーによるS3クロスアカウントファイル書き込みの手順
注意点の説明はこれくらいにして、具体的手順を説明します。
以下２種類の権限設定が必要です。

- バケットBのバケットポリシーでロールAのクロスアカウント書き込み許可
- ユーザAのIAMポリシーで書き込み許可

１つのアカウント内のS3アクセスであればどちらか片方の許可でOKなのですが、クロスアカウントの場合は双方の許可設定が必要です。
詳しくは、以下の記事にあります。
https://dev.classmethod.jp/cloud/aws/summarize-principal-settings-in-s3-bucket-policy/

## バケットポリシーの設定
以下のようにバケットポリシーを設定します。

- バケットBの名前：`account-b-no-bucket`
- アカウントAのID：`AAAAAAAAAAAA`　※本当は１２桁の数値です。(セキュリティの観点でこの例では伏せています)
- ユーザAの名前：`user-a`

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/196303/50d3fd20-8745-f9c2-6df7-b71b3eaf00d0.png)

バケットポリシーのJSONは以下の通り。

```javascript
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::AAAAAAAAAAAA:user/user-a"
            },
            "Action": [
                "s3:PutObject",
                "s3:PutObjectAcl"
            ],
            "Resource": "arn:aws:s3:::account-b-no-bucket/*"
        }
    ]
}
```

ここでのミソは、putObjectだけでなく、`s3:PutObjectAcl`も付けていることです。
そうしないと、上記アカウントBでファイルが読めない問題の対策であるファイルオーナーの付け替えができないので。

## IAMポリシーの設定
ユーザAに対し、IAMポリシーでバケットBに対して書き込み許可をします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/196303/1904a31a-b5de-f1a4-9b80-e46c280148e6.png)

※ちゃっかりcloud9の権限も付いていますが、これは本来的には不要です。
　このあとの手順でCLIによるS3書き込みをやろうと思ったので、それにcloud9を使おうかなと思ったので付いているだけです。

IAMポリシーのJSONは以下のとおりです。
※ここでのミソも`s3:PutObjectAcl`です。

```javascript
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "s3:PutObject",
                "s3:PutObjectAcl"
            ],
            "Resource": "arn:aws:s3:::account-b-no-bucket/*"
        }
    ]
}
```

## ファイル書き込み
### でもいったんハマってみる
TypeScriptの場合のコードは上述しましたが、`ACL: "bucket-owner-full-control"`をputObject関数に指定する必要がありました。
まぁ結果はわかっていますが、ここでは一旦罠にハマってみることとします。

検証を簡単にするために、ここではCloud9を使ってaws-cliにてやってみることにします。
※別に本来はcloud9を使う必要はありません。普通にIAMユーザのクレデンシャル等で認証しCLIを使っても同じことができます。

```bash
user-a:~/environment $ ls
README.md
user-a:~/environment $ aws s3 cp README.md s3://account-b-no-bucket/from-user-a/
upload: ./README.md to s3://account-b-no-bucket/from-user-a/README.md
user-a:~/environment $ 
```

以上でアップロード成功です。
バケットBにアップロードされたファイルを見てみましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/196303/050a3790-638f-06d8-4116-f3f42a427cb1.png)

※危なそうなので「所有者」と「Etag」は隠しました。

以上でアップロードはできたのですが、実は上述の`bucket-owner-full-control`の指定をしないでアップロードしています。
そのため、赤枠で囲った「サーバー側暗号化」のところが、`アクセス拒否`となっています。
これはつまり、Read権限がないのでKMS暗号化されているかどうかすらわからないことからこのように表示されているものと思われます。

ちなみに、上のキャプチャで隠していた「所有者」が、アカウントAの正規ユーザIDというものになっていたのです。実は。
つまり、バケットBのファイルはアカウントB内の物であるにも関わらず、アカウントAが所有しているという変な状況になっているということを表します。

次に、このファイルをダウンロードしてみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/196303/c39bd214-6de8-8cb3-a585-6dcc9a9c05a9.png)

※またなんとなく怖いので、「RequestId」と「HostId」は隠しました。

はい、AccessDeniedです。オーナーがアカウントA側になっているためアカウントB側のユーザではダウンロードできないということでした。

### ファイルオーナー問題の解決版！
次に、`bucket-owner-full-control`を付けてアップロードしてみます。
参考：https://aws.amazon.com/jp/premiumsupport/knowledge-center/s3-bucket-owner-access/

```bash
user-a:~/environment $ aws s3 cp README.md s3://account-b-no-bucket/from-user-a/with-acl/ --acl bucket-owner-full-control
upload: ./README.md to s3://account-b-no-bucket/from-user-a/with-acl/README.md
user-a:~/environment $ 
```

早速ファイルを見てみましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/196303/ec4257bf-c973-ed4b-da67-76b120a30041.png)

はい、今度は「サーバー側の暗号化」も”なし”と表示されており、READ権限がありそうですね。
ダウンロードももちろんできました。キャプチャは割愛。


# Assume Roleを使ったS3クロスアカウントアクセスの手順
以下のサイト等に詳しく載っています。詳細は本記事では割愛します。
https://dev.classmethod.jp/cloud/aws/how-to-use-s3-cross-account/

# Glueでクロスアカウント書込み
少し書こうと思いましたが、時間切れなので、有力情報を頂いた非常に貴重な記事を紹介して終わりにしちゃいます。
https://qiita.com/pioho07/items/f13cf2714d40cb6eefce

# おわりに
いかがでしたでしょうか。クロスアカウントにも３つの方法があり、それぞれ微妙に特性が違う。
しかもかなりよくドキュメントを読み込まないとその特性の違いが本当にわかりにくく、どの方法を選択してよいのか判断が結構難しいのではと思います。少なくとも私はそれなりに時間がかかりました。

実は本記事では、クロスアカウントS3アクセスだけでなく、クロスアカウントS3アクセスで更に別アカウントのKMS鍵を利用するケース、しかもGlueを織り交ぜつつを書こうと思ったのですが、思いの外情報が多くてアドベントカレンダーの〆切に間に合わなそう＆記事もデカくなり過ぎだったので急遽KMSのネタは削りましたｗ
この記事を書きたかった真の動機は`「クロスアカウント」「Glue」「KMS」が揃うと、もうわけわからねー！ちくしょー！`でしたので、そのうち書けたら書きたいと思います。

ありがとうございました。