Unity Catalogでクロスアカウントの外部ロケーションと外部テーブルを作成する

ハマったのでメモ。

外部ロケーションとはUnity Catalogでクラウドストレージにアクセスする際に使用するオブジェクトです。ストレージ資格情報と組み合わせて使用します。作成方法はこちら。

https://qiita.com/taka_yayoi/items/18fee92365eee58e0b94

Databricksワークスペースとアクセス先のS3が同じAWSアカウントであれば上の手順でOKなのですが、クロスアカウントの際は？となった次第です。

結論は、

1. **S3があるAWSアカウント**でS3にアクセスするためのIAMロールの信頼ポリシーにUnity Catalogの信頼関係を追加。
1. 上記S3にアクセスするためのIAMロールのARNをコピー。
1. Databricksワークスペースで上記ARNを用いてストレージ資格情報を作成。
1. 上記ストレージ資格情報を用いて外部ロケーションを作成。

という流れでした。すなわち、**DatabricksがデプロイされているAWSアカウントではAWSの設定変更は不要**ということです。

# S3があるAWSアカウントでの作業

1. S3にアクセスするためのIAMロールがない場合には[作成](https://qiita.com/taka_yayoi/items/18fee92365eee58e0b94#%E3%82%B9%E3%83%86%E3%83%83%E3%83%971-iam%E3%83%AD%E3%83%BC%E3%83%AB%E3%81%AE%E4%BD%9C%E6%88%90%E6%9B%B4%E6%96%B0)します。
1. このIAMロールの信頼ポリシーに以下の内容を追加します。ここでは、`<bucket-owner-acct-id>`はS3のあるAWSアカウントのアカウントID、`bucket-owner-acct-s3-access-role`はS3にアクセスするためのIAMロール、`<DatabricksアカウントID>`は、Unity Catalogが稼働しているDatabricksのアカウントIDです。

    ```json:JSON
   {
      "Effect": "Allow",
      "Principal": {
        "AWS": [
          "arn:aws:iam::414351767826:role/unity-catalog-prod-UCMasterRole-14S5ZJVKOTYTL",
          "arn:aws:iam::<bucket-owner-acct-id>:role/bucket-owner-acct-s3-access-role"
         ]
      },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": {
          "sts:ExternalId": "<DatabricksアカウントID>"
        }
      }
    }
    ```

1. 上記IAMロールのS3へのアクセス権を適宜[設定](https://qiita.com/taka_yayoi/items/18fee92365eee58e0b94#%E3%82%B9%E3%83%86%E3%83%83%E3%83%971-iam%E3%83%AD%E3%83%BC%E3%83%AB%E3%81%AE%E4%BD%9C%E6%88%90%E6%9B%B4%E6%96%B0)します。
1. `bucket-owner-acct-s3-access-role`のARNをコピーしておきます。

# Databricksでの作業

## ストレージ資格情報の作成

1. メタストア管理者あるいはアカウント管理者でDatabricksにログインします。
1. サイドメニューの**データ**を選択します。
![Screenshot 2023-01-26 at 20.41.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/12ba546e-cfd2-1a99-8733-661360e1dc01.png)
1. **ストレージ資格情報**をクリックします。
![Screenshot 2023-01-26 at 20.43.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/f2b139b9-05fe-f05b-b706-dcf2b2a09cd0.png)
1. **資格情報を作成**ボタンをクリックします。
    - わかりやすい名称をつけます。
    - IAMロールには上のステップでコピーしたARNを貼り付けます。
    - コメントを入力します。
![Screenshot 2023-01-26 at 20.44.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/cc71b9e9-926b-3ab4-7892-5214a8336dc3.png)
1. **作成**をクリックします。

## 外部ロケーションの作成

1. **外部ロケーション**をクリックします。
![Screenshot 2023-01-26 at 20.46.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/126df005-53ee-de8a-5e35-1caec1e69e6d.png)
1. **ロケーションを作成**ボタンをクリックします。
    - わかりやすい名称をつけます。
    - URLにはアクセス先のS3のURLを入力します。
    - ストレージ資格情報では、上で作成したストレージ資格情報を選択します。
    - コメントを入力します。
![Screenshot 2023-01-26 at 20.47.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/cf92f657-dc7b-db87-5d57-3ea18a6dcbb8.png)
1. **作成**をクリックします。

これでクロスアカウントの外部ロケーションが作成できました。
![Screenshot 2023-01-26 at 20.49.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/fe2370bc-1f1e-5ed2-b5e4-70139a8abf84.png)

:::note
**注意**
外部ロケーションを作成する際には接続チェックが行われます。エラーになる場合にはIAMの設定を確認してください。
:::

# 接続確認

1. Unity Catalogにアクセスするためのクラスターを作成します。[パーソナルコンピュート](https://qiita.com/taka_yayoi/items/4ccb265729775b54cca6)がお手軽でおすすめです。
![Screenshot 2023-01-26 at 20.51.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/1f3218fd-4896-e040-d106-7e22f68ac7a5.png)
1. SQLノートブックを作成してクラスターにアタッチします。
1. 外部ロケーションの詳細を確認します。

    ```sql:SQL
    DESCRIBE EXTERNAL LOCATION `cross-account-s3-external-location`;
    ```
1. メタデータを確認することができます。
![Screenshot 2023-01-26 at 20.52.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/4a2c9304-a36e-886a-0f8a-38f9e7c1b8d4.png)
1. 外部ロケーションの中身を確認します。

    ```sql:SQL
    LIST 's3://ty-databricks-cross-account-bucket';
    ```
    ![Screenshot 2023-01-26 at 20.53.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/c7cfb85a-8499-4b13-cc3a-9e926de1cc34.png)

[クロスアカウントの設定](https://qiita.com/taka_yayoi/items/e7e86c02c8dcb567c717)をするよりもお手軽です。

さらにこれを活用してテーブルを作成していきます。

# 外部テーブルの作成

データ読み込み元として上で作成した外部ロケーションを利用できることに加え、外部ロケーションにテーブルを作成することができます。

なお、以前は[DBFSマウント](https://qiita.com/taka_yayoi/items/897264c486e179d72247#dbfs%E3%81%B8%E3%81%AE%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%82%B9%E3%83%88%E3%83%AC%E3%83%BC%E3%82%B8%E3%81%B8%E3%81%AE%E3%83%9E%E3%82%A6%E3%83%B3%E3%83%88)を使用していた方もいらっしゃると思いますが、現状の外部ロケーションではマウントポイントではなく、直パスを指定する形になります。

## 元データの準備

ここでは、デモの目的もありサンプルデータを外部ロケーションに作成します。直パス`s3://ty-databricks-cross-account-bucket/testParquet`を指定して、Parquetを保存します。

```scala:Scala
%scala 
 
case class MyCaseClass(key: String, group: String, value: Int, someints: Seq[Int], somemap: Map[String, Int])
val dataframe = sc.parallelize(Array(MyCaseClass("a", "vowels", 1, Array(1), Map("a" -> 1)),
  MyCaseClass("b", "consonants", 2, Array(2, 2), Map("b" -> 2)),
  MyCaseClass("c", "consonants", 3, Array(3, 3, 3), Map("c" -> 3)),
  MyCaseClass("d", "consonants", 4, Array(4, 4, 4, 4), Map("d" -> 4)),
  MyCaseClass("e", "vowels", 5, Array(5, 5, 5, 5, 5), Map("e" -> 5)))
).toDF()
// now write it to disk
dataframe.write.mode("overwrite").parquet("s3://ty-databricks-cross-account-bucket/testParquet")
```

クロスアカウントのS3でディレクトリが作成されていることを確認できます。
![Screenshot 2023-01-27 at 8.27.48.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/f6a9aeb6-f079-6cc7-c8e8-078a5162fa94.jpeg)

外部ロケーションに格納されているファイルに直接クエリーを行うことも可能です。

```sql:SQL
SELECT * FROM parquet.`s3://ty-databricks-cross-account-bucket/testParquet`
```
![Screenshot 2023-01-27 at 8.06.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/ed50e700-b5f5-a069-701d-48ba320c6754.png)

Python(PySpark)でも同じことができます。

```py:Python
%python
display(spark.read.format("parquet").load("s3://ty-databricks-cross-account-bucket/testParquet"))
```

SQLがメインのワークロードの場合、毎回直パスを指定するのは面倒ですし、可読性も低下します。DWHと同じようにデータベース、テーブルのパラダイムでデータを活用できるように、Unity Catalogにテーブルに登録します。DBMS同様、`CREATE TABLE`、行のインサートという流れになります。

なお、Unity Catalogにおいては3レベルの名前空間を使用します。`カタログ.スキーマ(データベース).テーブル`という形式でテーブルにアクセスします。

## カタログの作成

1. サイドメニューの**データ**をクリックして、データエクスプローラに移動します。
![Screenshot 2023-01-27 at 8.14.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/47ee9621-b0a9-4512-a103-a867953f5768.png)
1. **カタログを作成**をクリックしてカタログの情報を入力します。
    - カタログ名は`external_catalog`とします。
    - **マネージドロケーション**には、カタログに格納されるテーブルデータを格納するロケーションを指定します。ここでは、[上のステップ](#外部ロケーションの作成)で作成した外部ロケーションのパスを指定します。
![Screenshot 2023-01-27 at 8.22.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/20261c0a-348b-4fdb-1bb4-40f4f57aa14f.png)
1. **作成**をクリックするとカタログが外部ロケーションに作成されます。
![Screenshot 2023-01-27 at 8.23.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/231bceb3-5196-7bb6-756e-6e54ea9a8dca.png)

カタログを作成すると、その配下にデフォルトスキーマ(データベース)`default`が作成されるので、以降ではこちらを使用します。なお、別途スキーマを作成することも可能です。

## テーブルの作成

ノートブックに戻って、以下のコマンドを実行します。`test`という空のテーブルが`external_data.default`に作成されます。

```sql:SQL
CREATE TABLE external_catalog.default.test;
```

以下の[COPY INTO](https://docs.databricks.com/sql/language-manual/delta-copy-into.html)コマンドを実行することで、`s3://ty-databricks-cross-account-bucket/testParquet`をテーブル`external_catalog.default.test`に読み込みます。

```sql:SQL
COPY INTO external_catalog.default.test
FROM (
  SELECT *
  FROM 's3://ty-databricks-cross-account-bucket/testParquet'
)
FILEFORMAT = parquet COPY_OPTIONS ("mergeSchema" = "true");
```

クロスアカウントのS3バケットを確認すると、ディレクトリが作成されていることを確認することができます。
![Screenshot 2023-01-27 at 8.27.48.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/42f7b998-70e6-ea2c-97fb-0c5ad5e1ee7d.jpeg)

これで、`external_catalog.default.test`でテーブルにアクセスできるようになります。

```sql:SQL
SELECT * FROM external_catalog.default.test;
```
![Screenshot 2023-01-27 at 8.34.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/5124eec8-b062-54f9-2852-89ee97ba1c89.png)

データエクスプローラでも確認できます。
![Screenshot 2023-01-27 at 8.38.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/6105e0d2-1666-f177-a939-01acb8dae701.png)
![Screenshot 2023-01-27 at 8.39.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/984cbcdf-d61b-a789-956b-73a013fadd53.png)

# 権限の設定

この状態では作成したテーブルに他のユーザーがアクセスできません。

1. カタログを選択し、**権限**を開き、**付与**をクリックします。
![Screenshot 2023-01-27 at 12.13.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/a7f33219-1671-5f45-937b-e600eeca1c56.png)
1. ここではすべてのユーザーに読み取り権限を与えるものとします。**ユーザーとグループ**では**account users**を選択し、**権限プリセット**では**データリーダー**を選択します。
![Screenshot 2023-01-27 at 12.13.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/bd5bf354-4850-6f2a-65c4-8a0815580deb.png)
1. **付与**をクリックすることで権限が付与されます。
![Screenshot 2023-01-27 at 12.16.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/2ce5e447-9b1f-d7a9-462b-a23e81809a53.png)


### Databricks 無料トライアル

[Databricks 無料トライアル](https://databricks.com/jp/try-databricks)