Qortora · Search · Indexed page

tech.every.tvFetched 2026-08-15T06:58:15Z

every Tech Blog

株式会社エブリーのTech Blogです。

Open original source · Full cached text

every Tech Blog every Tech Blog 株式会社エブリーのTech Blogです。 2026-08-14 Lakebaseの認証実装とORM選定をした話 インターンシップ2026 Databricks はじめに こんにちは、株式会社エブリーのデリッシュキッチンにて7月から約1か月間エンジニアとしてインターンシップに参加していました、亀井と申します。 この記事では、インターン中に取り組んだことの中心である「Databricks LakebaseへのOAuth M2M認証の導入」と、その先でデータを読むための「GoのPostgreSQL向けライブラリ選定」の2つについて、どう実装・検証・調査したのかを書きたいと思います。 インターンで取り組んだこと インターンでは、Go言語で書かれたデリッシュキッチンのAPIサーバーを主な対象として、次の4つに取り組みました。 デリッシュAIの表示順ソートのロジックの実装 Databricks LakebaseへのOAuth M2M認証の実装 GoのPostgreSQL向けデータアクセスライブラリ(ORM)の比較調査 M2M認証を動かすための環境変数・ECSタスク定義の追加 この記事では主に2と3について書きます。 開発の背景 デリッシュキッチンのサーバーから、Databricks上の新しいリソースに接続したいという需要が生まれていました。接続先はLakebase Autoscaling PostgresというDatabricksが提供するフルマネージドなPostgreSQLで、projects → branches → endpointsの3階層でリソースを管理する新世代のサービスです。 Databricks本体は分析寄りで、アプリが求める個別レコードを低レイテンシで引く用途には向きません。そこで、Databricksでテーブルを一元管理するカタログ機能であるUnity Catalog側のテーブルをsourceとしてLakebaseのPostgresに自動同期し(synced table)、アプリからは読み取り専用のPostgresテーブルとして高速に読む、という構成を取ります。 問題は認証です。既存のDatabricks接続はPAT(Personal Access Token)という長命トークンで認証していました。しかしPATは失効管理が手動で、そもそもLakebaseのPostgres認証(OAuth role)は「発行から60分で失効するトークン」を前提としており、PATでは接続できません。静的なパスワードで認証するroleも作れますが、DatabricksのID管理に紐づかない長命の静的クレデンシャルになるため、漏洩時のリスクを考えると避けたいところです。 Databricksはサービスプリンシパルの認可にはOAuth 2.0を推奨しており、ここではOAuth M2M認証、すなわちサービスプリンシパルとclient credentialsフローの組み合わせを実装しました。用語を整理すると: サービスプリンシパル(SP): 人間ではなくアプリ自身に与えるIDアカウント。人に紐づけると退職や異動等で処理が止まり、権限も広くなりがちなので、機械用のアカウントを別に立てます client credentialsフロー: ユーザーの同意画面を介さず、アプリがClient IDとClient Secretを認可サーバーに提示してアクセストークンを得るOAuth 2.0のフロー つまり、サーバーが自分自身のIDとシークレットでトークンをもらい、そのトークンでAPIを呼ぶ仕組みです。 M2M認証の実装と検証 設計方針 設計方針の要点は次の3つです。 環境分離はbranchで行う:Lakebaseのbranchはコピーオンライト(CoW)でデータを分岐でき、production(親)とdevelopment(子)で本番と開発を分離します。SPは環境別に2本立て、dev用SPのOAuth roleをproduction branchに作らないことが、dev環境から本番データへの到達を防ぐ唯一の境界になります。roleの状態はbranch間で独立している、というのが公式の仕様です。 認証専用のパッケージは作らない:トークンの発行・キャッシュ・更新はすべてSDK内部の責務で、自前コードに共通化すべきロジックが発生しないためです。 環境変数はSDKの標準名(DATABRICKS_HOST / DATABRICKS_CLIENT_ID / DATABRICKS_CLIENT_SECRET)を使う:databricks-sdk-goの統一認証チェーンは、これらの環境変数があればSPとしてM2M認証し、なければ開発者個人のCLIプロファイルに自動でフォールバックします。本番はSP・ローカルは個人認証という切り替えが分岐コードゼロで手に入り、SPのシークレットを開発者の端末に配らずに済みます。 この設計方針の下で実装を進めてきました。 実装 実装は公式ドキュメントのGoサンプルをほぼそのまま踏襲する形式になりました。核になるのは、GoのPostgreSQL接続ライブラリpgxが提供する接続プールのBeforeConnectフックです。BeforeConnectは、プールが新しい接続を張る直前に毎回呼ばれる関数で、ここで接続に使う設定を書き換えられます。 w, err := databricks.NewWorkspaceClient(&databricks.Config{}) // 空のConfigでSDKの統一認証チェーンに委ねる: // DATABRICKS_*環境変数(本番ECS)→ CLIプロファイル(ローカル) // Lakebaseは全接続にSSL/TLSを必須とするため、sslmodeはコード側で固定する poolCfg, err := pgxpool.ParseConfig("sslmode=require") // credentialは発行から60分で失効する。BeforeConnectは新規接続にしか // 効かないため、失効前に接続自体を作り直させる(公式サンプル準拠)。 // Jitterで寿命をばらつかせ、一斉失効による再接続の集中を防ぐ poolCfg.MaxConnLifetime = 45 * time.Minute poolCfg.MaxConnLifetimeJitter = 5 * time.Minute poolCfg.BeforeConnect = func(ctx context.Context, connCfg *pgx.ConnConfig) error { cred, err := w.Postgres.GenerateDatabaseCredential(ctx, postgres.GenerateDatabaseCredentialRequest{ Endpoint: endpointName, // projects/{project}/branches/{branch}/endpoints/{endpoint} }) if err != nil { return err } connCfg.Password = cred.Token // 接続確立ごとに新鮮なトークン return nil } ポイントは3つあります。 Database Credentialはキャッシュしない:60分で失効するトークンをキャッシュすると、失効間際のトークンを掴む事故が起きます。毎回発行するとAPIコールが増えそうに見えますが、発行が走るのは接続プールが新規接続を作るときだけなので、実際の頻度は低く抑えられます。 接続寿命をトークンのTTL(有効期間)未満に制限する:BeforeConnectが効くのは新規接続だけで、確立済みの接続にあとから新しいトークンを渡す手段はありません。接続を作りっぱなしにすると、認証に使ったトークンが失効した接続がプールに残り続けます。そこで公式サンプルと同じくMaxConnLifetime = 45 * time.Minuteで、失効前に接続自体を作り直させます。さらに、プールの接続はデプロイ直後などにまとまって作られるため、寿命が同じだと作り直しのタイミングも一点に集中します。これをずらすのが、寿命に上乗せするランダムな幅であるジッターです。ジッターを足しても接続寿命は最長50分で、TTLの60分を確実に下回ります。 接続先のdatabaseはコードで切り替えられるようにする:これはレビュー指摘で直した点です。当初は接続先のdatabase名を環境変数PGDATABASE任せにしており、接続先が1つに固定されていました。しかし今後は複数のdatabaseを読む可能性があります。そこでdatabase名を型として定義し、database単位に接続プールを分けて、接続文字列のdbnameで明示指定する形に改めました。pgxでは接続文字列の設定が環境変数より優先されるためです。endpointと認証credentialはdatabaseを問わず共通なので、BeforeConnectの仕組みは全プールでそのまま共有できます。 環境変数とタスク定義 このコードを本番で動かすには、AWSのコンテナ実行サービスであるECSのタスク定義への環境変数の追加が必要です。DATABRICKS_*の3変数に加えてPostgres接続用のPG*変数を、dev / prdそれぞれのタスク定義に追加しました。ひとつ注意が要るのはPGUSERで、本番ではSPのclient ID、ローカルでは開発者個人のIDと、環境で値の種類そのものが違うため、ハードコードせず環境ごとに注入します。 検証 作った接続コードが「本当に意図通り動くのか」を示すために、疎通確認用のCLIをリポジトリ内に用意しました。確認したいことが3層に分かれているので、CLIも段階を選べるようにしてあります。 --auth-only: Postgresには触らず、ワークスペースAPIへの認証だけを確認します フラグなし: credentialを発行してPostgresにログインし、SELECT 1を実行します --count 2 --interval 65m: 接続寿命を跨いで再接続できるかを確認します なぜ分けるかというと、1と2は通信経路も認可の仕組みも別物だからです。段階1はワークスペースのAPIに届くかの確認で、認可は、ワークスペースの機能を使う権利であるエンタイトルメントが担います。段階2は発行されたcredentialでPostgresにログインする確認で、認可はbranch単位のOAuth roleが担います。一気に確認すると、失敗したときにどの層が原因か切り分けられません。 データを読むライブラリをどう選ぶか 認証が通ったら、次はその接続でデータを読む実装です。ここで「GoからPostgreSQLを扱うのに何を使うか」というライブラリの比較を行いました。 結論から書くと、第一候補はBob、第二候補はsqlcです。理由は、クエリの書きやすさ、型の安全性、そして上で作った既存の*pgxpool.Poolをそのまま使えることです。 選定の前提は次の3つで、それぞれが結論の理由に対応しています。 対象はsynced tableです。スキーマはUnity Catalog側が所有し、アプリからはALTERを発行しません:主キーのないテーブルを扱えること、スキーマ変更に気付けることが効いてきます 接続は、BeforeConnectによる認証をバイパスしないよう既存の*pgxpool.Poolを使います:プールを直接扱えるライブラリが有利です 読み取るテーブルは今後増えていきます:1テーブルなら手書きでも書けますが、増えるほど書きやすさと型安全性が効いてきます 選定の理由 選定の過程では生成AIを用いてORMを列挙させ、上記の観点から7つのライブラリを選出しました。利点と欠点で比較をし、参考程度にDocker上のPostgreSQLで実測を行いました。その後議論を行い、以下のORMを選定しました。 Bob: 既存の*pgxpool.Poolを公式に直接扱えます。ビルダでクエリを書きやすく、コード生成で型安全性を足せます。主キーのないテーブルでも生成が通り、読み取り専用のモデルになるためsynced tableの性質と噛み合います。 sqlc: SQLを書いてコードを生成する型で、列名や型の誤りを生成時に検出できます。スキーマをUnity Catalog側が所有する今回の状況では、上流のスキーマ変更を再生成時のコンパイルエラーとして捕まえられる固有の強みがありま…