Kafka to Snowflake 101 : KafkaからSnowflake Sink Connectorを使ってクイックにデータ連携する

# はじめに

KafkaからSnowflakeにデータを連携する方法を公式サイトの情報を見ながら試してみます。Snowflakeはカラムナ（列指向）かつマイクロパーティションによる分割保存を基本としているため、JDBCコネクターを使用した連携とは異なる考慮事項があるのではないかと思います。難しいことはあとで考えるとして、まずはクイックにSnowflakeコネクターを使用してKafkaからSnowflakeにデータを連携してみます。

- 参考：Snowflake公式サイト
  - [Installing and configuring the Kafka connector（特に参考になったページ）](https://docs.snowflake.com/ja/user-guide/kafka-connector-install)


# 前提

- Kafka：Confluent 7.2.1
- Snowflake：Sink Connector 2.2.1
- java：11.0.14
- OS：Red Hat Enterprise Linux 8.9

# やること
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838636/f8b51d79-c2b9-670d-d1fb-1fedd9a9c65e.png)


# やってみた

- Kafkaコネクターを使用するロールの作成 
  - ロール： KAFKA_CONNECTOR_ROLE_1
  - ユーザー： YSSKJCK
  - データベース： MY_DATABASE
  - スキーマ： PUBLIC
  - テーブル： MY_KAFKA_TABLE
  
```sql:Snowflake Sink Connectorで使用するカスタムロールを作成する
-- Use a role that can create and manage roles and privileges.
USE ROLE SECURITYADMIN;

-- Create a Snowflake role with the privileges to work with the connector.
CREATE ROLE KAFKA_CONNECTOR_ROLE_1;

-- Grant privileges on the database.
GRANT USAGE ON DATABASE MY_DATABASE TO ROLE KAFKA_CONNECTOR_ROLE_1;

-- Grant privileges on the schema.
GRANT USAGE ON SCHEMA MY_DATABASE.PUBLIC TO ROLE KAFKA_CONNECTOR_ROLE_1;
GRANT CREATE TABLE ON SCHEMA MY_DATABASE.PUBLIC TO ROLE KAFKA_CONNECTOR_ROLE_1;
GRANT CREATE STAGE ON SCHEMA MY_DATABASE.PUBLIC TO ROLE KAFKA_CONNECTOR_ROLE_1;
GRANT CREATE PIPE ON SCHEMA MY_DATABASE.PUBLIC TO ROLE KAFKA_CONNECTOR_ROLE_1;

-- Grant the custom role to an existing user.
GRANT ROLE KAFKA_CONNECTOR_ROLE_1 TO USER YSSKJCK;

-- Set the custom role as the default role for the user.
-- If you encounter an 'Insufficient privileges' error, verify the role that has the OWNERSHIP privilege on the user.
ALTER USER YSSKJCK SET DEFAULT_ROLE = KAFKA_CONNECTOR_ROLE_1;
```

- Snowflake Sink Connectorを[Confluent Hub](https://www.confluent.io/hub/snowflakeinc/snowflake-kafka-connector)からインストール

```sh
confluent-hub install snowflakeinc/snowflake-kafka-connector:2.2.1
``` 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838636/56ac44e5-6fda-9116-cd32-be362ac1f265.png)

- Kafka Connectを再起動

```sh
confluent local services connect stop
confluent local services connect start
``` 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838636/c7d7d911-5466-5f5d-fdf7-a55f0819bce8.png)

- Snowflake Sink Connectorがコネクタープラグインとして有効になったことを確認
```sh
curl localhost:8083/connector-plugins | jq
``` 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838636/1e60e95b-6458-bfff-cb8c-269d645851c7.png)

- トピック「sample-topic」にAvro形式でメッセージをProduceしておく
参考：[Avro形式のメッセージをいますぐKafkaに送るためのサンプルコマンド](https://qiita.com/ysskjck/items/8fd53ed060a2b5fd4d9f)

```shell:kafka-avro-console-producerでProduce（対話モード）
kafka-avro-console-producer \
  --broker-list localhost:9092 \
  --property schema.registry.url=http://localhost:8081 \
  --topic sample-topic \
  --property parse.key=true \
  --property key.schema='{"type":"record",
                          "name":"myrecord",
                          "fields":[{"name":"col1","type":"string"}]}' \
  --property key.separator=" " \
  --property value.schema='{"type":"record",
                            "name":"myrecord",
                            "fields":[{"name":"col1","type":"string"},
                                      {"name":"col2","type":"string"},
                                      {"name":"col3","type":"string"}]}'
```
```shell:メッセージを入力
{"col1":"a1"} {"col1":"a1","col2":"a","col3":"test1"}
{"col1":"a2"} {"col1":"a2","col2":"aa","col3":"test2"}
{"col1":"a3"} {"col1":"a3","col2":"aaa","col3":"test3"}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838636/d9c91bb3-9752-89dc-ac6c-9f0f4710a864.png)


- 秘密鍵と公開鍵の作成　（参考：[キーペア認証の使用およびキーローテーション](https://docs.snowflake.com/ja/user-guide/kafka-connector-install#using-key-pair-authentication-key-rotation)）
  - 秘密鍵の暗号化バージョンを生成
  - 秘密鍵を参照して公開鍵を生成
  - ALTER USER を使用して、Snowflakeユーザーに公開キーを割り当て

```sh
openssl genrsa 2048 | openssl pkcs8 -topk8 -v2 aes256 -inform PEM -out rsa_key.p8
openssl rsa -in rsa_key.p8 -pubout -out rsa_key.pub
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838636/aded294c-e75a-4281-b654-ab6390ac4802.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838636/a25bc6b5-e7cc-fb4b-d946-4715b00bcde8.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838636/290e3cc1-1359-288f-545d-681f25b50ab5.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838636/c5de6677-ac03-772b-a8af-d1ac4081a5b1.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838636/7334ee50-88aa-ed2a-9694-7d070ad161b5.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838636/fd06383c-98fa-5b01-8add-4e389d6a2ecb.png)

```sql
ALTER USER YSSKJCK SET RSA_PUBLIC_KEY='MIIBIjANBg......fy7V6b9wSYZj+e82wIDAQAB';
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838636/baa6e38a-a865-af86-2403-4e596690ced7.png)

- Snowflake Sink Connectorを作成する（トピック「sample-topic」にアタッチ）
```shell
curl -sS -X PUT http://localhost:8083/connectors/sample-topic-snowsink/config \
-H "Content-Type: application/json" \
--data '
{
  "name":"sample-topic-snowsink",
  "connector.class":"com.snowflake.kafka.connector.SnowflakeSinkConnector",
  "tasks.max":"1",
  "topics":"sample-topic",
  "snowflake.topic2table.map":"sample-topic:MY_KAFKA_TABLE_1",
  "buffer.count.records":"10000",
  "buffer.flush.time":"60",
  "buffer.size.bytes":"5000000",
  "snowflake.url.name":"https://...........ap-.....-..aws.snowflakecomputing.com:443",
  "snowflake.user.name":"YSSKJCK",
  "snowflake.private.key":"MIIFLTBXBgkqhkiG......6oh1iBzrdDBr+qIg4AE",
  "snowflake.private.key.passphrase":"s.....n",
  "snowflake.database.name":"MY_DATABASE",
  "snowflake.schema.name":"PUBLIC",
  "key.converter":"io.confluent.connect.avro.AvroConverter",
  "key.converter.schema.registry.url":"http://localhost:8081",
  "value.converter":"io.confluent.connect.avro.AvroConverter",
  "value.converter.schema.registry.url":"http://localhost:8081"
}' | jq
```

```shell
# コネクターのステータス取得
curl localhost:8083/connectors/sample-topic-snowsink/status | jq
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838636/193c9f27-5fd0-c969-a6a6-9aec4abd387e.png)

- Snowflake側を確認する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838636/ea0d61f2-083e-aa6d-ff4f-01d62cd75676.png)
テーブルはできている。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838636/456a78cc-0b93-a6be-186e-4a794c9d2d97.png)
できたテーブルはVARIANT型のRECORD_METADATAとRECORD_CONTENTのカラムを持っている。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838636/52edef48-c334-96f0-20d9-ce255b8cb2fb.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838636/3bf28ccc-04b5-5461-49fc-c9d7986bd489.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838636/af1701db-c23b-f69b-89fa-c14a5a66bd6a.png)
VARIANT型のRECORD_METADATAにはトピック名、タイムスタンプ、オフセットに加えてメッセージのキー部が格納されている。RECORD_CONTENTにはメッセージのバリュー部が格納されている。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838636/325e0ee9-7205-834c-877c-8db3be1d2917.png)
一時的にSnowpipeに使用されると思しきステージが作られている。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838636/8399ed22-dc0e-fe30-a50d-87a8e8d9cd63.png)
Snowpipeのパイプ定義も作られている。

- さらにメッセージをトピックにProduceしてみる
```shell:メッセージを入力
{"col1":"z1"} {"col1":"z1","col2":"zzz","col3":"20240404-1:16:30"}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838636/5dd76d63-5459-0e10-df66-32272ca3e85c.png)

- Snowflake側を確認する（追加メッセージが格納されているか）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838636/e27e43a2-c4e4-b116-2795-5b0d31a7f8b5.png)
すぐにSnowflakeのテーブルにメッセージが連携された。

- Snowflake Sink Connectorを削除する
```sh
curl -X DELETE localhost:8083/connectors/sample-topic-snowsink
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838636/986eb355-d6a4-c862-9f0d-66df74c4df8f.png)

- Snowflake側を確認する（ステージやパイプが残っているか）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838636/8b76567a-c164-e5ad-cd42-1043e2526d8c.png)
残っているし実行中のまま。課金されたら嫌なので削除する。

- Snowflake側の後片付け
参考：[Kafkaコネクタで使用されるSnowflakeオブジェクトの削除](https://docs.snowflake.com/ja/user-guide/kafka-connector-manage#dropping-snowflake-objects-used-by-the-kafka-connector)
```sql
-- ステージのドロップ
DROP STAGE SNOWFLAKE_KAFKA_CONNECTOR_SAMPLE_TOPIC_SNOWSINK_1263312905_STAGE_MY_KAFKA_TABLE_1;
-- パイプのドロップ
DROP PIPE SNOWFLAKE_KAFKA_CONNECTOR_SAMPLE_TOPIC_SNOWSINK_1263312905_PIPE_MY_KAFKA_TABLE_1_0;
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838636/f8add90e-fc1a-8621-0a22-8fcd960e98ce.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838636/da42a520-a5dc-a82b-b680-175674d5a451.png)

# おわりに

SnowflakeコネクターはSnowflakeの内部ステージ（＝ステージはSnowflakeがクエリーできるファイルオブジェクトの置き場のようなもの）に一時ファイルの形で作成し、Snowpipeというストリーム処理によって順次対象テーブルにバルクロードするような仕掛けのようです。実運用を見据えるともうすこし踏み込んだ確認が必要そうですが、当初の目的である「Kafka to Snowflake をクイックに」は実現できたので今回はここまでとします。