店長に配る売上の画面を、Streamlit in Snowflake で作る|アプリの中の CURRENT_ROLE は所有者のロールになり、ロールで分けた行は0行だった
Streamlit in Snowflake のアプリの中では、CURRENT_ROLE が開いた人ではなく、アプリの所有者のロールを返します。店ごとの行をロールで分けた売上の表をそのままアプリにすると、店のロールを持つ人でも0行でした。ユーザーと店の対応表で行を決め直し、1人のユーザーで対応表を書き換えると見える店が替わったこと、READ SESSION の付け方、ウェアハウスランタイムで作る理由、開いたままの画面の費用の見方を、試した画面と SQL で書きます。別の人が開いた場合は確かめていません。
目次 クリックで開く
Streamlit in Snowflake は、Python で書いた画面を Snowflake の中で動かす仕組みです。アプリの中では、いま使っているロールを返す関数 CURRENT_ROLE が、開いた人ではなく、アプリの所有者のロールを返します。店ごとの行をロールで分けた売上の表をそのままアプリにすると、当社の試しでは、店のロールを持つ人でも0行でした。
そこで行を、ユーザーと店の対応表で決め直しました。試した1人のユーザーでは、対応表の行を書き換えると、見える店が替わりました。別の人が開いた場合は確かめていないので、配る前に店の担当のユーザーで一度開いて確かめます。
画面は、当社が検証用の環境に架空の3店の売上を入れて作ったもので、「見える行がありません」などの文言も当社のコードが出しています。
店のロールを持つ人がアプリで開いても、売上は0行だった
試したのは、3つの店の9月の売上の表 SALES です。表には、ロールで店を決める行アクセスポリシー STORE_BY_ROLE を付けました。行アクセスポリシーは、表を読むたびに、行ごとに見せるかどうかを決める仕組みです。
STORE_BY_ROLE は、ロールと店の対応表 ROLE_STORE を引き、CURRENT_ROLE に当たる店の行だけを返します。CURRENT_ROLE が返すのは、いま使っているロール(プライマリロール)の名前です。
試したのは、当社の作業者1人のユーザーです。このユーザーに、渋谷店の担当のロール STORE_SHIBUYA と、アプリの所有者のロール APP_OWNER の両方を付けました。アプリを作ったのも、このユーザーです。
SQL を実行する Snowsight のエディタで読むと、決まりどおりでした。STORE_SHIBUYA に切り替えると渋谷店の30日分(合計 11,076,000円)、APP_OWNER に切り替えると0行です。どちらも、同じユーザーに付いたほかのロール(セカンダリロール)を外して読んでいます。
同じユーザーでアプリを開くと、画面に出した CURRENT_ROLE は APP_OWNER で、見えている行は0でした。
画面の一番上の行は、確かめるためにアプリのコードが出している表示です。CURRENT_ROLE と CURRENT_USER を SQL で読んでいます。CURRENT_USER は名前の代わりに、取れたら「取得できた」、取れなければ「NULL」(値が無い)と出します。
公式の行アクセスポリシーのページにも、同じことが書かれています。アプリは所有者の権限で動くので、アプリの中の CURRENT_ROLE はいつも所有者のロールを返す、という説明です。そのため、CURRENT_ROLE を使う行アクセスポリシーはアプリでは使えない、という注です。
対応表に APP_OWNER と店の行を足せば、0行は消えるはずです。ただ、その行には開いた人が誰でも当たるので、APP_OWNER に書いた店を、誰が開いても同じように見ることになります。この形は試していません。開いた人ごとに店を分けるには、CURRENT_ROLE のほかの手がかりが要ります。
店を決める手がかりを、ロールからユーザーと店の対応表に替えた
手がかりにしたのは、ログインしているユーザーです。ユーザーと店の対応表 USER_STORE を作り、試したユーザーに渋谷店を書きました。表のポリシーは、CURRENT_USER で対応表を引く STORE_BY_USER に替えています。CURRENT_USER は、ログインしているユーザーの名前を返す関数です。
CREATE OR REPLACE ROW ACCESS POLICY STORE_DB.PUBLIC.STORE_BY_USER AS (s STRING) RETURNS BOOLEAN ->
EXISTS (SELECT 1 FROM STORE_DB.PUBLIC.USER_STORE m WHERE m.user_name = CURRENT_USER() AND m.store = s);
ALTER TABLE STORE_DB.PUBLIC.SALES DROP ROW ACCESS POLICY STORE_DB.PUBLIC.STORE_BY_ROLE,
ADD ROW ACCESS POLICY STORE_DB.PUBLIC.STORE_BY_USER ON (store);
対応表に書くユーザー名は、その人のユーザーで SELECT CURRENT_USER(); を実行して返る文字のとおりにします。照合順序を指定していない列では、文字は大文字と小文字を区別して比べるので、違うと当たりません。
ポリシーを替えると、SQL のエディタで読む人も、ロールではなく対応表で店が決まります。試したユーザーは、管理者のロール ACCOUNTADMIN に切り替えて読んでも、対応表に書いた渋谷店の30日分しか見えませんでした。
本部やエリアの担当のように複数の店を受け持つ人は、対応表に1人で複数の行を書くことになります。ポリシーは、当たる行が1つでもあれば見せる形だからです。この書き方は試していません。BI などのツールが自分のユーザーで同じ表を読んでいるなら、そのユーザーも対応表に無ければ0行になります。これはポリシーの式から決まることで、ツールでは試していません。この記事のポリシーに替えると、ロールで店を分けていた見え方(行アクセスポリシーの確かめ方の記事の形)は効かなくなります。ロールの条件と対応表の条件を1つのポリシーにまとめる書き方は、試していません。
ポリシーを替える前と後に開いた回から、7回を選んで並べます。
表のアプリの5回は、CURRENT_ROLE がどれも APP_OWNER でした。それでも、ユーザーで決めたあとの4回は、対応表に書いた店の30行が出ています。ただし、どれも試した1人のユーザーが開いた結果です。
配るときは、所有者のロールに READ SESSION を付ける
公式の行アクセスポリシーのページは、アプリで CURRENT_USER などの関数や、行アクセスポリシーの付いた表のデータを使う場合を挙げています。そのときは、ACCOUNTADMIN のロールで、所有者のロールにグローバル READ SESSION 権限を付ける、とあります。READ SESSION は、セッションの情報を読むための権限です。配るときは、この手順どおりに付けます。
付ける前の状態でも開いてみると、画面の CURRENT_USER は NULL でしたが、見えている行は30で、店は渋谷店だけでした。
このとき、画面のコードが読んだ CURRENT_USER と、ポリシーが読んだ CURRENT_USER は、別の結果になりました。画面のコードには NULL が返りました。ポリシーの中では値が入っていたはずです。NULL なら対応表のどの行にも当たらず、0行になるからです。
ただ、開いたのはアプリを作った当社の作業者本人で、別の人では開いていません。ポリシーの中の値が、開いた人と作った人のどちらのものだったかは、この試し方では分けられません。作った人の値だったなら、誰が開いても作った人の店が出ることになります。READ SESSION を付けずに配って、別の人にも同じ結果になるとは言えません。
READ SESSION を付けて開き直すと、画面の CURRENT_USER は「取得できた」になり、行は渋谷店の30行のままでした。
担当の店が替わったら、アプリは作り直さず、対応表の1行を書き換える
担当の店が替わったときに直すのは、対応表です。試したユーザーの行を、次の UPDATE で新宿店に書き換えました。
UPDATE STORE_DB.PUBLIC.USER_STORE SET store = '新宿店' WHERE user_name = CURRENT_USER();
SQL のエディタでは、新宿店の30日分(合計 13,740,000円)が見えます。アプリは作り直さずに開き直すと、店の選択は新宿店だけで、見えている行は30でした。
ここで書き換えたのは自分の行なので、WHERE に CURRENT_USER() を使いました。異動の手続きでは、管理者が担当者の行を書き換えるので、WHERE には相手のユーザー名を書きます。誰が対応表を書き換えるかも、配る前に決めておきます。
店を新宿店に替えたあとで、READ SESSION を取り消して開き直しました。画面の CURRENT_USER は NULL に戻り、行は新宿店の30行のままでした。付け外しで変わったのは、画面のコードが読む CURRENT_USER だけです。
所有者の権限で読むこのコードは、ウェアハウスランタイムで動かす
Streamlit のアプリを動かす場所の種類(ランタイム)は2つあります。ウェアハウスで動かすウェアハウスランタイムと、コンテナで動かすコンテナランタイムです。コンテナランタイムのアプリのコードは、ウェアハウスとは別の計算資源であるコンピューティングプールで動きます。
公式の行アクセスポリシーのページは、ウェアハウスランタイムの話として書かれています。コンテナランタイムについては、所有者の権限の接続でアプリが CURRENT_USER などを読む場合を挙げています。そのときは、開いた人ではなく所有者のロールの側の値が返る、とあります。そのため、対応表で開いた人の店を引く形には向きません。
所有者の権限の接続は、アプリの所有者の権限で SQL を実行する接続です。今回のアプリのコードは get_active_session でセッションを取っており、既定の所有者の権限で動きます。同じページには、制限付きの呼び出し元の権限の接続なら開いた人の値を返す、ともあります。こちらはプレビューの機能で、今回は試していません。
このアカウントでは、ランタイムを書かずに作ると、コンテナランタイムになりました。最初の CREATE STREAMLIT には、コードのファイルと題、使うウェアハウス APP_WH だけを書いていました。開くと「Spinning up the container…」と出て、起動を待つ表示が続きました。
DESCRIBE STREAMLIT で設定を見ました。runtime_name は SYSTEM$ST_CONTAINER_RUNTIME_PY3_11 でした。compute_pool の列には SYSTEM_COMPUTE_POOL_CPU と入っています。
公式の CREATE STREAMLIT のページは、RUNTIME_NAME の既定をウェアハウスランタイムとしています。名前に STREAMLIT を含むアカウントの設定を SHOW PARAMETERS で見ても、既定のランタイムを決める項目はありませんでした。既定のコンピューティングプールは、変更されていない既定の値の SYSTEM_COMPUTE_POOL_CPU です。なぜコンテナランタイムで作られたのかは、分かっていません。
リソースモニターは、ウェアハウスの使用量に上限を置く仕組みで、置き方はリソースモニターの記事で扱いました。公式はウェアハウスにだけ働くと書いているので、コンピューティングプールの分は、その上限では止まらないと読めます。
このアプリは起動を待つうちに閉じて消し、RUNTIME_NAME = 'SYSTEM$WAREHOUSE_RUNTIME' を書いて作り直しました。DESCRIBE の runtime_name は SYSTEM$WAREHOUSE_RUNTIME になり、compute_pool は空でした。
1分あたりの費用はコンテナのほうが安い、と公式は書いています。それでもこのコードで開いた人ごとに店を分けるなら、作ったら runtime_name を見て、ウェアハウスランタイムかを確かめます。
開いたままのアプリは、18分のあいだウェアハウスを止めなかった
ウェアハウスランタイムのアプリは、開いているあいだ、コードを動かすウェアハウスを使います。今回は X-Small の APP_WH で、自動一時停止は60秒にしました。最後に開き直したあと、タブを開いたまま、操作せずに置きました。別のタブで SHOW WAREHOUSES を実行し、APP_WH の状態を見ています。
18分24秒たっても、APP_WH は動いたまま(STARTED)でした。タブを閉じると、65秒後には止まって(SUSPENDED)いました。
公式の費用のページは、開いた人の最後の操作から約15分で、アプリとの接続が切れると書いています。ブラウザーの設定や操作に左右され、アプリの上でマウスを動かすと操作に数える、とも書いています。今回のタブはブラウザーの裏に置いており、それが止まらなかったこととどう関わるかは分かりません。
画面用のウェアハウスの使用量は約0.65クレジットでした。クレジットは Snowflake の使用量の単位です。これは1時間の区切りの合計です。準備の SQL、エディタでの確かめのクエリ、アプリを開いた回、開き直してから止まるまでの約20分が、すべて入っています。開いたままの分だけを分けた数字はありません。
見積もるときは、料金の記事でも書いたとおり、ウェアハウスが動いていた時間で考えます。公式の表では、第1世代(Gen1)の標準のウェアハウスの X-Small は1時間あたり1クレジットです。第2世代(Gen2)の X-Small は、Snowflake のサービス消費表で、AWS では1時間あたり1.35クレジットです。世代を書かずに作ると、地域で使えるなら第2世代になる、と公式は書いています。今回の APP_WH がどちらだったかは見ていません。作ったら、SHOW WAREHOUSES の resource_constraint の列で世代を確かめます。その値に、ウェアハウスが動いている時間を掛けます。この掛け算は当社が置いた目安で、請求の額では確かめていません。
店長に配るなら、開いたままの画面の分も費用に見込みます。止める手として公式が挙げるのは、Snowsight からアプリを止める、アプリのタブをすべて閉じる、スリープタイマーを付ける、の3つです。スリープタイマーは、アプリの .streamlit/config.toml に止めるまでの分を書く設定で、5〜240分で決められます。
配るまでに実行する SQL と、アプリのコード
実行したのは、Enterprise Edition のトライアルのアカウント(AWS の東京リージョン)です。「(ここは試していない)」と付けた文は、当社は実行していません。それ以外は実行した文で、名前は記事用に替えてあります。アプリのコードも、ロール名の表示を整える1か所を替えました。
書いた順に実行すると、ユーザーで店を決める形になるように並べ替えています。並べ替えのほかに変えたのは3つです。ユーザー名を変数で入れた所は手で書く形にし、ポリシーを付け替えた文は付けるだけの文にし、確かめの SELECT は省きました。
(1) では、表と対応表、ユーザーで店を決めるポリシー、2つのロールを作ります。実行したロールは ACCOUNTADMIN です。売上の表は、3店の9月分を作る文のままです。自社の表で試すなら、この文を外します。
-- (1) 表・対応表・ユーザーで店を決めるポリシー・ロール
USE ROLE ACCOUNTADMIN;
USE SECONDARY ROLES NONE;
CREATE WAREHOUSE IF NOT EXISTS APP_WH WITH WAREHOUSE_SIZE='XSMALL'
AUTO_SUSPEND=60 AUTO_RESUME=TRUE INITIALLY_SUSPENDED=TRUE;
CREATE DATABASE IF NOT EXISTS STORE_DB;
USE SCHEMA STORE_DB.PUBLIC;
USE WAREHOUSE APP_WH;
CREATE OR REPLACE TABLE SALES AS
SELECT s.store, DATEADD(day, d.n - 1, '2026-09-01'::DATE) AS sale_date, s.base + MOD(d.n * s.k, 9) * 12000 AS amount
FROM (SELECT ROW_NUMBER() OVER (ORDER BY SEQ4()) AS n FROM TABLE(GENERATOR(ROWCOUNT => 30))) d
CROSS JOIN (SELECT '渋谷店' AS store, 320000 AS base, 7 AS k
UNION ALL SELECT '新宿店', 410000, 5
UNION ALL SELECT '池袋店', 280000, 3) s;
CREATE OR REPLACE TABLE USER_STORE (user_name STRING, store STRING);
INSERT INTO USER_STORE VALUES ('<SELECT CURRENT_USER() が返す、作業する人のユーザー名>', '渋谷店');
CREATE OR REPLACE ROW ACCESS POLICY STORE_DB.PUBLIC.STORE_BY_USER AS (s STRING) RETURNS BOOLEAN ->
EXISTS (SELECT 1 FROM STORE_DB.PUBLIC.USER_STORE m WHERE m.user_name = CURRENT_USER() AND m.store = s);
ALTER TABLE STORE_DB.PUBLIC.SALES ADD ROW ACCESS POLICY STORE_DB.PUBLIC.STORE_BY_USER ON (store);
CREATE ROLE IF NOT EXISTS APP_OWNER;
CREATE ROLE IF NOT EXISTS STORE_SHIBUYA;
GRANT ROLE APP_OWNER TO USER <作業する人のユーザー名>;
GRANT ROLE STORE_SHIBUYA TO USER <作業する人のユーザー名>;
GRANT USAGE ON WAREHOUSE APP_WH TO ROLE APP_OWNER;
GRANT USAGE ON WAREHOUSE APP_WH TO ROLE STORE_SHIBUYA;
GRANT USAGE ON DATABASE STORE_DB TO ROLE APP_OWNER;
GRANT USAGE ON DATABASE STORE_DB TO ROLE STORE_SHIBUYA;
GRANT USAGE ON SCHEMA STORE_DB.PUBLIC TO ROLE APP_OWNER;
GRANT USAGE ON SCHEMA STORE_DB.PUBLIC TO ROLE STORE_SHIBUYA;
GRANT SELECT ON TABLE SALES TO ROLE APP_OWNER;
GRANT SELECT ON TABLE SALES TO ROLE STORE_SHIBUYA;
GRANT CREATE STREAMLIT ON SCHEMA STORE_DB.PUBLIC TO ROLE APP_OWNER;
CREATE OR REPLACE STAGE APP_FILES;
GRANT READ ON STAGE APP_FILES TO ROLE APP_OWNER;
1節の0行を再現するなら、(1) のユーザーで決めるポリシーの2文の代わりに、次の4文を実行します。
-- 1節を再現するときだけ:ロールで店を決めるポリシー
CREATE OR REPLACE TABLE ROLE_STORE (role_name STRING, store STRING);
INSERT INTO ROLE_STORE VALUES ('STORE_SHIBUYA', '渋谷店');
CREATE OR REPLACE ROW ACCESS POLICY STORE_BY_ROLE AS (s STRING) RETURNS BOOLEAN ->
EXISTS (SELECT 1 FROM STORE_DB.PUBLIC.ROLE_STORE m WHERE m.role_name = CURRENT_ROLE() AND m.store = s);
ALTER TABLE SALES ADD ROW ACCESS POLICY STORE_BY_ROLE ON (store);
(2) では、アプリのコードのファイル streamlit_app.py を、ステージに置きます。ステージは、Snowflake の中のファイル置き場です。公式の手順では、Snowsight の画面からステージにファイルを上げられます。当社はこの道を試しておらず、Snowflake の中に作った Python の手続きで書き込みました。
手元の端末から SQL で置くなら、PUT を使います。PUT は Snowsight のエディタでは実行できず、Snowflake CLI(snow sql)などから実行します。ステージに書く権限(WRITE)を持つロールで実行し、(1) の形では、ステージを作った ACCOUNTADMIN です。
-- (2) Snowflake CLI などから、ACCOUNTADMIN で実行する(ここは試していない)
PUT file:///<手元のフォルダ>/streamlit_app.py @STORE_DB.PUBLIC.APP_FILES AUTO_COMPRESS = FALSE OVERWRITE = TRUE;
(3) は、所有者のロールでアプリを作る文で、RUNTIME_NAME を書きます。ADD LIVE VERSION FROM LAST は、作ったアプリを、開いた人に見える版(ライブバージョン)にします。USAGE の権限だけを持つ人が見るには、この文が要ります。
-- (3) アプリを作り、開いた人に見える版にして、ランタイムを確かめる
USE ROLE APP_OWNER;
USE SECONDARY ROLES NONE;
CREATE STREAMLIT STORE_DB.PUBLIC.SALES_APP
FROM @STORE_DB.PUBLIC.APP_FILES
MAIN_FILE = 'streamlit_app.py'
QUERY_WAREHOUSE = APP_WH
RUNTIME_NAME = 'SYSTEM$WAREHOUSE_RUNTIME'
TITLE = '店舗別の売上';
ALTER STREAMLIT STORE_DB.PUBLIC.SALES_APP ADD LIVE VERSION FROM LAST;
DESCRIBE STREAMLIT STORE_DB.PUBLIC.SALES_APP;
DESCRIBE の default_packages の streamlit には、版の指定がありません。今回の画面の Streamlit は 1.22.0 でした。公式は、新しく作ったアプリはその環境でサポートされている最新の版を使う、と書いています。
コードを直したときは、ファイルを置き直し、アプリを消して作り直しました。ステージのファイルは、CREATE のときに1回だけコピーされるからです。
(4) は、アプリのコードです。
import streamlit as st
from snowflake.snowpark.context import get_active_session
session = get_active_session()
st.title("店舗別の売上")
try:
ctx = session.sql("SELECT CURRENT_ROLE() AS R, CURRENT_USER() AS U").collect()[0]
role = ctx["R"] or "NULL"
user = "取得できた" if ctx["U"] else "NULL"
except Exception as e:
role, user = "(エラー)", "(エラー)" + str(e)[:120]
st.caption(f"アプリの中から見た CURRENT_ROLE:{role} / CURRENT_USER:{user} / Streamlit {st.__version__}")
df = session.sql(
'SELECT store AS "店舗", sale_date AS "日付", amount AS "売上" '
"FROM STORE_DB.PUBLIC.SALES ORDER BY 2"
).to_pandas()
st.metric("この画面で見えている行", len(df))
if len(df) == 0:
st.warning("見える行がありません")
else:
stores = sorted(df["店舗"].unique())
pick = st.multiselect("店舗", stores, default=stores)
d = df[df["店舗"].isin(pick)]
st.line_chart(d.pivot_table(index="日付", columns="店舗", values="売上", aggfunc="sum"))
st.dataframe(d.groupby("店舗", as_index=False)["売上"].sum())
最初の版では、表の行番号を隠す hide_index を st.dataframe に付けていて、次のエラーで止まりました。1.22.0 では受け付けない引数で、外すとエラーは消えました。
TypeError: DataFrameSelectorMixin.dataframe() got an unexpected keyword argument 'hide_index'
(5) で、所有者のロールに READ SESSION を付けます。取り消すときは、同じ形の REVOKE … FROM ROLE を実行します。
-- (5) 所有者のロールに READ SESSION を付ける
USE ROLE ACCOUNTADMIN;
GRANT READ SESSION ON ACCOUNT TO ROLE APP_OWNER;
(6) は、店長を1人足す手順で、ACCOUNTADMIN で実行します。後から (6) だけを実行してもよいように、ロールとウェアハウスを指定する2文を頭に入れました。例は新宿店の店長で、(1) で作業する人に書いた渋谷店とは違う店にしています。新宿店のロールを作ってアプリを開く権限を付け、そのロールを店長のユーザーに付け、対応表に店長の行を入れます。開く人は、Snowflake のユーザーを持ち、サインインしている必要があります。対応表に書くユーザー名は、店長本人が SELECT CURRENT_USER() を実行して見た文字のとおりにします。
-- (6) ACCOUNTADMIN で、新宿店の店長を1人足す(ここは試していない)
USE ROLE ACCOUNTADMIN;
USE WAREHOUSE APP_WH;
CREATE ROLE IF NOT EXISTS STORE_SHINJUKU;
GRANT USAGE ON DATABASE STORE_DB TO ROLE STORE_SHINJUKU;
GRANT USAGE ON SCHEMA STORE_DB.PUBLIC TO ROLE STORE_SHINJUKU;
GRANT USAGE ON STREAMLIT STORE_DB.PUBLIC.SALES_APP TO ROLE STORE_SHINJUKU;
GRANT ROLE STORE_SHINJUKU TO USER <新宿店の店長のユーザー名>;
INSERT INTO STORE_DB.PUBLIC.USER_STORE VALUES ('<新宿店の店長のユーザー名>', '新宿店');
確かめたのは1人のユーザーだけで、開いた人と作った人は分けられていない
今回のユーザーは1人で、アプリを開いた人、アプリを作った人、所有者のロールを持つ人を兼ねていました。別の人が開いたときにその人の店だけが出るかも、READ SESSION なしで行が出ることがほかの人にも当てはまるかも、確かめていません。担当が替わったときの確かめは、対応表のこのユーザーの行を書き換えて代えました。
コンテナランタイムのアプリは起動を待つうちに閉じたので、最後まで動かしておらず、その約1分の費用も見ていません。20分より長く開いておくこと、スリープタイマー、environment.yml での版の指定は、今回の範囲の外です。社外の人への共有と、Snowsight の画面からのアプリの作成も同じです。
配る前に、所有者のロールを持たない店の担当のユーザーで、一度開いて確かめる
最初にやるのは、所有者のロールを持たず、アプリを作ってもいない店の担当のユーザーで、アプリを開いてもらうことです。(6) のとおり、その人に店のロールとアプリの USAGE を付け、対応表にその人の行を入れます。対応表には、作った人の行とは違う店を書いておきます。
その店の行だけが出れば、開いた人の店が出ています。開けなければ (6) の権限とロールを見直します。開けても0行なら、対応表のユーザー名と READ SESSION を見ます。作った人の店が出たら、対応表で決める形そのものを見直します。
ロールの組み方や、用途ごとのウェアハウスの分け方・自動一時停止の秒数は、当社の Snowflake 導入支援で設計する項目です。
データ分析・データ基盤支援
Snowflake・BigQuery・Treasure Dataといったデータ基盤の選定から、基幹・SaaS・広告・POSに散らばったデータの集約、dbtでの指標統一、Lookerなどでのダッシュボード化までを支援します。
