
Go
イベント
該当するコンテンツが見つかりませんでした
マガジン
技術ブログ
はじめに こんにちは。グループIT推進本部の Roki です。 Claude CodeやOpenAI ...
はじめに こんにちは、株式会社エブリーのデリッシュキッチンにて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側が所有する今回の状況では、上流のスキーマ変更を再生成時のコンパイルエラーとして捕まえられる固有の強みがあります。 他の候補を見送った理由も簡単に説明します。GORMはフルORMの利点が今回の要件では効きません。entはジェネレータが主キー id を必須とするため、主キーのないsynced tableへの全件取得が失敗します。scanyは短く書けますが開発が止まっています。手書きのpgxは型が go build 時点で固定される書き方ですが、テーブルが増えるほど手書きの Scan が積み上がります。 まとめ インターンを通して、実際にユーザーに使われているデリッシュキッチンのサーバーに触れられたのは、とても貴重な経験でした。チームの設計方針や、既存のコードを読み解きながら実装を行う経験は開発現場だからこそ得られるものだと思います。インターンシップで学んだことも活かしつつ、今後もさまざまな技術に触れながら、エンジニアとして成長していきます。 最後に、1か月間サポートしてくださったデリッシュキッチンの皆様、本当にありがとうございました。 参考 Connect external app to Lakebase using SDK OAuth machine-to-machine (M2M) authentication Branching Manage Postgres roles Connection security databricks-sdk-go Bob: pgx driver — NewPool ent: Fields — ID
動画
該当するコンテンツが見つかりませんでした













