Databricks の UniForm が有効なテーブルを Snowflake にて Secure Data Sharing する方法

## 概要

Databricks の UniForm が有効になっているテーブルを、Snowflake の Apache Iceberg テーブル（外部カタログを使用するテーブル）として登録し、別の Snowflake アカウントに共有する手順を紹介します。

本記事は下記記事の一部です。

- [Databricks のデータを Snowflake の Secure Data Sharing 機能でデータ共有する際のベストプラクティス #deltalake - Qiita](https://qiita.com/manabian/items/7469be5112319c4c0080)

## 手順

### Databricks 上でテーブルを作成

まず、データベースとテーブルを作成し、データを挿入します。

```python
%sql
CREATE CATALOG IF NOT EXISTS sf_iceberg;
```

```python
%sql
CREATE TABLE sf_iceberg.default.nation_iceberg (
  n_nationkey  integer
  ,n_name       string
  ,n_regionkey  integer 
  ,n_comment    string
)
TBLPROPERTIES(
  'delta.columnMapping.mode' = 'name',
  'delta.enableIcebergCompatV2' = 'true',
  'delta.universalFormat.enabledFormats' = 'iceberg'
);
```

```python
%sql
INSERT OVERWRITE sf_iceberg.default.nation_iceberg
SELECT
  n_nationkey
  ,n_name
  ,n_regionkey 
  ,n_comment
FROM
  samples.tpch.nation
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/cdb45546-25cf-4bc3-bcc8-827925c507d3.png)

テーブルのデータを取得できることを確認します。

```python
%sql
SELECT * FROM sf_iceberg.default.nation_iceberg;
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/2a77687c-524e-4a1f-b0f7-275f7b0c06cf.png)

### Snowflake で利用する Databricks のサービスプリンシパルを作成

Databricks SDK を使用して、Snowflake で利用するサービスプリンシパルを作成します。

```python
from databricks.sdk import WorkspaceClient

client = WorkspaceClient()
```

```python
service_account = client.service_principals.create(
    display_name="snowflake-share-account",
)
print("Service account created with ID:", service_account.id)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/5f599ed4-8ab2-4852-97ff-5bb3fbea59f1.png)

次に、このサービスプリンシパルに対してデータベースオブジェクトに対する権限を付与します。

```python
application_id = service_account.application_id
spark.sql(f"""
GRANT USE CATALOG ON CATALOG sf_iceberg
    TO `{application_id}`;
""").display()
spark.sql(f"""
GRANT SELECT ON SCHEMA sf_iceberg.default
    TO `{application_id}`;
""").display()
```

続いて、サービスプリンシパルの OAuth シークレットを取得します。まず、Databricks Workspace の右上にあるアカウントメニューから `設定` を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/16d983e5-6763-4967-92b8-ab68291f0ffd.png)

`IDとアクセス` タブを選択し、`サービスプリンシパル` の `管理` を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/50892511-72b2-4709-a775-8e7846180077.png)

先ほど作成したサービスプリンシパルを選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/5f6440b2-3ed9-4227-8a79-4aca620a4664.png)

`シークレット` タブを開き、`シークレットを生成` をクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/eb3200b9-5758-4459-9124-77a41b4b86ec.png)

存続期間を任意の日数に設定し、`生成` を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/6bc264f1-ca93-40d3-9301-4388d7361029.png)

表示されたシークレットの値を控えます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/74ccf595-93f4-4b1a-9155-b8652106d422.png)

### 共有元 Snowflake にテーブル作成

まず、カタログ統合を作成します。

```sql
CREATE OR REPLACE CATALOG INTEGRATION {catalog_integration_name}
  CATALOG_SOURCE = ICEBERG_REST
  TABLE_FORMAT = ICEBERG
  CATALOG_NAMESPACE = 'default'
  REST_CONFIG = (
    CATALOG_URI = '{workspace_url}/api/2.1/unity-catalog/iceberg',
    WAREHOUSE = '{catalog_name}'
  )
  REST_AUTHENTICATION = (
    TYPE = OAUTH
    OAUTH_TOKEN_URI = '{workspace_url}/oidc/v1/token'
    OAUTH_CLIENT_ID = '{client_id}'
    OAUTH_CLIENT_SECRET = '{secret}'
    OAUTH_ALLOWED_SCOPES = ('all-apis', 'sql')
  )
  ENABLED = TRUE;
```

| プレースホルダー             | 説明                                                         |
| ---------------------------- | ------------------------------------------------------------ |
| `{catalog_integration_name}` | カタログ統合の名前を指定します。統合設定を一意に識別するための名称です。 |
| `{workspace_url}`            | Databricks ワークスペースの URL を指定します。               |
| `{catalog_name}`             | Databricks のカタログ名を指定します。                        |
| `{client_id}`                | OAuth 認証のクライアント ID を指定します。                   |
| `{secret}`                   | OAuth 認証のシークレットを指定します。                       |

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/335331dc-192a-4cab-acb9-ceb1dfe90405.png)

次に、Snowflake で外部 Volume を作成します。

```sql
CREATE OR REPLACE EXTERNAL VOLUME {external_volume_name}
STORAGE_LOCATIONS = (
    (
        NAME = '{name}'
        STORAGE_PROVIDER = 'AZURE'
        STORAGE_BASE_URL = 'azure://{azure_storage_account_name}.blob.core.windows.net/{azure_storage_container_name}'
        AZURE_TENANT_ID = '{tenant_id}'
    )
);
```

| プレースホルダー                 | 説明                                                                                             |
| -------------------------------- | ------------------------------------------------------------------------------------------------ |
| `{external_volume_name}`         | Snowflake の外部 Volume 名を指定します。                                                         |
| `{name}`                         | Snowflake の外部 Volume の名前を指定します。                                                     |
| `{azure_storage_account_name}`   | Azure ストレージアカウントの名前を指定します。                                                   |
| `{azure_storage_container_name}` | Azure ストレージアカウントのコンテナ名を指定します。                                             |
| `{tenant_id}`                    | Azure のテナント ID を指定します。                                                               |

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/a58cfccb-248f-4e7b-b27c-63f087ea03ab.png)

続いて、下記クエリで `AZURE_CONSENT_URL` の値を取得します。

```sql
DESC EXTERNAL VOLUME azure_sf_share_exvol;
SELECT 
    PARSE_JSON($4):AZURE_MULTI_TENANT_APP_NAME::string AS AZURE_MULTI_TENANT_APP_NAME,
    PARSE_JSON($4):AZURE_CONSENT_URL::string AS AZURE_CONSENT_URL
FROM TABLE(RESULT_SCAN(LAST_QUERY_ID()))
WHERE $2 = 'STORAGE_LOCATION_1';
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/f47482c9-6c19-4fec-9828-abe84b94daa5.png)

Azure にログインしているブラウザで `AZURE_CONSENT_URL` にアクセスし、表示された画面で `承諾` を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/c2e25baf-8add-43b4-932a-c6ad3a4dd8e8.png)

すると、このサービスプリンシパルが Azure テナントに追加されます。そのため、必要に応じてストレージに対する権限（例：`ストレージ BLOB データ共同作成者`）を付与します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/78fe9e82-3de3-4fde-b75c-64449a6ce96d.png)

外部 Volume へのアクセスが問題なく行えるか、下記クエリで確認します。

```sql
SELECT SYSTEM$VERIFY_EXTERNAL_VOLUME('azure_sf_share_exvol');
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/a805e4ed-04a6-4f91-826e-2c79def35cd8.png)

次にデータベースを作成します。

```sql
CREATE DATABASE IF NOT EXISTS databricks_uf_share_db;
USE databricks_uf_share_db;
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/4720e0ac-c0a0-47fb-b31c-ab78c90e3fca.png)

Databricks の UniForm が有効なテーブルを、Snowflake 上で Iceberg テーブルとして作成します。

```sql
CREATE OR REPLACE ICEBERG TABLE nation_iceberg_from_db_by_uf
    EXTERNAL_VOLUME = 'azure_sf_share_exvol'
    CATALOG = 'databricks_uniform_catalog_integration'
    CATALOG_TABLE_NAME = 'nation_iceberg'
    CATALOG_NAMESPACE = 'default'
    AUTO_REFRESH = TRUE;
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/3d3a039f-7c93-40a7-8248-9ce725d02fa0.png)

テーブルからデータが正しく取得できることを確認します。

```sql
SELECT * FROM nation_iceberg_from_db_by_uf;
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/19b3424f-5bea-4faa-8fed-f049c7a7cfdb.png)

### データ共有先アカウントを作成

はじめに、`ORGADMIN` ロールに切り替えます。

```sql
-- ORGADMIN にカレントロールを変更
USE ROLE ORGADMIN;
SELECT CURRENT_ROLE();
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/4375b4e5-b01c-4bd8-ac02-57987160fa47.png)

データ共有先の Snowflake アカウントを新規で作成します。

```sql
-- データ共有先の Snowflake アカウントを作成
-- ToDo EMAIL と REEGION をかえる必要あり
CREATE ACCOUNT manabian_comsumer_01_account
   ADMIN_NAME = 'comsumer_admin'
   ADMIN_PASSWORD = 'MySecurePassword2025!'
   EMAIL = 'admin@example.com'
   MUST_CHANGE_PASSWORD = FALSE
   EDITION = ENTERPRISE
   REGION = 'AZURE_EASTUS2';
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/883211f2-e689-4faf-87ba-898cb52d0e2b.png)

次に、データ共有先の新アカウントの URL を取得します。

```sql
-- アカウントの URL を取得
SHOW ACCOUNTS LIKE 'manabian_comsumer_01_account';
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/d639b85c-da0f-4d1e-9ba5-c57690b783ec.png)

上記で作成したアカウントにログインします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/9c5191bb-b019-4f80-ac51-7d1e84fe3145.png)

ワークスペースで下記クエリを実行し、`Data Sharing Account Identifier` の値を取得します。必要に応じてクラスタを作成してください。

```sql
SELECT
    CURRENT_ORGANIZATION_NAME() || '.' || CURRENT_ACCOUNT_NAME() AS "Data Sharing Account Identifier";
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/4707d96e-d2cf-4531-a963-ac1705d3147f.png)

### 共有元 Snowflake アカウントにて共有の作成と権限付与

共有を作成します。

```sql
CREATE OR REPLACE SHARE db_by_uf_share;
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/5390b20e-7e3f-4c53-9da3-485f40bac132.png)

続いて、共有に対する権限を付与します。

```sql
GRANT USAGE ON DATABASE databricks_uf_share_db TO SHARE db_by_uf_share;
GRANT USAGE ON SCHEMA databricks_uf_share_db.public TO SHARE db_by_uf_share;
```

```sql
GRANT SELECT ON ICEBERG TABLE nation_iceberg_from_db_by_uf TO SHARE db_by_uf_share;
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/99214b40-7c19-4dee-9b37-7ac61bf937d3.png)

先ほど取得した Data Sharing Account Identifier を使用して、共有先アカウントがこの共有を利用できるよう設定します。

```sql
-- 共有先アカウントで取得した Data Sharing Account Identifier の値をセット
SET data_sharing_account_identifier = 'LAMPVRN.MANABIAN_COMSUMER_01_ACCOUNT';
ALTER SHARE db_by_uf_share
    ADD ACCOUNTS = $data_sharing_account_identifier;
```

### 共有先 Snowflake アカウントにて共有に基づきデータベースを作成

共有先の Snowflake アカウントで、`ACCOUNTADMIN` ロールに切り替えます。

```sql
USE ROLE ACCOUNTADMIN;
SELECT CURRENT_ROLE();
```

次に、`NAME` が `DB_BY_UF_SHARE` の `owner_account_and_name` を確認します。

```sql
SHOW SHARES;
SELECT
    CONCAT($3, '.', $4) AS owner_account_and_name
    ,$3 AS owner_account
    ,$4 AS name
FROM TABLE(RESULT_SCAN(LAST_QUERY_ID()));
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/b7ef7ff7-024e-4308-8bf8-37ac7edad23b.png)

取得した `owner_account_and_name` をもとに、共有されたテーブルを含むデータベースを作成します。

```sql
CREATE OR REPLACE DATABASE SHARE_BY_DB_UF_DB
    FROM SHARE LAMPVRN.OFA41652.DB_DELTA_UF_SHARE;
USE DATABASE SHARE_BY_DB_UF_DB;
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/58f310f3-f322-4510-afc3-1c886a032bab.png)

テーブルにアクセスできるか確認します。

```sql
SHOW TABLES IN DATABASE SHARE_BY_DB_UF_DB;
```

```sql
SELECT * FROM NATION_ICEBERG_FROM_DB_BY_UF;
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/bd3140bb-7011-4a49-a68d-f670c286ce97.png)

### Databricks にて DELETE 実行後の 共有先 Snowflake の反映確認

Databricks 側でテーブルのデータを削除してみます。

```python
%sql
DELETE FROM sf_iceberg.default.nation_iceberg WHERE n_nationkey > 3;
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/5b95162e-0e74-49b5-aed1-537c24f03615.png)

共有先の Snowflake でデータが反映されているか確認します。

```sql
SELECT count(*) FROM NATION_ICEBERG_FROM_DB_BY_UF;
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/3ee9c3e4-342e-4187-b4ff-3d1cfae69df8.png)