Athenaで別アカウントのS3をクエリーする最短手順

## やりたいこと
別アカウントのS3バケットにあるデータをAthenaからクエリーしたい。
別アカウント側のGlueカタログ（Glueのクロスアカウントアクセス）は使わず、あくまで自アカウントのGlueカタログに別アカウントのS3バケットを登録して、検索を実行するのがゴール。

## 前提
- アカウントA：読み取られる側
- アカウントB：読み取る側

とする。
こんなイメージ。
![スクリーンショット 2021-03-13 午前1.05.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/609876/905f9b88-7fc5-dca0-a7f2-a73016ad9414.png)

## ステップバイステップ

### アカウントAの準備

- S3バケットを作成し、テストデータを置く。
    - バケット名は`athena-crossaccount-20210310`とする。
    - テストデータは[こちらの記事](https://qiita.com/itsuki3/items/a7988752562cb663842b)で使用したものを再利用する。
- クロスアカウントアクセスを許可するため、バケットポリシーを設定する。
    - アカウントBでこの後作成されるIAMロールに対してのみ許可する。ロール名は`AthenaCrossAccountRole`と仮定。

>（2021/6/2追記）
> 存在していないロールをバケットポリシーに入れることはできないので、この手順は正しくは「アカウントBの準備」で`AthenaCrossAccountRole`を作成した後に実行するか、バケットポリシー保存直前で寸止めしておく必要があります。
（ご指摘いただいた@cols-wisteriaさん、ありがとうございました！）

```json:バケットポリシー
{
    "Version": "2012-10-17",
    "Id": "Policy1495458089866",
    "Statement": [
        {
            "Sid": "Policy_GetObject",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::2XXXXXXXXXX2:role/AthenaCrossAccountRole"
            },
            "Action": [
                "s3:GetObject",
                "s3:ListBucket"
            ],
            "Resource": [
                "arn:aws:s3:::athena-crossaccount-20210310",
                "arn:aws:s3:::athena-crossaccount-20210310/*"
            ]
        }
    ]
}
```


### アカウントBの準備

- クエリー結果保管バケットを作り、Athenaワークグループ"primary"（デフォルト）にセットする。
    - ここでは`athena-query-result-4-sandbox`とした。
- Athenaでクエリーを実行するためのIAMポリシー`AthenaCrossAccountAccessPolicy `を作成する。
  - `AmazonAthenaFullAccess`というマネージドポリシーがあるので、これを参考に作る。
  - ちなみに上記のマネージドポリシーは`s3:GetObject`権限がチュートリアル用バケットと（デフォルト仕様の）クエリー結果保管バケットにしかないので、そのままでは現場で使えない。他にもせっかく絞った`s3:ListBucket`が後段で全開放されてたりと若干つっこみどころの多いポリシーだが、ここでは細かいことは措いて先に進むことにする。
- IAMロール`AthenaCrossAccountRole`を作成し、上記ポリシーをアタッチする。
  - IAMロールではなくIAMユーザーにアタッチしてもよいが、今回は多少汎用性を高める必要があったのでロールとし、このロールにアカウントAからスイッチロールできるように設定（アカウントAをAssumeRoleの信頼元Principalに指定）した。
  - 実際には、ロールの信頼元をEC2やLambdaに指定して、プログラマティックにAthenaを呼び出すイメージ。

```json:Athena用ポリシー
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "athena:*"
            ],
            "Resource": [
                "*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "glue:CreateDatabase",
                "glue:DeleteDatabase",
                "glue:GetDatabase",
                "glue:GetDatabases",
                "glue:UpdateDatabase",
                "glue:CreateTable",
                "glue:DeleteTable",
                "glue:BatchDeleteTable",
                "glue:UpdateTable",
                "glue:GetTable",
                "glue:GetTables",
                "glue:BatchCreatePartition",
                "glue:CreatePartition",
                "glue:DeletePartition",
                "glue:BatchDeletePartition",
                "glue:UpdatePartition",
                "glue:GetPartition",
                "glue:GetPartitions",
                "glue:BatchGetPartition"
            ],
            "Resource": [
                "*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:GetBucketLocation",
                "s3:GetObject",
                "s3:ListBucket",
                "s3:ListBucketMultipartUploads",
                "s3:ListMultipartUploadParts",
                "s3:AbortMultipartUpload",
                "s3:CreateBucket",
                "s3:PutObject"
            ],
            "Resource": [
                "arn:aws:s3:::athena-query-result-4-sandbox",
                "arn:aws:s3:::athena-query-result-4-sandbox/*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:GetObject",
                "s3:ListBucket"
            ],
            "Resource": [
                "arn:aws:s3:::athena-crossaccount-20210310",
                "arn:aws:s3:::athena-crossaccount-20210310/*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:ListBucket",
                "s3:GetBucketLocation",
                "s3:ListAllMyBuckets"
            ],
            "Resource": [
                "*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "sns:ListTopics",
                "sns:GetTopicAttributes"
            ],
            "Resource": [
                "*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "cloudwatch:PutMetricAlarm",
                "cloudwatch:DescribeAlarms",
                "cloudwatch:DeleteAlarms"
            ],
            "Resource": [
                "*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "lakeformation:GetDataAccess"
            ],
            "Resource": [
                "*"
            ]
        }
    ]
}
```

- Glueデータベースを作成する。
 - Glueデータベースは、Crawlerを使う場合もAthenaでDDLを実行する場合も、テーブルを格納するために等しく必要。
 - ここでは`athena_crossaccount_20210310`とする。
- DDLを実行する。
 - テーブルについては、CrawlerかDDL（あるいはCloudFormationか）のどちらかだけ必要。
 - JSONLであればCrawlerで自動認識されるスキーマでほぼほぼ無修正で行けるはずだが、ここではAthena単体でどういう権限が必要かを切り分けたいので、あえてDDLで作成する。
 - SerdeにはHiveとOpenXが選択可能だが、Glue Crawlerでは後者が使われていること、実際にクエリーしてみるとOpenXの方がエラーが少ないことから、ここではOpenXを選択。

```sql
CREATE EXTERNAL TABLE jsontest (
	id int,
	name string,
	age int
)
ROW FORMAT serde 'org.openx.data.jsonserde.JsonSerDe'
with serdeproperties ( 'paths'='age, id, name' )
LOCATION 's3://athena-crossaccount-20210310/';
```

### クエリー実行

- マネジメントコンソールで、アカウントAから、アカウントBの`AthenaCrossAccountRole`にスイッチ。
- Athenaでクエリーを実行。
- 成功。
![スクリーンショット 2021-03-16 午後5.18.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/609876/e63224fb-35fd-ed11-209b-722c1b84ec3f.png)

### ハマリポイント

- SerdeをHiveで作成すると、このデータセットではエラーとなった（nullが原因かも）。OpenXの方が融通利く模様。
- Glueカタログを介して外部テーブルを読みに行く以上、Glueサービスロールにもクロスアカウント権限が必要か？とも思ったが、Glue CrawlerやGlue Jobを使わない限りはそもそもそのロール使わないので、今回試したロールのみバケットポリシーで許可すればOKだった。
- 今回はAthena、というかAthenaの実行主体であるIAMロール（`AthenaCrossAccountRole`）は、クロスアカウントアクセスにassumeRoleを使ってない（あくまで検証上、アカウントBにスイッチする際にassumeRoleしているだけで、クロスアカウントのクエリー自体はS3のリソースベースポリシーを介して、ロールをスイッチすることなく実行されている）。assumeRoleを用いるパターンだと、sts:AssumeRoleの付与やリソース/プリンシパル縛りなど、これはこれで細々とした設定が必要になる。はず。