Redash の Snowflake データソースを Key-Pair 認証に対応させてみた
はじめに
こんにちは!シンプルフォームの山岸です。
昨年、Snowflake の認証方式の仕様変更がアナウンスされましたが、スケジュール通りに完了すると最早で2025年11月に Redash から Snowflake への接続ができなくなります。本記事では Redash からの Snowflake 接続を半ば強引に Key-Pair 認証対応させることで、そのような事態を回避する方法についてご紹介できればと思います。
背景
既にご存知の方も多いかとは思いますが、本記事に至った背景について改めて触れておきます。
Snowflake 社はセキュリティ強化の一環として、2024 年に認証方式の仕様変更に関する以下のようなアナウンスを出しています。
これらの変更が完了すると、Snowflake への接続においてパスワード単体での認証は一切出来なくなります。人間ユーザーの認証においては、シングルサインオン (SSO) での認証が推奨されていますが、MFA を設定することでパスワード認証も引き続き利用可能です。
問題になるのは、dbt や各種 BI 製品のようなサービスユーザーの認証です。これらには MFA を設定することが出来ないので、ユーザータイプを TYPE = SERVICE
とした上で OAuth 認証や Key-Pair 認証を利用することが求められています。
しかし、これらの認証方式を利用できるかどうかは製品の仕様・実装に依存します。そして弊社が現在 BI ツールとして利用している Redash は、残念ながらパスワード認証にしか対応していません。一応、ユーザータイプを TYPE = LEGACY_SERVICE
にしておくことで、2025年11月まではパスワード認証を引き続き利用できることになっていますが、それ以降はユーザータイプが TYPE = SERVICE
に強制移行され、パスワード認証を利用できなくなります。
少し前置きが長くなりましたがまとめると、(ベンダー側で特に対応がなされない限り)最早で 2025年11月に Redash から Snowflake への接続できなくなります。
対応方針
BI 移行を考えるにしてもそんなにすぐに対応するというのも厳しいので、何とかそれまでの間 Redash を使えるようにしておきたいというのが本記事の趣旨になります。
OSS 版 Redash の実装を見てみると、内包されている Snowflake コネクタの実装は以下の部分です。この辺りを Key-Pair 認証向けに書き換えたものを自作し、コンテナイメージをビルドし直してあげれば何とかなりそうです。
というのを踏まえて、以下のようなコネクタを作ってみました。
秘密鍵の情報は直接入力せず AWS Secrets Manager から取得するものとし、Redash の画面からは Secret ID
(シークレット名、またはシークレット ARN)を渡します。Optional の属性として Passphrase
を用意し、入力がある場合には暗号化済み秘密鍵として復号処理を施します。
前提
実装手順に入る前に、前提となる弊社の Redash アーキテクチャについて言及しておきます。
といっても特段変わったものはなく、プライベートサブネット上に Redash が稼働する ECS サービス、およびアプリケーション用 DB として Aurora PostgreSQL を構築しています。ユーザーが Redash にアクセスする際はパブリックサブネット上に配置した ALB を経由させています。
ECS サービスの中で利用しているコンテナイメージは、Docker Hub から取得可能な公式の Redash と Redis のイメージです。このうち、Redash 用イメージにカスタム実装を加え、ビルドしたイメージを ECR リポジトリに格納します。ECS タスク定義の中で Redash イメージを公式からカスタムのものに修正し、ECS サービスが再起動されればデプロイは完了です。
実装
Redash カスタムイメージの構築
本題の Redash カスタムイメージの構築手順について、以下に説明します。
Dockerfile
以下のようなディレクトリ構成を想定します。
.
├── Dockerfile # ビルド対象イメージ用
├── assets
│ └── images
│ └── db-logos
│ └── snowflake_keypair.png
└── src
├── query_runner
│ └── snowflake_keypair.py
└── settings
└── __init__.py
Dockerfile の内容はシンプルで、公式イメージをベースにいくつかのファイルを追加するだけです。追加するファイルの内容について以降で見ていきます。
FROM redash/redash:x.y.z.xxxx
COPY ./src/query_runner/snowflake_keypair.py /app/redash/query_runner/snowflake_keypair.py
COPY ./src/settings/__init__.py /app/redash/settings/__init__.py
COPY ./assets/images/db-logos/snowflake_keypair.png /app/client/dist/images/db-logos/snowflake_keypair.png
assets/images/db-logos/snowflake_keypair.png
公式リポジトリにある snowflake.png をファイル名だけ変えてそのまま拝借します。
src/settings/__init__.py
公式リポジトリの既存の __init__.py をベースに、今回追加する Query Runner に対応する設定を default_query_runners
のリストに追加します。
# Query Runners
default_query_runners = [
...
"redash.query_runner.snowflake",
+ "redash.query_runner.snowflake_keypair",
"redash.query_runner.phoenix",
...
]
src/query_runner/snowflake_keypair.py
Query Runner の実装例を以下に示します。(要件に合わせて適宜修正してください)
import json
import boto3
try:
import snowflake.connector
enabled = True
except ImportError:
enabled = False
try:
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend
except ImportError:
serialization = None
from redash.query_runner import BaseSQLQueryRunner, register
from redash.query_runner import TYPE_STRING, TYPE_DATE, TYPE_DATETIME, TYPE_INTEGER, TYPE_FLOAT, TYPE_BOOLEAN
from redash.utils import json_dumps, json_loads
TYPES_MAP = {...}
class SnowflakeKeyPair(BaseSQLQueryRunner):
noop_query = "SELECT 1"
@classmethod
def name(cls):
return "Snowflake [Key-Pair]"
@classmethod
def type(cls):
return "snowflake_keypair"
@classmethod
def configuration_schema(cls):
return {
"type": "object",
"properties": {
"account": {"type": "string"},
"user": {"type": "string"},
"secret_id": {"type": "string"},
"passphrase": {"type": "string"},
"warehouse": {"type": "string"},
"database": {"type": "string"},
"host": {"type": "string"},
},
"order": ["account", "user", "secret_id", "passphrase", "warehouse", "database", "host"],
"required": ["user", "account", "database", "warehouse", "secret_id"],
"secret": ["passphrase"],
"extra_options": ["host"],
}
@classmethod
def enabled(cls):
return enabled
@classmethod
def determine_type(cls, data_type, scale):
t = TYPES_MAP.get(data_type, None)
if t == TYPE_INTEGER and scale > 0:
return TYPE_FLOAT
return t
def _get_private_key(self):
"""
Fetches private key data from a Secrets Manager secret,
and converts it into a DER-encoded byte array.
"""
def _get_secret(secret_id):
secretsmanager_client = boto3.client("secretsmanager")
response = secretsmanager_client.get_secret_value(SecretId=secret_id)
return json.loads(response["SecretString"])
secret_id = self.configuration["secret_id"]
secret = _get_secret(secret_id)
private_key = secret["PRIVATE_KEY"]
passphrase = self.configuration["passphrase"]
private_key_obj = serialization.load_pem_private_key(
private_key.encode(),
password=passphrase,
backend=default_backend()
)
private_key_der = private_key_obj.private_bytes(
encoding=serialization.Encoding.DER,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption()
)
return private_key_der
def _get_connection(self):
connection = snowflake.connector.connect(
user=self.configuration["user"],
account=self.configuration["account"],
host="{}.snowflakecomputing.com".format(self.configuration["account"]),
private_key=self._get_private_key(),
)
return connection
def _parse_results(self, cursor):
...
def run_query(self, query, user):
...
def _run_query_without_warehouse(self, query):
...
def _database_name_includes_schema(self):
...
def get_schema(self, get_stats=False):
...
register(SnowflakeKeyPair)
ECR リポジトリへのプッシュ
イメージ格納用の ECR リポジトリを用意し、ビルドしたイメージをプッシュします。
ACCOUNT_ID="000011112222"
REGION_NAME="ap-northeast-1"
AWS_PROFILE="your-profile-name"
REGION_NAME="your-repository-name"
aws ecr get-login-password --profile $AWS_PROFILE --region $REGION_NAME | docker login --username AWS --password-stdin $ACCOUNT_ID.dkr.ecr.$REGION_NAME.amazonaws.com
docker build -t $REPOSITORY_NAME .
docker tag $REPOSITORY_NAME:latest $ACCOUNT_ID.dkr.ecr.$REGION_NAME.amazonaws.com/$REPOSITORY_NAME:latest
docker push $ACCOUNT_ID.dkr.ecr.$REGION_NAME.amazonaws.com/$REPOSITORY_NAME:latest
Redash 用 ECS タスクロールからのシークレット値読み取り
今回の実装では Secrets Manager から秘密鍵を取得しているため、Redash 用 ECS タスクロールから対象シークレットへの secretsmanager:GetSecretValue
権限が付与されている必要があります。また、Secrets Manager シークレットのリソースポリシー側も、該当 ECS タスクロールからの値読み取りを制限しないものになっている必要があります。
さらに Secrets Manager シークレットが ECS タスクロールとは別の AWS アカウントに存在していてクロスアカウントで読み取る必要がある場合、上記に加えて KMS の権限設定も必要になりますが、これについては後述します。
動作確認
前述の通り、ECS タスク定義の中で Redash 用コンテナのイメージ URI を、今回ビルド・プッシュしたカスタムのものに修正し、ECS サービスを再起動すればデプロイは完了です。
Redash 用 URL にアクセスして今回追加した Key-Pair 認証対応の Snowflake コネクタで接続を作成し、[Test Connection]
が成功することを確認します。
落穂拾い
Redash のバージョンアップ
OSS 版 Redash は、2025年1月に前回の v10.1.0 から約3年ぶりとなる v25.1.0 がリリースされています。DB スキーマにも変更があり、アップグレードする際は DB マイグレーションの実行が必要になります。
今回のようなカスタム変更を加える場合は、アプリケーション不整合が起きないよう現環境のバージョンとベースイメージに使用するバージョンは揃えるようにしてください。
Secrets Manager シークレットのクロスアカウント読み取り
Redash が稼働する AWS アカウントと、秘密鍵を保持する Secrets Manager シークレットが存在するアカウントが異なる場合を想定します。
Secrets Manager は特に指定しない場合、シークレット作成時に aws/secretsmanager
のエイリアス名を持つ AWS マネージド型 KMS キーを作成し、これをシークレットの暗号化に使用します。しかし、AWS マネージド型キーはクロスアカウントでの kms:Decrypt
を許可できないため、シークレットのクロスアカウント読み取りもできません。
シークレットをクロスアカウントで読み取るには、KMS CMK を作成し、これをシークレットの暗号化キーに使用します。Redash Task Role に対して、シークレットの読み取り権限だけでなく、CMK に対する kms:Decrypt
権限を許可するよう、Identity-Based ポリシーと Resource-Based ポリシーをそれぞれ構成します。
手順の詳細については、以下の公式ドキュメントをご確認ください。
最後に
Redash からの Snowflake 認証における Key-Pair 認証対応について書いてみました。
弊社のように Snowflake × Redash でデータ分析基盤を構築しており、認証方式の仕様変更のアナウンスに困っていた方も多いのではないでしょうか。今後ベンダー側でも対応があるかもしれませんが、それらの動向に依らずひとまずは急場を凌そうなワークアラウンドを見つけることが出来ました。今後、落ち着いて次なる BI についても検討していけたらと考えています。
また本記事の内容について、コネクタ開発部分のローカル検証は当社他メンバーに担当頂き、大いに助けて頂きました。本人の意向もあり名前は伏せますが、この場をお借りして改めて感謝したいと思います。
最後まで読んで頂き、ありがとうございました。

Snowlfake データクラウドのユーザ会 SnowVillage のメンバーで運営しています。 Publication参加方法はこちらをご参照ください。 zenn.dev/dataheroes/articles/db5da0959b4bdd
Discussion