AWS Clean Rooms（プレビュー）を触ってみた

# はじめに
昨年末のre:InventでAWS Clean Roomsというサービスが発表されました。

https://aws.amazon.com/jp/clean-rooms/

昨今、データクリーンルームという言葉が聞かれ始めたので、AWSはどういうサービスを提供するのか興味ありました。

そして先日プレビューが開始されました。

https://aws.amazon.com/jp/about-aws/whats-new/2023/01/aws-clean-rooms-now-available-preview/

なので触ってみた内容や、所感などを書いていきます。
まだプレビュー版であり、私自身素人ですので、判断される際は他の情報と照らし合わせたり、自身で試用して確認してください。

# サービスの概要

- 「コラボレーション」という場が作れます
- コラボレーションには、AWSアカウントが参加できます
    - メンバーと呼ぶようです
    - 最大5メンバー
- メンバーには2種類あります
    - クエリを実行して結果を受け取ることができるメンバー
        - １メンバーのみ
    - データを提供するメンバー
        - 上記メンバー以外
- 料金は、クエリ実行に対してのみ
    - コラボレーション作成・維持にはかからない？
    - 「どのデータを提供する？」の設定も対象外？
    - 公式の料金説明：https://aws.amazon.com/jp/clean-rooms/pricing/
- データ提供用に別途リソースを作る必要がなさそう
    - 専用のIAMロールは必要
- 見せるカラムを選択する
- 許可する集計の列と集計方法も選択する

使っていて、以下のようなイメージが浮かびました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/4b5e8a61-7221-ec73-099c-f85f4cb7de2f.png)


# やってみた

AWSアカウントを２つ用意して、「受領企業」と「提供企業」として扱います。

## コラボレーション作成

「受領企業」アカウントにて、コラボレーション作成をクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/6f6c1f3a-478f-9286-7f34-c7aa895ec195.png)

詳細にて、名前と説明を入力します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/33e460b1-2676-1d6e-a51e-02b722241874.png)

メンバーにアカウントを指定します。自アカウントのIDはデフォルトで入っています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/b6bce4c6-f54d-6558-de43-26c7b1eaefea.png)

「メンバー能力」の箇所で、データを見せてもらうアカウントを指定します。今回は「受領企業」アカウントを指定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/467fbf02-b2ab-dc59-1cb2-ad17a1d4abee.png)

それ以降はチェックなしで作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/c5918222-f901-e79a-fd75-ef7b71509f1d.png)

確認画面が出ますので、作成及び参加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/824af80b-c8eb-4c32-821a-0d5e50da239c.png)

作成直後で、「提供企業」アカウントが招待を受領する前の状況は以下のようになっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/4df61727-e2f3-f9f4-8df6-4f1358727a4c.png)

## 招待を受ける

招待を受けたアカウント側では、先のコラボレーションが参加可能タブに表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/29f0f351-1671-8b88-c150-7a942ab1b21e.png)

招待を受けるには、メンバーシップを作成、から行います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/8de0f918-71e7-8ff1-b5d1-7aa4a0916418.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/99b88a72-608d-c319-17aa-c1f774aac475.png)
メンバーシップを作成されると、ステータスがアクティブになりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/bebc9e46-f5a3-b75b-4d04-9ff013505b6c.png)


役割（メンバー能力）で、表示されるタブに違いがありました。

受領企業アカウントでは、「クエリ」があります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/6cf9b7de-eb73-7ce6-02dd-2fa71a5d603e.png)

一方、提供企業アカウントにはありません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/91649faf-13c4-d8e5-36fb-62909a7666de.png)

## データ提供（Glueテーブル公開）

提供企業アカウント側でデータを用意し、受領企業アカウント側に公開していきます。

Glueテーブルを公開するので、関連するリソースをCloudFormationで作ります。

<details><summary>クリックで表示</summary><div>

データ格納バケット、Athena結果出力バケット、Athenaのワークグループ、Glueデータベースを作ります。昔のものを流用しているので、一部アウトプットが余計ですがご容赦ください。
```yaml
AWSTemplateFormatVersion: '2010-09-09'

Parameters:
  Env:
    Type: String

Resources:
  RawDataBucket:
    Type: AWS::S3::Bucket
    Properties: 
      BucketName: !Sub raw-data-${AWS::AccountId}-${AWS::Region}-${Env}
      BucketEncryption: 
          ServerSideEncryptionConfiguration: 
            - 
              ServerSideEncryptionByDefault: 
                  SSEAlgorithm: 'AES256'
              BucketKeyEnabled: false
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true

  AthenaQueryResultBucket:
    Type: AWS::S3::Bucket
    Properties: 
      BucketName: !Sub query-result-${AWS::AccountId}-${AWS::Region}-${Env}
      BucketEncryption: 
          ServerSideEncryptionConfiguration: 
            - 
              ServerSideEncryptionByDefault: 
                  SSEAlgorithm: 'AES256'
              BucketKeyEnabled: false
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      LifecycleConfiguration: 
        Rules: 
          - 
            Id: 'auto-delete'
            Status: 'Enabled'
            ExpirationInDays: 7

  AthenaWorkGroup:
    Type: AWS::Athena::WorkGroup
    Properties:
      Name: !Sub athena-work-group-${Env}
      RecursiveDeleteOption: true
      WorkGroupConfiguration:
        ResultConfiguration:
          OutputLocation: !Sub s3://${AthenaQueryResultBucket}/data
          EncryptionConfiguration: 
            EncryptionOption: 'SSE_S3'
        EnforceWorkGroupConfiguration: true
        PublishCloudWatchMetricsEnabled: true

  GlueDatabase:
    Type: AWS::Glue::Database
    Properties: 
      CatalogId: !Ref AWS::AccountId  
      DatabaseInput:
        Name: !Sub glue-database-${Env}

Outputs:
  RawDataBucket:
    Value: !Ref RawDataBucket
    Export:
      Name: !Sub "${Env}-RawDataBucket-Name"
  AthenaQueryResultBucket:
    Value: !Ref AthenaQueryResultBucket
    Export:
      Name: !Sub "${Env}-AthenaQueryResultBucket-Name"
  AthenaWorkGroup:
    Value: !Ref AthenaWorkGroup
    Export:
      Name: !Sub "${Env}-AthenaWorkGroup-Name"
  GlueDatabase:
    Value: !Ref GlueDatabase
    Export:
      Name: !Sub "${Env}-GlueDatabase-Name"
```

Athenaを作成します。先のCloudFormationで設定したパラメータと同じパラメータを指定してください。
```yaml
AWSTemplateFormatVersion: '2010-09-09'

Parameters:
  Env:
    Type: String

Resources:

  ItemTable:
    Type: AWS::Glue::Table
    Properties:
      CatalogId: !Ref AWS::AccountId
      DatabaseName:
        Fn::ImportValue:
          !Sub "${Env}-GlueDatabase-Name"
      TableInput:
        Name: item
        TableType: EXTERNAL_TABLE
        Parameters:
          skip.header.line.count: 1
          has_encrypted_data: false
          serialization.encoding: utf-8
          EXTERNAL: true
        StorageDescriptor:
          OutputFormat: org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat
          Columns:
            - Name: item_cd
              Type: string
            - Name: item_name
              Type: string
            - Name: store
              Type: string
            - Name: salse
              Type: int
          InputFormat: org.apache.hadoop.mapred.TextInputFormat
          Location:
            Fn::Join:
              - ''
              - - 's3://'
                - Fn::ImportValue: !Sub "${Env}-RawDataBucket-Name"
                - '/item' 
          SerdeInfo:
            Parameters:
              field.delim: ","
              serialization.format: ","
            SerializationLibrary: org.apache.hadoop.hive.serde2.lazy.LazySimpleSerDe

```

作成されたバケットに、以下のファイルを置きます。Athena作成時のロケーションに指定した`item`フォルダ以下に置くようにします。
```:item/item.csv
item_cd,item_name,store,salse
AAAA,商品Ａ,店舗１,1
AAAA,商品Ａ,店舗２,10
AAAA,商品Ａ,店舗２,100
BBBB,商品Ｂ,店舗１,1
BBBB,商品Ｂ,店舗２,10
CCCC,商品Ｃ,店舗１,1
```

</div></details>


作ったAthenaを関連付けます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/57e640ee-5800-d943-5181-9deecfaece02.png)

データベースとテーブルを設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/3db9e679-2771-8ccc-419f-eaba710ffe32.png)

すべての列を許可して設定。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/28313230-9930-2d97-8d36-280b4058b5cd.png)

設定すると分析ルールを設定するよう促されますので、設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/a7ca237b-ce81-a0dc-5c0b-0e90c8b1d838.png)

タイプは集約を選択。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/06d19787-b258-dc92-7530-f23b921d70ac.png)

作成時にはガイドフローを使います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/3bbeaced-82fa-0dff-3a59-c70020ede7c4.png)

集約関数を設定します。数値項目（っぽい）「salse列に対しSUMを許可」で指定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/4e98b7a5-967e-9748-59df-4ac441698723.png)

結合コントロールで「はい」を指定することで、他テーブルと結合しなくても使えるようです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/0e1a6001-7201-f789-8976-eddafa8760f0.png)

SELECTに使いたい項目を設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/64a4b7c3-d6df-36e2-bf46-c375d3d56c2e.png)

スカラー関数はすべて使うこととして次へ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/205250e4-2b32-5c91-092e-666c8cfea6aa.png)

集約制約なるものを設定します。（よくわからなかったので適当に設定しています。）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/7d110383-a81a-78c1-cb26-df33a4d396d4.png)

:::note
集約制約については、後で「こうじゃないかな・・・？」というのを説明します。
:::

作成がうまくいったようです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/ddb039d1-e252-c08a-5d1a-ced9708619e4.png)

分析ルールを作成後は、コラボレーションに関連付けます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/dfd29786-76d6-e06a-57ad-e7eab1d2005e.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/a05cb86b-b8f5-5468-b8ce-2e64abdb6e83.png)

必要なロールは自動作成で作ってくれます。（ロール名は自分で指定可能なので、テーブル名や相手先など、わかりやすい名前を付けておくとよいと思います。）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/75eb793a-a731-9dc7-48e5-6c8dc30d734e.png)

受領企業アカウントに移り確認すると、関連付けを行ったテーブルに対してクエリ可能になりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/6c782f3c-0df5-7d65-8a78-bf0a7173e13d.png)


## クエリしてみる

受領企業アカウントに移り、クエリを実行しますが、最初にバケットを指定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/c31cc0f6-2ccc-8cf2-4f5c-baa0fa1479fe.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/14ddd9d4-058e-9888-4000-6c0e8481282c.png)

試しに以下のクエリを実行します。
```sql
SELECT
    "item"."store",
    SUM("item"."salse")
FROM  "item"
GROUP BY  
    "item"."store"
```
結果は出てきました。結構時間はかかります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/2dbde6b5-8b7d-a5cf-b066-04a13ccc5da0.png)


### 集約制約の影響

今度は`item_name`でグルーピングして検索してみます。
```sql
SELECT
    "item"."item_name",
    SUM("item"."salse")
FROM  "item"
GROUP BY  
    "item"."item_name"
```
が、何も出てきません・・・
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/b0c030d3-540f-f004-2263-11c7eacd214b.png)

先ほどの制約を修正して、`item_cd`から`store`に変更しました。編集するときはガイドフローは使えませんので、JSONを編集します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/3db9c1c4-7b67-cb43-3f4d-1855ec7ac112.png)


そして同じSQLを実行してみます。
```sql
SELECT
    "item"."item_name",
    SUM("item"."salse")
FROM  "item"
GROUP BY  
    "item"."item_name"
```
今度は結果がかえってきましたが、商品Ｃが出てきません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/2b29a093-45df-04da-53eb-d65e10789675.png)

試しに以下のファイルをフォルダにいれて再実行すると、以下のような結果がかえってきます。

```:item/item2.csv
item_cd,item_name,store,salse
CCCC,商品Ｃ,店舗９,1000
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/eb60e3c1-a38e-0080-5288-36601bdac582.png)

### 集約制約の考察

これらのことから考えるに「グループ化した際に各グループで、集約制約で指定した列の一意の個数が最小値を超えている場合に、対象とする」となっているように見られました。


#### ケース１
最初に実行したクエリでは、`sotre`でグループ化して、「`item_cd`が2以上」という制約でした。
```sql
SELECT
    "item"."store",
    SUM("item"."salse")
FROM  "item"
GROUP BY  
    "item"."store"
```

対象のデータを、`store`と`item_cd`で一意にすると以下のようになり、各店舗ごとで`item_cd`の数が2以上になり、集計されたようです。
```
store,item_cd
店舗１,AAAA
店舗１,BBBB
店舗１,CCCC
店舗２,AAAA
店舗２,BBBB
```

#### ケース2
2番目に実行したクエリでは、`item_name`でグループ化して、「`item_cd`が2以上」という制約でした。
```sql
SELECT
    "item"."item_name",
    SUM("item"."salse")
FROM  "item"
GROUP BY  
    "item"."item_name"
```

先ほどと同様に、`item_name`と`item_cd`で一意にすると以下のようになり、「商品Ａ」「商品Ｂ」「商品Ｃ」いずれも1つになってしまったので、対象から外れた、と考えられます。
```
item_name,item_cd
商品Ａ,AAAA
商品Ｂ,BBBB
商品Ｃ,CCCC
```

#### ケース3
同じSQLに対して制約条件を変えて、`item_name`でグループ化して、「`store`が2以上」と設定するとどうでしょう。
`item_name`と`store`で一意にすると以下のようになり、「商品Ｃ」が1つになってしまったので、対象から外れた、と考えられます。
```
item_name,store
商品Ａ,店舗１
商品Ａ,店舗２
商品Ｂ,店舗１
商品Ｂ,店舗２
商品Ｃ,店舗１
```

#### ケース4
その後、同じSQLで同じ制約条件、データに「商品Ｃに対して、店舗１以外のデータ」を追加した結果、「店舗Ｃ」も2以上になり集計対象になった、という動きをしていました。

```
item_name,store
商品Ａ,店舗１
商品Ａ,店舗２
商品Ｂ,店舗１
商品Ｂ,店舗２
商品Ｃ,店舗１
商品Ｃ,店舗９
```

# おわりに

プレビュー版ですが、新サービスのAWS Clean Roomsを触ってみました。
提供側が（Glueテーブルを作ってあれば）わざわざ新規でデータを用意しなくても提供できる、というのはかなり良いかと思います。

別のデータと結合したり、別のタイプ「リスト」というのもありますが、今回はそこまでできませんでした。時間があったら見てみようと思います。