❄️

【Snowflake】Snowflake Notebook内で作成したファイルやグラフ画像をそのままSlackへ「送信」する方法

2024/06/01に公開

本記事で参考になるケース

  • Snowflake内からSlackにファイルや画像を送信したい
  • 特にSnowflake Notebook上で作成したグラフ画像やテーブルをcsvにしたファイルをSlackへ送信したい
  • GET_PRESINGED_URL等は利用しない (Slackに完全に送信する形式を取ります)
メッセージのみを送信する方法は下記記事に記載

https://zenn.dev/y_matsubara/articles/snowflake-slack-notify

記事の概要

  • Slack通知までに必要なSnowflake内の設定を記載
  • Slack側はIncoming Webhookを利用する方法とSlack Appを利用する方法がありますが、今回はSlack Appを利用する方法で記載
  • Snowflake External Network Accessを利用

方法

1. Slackへのネットワークを設定

CREATE DATABASE SLACK;
CREATE SCHEMA SLACK.NOTIFY;

CREATE OR REPLACE NETWORK RULE SLACK.FILE_NOTIFY.RULE
  MODE = EGRESS
  TYPE = HOST_PORT
  VALUE_LIST = ('slack.com', 'files.slack.com');


CREATE OR REPLACE SECRET SLACK.FILE_NOTIFY.SLACK_APP_TOKEN
  TYPE = GENERIC_STRING
  SECRET_STRING = 'xoxb-xxxxxxxx';

-- SECRET_STRINGに設定された xoxb-xxxxxx は
-- Slack Appの OAuth Tokens For Your Workspace ・ Bot User OAuth Token である

CREATE OR REPLACE EXTERNAL ACCESS INTEGRATION SLACK_FILE_API
  ALLOWED_NETWORK_RULES = ( SLACK.FILE_NOTIFY.RULE )
  ALLOWED_AUTHENTICATION_SECRETS = ( SLACK.FILE_NOTIFY.SLACK_APP_TOKEN )
  ENABLED = true;

2. Slackへ画像やファイルを投稿する際に利用する関数を作成

CREATE DATABASE USERDB;
CREATE SCHEMA USERDB.UTIL;

CREATE OR REPLACE FUNCTION USERDB.UTIL.SLACK_POST_FILES(
    channel varchar , files array 
    )
RETURNS varchar
LANGUAGE python
RUNTIME_VERSION = 3.9
HANDLER = 'main'
EXTERNAL_ACCESS_INTEGRATIONS = (SLACK_FILE_API)
PACKAGES = ('requests')
SECRETS = ('cred' = SLACK.FILE_NOTIFY.SLACK_APP_TOKEN)
AS
$$
import _snowflake
import json
import os
import requests
import base64
def main(
    channel,
    files
    ):
    api_key = _snowflake.get_generic_secret_string('cred')
    id_list=[]
    for i in range(0, len(files), 3):
        filename=files[ i : i + 3][1]
        data=base64.b64decode(files[ i : i + 3][2].encode('ascii'))
        response_get = requests.get(
            url='https://slack.com/api/files.getUploadURLExternal',
            headers={'Authorization': 'Bearer '+ api_key},
            params ={ 'filename' : str(filename), 'length' :len(data)}
        )
        if not response_get.json()['ok']:
            return str(response_get.json())
            
        id_list.append([ files[ i : i + 3][0],response_get.json()['file_id']])
        response_post_file = requests.post(
            url=response_get.json()['upload_url'],
            data=data
        )
        if response_post_file.status_code != requests.codes.ok:
            return str(response_post_file)
            
    response_post_chennel = requests.post(
        url='https://slack.com/api/files.completeUploadExternal',
        headers={'Content-Type': 'application/json', 'Authorization': 'Bearer '+ api_key},
        json = {
            'files': [{'title': f[0], 'id': f[1]} for f in id_list],
            'channel_id': channel
        }
        )
    return str(response_post_chennel.json())
    
$$;

3. ユーザ関数実行権限を付与(ここは読み飛ばしてもOK)

GRANT USAGE ON DATABASE USERDB TO ROLE <user_role>;
GRANT USAGE ON SCHEMA USERDB.UTIL TO ROLE <user_role>;
GRANT USAGE ON USERDB.UTIL.SLACK_POST_FILES(VARCHAR, ARRAY) TO ROLE <user_role>;

4. slackへテーブルのcsvファイルやグラフ画像や通知

①Snowflake NotebookでSQLの結果をcsvファイルとしてSlackに投稿

Notebookの記載

Slack通知結果 (SLACK_POST_FILES引数の「投稿タイトル」を'test'としている)

コピペ用
import streamlit as st
import pandas as pd
import base64
from io import BytesIO
from snowflake.snowpark.context import get_active_session
session = get_active_session()
buf=BytesIO()

################################
# cell2 の結果をcsvとして読み込み
################################
df=cell2.to_pandas().to_csv(buf, index=False)  
arg=base64.b64encode(buf.getvalue()).decode('ascii')

channelId = 'CXXXXXXX'

################################
# Slackにcsvファイルとして投稿
################################
sql = f"select select USERDB.UTIL.SLACK_POST_FILES('{channelId}',['投稿タイトル','ファイル名.csv','{arg}'])"
data = session.sql(sql).collect()

②Snowflake Notebookで生成したグラフ画像をSlackに投稿

Notebookの記載

Slack通知結果

コピペ用

import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np
from io import BytesIO

r = np.random.RandomState(42)
m = [0, 0]
c = [[1, 2], [2, 5]]
X = r.multivariate_normal(m, c, 100)

fig = plt.figure()
plt.scatter(X[:, 0], X[:, -1])
print(plt)

import base64
from io import BytesIO

buf = BytesIO()
plt.savefig(buf, format="png")
arg=base64.b64encode(buf.getvalue()).decode('ascii')

channelId = 'CXXXXXXX'

################################
# Slackに画像ファイルとして投稿
################################
sql = f"select select USERDB.UTIL.SLACK_POST_FILES('{channelId}',['投稿タイトル','ファイル名.png','{arg}'])"
data = session.sql(sql).collect()

発展(複数のファイルをまとめてSlackに1回で投稿する

USERDB.UTIL.SLACK_POST_FILESの引数であるARRAYに追加

################################
# Slackに画像ファイルとして投稿
################################
sql = f"select select USERDB.UTIL.SLACK_POST_FILES(
    '{channelId}',
    [
        '投稿タイトル1','ファイル名1.png','{arg1}',
        '投稿タイトル2','ファイル名2.png','{arg2}',
        ...
        .
    ]
    )"
data = session.sql(sql).collect()
Slack通知結果 (SLACK_POST_FILES引数の「投稿タイトル」を'title1','title2'とし、ファイルをテキストにしている)

まとめ

Snowflake Notebookの定期実行機能と合わせると、定期的に自動で分析/グラフ作成&SlackにPOSTしてくれるようにできたりと、夢が広がりますね!

参考

https://zenn.dev/y_matsubara/articles/slack-post-file-202405

Discussion