TECH PLAY

BASE株式会社

BASE株式会社 の技術ブログ

590

はじめに この記事は BASE アドベントカレンダー21日目の記事です。 devblog.thebase.in こんにちは、バックエンドエンジニアの小笠原( @yukineko_819 )です。 今回は、私がこの2年間ほどをかけて取り組んできたBASEにおけるサービスレベルマネジメントの取り組みの歩みと、今後の展望についてお話しようと思います。 始まり 最初のきっかけは、New Relic社主催のFutureStack Tokyo 2023に参加したことでした。 devblog.thebase.in BASEではこれより以前からNew Relicを導入して活用していましたが、イベントの参加を通じてまだまだNew Relicを十分に活かしきれていなかったことを知りました。 そして、ここではじめてサービスレベルマネジメントという概念を知り、私たちも取り組んでみようか、という話になりました。 最初の一歩 まずは小さく身近なところから始めてみようということで、当時の所属チームが担当していたCRM領域、特に当時開発中であったメンバーシップAppにおけるSLI/SLOを考えるところからスタートしました。 とは言っても最初は何もわからない状態からのスタートだったため、New RelicやGoogleが公開しているSLOに関する資料や、他社事例のテックブログなどを参考にインプットを重ねながら、およそ2ヶ月をかけて以下のような議論を進めていました。 メンバーシップAppにおける絶対に毀損してはいけない体験とは何か? フロントエンド、ALB、バックエンドAPI、どのレイヤーのイベントをSLIとするのか? SLIにおける「成功のイベント数」はどのようなイベントを成功とみなすのか? SLOは何を基準に定めれば良いのか? 活動の中で「購入者がメンバーシップ会員として商品を購入することができる」という体験をCritical User Journey(CUJ)の一つとして定めてSLOを設定しようと議論を重ねていきましたが、残念ながら結局SLOを設定するところまで至ることはできませんでした。 しかし、この2ヶ月間の活動を通して取り組んだメンバーの間では、CUJを検討する過程でそのサービスが提供している価値をより深く理解する必要があり、加えて適切なSLIを選定するためにはそのサービスがどのような機能やアーキテクチャ、インフラストラクチャで構成されているのかを理解する必要がある、ということを強く認識することができた活動でした。 ある1領域のSLMからサービス全体のSLMへ SLMとは何なのか、そしてSLOを設定することの難しさを痛感した我々ではありましたが、次の一歩を踏み出しました。 一般的なECサービスが提供しているようなECサイトを公開したり商品を購入したりといった体験は、ショップオーナーや購入者から見ても最もコアとなる体験であり、BASEとしてもそれらの体験は重要であるはず、という考えのもと、「ショップを開設して商品を登録して出品し、その商品が実際に購入された後、発送を完了してショップに売上が立つ」という一つのサイクルをBASEにおける最も重要な体験であると定義して、このサイクルを構成するCUJ群に「Tier1 CUJ」と名付けてSLOを設定することにしました。 SLOの設定にあたっては、最初から良いものを作ろうと深く悩みすぎたことでなかなかSLOを設定するところまで辿り着けなかった前回の反省を踏まえ、ひとまずは計測することを第一目標として掲げ、集まった有志数人でこれらのCUJに対してSLOを設定していきました。 これまでの「商品ページが重い」「機能がうまく動いていないというお問い合わせが何件か来ている」といった定性的な指標に加えて、この水準でこのくらいの人が使えている、のようにSLOという定量的な指標で表現できるようになった瞬間でした。 ここまで取り組んできたサービスレベルマネジメントの取り組みとして、一つの大きな成果です。 惜しむらくは、それを知っているのが設定を行った数名の有志だけであること、質より速度を優先したことで設定したメンバー達ですらSLOの妥当性に対する信頼性に不安があったこと、そしてSLOが未達でもそれを改善するという活動に繋げるまでのフローをまだ整備できていないためにSLO未達の状態が続いてもアクションを取れずにいる、ということでした... 一般的なSLOからBASEのSLOへ さて、いろいろ課題が残るもののTier1 CUJをカバーするようにSLOを設定することはひとまずできたので、次の段階としてよりSLOの精度をあげる取り組みをすることにしました。 サービスレベルマネジメントとしては運用・監視・改善のサイクルを回すことが大事であり、改善の部分が抜け落ちてしまっている状態ではありましたが、SLOの精度を上げてアラートがなった場合はなおさねばならない状況を作った方が運用を軌道に乗せるにはスムーズだろう、と判断してのことです。 購入にまつわる決済や発送の領域に詳しいエンジニア、インフラストラクチャに詳しいSREチーム、アーキテクチャに詳しいplatformチームといったメンバーに声をかけて総勢12名の「サービスレベル定点観測会」という会議体を組成しました。 BASEのサービスやTier1 CUJの体験を実現する各種機能に対して深い知識と理解を持っているメンバーでSLO検討を実施することで、より実態に即した質の高いSLOを設定できると考えたためです。 この取り組みはうまくいった部分もあれば、失敗した部分もありました。 成功した点は以下が挙げられます。 参加したメンバーにサービスレベルマネジメントの意義を理解してもらうことができた CUJの検討からSLOを設定するところまでの1サイクルを経験してもらうことができた 実際に幾つかのCUJに対して、より検討を深めた上でSLOを追加したり置き換えたり、そのままで良いという確認が取れたりといったSLOの品質向上が実現できた 一方で失敗した点としては、 メンバーをいきなり数倍に増やしたがそれに対応した会議体のマネジメントは私のキャパシティを超えていた 二週間に一度の開催枠の中で活動をしていたので都度どこまでやったかの思い出し作業が発生した といった点が反省点でした。 SLI/SLOワークショップの開催 また、サービスレベル定点観測会の取り組みとは別に、New RelicのBASEのサポート担当の方に支援いただき、BASE社内向けのSLI/SLOワークショップを開催しました。 参加するメンバーが多く2回に分けて開催したのですが、1回目はNew Relicの方が、2回目はBASE社内のエンジニアが進行を担当して、どちらの回も大変好評で良い結果となりました。 ワークショップによって一定の認知を広げることができたことで、これ以降開発チームが自発的にリリース予定の機能についてSLOを設定してみるといった事例も見受けられ、BASEにサービスレベルマネジメントの種をまいたとても重要な施策だったと思います。 SLOの再定義とエンジニア以外との協力 いきなり少し規模を大きくしすぎたという前回の反省から、メリットの部分は維持しつつ1チームとして機動力高く動けそうな人数として4人にまで絞り込み、改めてスタートしました。 そしてある程度品質も向上したSLOが揃ってきたため、次のフェーズとしてこのSLOをエンジニアが活用する運用指標の一つというだけではなくBASEとして共通の守るべきサービス品質基準として整えていくために、改めてTier1 CUJを以下のように整理しなおしました。 区分 内容 Revenue Critical 商品の購入や注文の発送など、BASEの売上に直結する最重要のCUJ群 EC Service Core ECサービスとして一般的に備えているCUJのうち、Revenue Criticalに該当しないCUJ群 Engagement Support ショップオーナーの集客をサポートするCUJ群 その上で、この区分やCUJ群がBASEとして重要であるとすることに違和感がないか、不足がないか、といった観点でエンジニア以外のチームとも相談を実施し、ここに違和感がないと合意した上でまだ設定されていない箇所のSLOの設定を進めました。 特にRevenue Criticalについては事業KPIに強く関連するCUJに焦点を当てて、事業KPIがどのような要素から構成されているのかを紐解き、そこからよりビジネスと相関を持ちうるSLOを選定して設定することを進めていきました。 エンジニアの指標からみんなで見る指標へ 同時に、New Relic上で管理しているSLOを誰でも見ることのできる指標として整備し、事業KPIをはじめとする各種データと絡めて分析ができる環境を整える取り組みにも着手しました。いわゆるビジネスオブザーバビリティと言われる取り組みで、具体的には以下の2点を実施しました。 毎月、先月のSLOが達成できていたか、できなかった場合どのような要因があったのか、という点をサービスベルサマリーレポートとしてスライドにまとめSlack上で全社共有する New Relic上で設定・管理しているSLOを、MetricデータをNerdGraph APIを用いてBigQueryに溜め込むことでLookerで閲覧できるようにする 2のSLOをLookerで閲覧できるようにした理由は、エンジニア職以外はLookerを使ってデータの分析や事業KPIの分析などを行なっていたためです。BASEではエンジニアは全員がNew Relicのアカウントを付与されているのですが、エンジニア職以外はLookerを使ってデータの分析や事業KPIの分析などを行なっており、New Relicのアカウントを持っている方はほぼいませんでした。 データ資産という意味でもLookerで分析するために整えられた各種データが揃っている状態だったため、改めてNew Relicにそれらのビジネスメトリクスを取り込むよりは、New Relic上にしかないSLOのデータもLooker上で分析できるようにして統一する方が良い、と考えたためです。 これらの取り組みはすぐに効果が出て全員がSLOに興味を持って活用してくれるようになる類のものではありませんが、それでも今まで見ることのできなかったデータを公開したこと、定期的にそのデータの見方や分析内容を発信すること、には意義があったと思っています。 データを公開したとして本当に興味を持ってもらえるのだろうか、データの公開よりも先にやった方がいいことがあるのではないか、という点はサービスレベル定点観測会のメンバーとしても不安を抱えながら作業を進めていましたが、いざ公開してみると詳しく話を聞いてみたい、この応答速度のSLOは維持できているとしても遅いと思うので改善したほうがいいのではないか、といったような声を何件かかけてもらうことができるようになっていきました。 SLOでサービスの品質を定量的に観測して維持するための改善をしていく、というサービスレベルマネジメントを全社で実施していくことの芽が出始めたことを実感しました。 全体を俯瞰するSLOから、どこに影響があったのかわかるSLOへ SLOを設定する上で難しいなと感じるのは、どこをSLIとして定義するか、という選定の部分だと思います。例えば一つ一つのAPI単位で細かく設定することもできてしまいますし、あるいはもっと大雑把にフロントエンドの画面描画を指標として設定することもできます。 サービスレベル定点観測会では、購入体験を支えるカート機能はとりわけ重要なサービスであると考え、次の観点としてこのカートでの購入体験をより詳細にSLOでカバーしていくことを目標としました。 ビジネス面でも大量のトラフィックを高速に処理できるかどうかは事業KPIであるGMVの増加に影響がありますし、ユーザー体験の面でも商品を購入するというECサービスを使用する購買層のユーザーとしては最もコアとなる価値体験を司る部分です。 単に購入が正常に高速に完了できたのか、という指標だけではなく購入者のカート体験として「カートに商品を追加する」「カート内の送り先情報を編集する」「確認画面で内容が正しいかを確認する」といった、購入を行う上での一連の体験を全て洗い出し、必要な箇所を選定してSLOを設定していきました。 これにより、購入体験に関してより具体的にどのような影響が出ていたのかを把握できるようになりました。 ここから、さらに一歩先へ このように、手探りで模索を続けながらも、サービス全体を俯瞰してどこに何が起こっているかを把握する、ビジネス戦略に強く影響がある箇所に問題が起きていないかを把握する、コアとなるユーザー体験で快適なサービスを提供できている、といった様々な角度観点からアプローチをかけてBASEとして重要なサービス価値とは何か、どこにSLOを設定していくべきかを考えて進めてきました。 ここまでの活動を通して、サービスレベル定点観測会の中にも、それぞれ自発的にSLOの設定にチャレンジしてくれたエンジニア達の中にも、その知見がだいぶ溜まってきたのではないかと思います。 次なるチャレンジとして、来年からは今までサービスレベル定点観測会が推進してきたサービスレベルマネジメントを、いよいよ開発組織全体で分担して担当していくということに挑戦していきたいと考えています。 おわりに 改めて振り返ってみると、上長や同僚に大いに助けてもらいながら、なんとかここまで形にしようと賛同し協力してくれた皆さんと一緒に一歩一歩、少しずつ進んできた2年間だったと思います。 特に印象深いのは、誰に相談を持ちかけてもサービスレベルマネジメントの意義を理解していただけて、その上で「ぜひ一緒にやりましょう!」という言葉をいただけたことです。 より良いユーザー体験を提供するため、安定してサービスを提供していくためには協力を惜しまない、という温かい後押しが私の背中を常に支えてくれていました。 ようやくここまで辿り着き、そしてまだまだここからという状態ではありますが、引き続きユーザーに安定してサービスを提供していけるようにサービスレベルマネジメントの取り組みを推進していきます。 最後に、BASEでは今後の事業成長を一緒に支えてくれるエンジニアを募集しています。 ご興味があれば、ぜひ採用情報をご覧ください。 binc.jp 明日のBASEアドベントカレンダーは松原(@simezi9)さんの記事です。お楽しみに!
BASE ADVENT CALENDAR 2025 DAY.20 はじめに この記事はBASE アドベントカレンダー 2025の20日目の記事です。 Pay ID Platform Group の 大木です。 本記事では、Feature Flag(aka Feature Toggles)の標準化仕様及びSDKである OpenFeature と、Feature Flag As A Service(以下FFaaS)である AWS AppConfig を利用したサービスを約1年間運用してきたため、OpenFeatureを中心にFeature Flagの現在とAppConfigの運用に関してをお話しします。システムの開発言語はGoを利用しているため、主にそれをベースにお話しします。 Feature Flag概要 Feature Flagと聞いて何を思い浮かべるでしょうか? On/Offのスイッチのようなもので、段階的機能公開を行うカナリアリリースで利用したり、問題があった場合にロールバックできる A/B テストで、ユーザセグメントごとに異なる機能やUIを動的に振り分けたりするなどに使う コード差分を頻繁にmainブランチへマージするコードのフレッシュさを保つトランクベース開発 具体的なケースを思い浮かべると、複数の機能を持っているように思えますが 、簡単に言えば、 Feature Flagとは、コードを変更することなくシステムの動作を変更できる手法 で、システムへのコード反映(デプロイ)と機能公開(リリース)を分離することができるものです。 その活用方法として、大きく4つのカテゴリに分類することができ、設計上の制約、管理方法がそれぞれ異なります。カテゴリの分類目安として、フラグをどのくらいの期間利用するかという存続期間と、フラグの評価ロジックや値の決定をどの程度動的に行うかによって分類できるようです。 Release Flag: 開発中の未完成な機能を本番環境から隠すための、比較的短命なフラグ。 Ops Flag: システムの運用担当者が、パフォーマンスの最適化や機能の緊急停止のために使用するフラグ。比較的長命になることがあります。 Experiment Flag: A/Bテストなど、ユーザーの振る舞いを比較するために使用するフラグ。実験期間中のみ存在します。 Permission Flag: 特定のユーザーグループに機能へのアクセス権を与えるための、永続的または非常に長命なフラグ。 引用: https://martinfowler.com/articles/feature-toggles.html 我々は主に、中長期運用するOps FlagやPermission Flagに関して、OpenFeature SDKとAppConfigを利用した運用管理を行なっており、OpenFeatureの主要機能や予備知識を交えながら説明したいと思います。 OpenFeatureについて OpenFeature とは、コミュニティ主導のFeature Flag標準化プロジェクトですが、標準化仕様の策定とSDKを提供しています。LaunchDarklyやAppConfig等フラグ管理を行うベンダーが提供するAPIや独自SDKを直接利用するのではなく、OpenFeatureを利用する利点はどのようなところにあるのでしょうか? 例えば、以下のような点が挙げられると思います。 どのフラグ管理バックエンドを選択しても、アプリケーションからは、OpenFeature SDKの評価APIにより、統一的なインタフェースでフラグ評価ロジックを実装可能 FFaaSやDB、ローカルファイルなど、フラグ管理するバックエンドは、それぞれに対応するOpenFeature Providerを利用することで、アプリケーション側のフラグ評価ロジックを、ほぼ変更せずに差し替えが可能 Hooksを利用し、フラグ動的評価を行うための評価コンテキスト(Evaluate Context)を編集したり、ログ出力、Telemetryの実施など機能拡張を行うことが可能 以下の図は、OpenFeatureが、任意の仮想的なフラグ管理システム「Flags-R-us」と統合されることを示しています。あたかもクラウド上のシステムと統合されそうな印象を受けますが、3rd partyベンダーが提供する独自SDKや、DB、ローカルファイルなどフラグ管理が行える仕組みと、それに対応するProviderを用意できれば、共有の標準化されたフラグクライアント(SDK)を利用し、一貫性のある統合APIを利用して実装することが可能です。 引用: https://openfeature.dev/docs/reference/intro アプリケーションで、OpenFeature SDKを利用して、Feature FlagをGoで扱うには以下のような実装となります。もしフラグ管理バックエンドを差し替えたい場合は、1.でフラグ管理バックエンドに対応するProviderに差し替えれば、フラグ評価や利用する箇所の修正は不要です。 package main import ( "fmt" "context" "github.com/open-feature/go-sdk/openfeature" ) func main() { // 1. フラグ管理バックエンドを扱うProviderを登録 openfeature.SetProviderAndWait(openfeature.NoopProvider{}) // 2. そのProviderを、アプリケーションから利用するためのクライアントを作成 client := openfeature.NewClient( "app" ) // 3. フラグの評価実行 v2Enabled := client.Boolean( context.TODO(), "v2_enabled" , true , openfeature.EvaluationContext{}, ) // 4. 評価されたフラグ値を使う if v2Enabled { fmt.Println( "v2 is enabled" ) } } これで終わるなら、直接フラグ管理システムが提供するAPIやSDKを使えば良いかと思いますが、他にもたくさん仕様が考えられていますので、いくつか紹介します。 1. 共通のI/Fで実装したいが、アプリ制御とA/Bテストは、別々のフラグ管理システムを利用したい A/Bテストを行う際にもFeature Flagを利用することができます。我々はAWSのAppConfigをフラグ管理システムとして利用していますが、A/Bテストでは、分析基盤が用意されているかどうかがとても重要です。現在、 Amazon CloudWatch Evidently というモニタリングや分析を行える機能はサポートを終了しており、分析基盤を別途用意するのも大変です。そういった場合、以下のように複数Providerを登録することが可能です。それぞれのProviderに対応するクライアントを作成することで複数のフラグ管理システムを、用途ごとに使い分けることができます。 import "github.com/open-feature/go-sdk/openfeature" // アプリ制御用Providerを登録 openfeature.SetProviderAndWait(NewLocalProvider()) // 名前付きでProviderを登録 openfeature.SetNamedProvider( "abtesting" , NewABProvider()) // デフォルトのProviderを利用するクライアントを作成 clientWithDefault := openfeature.NewDefaultClient() // 名前付きで登録したProviderを利用するクライアントを作成 clientForABTesting := openfeature.NewClient( "abtesting" ) 2. フラグ管理システムを別システムに移行したい 例えば、サービス終了で別システムに移行しなければならないというケースを考えてみましょう。 実験的機能となりますが、 Multi-Provider というものを利用すれば、複数のProviderをまとめて登録し、一つのクライアントを使い並行して実行できます。これにより、先に新システムに対応するProviderを追加登録しておいて、後からゆっくりと新しいシステムにフラグを登録するといったことが可能になります。 import ( "github.com/open-feature/go-sdk/openfeature" "github.com/open-feature/go-sdk/openfeature/multi" "github.com/open-feature/go-sdk/openfeature/memprovider" ) mprovider, err := multi.NewProvider( multi.StrategyFirstMatch, multi.WithProvider( "providerA" , memprovider.NewInMemoryProvider( /*...*/ )), multi.WithProvider( "providerB" , myCustomProvider), ) if err != nil { return err } openfeature.SetNamedProviderAndWait( "multiprovider" , mprovider) 3. 複数フラグ管理システムを組み合わせてシームレスに利用したい 2.のケースと同じように複数のProviderを利用するケースなのですが、組み合わせて使うケースを考えてみましょう。FFaaSを利用する場合、ネットワーク越しにフラグデータセットを取得してこなければならないため、システムがダウンしている場合、アクセスできないことがあります。そこで、バックアップとして、環境変数やローカルファイルを利用するProviderを併せて登録しておけば、その間は、フォールバックしてフラグを利用するといったことが可能になります。以下の中からProvider利用戦略を指定することで、柔軟に結果を利用することができます。 First Match: Providerを順番に呼び出し、最初にフラグが存在するものを返す First Success: Providerを順番に呼び出し、エラーが発生しなかったProviderの結果を返す Comparison: 並列にProviderを呼び出し、結果を比較し一致した場合はその結果を、そうでない場合はFallback Providerの結果またはデフォルト値を返す Custom: 自前で利用ロジックを実装 OpenFeatureでは、ターゲティングと呼ばれる仕組みがあり、アプリケーションやユーザーに関する情報を利用して、動的に評価するルール設定を定義し、フラグのステータスを制御します。 AppConfigの場合、 マルチバリアントフラグ というものを利用する必要があります。その場合、AppConfigからAPIで直接取得する方式は利用できず、AppConfig AgentをSidecarコンテナで立てて利用するのが必須となっています。AgentがAppConfigから、設定値やターゲーティングに利用するルールセットをポーリングにより定期的にダウンロードするため、アプリケーションからは、ローカルホストへの通信により、Agentでフラグ評価を実行できます。 しかし、 Agentのイメージ が壊れた場合、正しくフラグを取得できなかったり、リソース枯渇や過負荷になった場合、Agentへの接続エラーになることが極まれに発生します。そうした場合、バックアップとして、ローカル設定からフラグの結果を返せるProviderを併せて登録しておくことで、問題が起きにくくなるかなと思います。 引用: https://docs.aws.amazon.com/ja_jp/appconfig/latest/userguide/appconfig-agent.html 4. ターゲティングをサポートする評価コンテキストを柔軟に構築できる OpenFeatureは、ターゲティングを行う入力値として、評価コンテキストというコンテナを利用します。ここに、ユーザのIPやメールアドレス、サーバの位置情報(AWS Regionなど)を載せてリクエストすることで、柔軟にフラグ評価を行えます。 フラグ管理システムがフラグを動的に評価し結果を返す仕組みを提供していればの話ですが、それによって、柔軟に結果を変化させることができます。これはグローバルに設定したり、特定クライアント、実行時にも設定できます。様々なフェーズで設定されたコンテキストのデータはマージされて、評価リクエストに乗ります。 これを使った実装として、カートのCheckout時とPay IDアプリの非Checkout時にリクエストする同じHTTP APIがあり、そこでこのターゲティングを利用したフラグを利用しています。Permission Flagの一種の使い方ですね。 Checkout時には、前段で独自に審査ロジックが動作しているため、無駄に審査や料金が発生しないように審査ロジックをスキップするが、Pay IDアプリの非Checkout時には何も審査していないため、APIで必ず審査ロジックを実行する必要があります。その制御にこの仕組みを使っています。 // OpenFeature全体で利用する評価コンテキストを設定 openfeature.SetEvaluationContext(openfeature.NewTargetlessEvaluationContext( map [ string ]any{ "region" : "us-east-1-iah-1a" , }, )) // クライアントに設定(これによって、利用するProviderのみに伝搬される) client := openfeature.NewClient( "my-app" ) client.SetEvaluationContext(openfeature.NewTargetlessEvaluationContext( map [ string ]any{ "version" : "1.4.6" , }, )) // 実行時に評価コンテキストを設定 evalCtx := openfeature.NewEvaluationContext( "user-123" , map [ string ]any{ "company" : "Initech" , }, ) boolValue, err := client.BooleanValue( "boolFlag" , false , evalCtx) また、開発言語によっては、トランザクションコンテキストを利用できる場合があり、リクエストスコープなデータをそこに載せて伝搬することが可能です。 goの場合は、 context packag e を使って実現しています。これによって、HTTPリクエスト受信時にミドルウェアでIPやユーザIDを得られたら、とりあえずトランザクションコンテキストを利用し、評価コンテキストにマージするといった使い方が可能です。 import "github.com/open-feature/go-sdk/openfeature" // set the TransactionContext ctx := openfeature.WithTransactionContext(context.TODO(), openfeature.EvaluationContext{}) // get the TransactionContext from a context ec := openfeature.TransactionContext(ctx) // merge an EvaluationContext with the existing TransactionContext, preferring // the context that is passed to MergeTransactionContext tCtx := openfeature.MergeTransactionContext(ctx, openfeature.EvaluationContext{}) // use TransactionContext in a flag evaluation client.BooleanValue(tCtx, ....) 5. Hooksで機能拡張できる Hooksは、アプリケーション開発者が、フラグ評価に任意の動作を追加できる機能です。 以下の4つのステージでロジックを追加できます。 before: フラグ評価の直前 after: フラグ評価が成功した直後 error: フラグ評価が失敗した直後 finally: フラグ評価後に無条件に これは、評価コンテキストと同様、グローバルや特定クライアント毎や、評価実行時のいずれかで実行するように設定可能です。 ユースケースとしては、評価コンテキストへの編集や追加、評価後のフラグ値の検証、 Telemetryデータ計測 、ログ記録に利用できます。 OpenFeature Hooks LifeCycle 評価コンテキストへの編集に利用するフックの例としては、以下のようなものです。 AppConfigの場合、Contextはリクエストヘッダで送信します。提供されているProviderで、評価リクエストのフォーマットに合うように大体は変換されると思いますが、一部データ型の変換が、評価ルールが想定しているデータ型に合わないケースが、発生することがあります。 変換ロジックを、アプリケーション側のコードで実装することも可能ですが、フラグ管理システムを差し替えた場合、フォーマットが一致しない可能性があるため、その部分を書き直す必要が生じるかもしれません。フックを実装しOpenFeature SDKに登録すれば、アプリケーションロジックに影響を与えず、データ変換ロジックを実行することが可能となります。 package featureflag import ( "context" "maps" "strings" "time" "github.com/open-feature/go-sdk/openfeature" ) // AppConfigのContextに合うように変換するHookを実装します。 type AlterAppConfigContextHook struct { openfeature.UnimplementedHook } // AppConfigでは、HTTP Header: Contextに複数値を詰め込むため、カンマ区切りはそれらの値を分けるために使用されます。 // そのため、カンマ区切りの値を持つ配列値等の場合は、別の区切り文字に置き換える必要があります。 // そもそも配列もサポートしていなそうなため、スペース区切りに変換するようにします。 // // See https://docs.aws.amazon.com/ja_jp/appconfig/latest/userguide/appconfig-creating-multi-variant-feature-flags-rules-operators.html // // Example: // // HTTP Header: Context: "targetingKey=1qaz2wsx3edc,machineId=1qaz2wsx3edc,region=us-east-1-iah-1a,tenant=e1,service=payid-api,scope=payid-app account,accessDate=2025-03-31T04:00:16" func (h *AlterAppConfigContextHook) Before(ctx context.Context, hookContext openfeature.HookContext, hookHints openfeature.HookHints) (*openfeature.EvaluationContext, error ) { oldECtx := hookContext.EvaluationContext() newAttrs := map [ string ]any{} maps.Copy(newAttrs, oldECtx.Attributes()) // 記載されている評価ルールとして利用可能なオペランドのフォーマットに合わせる for k, v := range maps.All(newAttrs) { if t, ok := v.([] string ); ok { newAttrs[k] = strings.Join(t, " " ) } if t, ok := v.(time.Time); ok { newAttrs[k] = t.Format(time.RFC3339) } } newECtx := openfeature.NewTargetlessEvaluationContext(newAttrs) return &newECtx, nil } func NewAlterAppConfigContextHook() *AlterAppConfigContextHook { return &AlterAppConfigContextHook{} } サーバサイドにおけるFeature Flag評価と取得に関する課題 これまで、フラグ管理システムがどこにあるかを意識せずに話していました。しかし、FFaasの場合、クラウド上のどこかに存在するため、どこかの段階でフラグデータセットをネットワーク越しに通信して、取得する必要があります。 その際に考えることはいくつかあるかと思います。フラグが必要になるたびに、リモートに通信してその都度評価してもらうのか、データセットを取得しローカルで評価するのかがありそうです。 Different approaches for server-side SDK architectures という記事によると、サーバーサイドSDKが一般的に採用しているアーキテクチャ上のアプローチは3つあるといいます。 Different approaches for server-side SDK architecture アーキテクチャ 仕組み 利点 欠点 "Direct" API Bridge フラグ評価のたびに、SDKがフラグ管理サービスのAPI(REST/gRPC)を直接呼び出す。 SDKの実装がシンプル。評価ロジックをサービス側に集約できる。 ネットワークのオーバーヘッドが大きく、パフォーマンスが低い。ネットワーク障害の影響を受けやすい。 API with Cache APIからのレスポンスをSDKがキャッシュし、後続の同じリクエストにはキャッシュから応答する。 ネットワークトラフィックを削減できる。 初回リクエストは遅い。キャッシュされていない動的な評価には不向き。キャッシュの更新ラグがある。 Local Evaluation SDKがフラグの全設定データをローカルに保持し、評価をメモリ上で完結させる。設定の更新はストリーミング(SSE, WebSockets)や定期的なポーリングで行う。 非常に高速でネットワークの影響を受けない。耐障害性が高い。 SDKの実装が複雑。全設定データを保持するため、メモリ使用量が増加する可能性がある。 これらのうち、Local Evaluationが最も高速かつ効率的で耐障害性に優れています。しかし、フラグ管理システムが、単純にAPIしか提供していない場合、OpenFeature Providerでこれらの仕組みを頑張って実装する必要があります。 また、独自SDKを提供している場合、そのSDKに対して呼び出しを行うOpenFeature Providerを利用もしくは作成すれば、実装コストを下げることが可能です。 さて、AppConfigの場合、どうなるのでしょうか? 先ほどお見せしたAppConfigの図を再掲します。 引用: https://docs.aws.amazon.com/ja_jp/appconfig/latest/userguide/appconfig-agent.html Agentとアプリケーションを一つのシステムとするなら、Local Evaluationと言えそうです。ただし、1の部分でローカルホスト宛に通信が発生しており、この通信も過負荷時、極まれに失敗することがあります。また、AppConfigでターゲティングを行う場合、このアーキテクチャは変更できません。 このAppConfig Agentを利用するProviderを実装する場合、最も単純な仕組みとするなら、対Agent向けに通信を行い、そちらでフラグ評価を実行する"Direct" API Bridge のアプローチとなります。我々は、OpenFeature Providerとして、 https://github.com/Arthur1/openfeature-provider-go-aws-appconfig を利用しています。 AppConfigにフラグを追加更新、デプロイするには? AppConfigでは、マネージメントコンソールで、直接設定データを作成し、デプロイを実行するケースのほか、 https://docs.aws.amazon.com/ja_jp/appconfig/latest/userguide/appconfig-type-reference-feature-flags.html のJSONスキーマに基づいた設定データを作成し、AWS SDKやCLIを使ってフラグデータセットの更新とデプロイによる設定反映が行えます。デプロイの際、デプロイ戦略を使うことにより、段階的リリースも行うことが可能です。データセットの定義は、コードベースでGit管理しており、CI/CDにより、AWS環境に反映しています。 以下の図の通り、GitHub Actionsでマージ時に差分を検知し、データセットをS3にアップロードすると、Step functionsの各ステップでLambdaが実行され、AppConfigへの設定反映とデプロイが実行されます。 このワークフローでは、定義済みデプロイ戦略: AppConfig.AllAtOnceと同様の戦略を使用しているため、段階的ではなくデプロイ完了後即時リリースとなります。また、手動トリガーでのワークフローも用意しています。 appconfig workflow フラグデータセットについて コードベースにある設定ファイルの形式は、JSONではなくYAMLにしています。ダブルクォートのエスケープなど色々考慮しないといけないためです。 AWS.AppConfig.FeatureFlags のJSONスキーマに準拠したデータ構造で、YAMLファイルに記述して管理しています。これをAppConfigへ反映する際には、JSONに変換します。 version: "1" flags: sampleflag: description: 説明 attributes: attribute_name: description: 属性説明 constraints: type : number minimum: 1 values: sampleflag: enabled: false # または _variants また、ターゲティングでマルチバリアントフラグを利用するためには、以下のように _variants にそれぞれの評価ルールを定義したデータセットを定義する必要があります。 OpenFeatureの評価コンテキストに格納された値を利用し、ruleに一致するかを判定します。以下の設定例だと、評価コンテキストに、 environment=dev と格納された場合は、一番目が選択されます。 values: sign: _variants: - name: High cache enabled: true rule: (eq $environment "dev" ) attributeValues: cache_interval: 3600 expires_in: 3600 - name: default enabled: true attributeValues: cache_interval: 300 expires_in: 3600 attributeValues を併せて定義することにより、そのフラグに関連づけられる属性を追加することができます。OpenFeatureでは、それをMetadataとして取得することが可能です。 AppConfigのフラグ属性は、Boolean形式のFeatureFlagの場合、enableの時のみ利用でき、またデータセット定義で、 _variants を使った場合でないと利用できません。 それにより、アプリケーションに様々な特性を付与することもできます。 上記のデータセット例は、AWS KMSを利用した署名リクエストを制御するフラグを想定しているのですが、毎回署名するのはコストがかかるため、署名をキャッシュしたり有効期限を設定するのに利用したりしています。 ちなみに、OpenFeature Go SDKだと、Metadataは型 map[string]any となっており、フラグ管理システムの定義または、Providerのパース処理によっては、型情報が失われてしまいます。 以下のように、ヘルパー関数を実装するのが良いでしょう。 func GetInt(metadata openfeature.FlagMetadata, key string ) (result int64 , err error ) { // map[string]any なので、int64, float64 の両方を試す intVal, err := metadata.GetInt(key) if err == nil { result = intVal return } floatVal, err := metadata.GetFloat(key) if err == nil { result = int64 (floatVal) return } return } func GetStringArray(metadata openfeature.FlagMetadata, key string ) ([] string , error ) { v, ok := metadata[key] if !ok { return nil , fmt.Errorf( "key %s does not exist in FlagMetadata" , key) } s, ok := v.([]any) if !ok { return nil , fmt.Errorf( "key %s is not an array" , key) } var strArr [] string for _, v := range s { if str, ok := v.( string ); ok { strArr = append (strArr, str) } } return strArr, nil } 注意点 FeatureFlagやFFaaSを利用すると、色々いいことづくめのように見えますが、必ずしも万能ではありません。 1. 後方互換性を保てないリリースをした場合、問題が見つかった後にさっと前の状態に戻すのは難しい コードベースをできるだけ最新に保つトランクベース開発には便利ですが、時には、後方互換性を損なう機能をリリースしないといけない場合があります。 問題が見つかった場合、フラグの切り替えで、コードパスの切り替えはできるかもしれませんが、データ構造が変わってしまった場合、緊急メンテナンスを実施し、データの更新が必要になるかもしれません。 そのため、時にはFeatureブランチで、mainブランチとは長期間切り離した状態で、開発した方が安全な可能性はあります。ただし、その戦略をとった場合、合流時にいわゆるビックバンリリースとなり、作業は大変になるかと思います。 2. 不特定多数のターゲティングには向いているが、特定ユーザの認可の代わりにはならない 認可は、ユーザーが特定のアクションを実行したり、リソースにアクセスしたりする 権限があるかどうか を検証するセキュリティメカニズムのため、目的が異なります。 何でもかんでも、FeatureFlagで制御するのはよろしくない です。 3. 不要になったRelease Flagのお掃除が面倒 リリース後安定運用でき、ロールバックしないとわかった場合、そのフラグは不要となります。ただ、FFaaSの設定を気軽に触りたくないし、そのままにした場合、もうフラグ設定を参照する必要がないのに、不要なアクセスで課金されてしまいます。 削除するにしても、併せてコードも書き換える必要があります。フラグを使わずに済むのならば、その方がリリース後に修正が不要なため、簡潔に実装できます。また、リリース後にも、無駄に課金されることを考えると、場合によっては、環境変数やローカル設定経由の定義で済ませた方が良いということもあります。 一方で、高負荷時やアラートをトリガーに機能オフにするキルスイッチとして、リリース後にもOps Flag的に利用するのなら、FFaasを利用する価値はあります。 おわりに OpenFeatureを中心に、FeatureFlagの現在についてお話ししました。参考になれば幸いです。 BASEでは、今後の事業成長を支えるエンジニアを募集しています。 ご興味があれば、ぜひ採用情報をご覧ください。 採用情報 | BASE, Inc. - BASE, Inc. 明日のBASEアドベントカレンダーは小笠原さんの記事です。お楽しみに!
はじめに この記事はBASEアドベントカレンダー2025の19日目の記事です。 こんにちは。BASEのプロダクト開発チームでバックエンドエンジニアをしている大塚です。 この記事ではNew Relicのダッシュボードを「動く仕様書」にするために、機能ごとに標準化されたダッシュボードをTerraformで手軽に出来るようにする取り組みを紹介させていただきます。 まだまだ構想と検証段階なので、こんなことしようとしているよというニュアンスで紹介させていただきます! New RelicとTerraformについて 取り組みについての紹介に入る前に、New RelicとTerraformについて簡単に紹介させていただきます。 New Relicとは New Relicは、アプリケーションパフォーマンス監視(APM)やインフラストラクチャ監視を提供するクラウドベースの可観測性プラットフォームです。 主な特徴: リアルタイム監視: アプリケーションやインフラストラクチャのパフォーマンスをリアルタイムで可視化 分散トレーシング: マイクロサービスアーキテクチャにおけるリクエストの流れを追跡 カスタムダッシュボード: NRQLというクエリ言語を使って、独自のダッシュボードを作成可能 アラート機能: 異常検知時に通知を送信し、迅速な対応をサポート Terraformとは Terraformは、HashiCorpが開発したInfrastructure as Code(IaC)ツールです。コードでインフラストラクチャを定義し、バージョン管理や自動化を実現してくれます。 主な特徴: 宣言的な記法: HCL(HashiCorp Configuration Language)を使って、あるべき状態を定義 マルチクラウド対応: AWS、GCP、Azureなど、様々なクラウドプロバイダーに対応 状態管理: インフラの現在の状態を追跡し、差分を検出 豊富なプロバイダー: New Relicを含む多くのサービスに対応したプロバイダーが存在 New Relic × Terraformの組み合わせ TerraformのNew Relicプロバイダーを使用することで、ダッシュボード、アラートポリシーなどをコードで管理できます。 これにより以下のようなことが実現できます。 ダッシュボードの構成をGitで管理し、レビューや変更履歴の追跡が可能に 環境ごと(開発、ステージング、本番)に同じ構成のダッシュボードを簡単に展開 チーム全体で標準化されたダッシュボードを共有 手動操作によるミスを削減 これらのメリットを活かして、BASEではNew Relicのダッシュボードを「動く仕様書」として機能させる取り組みを進めています。 ただ、メリットがある一方、Terraform化には以下のような課題もあります。 全リソースを管理できないため、手動での管理とTerraformでの管理のリソースが混在 Terraform管理のリソースを手動で変更することによる競合 これらの課題は、Terraform管理するリソースを絞ることで回避しています。 今後Terraform管理のリソースを増やしていく想定なので、これらの課題に対しての対応も合わせて検討を進めていく予定です。 なぜやるのか 課題 BASEでは各機能のパフォーマンスや挙動を監視するためにNew Relicのダッシュボードを活用していますが、以下のような課題がありました。 ダッシュボードの属人化: 個々のエンジニアが必要に応じて手動でダッシュボードを作成するため、レイアウトや粒度がバラバラになりがち メンテナンスの困難さ: 機能追加や変更があった際、ダッシュボードの更新が漏れたり、誰がメンテナンスすべきか不明確になっている 標準化の欠如: 新しい機能を追加する際に「どんなメトリクスを見るべきか」の指針がなく、監視の抜け漏れが発生しやすい 解決策 これらの課題を解決するために、Terraformを使ってダッシュボードをコード化することで、以下を実現できると考えました。 課題の解決に加えオブザーバビリティを向上させるためにも有効だと考えています。 1. ダッシュボードを「動く仕様書」に 各機能に対して標準化されたダッシュボードレイアウトを定義することで、そのダッシュボード自体が機能の仕様や監視すべきポイントを示す「動く仕様書」として機能します。新しくジョインしたメンバーも、ダッシュボードを見れば「この機能で何を監視すべきか」や「どのように動作しているか」が一目でわかる。 2. コードレビューによる品質担保 ダッシュボードの構成をコードで管理することで、Pull Requestを通じたレビュープロセスを導入できます。これにより、チーム全体で監視項目の妥当性を検討し、知見を共有できる。 構成 New RelicのダッシュボードをTerraform管理する用のリポジトリを用意してコードを管理しています。 構成としてはざっくり以下のようになっています。 ├── main . tf # New Relic provider の設定だけを持つルートモジュール ├── variables . tf # New Relic アカウント情報など共通変数 ├── modules / # 再利用可能な Terraform module 群 │ └── dashboard / │ └── standard_feature_dashboard / │ ├── main . tf # ダッシュボード共通レイアウトの実装 │ └── outputs . tf # 作成したダッシュボードの URL / ID を出力 │ ├── dashboards / # ダッシュボード定義 │ └── ⚪︎⚪︎⚪︎_dashboard . tf # ⚪︎⚪︎⚪︎ダッシュボード │ └── .github / └── ISSUE_TEMPLATE / └── standard_feature_dashboard . yml # ダッシュボード作成依頼用の Issue Form ルートには New Relic provider と共通変数だけを置き、実際のダッシュボード定義は modules/dashboard/... の共通 module dashboards/... 配下の各tfファイルから呼び出す構成にしています。 これにより、ダッシュボードのレイアウトを 1 箇所で統一しつつ、機能単位では URL やトランザクション名などの差分だけを記述すればよい運用になっています。 運用 BASEの機能開発は複数ある開発チームが並行で進めています。 開発の後半でNew Relicのダッシュボードや監視項目の検討、アラートの設定などを実施している開発チームが多いので、その際に標準ダッシュボードを作成するフローを検討しています。 現状、各チームのエンジニアがダッシュボード用のtfファイルをいじるのではなく、知見を持っているメンバーが開発チームの依頼を受けてtfファイルを作成するというフローを採用しています。 GitHub Issue Form と組み合わせることで、「依頼 → Terraform 化 → New Relic 反映」までをスムーズに回せるようにしているのもポイントです。 成果物 Terraformで出力されるダッシュボードは以下のようなダッシュボードです。 複数ページに分かれていますが、例としてbackendアプリケーションのページを紹介します。 New Relicダッシュボード 汎用的なダッシュボードにするために、グラフの種類などはかなり少なめで、アプリケーションの動態が分かる最低限の内容にしています。 トランザクション一覧 スループット エラーレート など Markdownで自由に記載できる項目も用意し、機能のページや仕様書へのリンクなどを置けるようになっています。 おわりに New Relicのダッシュボードを機能ごとの汎用的な「動く仕様書」としてTerraformで出力し、組織のオブザーバビリティを高める活動を紹介させていただきました。 まだ検証段階ですが、こちらの取り組みを組織に広げることで組織のオブザーバビリティを向上させていきたいと思っています。 New RelicとTerraformはダッシュボード以外にも色々なことが出来るので、皆さんもぜひ試してみてください。 BASEでは組織のオブザーバビリティを向上させる様々な取り組みを実施中です。 ご興味のある方は以下のリンクから採用情報などもみていただけると幸いです。 binc.jp
BASE ADVENT CALENDAR 2025 DAY.18 はじめに こんにちは!Data Strategy teamでデータエンジニアをしているshota.imazekiです。 昨今、業務の中でLLMを活用する場面が増えてきており、その流れを受けて弊社でもさまざまな取り組みを進めています。本記事では、その中の一つとして今年挑戦した「SQL自動生成」について紹介します。 SQL自動生成のスコープ 読み進めるにあたって誤解が生じないよう、本記事における「SQL自動生成」のスコープをあらかじめ整理しておきます。 分析基盤上で、分析者が分析目的で実行するSQLを自動生成の対象とします 主に SELECT文の生成を対象とし、CREATE / DELETE / UPDATE といったDDL・DMLは対象外とします LLMを用い、自然言語を入力としてSQLを生成することを前提とします。BIツールのように、UI操作による分析体験を目指すものではありません 取り組んだ背景 BASEでは、分析基盤としてBigQueryを採用し、BIツールにはLookerを利用しています。Lookerは社内で広く活用されており、現在では社員のおよそ3分の1が、毎週Lookerを通じて何らかのデータを確認している状況です。 一方で、SQLを直接書いて自由に分析できるユーザーはごく一部に限られているという課題も抱えていました。Looker上の既存のExploreやダッシュボードでは十分でないケースにおいても、SQLを書くハードルの高さから、簡単なデータ抽出依頼であってもData Strategy teamに来ることが度々ありました。 この状況は、分析者の裾野を広げたいという観点だけでなく、Data Strategy teamの負荷軽減という点でも、改善の余地があると感じていました。そこで今回、「SQLを書く」というボトルネックをLLMでどこまで解消できるのかを検証する取り組みとして、SQL自動生成に挑戦しました。 SQL自動生成における課題 SQL自動生成における課題は大きく分けて2つあると考えてます。 1. 膨大なテーブル構成に対する理解 BASEの分析基盤には数百個のテーブルが存在しており、分析を行う際には次のような知識が求められます。 ある指標や事象を分析する際に、どのテーブルを参照すべきか 複数のデータを組み合わせて分析したい場合に、どのようなキーでテーブルを結合するのか これらの前提をLLMが理解できていない場合、関係のないテーブルを参照したり、そもそも存在しないテーブルを用いたSQLを生成してしまうことがあります。そのようなSQLは、当然ながら分析に利用することはできません。 2. KPIなどのビジネス指標への理解 もう一つの課題は、BASE固有のビジネス指標(KPI)に対する理解です。 例えば、BASEにおけるGMV(流通総額)は、生データとしてそのまま存在しているわけではなく、複数のテーブルを組み合わせた上で、特定の条件に基づいて集計することで初めて算出される指標です。そのため、事前情報がない状態で「GMVを出して」とLLMに指示した場合、BASEが定義するGMVとは異なる値が生成されてしまう可能性があります。 課題の本質 LLM自体はSQLを書くための一般的な知識を十分に備えています。しかし、BASE固有のテーブル構成や業務ドメイン、指標の定義については、LLMが知り得るものではありません。 この「ドメイン知識の欠如」こそが、SQL自動生成における最大の課題だと考えています。そして、これら2つの課題を解決するために、今回の取り組みではディメンショナル・モデリングを用いてテーブル構成を再整理するというアプローチを採用しました。 ディメンショナル・モデリングを用いたSQL自動生成 ディメンショナル・モデリングとは ディメンショナル・モデリングは、分析用途を主眼に置いたデータモデリング手法の一つです。一般に、業務システムで利用されるトランザクション処理向けのデータベースでは、リレーショナルデータモデリングが採用されることが多く、データは正規化された形で設計されます。 このような設計は、個々のトランザクションを正確かつ効率的に処理することを目的としており、その結果として、分析のしやすさよりも更新や整合性を重視した構造になりがちです。そのため、分析を行う際には多くのテーブルを結合する必要があったり、データの意味を理解するために多くの前提知識が求められるケースも少なくありません。 一方、ディメンショナル・モデリングは、ユーザーがデータを分析しやすいことを重視してテーブル構造を設計することを目的としています。 分析の軸となるディメンションと、数値を持つファクトを中心にデータを整理することで、クエリの記述や指標の理解がしやすい構造を実現することが、このディメンショナル・モデリングのゴールとなります。 スタースキーマについて ディメンショナル・モデリングを語る上で、代表的な構成として スタースキーマ があります。スタースキーマは、分析の中心となるファクトテーブルと、その周囲に配置されるディメンションテーブルによって構成されるシンプルなデータ構造です。 ファクトテーブルには、売上金額や注文件数といった集計対象となる数値データが格納され、ディメンションテーブルには、日付・商品・購入者など、分析の切り口となる属性情報がまとまった形で格納されます。これらが、ファクトテーブルを中心に放射状に結合されることから、スタースキーマと呼ばれています。 出典:スタースキーマとは - Power BI|Microsoft Learn https://learn.microsoft.com/ja-jp/power-bi/guidance/star-schema この構成の特徴は、 テーブルの役割が明確であること 結合パターンが限定されること 分析クエリを直感的に記述しやすいこと といった点にあります。そのため、分析者にとって理解しやすいだけでなく、どのテーブルをどのように結合すればよいかを推測しやすい構造になっています。 今回のSQL自動生成の文脈においても、スタースキーマのように構造が明確なデータモデルは、LLMがテーブル間の関係性を誤解しにくく、安定したSQLを生成しやすいという点で相性が良いと考えました。 出典:スタースキーマ (Star Schema)|Databricks https://www.databricks.com/jp/glossary/star-schema なお、分析用途においては One Big Table のように、あらかじめすべてを結合したテーブルを用意する選択肢もあります。一方で今回は、まずはテーブルの役割や関係性を明確にし、段階的にモデルを育てていくことを重視し、スタースキーマから取り組むことにしました。 ディメンショナル・モデリングの導入 BASEの分析基盤では、これまでディメンショナル・モデリングは採用されておらず、データレイクに近い生テーブルや、用途ごとに作成されたデータマートに対して直接クエリを実行するケースが多くありました。このような構成は柔軟性が高い一方で、どのテーブルをどのように使えばよいのかを理解するための前提知識が必要となり、SQLを直接書いて分析するハードルを高めていた要因の一つだと考えています。 今回のSQL自動生成の取り組みでは、この課題に向き合うと同時に、分析者にとっても、LLMにとっても理解しやすいデータ構造を用意することが重要だと考えました。そのため、SQL自動生成の検証と並行して、ディメンショナル・モデリングを導入することにしました。 過去にData Strategy teamへデータ抽出の依頼が集中していた時期があり、その過程で社内の主要なビジネス指標や集計ロジックについて一定の知見が蓄積されていたこともあり、モデリング自体は比較的スムーズに進めることができました。 ディメンショナル・モデリングを導入したことで、結果としてLLMにとっては次のようなメリットが得られました。 参照すべきテーブル数が、数百規模から数個にまで絞られた GMVのようなビジネス指標をあらかじめ集計済みのファクトテーブルとして定義することで、指標の算出ロジックを都度推論する必要がなくなった LLMの選定 ディメンショナル・モデリングを導入した後、次に検討すべきポイントとなったのが、どのLLMを用いてSQL自動生成を行うかという点でした。今回の対象ユーザーは、SQLに習熟していない社内の分析者全般を想定しています。そのため、GitHub Copilotのようにエディタ上での利用を前提とするものや、ローカルLLMのように各自のPCで環境構築が必要となる手法は、利用のハードルが高いと判断しました。 そこで、できる限り導入・利用のハードルが低く、日常業務の延長線上で使える選択肢として、 BigQuery上で直接利用できるGemini in BigQuery ブラウザから利用可能でプロンプトなどを柔軟にカスタマイズできるGPTs の2つに候補を絞って検討を進めることにしました。GPTsは有料プランの機能ではあるものの、社内の多くのユーザーがすでに利用可能な環境であったため、今回の取り組みにおける利用ハードルは低いと判断しています。 Gemini in BigQuery Gemini in BigQueryは、その名の通りBigQuery上で利用できるGeminiです。クエリエディタ内に自然言語をコメントとして記述することで、SQLを自動生成することができます。生成されたSQLはそのままBigQueryのコンソール上で実行されるため、利用ハードルという点では最も低い選択肢だと感じていました。検証時点では無料で利用できたことも、大きな魅力の一つでした。 しかし、今回のディメンショナル・モデリングを前提としたSQL自動生成という文脈では、以下の点から相性が悪いと判断しました。 クエリエディタで、最近表示またはクエリしたテーブルに関する SQL コメントを記述します。 ( 公式ドキュメント より) この仕様上、ユーザーが直前に閲覧・クエリしていたテーブルがディメンショナル・モデリングされたテーブル以外であった場合、意図しないテーブルまで参照してSQLが生成されてしまう可能性があります。 今回の取り組みでは、参照すべきテーブルを明確に制御することが重要でしたが、その制御方法が見つからなかったため、Gemini in BigQueryの採用は見送ることにしました。 GPTs GPTsは、ChatGPTを特定の用途や目的に合わせてカスタマイズできる機能です。指示(Instructions)や知識(Knowledge)を事前に与えることで、特定のドメインやユースケースに特化した振る舞いをさせることができます。以下のスクリーンショットのように、あらかじめ指示や知識を設定することで、SQL自動生成に特化したGPTを作成しました。 今回の取り組みでは、GPTsに対して主に 「指示」と「知識」 の2つを設定しています。 指示 指示には、GPTがどのような役割を担い、どのように振る舞うべきかをまとめました。具体的には、以下のような内容を記述しています。 あなたは BigQueryに精通したSQLエンジニア であること ディメンショナル・モデリングされたテーブル構成や、想定される結合方法を前提にSQLを生成すること ユーザーからのリクエストが、ディメンショナル・モデリングされたテーブル群では対応できない場合は、「Data Strategy teamに問い合わせてください」といった旨の文言を返すこと その他、補足しておきたいBASE固有のドメイン知識など ここで重要なのは、「何でもSQLを生成する」のではなく、対応できないケースでは無理に生成せず、適切にエスカレーションさせる振る舞いを明示している点です。 知識 知識には、GPTが参照できる具体的な情報として、以下の2種類のファイルを設定しています。 ディメンショナル・モデリングされたテーブル群のDDL 各カラムには必ず description を記載し、どのカラムが何を意味するのかをGPTが正確に把握できるようにしています サンプルクエリ ディメンショナル・モデリングだけでは意図が伝わりづらい集計については、実際のSQL例を2つほど与え、生成されるクエリの方向性を補強しています これにより、GPTはテーブル構造や指標の意味を単なるDDLの羅列としてではなく、「どのように使われるのか」という文脈込みで理解できるようになります。 検証結果 作成したGPTが実際の業務で利用可能かを確認するため、あらかじめ用意しておいた約10問の検証用SQL自動生成問題をGPTに解かせてみました。 なお、以下に掲載するSQLは、テーブル名やカラム名をマスク、もしくは実際とは異なる名称に置き換えています。 2025年4月にGMVが100万円以上あったショップの抽出 期間指定・集計・結合といった基本的な構文に加え、HAVING句を用いた条件指定まで含めたSQLを生成することができました。 SELECT u.shop_id, -- ショップID(user_idの文字列) SUM (f.gmv) AS total_gmv -- 合計GMV FROM ` xxx . xxx .fact_tables` f JOIN ` xxx . xxx .dim_users` u ON f.user_id = u.user_id WHERE f.ordered >= ' 2025-04-01 ' -- 2025年4月の開始 AND f.ordered < ' 2025-05-01 ' -- 2025年4月の終了 GROUP BY u.shop_id HAVING total_gmv >= 1000000 -- 100万円以上(単位:円) ORDER BY total_gmv DESC ; 都道府県別のユーザー数ランキング 単純な集計にとどまらず、ウィンドウ関数(RANK)を用いたランキング処理も正しく生成できていることが確認できました。 SELECT prefecture, -- 都道府県 COUNT (*) AS user_count, -- ユーザー数 RANK () OVER ( ORDER BY COUNT (*) DESC ) AS rank -- ユーザー数の多い順に順位付け FROM ` xxx . xxx .dim_users` GROUP BY prefecture ORDER BY rank ; 上記の例以外にも、CASE文、WITH句、各種ウィンドウ関数などを含むクエリについても検証を行いましたが、分析用途でよく使われるSELECTクエリについては、ある程度の複雑さまで対応できそうという印象を持ちました。また、事前に用意していた10問の検証ケースについては、すべて意図どおりのSQLを生成することができています。 そのため、本取り組みはPoCに留めるのではなく、社内向けに展開し、現在は分析者を中心に実際の業務で利用されています。 今後の展望 今回は、分析基盤におけるSQL自動生成というテーマで取り組みを紹介しました。ここでは、現時点で見えている今後の展望について整理します。 テーブルやカラムの追加による対応範囲の拡充 ディメンショナル・モデリングの導入によって、把握すべきテーブル数は大きく絞られました。一方で、その構成に含まれていないデータについては、現時点ではSQL自動生成の対象外となっているのも事実です。今後は、スタースキーマ型のテーブルを段階的に拡充していくことで、より多くの分析ニーズに対応できるようにしていきたいと考えています。 対応範囲を広げつつも、テーブル構造の分かりやすさを保つことを意識しながら、モデルを育てていく予定です。 ハルシネーション対策 LLMを利用する以上、ハルシネーションのリスクを完全に排除することはできません。 ディメンショナル・モデリングの導入や指示文の工夫によって、一定の抑制は可能ですが、常に正しいSQLが生成されることを前提にするのは現実的ではないと考えています。 そのため、利用者側にも最低限の前提知識は必要になります。高度なSQLを書くスキルまでは求めませんが、 ディメンショナル・モデリングされたテーブル構成の理解 生成されたSQLを読み、妥当性を判断できる力 は重要だと考えています。 今後は、社内勉強会などを通じてこれらの理解を深め、LLMを過信せず、うまく付き合っていくための土台作りにも取り組んでいきたいと思います。 おわりに BASEでは、LLM活用に限らず、分析基盤全体の改善に継続的に取り組んでいます。 こうした取り組みにご興味のある方は、ぜひお気軽にご応募ください! A-1.Tech_データエンジニア / BASE株式会社 明日のBASEアドベントカレンダーは大塚さんの記事です。お楽しみに!
はじめに この記事は BASE アドベントカレンダー17日目の記事です。 devblog.thebase.in こんにちは、BASE CSE Group のグループマネージャーをしている @izuhara です。 BASEは「誰でもかんたんにネットショップを開設できる」サービスとして成長し、多くのショップオーナーに利用されてきました。その裏側では、事業規模が拡大するにつれ、オペレーションも複雑さを増し、バックオフィスやオペレーションを行うチームに属人化や手作業が蓄積していくという課題が生まれていました。 こうした背景のもと、事業運営を技術で支えるために立ち上がったのが CSE(Corporate Solution Engineering)チーム です。 現在CSEでは、以下の3つを柱として業務改善・自動化を推進しています。 月次売上計上業務の自動化対応 決済を中心とした社内システムの構築 AI活用を前提とした業務の再構築 本記事では、BASEの裏側を支えるCSEチームの変遷をこの3フェーズに分けて紹介します。 フェーズ1:経理向け月次売上計上業務の自動化(2020年〜) CSEが最初に向き合ったのは、BASEの事業基盤となる月次売上計上業務でした。 当時、売上データの集計や修正作業はすべて手作業で行われており、月次締めの度に数日間にわたる作業が発生していました。 さらに、上場直後であったこともありJ-SOXへの対応強化が求められ、売上金の透明性や監査対応の厳密さが一段と必要とされるタイミングでもありました。 CSEの主な取り組み 売上データ集計・計上処理の自動化 BASE側の決済データと各決済サービスの入金データ、ショップ売上金の整合性チェック機能の構築 詳細はこちらをご覧ください。 devblog.thebase.in 売上計上の整合性チェック 成果 月次作業が数日→数時間に短縮 クエリの叩き間違いなどによる売上計上のヒューマンエラーが大幅に減少 経理チームが安心して業務を進められる安定したプロセスを提供 このフェーズは、CSEがまず「足元の重要業務を支えるエンジニアリングチーム」として役割を確立した時期でした。 フェーズ2:決済を中心とした社内システムの構築(2022年〜) 事業成長に伴い、経理領域以外にも自動化ニーズが急増したタイミングです。 また、このタイミングでIT統制領域が Product Governance チームとして分離され、CSEはより社内業務改善に特化した組織 へと方向転換しました。 社内ではEUC(End User Computing)によるスプレッドシート・手入力運用が多く、業務のブラックボックス化やミスの温床になりつつありました。これらを計画的にシステム化し、再現性と可視性の高い業務基盤へと移行していくことが求められました。 CSEの主な取り組み 請求書発行プロセスの自動化、債権回収モニタリング機能の開発 kintoneを活用した業務アプリケーションの高速構築 EUC依存からの脱却と、業務のシステム化の整備 インボイス制度への対応 成果 手運用で行われていた社内オペレーションの多くをシステム化 kintone等を活用し、小さく始めて早く改善する内製プロセスを社内に定着 請求や債権管理など、ミスが許されない領域で運用リスクを大幅に低減 このフェーズを通じて、CSEは「社内システムの開発パートナー」としての立ち位置を確立しました。 フェーズ3:AI活用を前提とした業務の再構築(2025年〜) 生成AIの登場により、業務改善は新たなステージへ入りました。 従来の「人がやっていた作業を自動化する」から一歩進み、業務プロセスそのものをAI前提で再設計するフェーズです。 まずPoCとして着手したのは、社内でも問い合わせが多い人事・労務領域のAI(RAG)による自動応答です。FAQの回答や書類手続きの案内など、繰り返し発生するコミュニケーションをAIで対応する仕組みづくりを進めました。 PoCで得たAIによる回答品質の高め方や、セキュアな情報をAIで取り扱うための基盤構築などは、その後のカスタマーサポート業務のAI導入にも活かされています。 CSEの主な取り組み 人事・労務・総務など、社内問い合わせのAI自動応答の構築 カスタマーサポート業務のAIによる業務置き換え 社内データを安全に扱うための基盤整備 詳細はこちらの記事をご覧ください。 devblog.thebase.in 社内問い合わせのAI自動応答 成果 問い合わせ対応のAIによる自己解決 AIを活用した業務改善の成功例が蓄積し、AI活用の窓口としての役割の拡がり AI活用はまだまだ始まったばかりで、改善途中にも新たな技術革新が繰り返されているところですが、このフェーズを通じて、CSEは「AI活用で業務を再構築」する新たな改善施策を進められるようになりました。 おわりに CSEチームは、BASEの事業成長に合わせて 「業務の自動化 → 社内システム構築 → AI活用で業務を再構築」 という進化を続けてきました。 今後も社内のあらゆる業務がAIで再構築されていく未来を見据え、BASEの事業を支える目に見えない基盤をつくり続けていきます。 BASEでは、今後の事業成長を支えるエンジニアを募集しています。 ご興味があれば、ぜひ採用情報をご覧ください。 binc.jp 明日のBASEアドベントカレンダーは @ImazekiShota さんの記事です。お楽しみに!
はじめに この記事はBASEアドベントカレンダー2025の16日目の記事です。 こんにちは。Pay ID プラットフォーム Group で エンジニアをしている noji です。最近は Pay ID の認証基盤のフロントエンド開発を担当しています。 本記事では BASE のショップや Pay ID アプリでの買い物時にカートでの Pay ID ログイン機能を提供している JavaScript(以後 payid-js)のビルド環境を webpack/Babel から esbuild に移行した話を紹介します。 payid-js について payid-js は Pay ID ログイン機能を提供している埋め込み用の JavaScript です。Pay ID ログインすることで、Pay ID に登録されている住所情報や決済手段情報を連携することで、ユーザーはスムーズに購入手続きを進めることができます。 BASE のカートのフロントエンドで payid-js を読み込み、用意された関数を呼び出すと画面上にログイン用の画面が iframe 上に表示され、Pay ID にログインできます。iframe 内でログイン処理を行い、結果を postMessage API を使ってカートのフロントエンドに通知します。 payid-js は iframe 内外でやり取りを行うインターフェースを提供しており、iframe 内でログインが完了すると、結果をカートのフロントエンドに返すようになっています。(iframe の内側の画面については別システム) 技術としては、TypeScript で実装されており、ビルドには webpack と Babel を使用していました。 移行背景 payid-js は BASE のカートと iframe で表示される Pay ID ログイン画面の橋渡しをするだけのコンポーネントなので、軽量な JavaScript です。 軽量であるので、webpack のビルドに時間がかかるとは感じていませんでしたが webpack 時代のバージョンアップや設定変更が大変 Babel を含む関連ライブラリの設定が複雑 依存関係の脆弱性が多い payid-js には webpack ほど高度な機能が不要である などの課題があり、よりシンプルなビルドツールへの移行を検討しました。 esbuild を選んだ理由 候補として esbuild、vite、Rollup などがありましたが、最終的に esbuild を選択しました。理由は以下です。 シンプルで高速なビルドが可能 Go 製でビルドが非常に速いのに加えて、設定もシンプルでわかりやすく、TypeScriptのトランスパイルも内蔵されていてBabelも不要 依存関係が少なく、メンテナンスコストや脆弱性リスクが低い webpack や Babel に比べて関連ライブラリ等の依存関係が少なく、アップデートや脆弱性の対応に追われる負荷が軽減されそう 他ツールとの比較 vite:SPA 向けの開発サーバーは強力だけど、payid-js のような埋め込み用 JavaScript にはオーバースペック Rollup:esbuild ほど高速ではなく、設定もやや複雑になる。ライブラリ向けには良いが、今回は見送り esbuild は HMR(Hot Module Replacement) をサポートしていないですが、payid-js は埋め込み用の JavaScript であり、開発時に HMR は必要ないため問題ありませんでした。 参考 esbuild vite Rollup 移行で詰まったポイント ローカルの 開発サーバーの構築 今までは webpack-dev-server を使用してローカル開発環境を構築していました。 webpack-dev-server はビルドしたアセットをメモリに保持し、変更があれば自動で配信内容を更新してくれる開発サーバーを内蔵しています。 Docker からのアクセスでも常に最新が返ってくるため、ビルド・配信・更新反映をひとまとめに解決してくれる優れた仕組みでした。 一方、esbuildにはwebpack-dev-serverのような開発サーバーは内蔵されておらず、あくまで”ビルド”のみの機能です。今回は serve で簡易的に http-server を立ち上げるスクリプトを用意しました。watch だけだと変更を検知して Docker コンテナに反映させることができなかったので、 chokidar も利用し、変更を検知して明示的に再ビルドできるようにしました。 #!/usr/bin/env node import path from "path" ; import { fileURLToPath } from "url" ; import * as esbuild from "esbuild" ; import { spawn } from "child_process" ; import chokidar from "chokidar" ; const __filename = fileURLToPath ( import . meta . url ) ; const __dirname = path . dirname ( __filename ) ; const outdir = path . resolve ( __dirname , "dist" ) ; // esbuild の watch 用コンテキストを作成 const ctx = await esbuild . context ({ entryPoints : [ path . resolve ( __dirname , "src" , "index.ts" )] , bundle : true , sourcemap : true , platform : "browser" , outdir , entryNames : "bundle" , minify : true , loader : { ".html" : "text" , } , }) ; await ctx . watch () ; console . log ( "esbuild: watching" , outdir ) ; // chokidar でファイル変更を監視して rebuild const watcher = chokidar . watch ([ path . resolve ( __dirname , "src" )] , { ignoreInitial : true , usePolling : true , interval : 100 , }) ; let rebuilding = false ; async function scheduleRebuild ( event , filePath ) { if ( rebuilding ) return; rebuilding = true ; console . log ( `change detected ( ${ event } ):` , filePath ) ; try { await ctx . rebuild () ; console . log ( "esbuild: rebuild complete" ) ; } finally { setTimeout (() => ( rebuilding = false ) , 50 ) ; } } watcher . on ( "add" , ( p ) => scheduleRebuild ( "add" , p )) ; watcher . on ( "change" , ( p ) => scheduleRebuild ( "change" , p )) ; watcher . on ( "unlink" , ( p ) => scheduleRebuild ( "unlink" , p )) ; // ローカルサーバー (npx serve) spawn ( "npx" , [ "serve" , "-s" , outdir , "-l" , "9000" ] , { stdio : "inherit" , shell : true , }) ; このスクリプトを実行すると、chokidar がソースコードの変更を監視し、変更があった場合に再ビルドを行います。また、 npx serve を使用してローカルサーバーを立ち上げ、ブラウザから埋め込み用 JavaScript を確認できるようにしています。 ビルドの成果物の違い 基本的に成果物はほぼ同じでしたが、loaderの指定によりHTML の import 部分で差異がありました。 webpack: HTML モジュールをオブジェクトとして扱う esbuild: HTML モジュールを文字列として扱う そのため後々の移行の手順にもあるように、一定期間同じコードベースで webpack/esbuild の両方をビルドする必要があったため、どちらのビルド方法でも動作するように、下記のようなユーティリティ関数を追加しました。 const rawModule = require( "./container.html" ); const html = getHtmlStringFromModule(rawModule); // `*.html` をバンドルする方法はバンドラによって異なります。 // - esbuild や rollup の一部設定では、インポートはそのまま文字列になります。 // - もしくは `{ default: string }` のようなオブジェクトを返す場合もあります。 // ここで形を正規化することで常に文字列として扱えるようにします。 const getHtmlStringFromModule = ( mod : unknown ): string => { if ( typeof mod === "string" ) { return mod; } if ( typeof mod === "object" && mod !== null ) { const maybeDefault = (mod as Record < string , unknown >). default ; if ( typeof maybeDefault === "string" ) { return maybeDefault; } } throw new Error ( "unexpected HTML module shape" ); } ; ビルド用の設定 #!/usr/bin/env node import path from "path" ; import { fileURLToPath } from "url" ; import { build } from "esbuild" ; const __filename = fileURLToPath ( import . meta . url ) ; const __dirname = path . dirname ( __filename ) ; const outdir = path . resolve ( __dirname , "dist" ) ; const outfile = path . join ( outdir , "bundle.js" ) ; // 本番ビルド await build ({ entryPoints : [ path . resolve ( __dirname , "src" , "index.ts" )] , bundle : true , sourcemap : true , platform : "browser" , outfile , minify : true , define : { API_BASE_URL : JSON . stringify ( process . env . API_BASE_URL || "" ) , } , loader : { ".html" : "text" , } , logLevel : "info" , }) ; console . log ( "esbuild: built" , outfile ) ; ビルド用のスクリプトも非常にシンプルです。esbuild の build 関数を使用して、エントリーポイントや出力先、バンドル設定などを指定しています。 ビルドされたファイルを CircleCI のジョブで S3 にアップロードし、CDN 経由で配信する仕組みは以前と同様に維持しています。 移行の手順 移行は段階的に行いました。 ローカル/dev 環境のみ esbuild に切り替え stg/本番も esbuild に切り替え webpack/Babel 関連の設定・依存関係を削除 結果として、問題なく移行でき、切り替えによる影響もありませんでした。 移行結果 元々軽量な JavaScript であったため、ビルド時間の劇的な改善はありませんでしたが、設定が大幅にシンプルになり、依存関係の脆弱性も出にくくなりました。 元々が CircleCI 上で 2 ~3秒程度のビルド時間でしたが、esbuild に移行したことで 1 秒未満に短縮されました!! おわりに payid-js のビルドを webpack/Babel から esbuild に移行したことで、設定のシンプル化と依存関係の削減が実現できました。 今後も payid-js の開発を続けていく中で、さらなる改善点が見つかれば積極的に取り組んでいきたいと思います。 BASE / Pay IDではエンジニアを募集しているので、興味ある方は以下からご連絡ください。 明日のBASEアドベントカレンダーはIzuharaさんの記事です。お楽しみに。 binc.jp
はじめに BASE Dept で アプリケーションエンジニア をしている Capi(かぴ) です。 BASEでは機能開発に加え、プロダクトの品質を向上させるため非機能要件の強化も行なっております。今回は自分が半年間ほど担当してきた SASTツールPoC についてお話ししていきます。PoCのプロジェクトが立ち上がり今日までに行なってきたことを可能な限り紹介していきます。 ※ SASTツールとは SAST (Static Application Security Testing) とはアプリケーションのソースコード、バイトコード、バイナリコードに対して脆弱性が内在するか否かを確認するテスト手法であり、ホワイトボックステストの一種である。 SASTは、アプリケーション機能をブラックボックステストするDAST (Dynamic Application Security Testing)と異なり、アプリケーションのコードコンテンツ、ホワイトボックステストに焦点を当てている。SASTツールは、関数レベル、ファイルまたはクラスレベル、アプリケーションレベルなどの分析レベルによりソフトウェアとアーキテクチャに潜在するセキュリティの脆弱性を特定する。 NECソリューションイノベータ, 「SAST (Static Application Security Testing)」, ( https://www.nec-solutioninnovators.co.jp/ss/insider/security-words/74.html ) 自分が所属する組織が抱えている「開発フローに組み込める実用的なSASTツールを評価・選定することで、セキュリティリスクの早期検知体制を確立したい」という課題に対して調査、提案、検証、まとめを一貫して行うことができたのは良い経験でした。 今回の記事でPoCプロジェクトのメンバーだけでなく他の部署、マネージャー陣も巻き込み、色々工夫しながら進めてきた記録が少しでも伝えられれば幸いです。また、SASTツールの導入はもちろん、SASTツール以外で新しいツールの導入を考えている方にこの記事が少しでも参考になれば幸いです。 業界トレンドと社内調査 「SASTツール入れたい!!」と言うだけではなかなか導入の舵は切れません。まずは業界のセキュリティトレンド、昨今のセキュリティ事例、社内のセキュリティ対応状況のインプットから始めました。 書籍でインプット(一部抜粋) 体系的に学ぶ 安全なWebアプリケーションの作り方 第2版[固定版] 脆弱性が生まれる原理と対策の実践 セキュアで信頼性のあるシステム構築 ―Google SREが考える安全なシステムの設計、実装、保守 組織、団体公開しているセキュリティ情報をインプット(一部抜粋) OWASP Top10 ECサイト構築・運用セキュリティガイドライン (IPAが公開している資料) セキュリティ事例をインプット 過去に社内インシデントがあるかどうか 外部が公開しているECサイトの情報漏洩、不正アクセス、不正決済事例の調査 セキュリティ対応状況をインプット これまで行ってきた社内のセキュリティ施策の洗い出し 社内セキュリティロードマップの確認 調査をもとに現在の課題と解決策に期待する効果を図解する インプットが終わり、自分たちが行っていきたいイメージがついてきたところで社内の開発フローと社内のセキュリティ対応を一枚の図に起こしました。 実際に図解するとセキュアな実装について考える頻度は少なく「現状どこが不足しているのか」、「今後SASTツールの導入でどこが強化できそうか」をわかりやすくすることができました。 なぜSASTツール導入をしたいのかをよりはっきりさせることができたと考えています。 図解の様子 ※ 付箋の色の意味(一部抜粋) 緑色: 現在の開発フローで行われていること 水色: セキュリティ対応に関すること 赤色: SASTツール導入で期待すること ツール調査と選定 ツール選定ではカバー株式会社さんの資料を参考にさせていただきました。 note.cover-corp.com まずは下記項目でツールを評価し、PoCで試したいものを選定しました。 我々の開発言語に対応しているか。ルールセットがどれだけあるのか 有料ツールを使った場合のコスト CI対応、IDE連携対応 ツール選定の様子(一部) 調査が進むにつれて「今回がPoCということもあり、最初から有料ツールを導入するよりもOSSで小さく始めるのが良いのではないか」という意見が多く出てきました。そして最終的にPoCで検証したいツールを SonarQube と Semgrep に絞りました。 コード品質、セキュリティ、および静的解析ツール(SonarQube) | Sonar Semgrep App Security Platform | AI-assisted SAST, SCA and Secrets Detection 課題の整理と提案 業界トレンドと社内調査、ツール選定を終えた段階で他部署やマネージャー陣に提案をしました。 OWASP Top10やIPAの資料など信頼できる情報源を参考にしたり、現状と理想を図解することでわかりやすくSAST導入の背景やSAST導入で期待する効果を伝えることができました。また、提案資料は全てドキュメントに残します。 ドキュメントに残す理由は意思決定の背景をのちほど参照できるようにするためです。当時の目標や当時の意思決定の根拠を残すことで振り返りの材料を用意しておきます。将来の改善に次に繋げることができると考えたためです。 検証実施 スキャン実施、IDE連携を試したりしました。 下記項目でツールを評価しました。 修正のしやすさ(検出された内容がわかりやすいか、直すのが容易か) 設定の容易さ(設定の手間) 誤検知率(多さ) CIへの組み込みやすさ(設定の手間、環境変数の設定忘れで起こることとその対応工数) ダッシュボードの使いやすさ etc… 定量面と定性面で評価を進めました。実際に使ってみると「想像してたのと違った」という気づきがありました。 検証の中で作ってみたデモ SonarQubeとSemgrepを検証した結果、プロジェクトチーム内で「Semgrepの方が適していそうだ」という判断をしました。そして実戦投入を想定した仕組みのデモ構築を進めました。 デモで構築したものはCIでSASTツールを使い定期スキャンを実行、スキャン結果で対応優先度が高いものはGitHubのIssuesに登録されるというものです。 GitHubのIssueに登録することでGitHub Copilotで修正してもらうことができるのではと考えました。修正の全てをCopilotに任せるだけでなく最初の修正案を出してくれる存在としても活用できると考えています。 下記はSASTツールに無料版のSemgrepを利用して構築したものの図です。 デモで作成したプロジェクトのIssuesです。Issue登録時にラベルを付与することで検索しやすくしています。 自動でIssueに登録されたもの デモを作る時に困ったことと解決方法 検証の中でこんなことしたいあんなことしたいがたくさん思いつきましたが、すんなりいかない時や悩みがたくさんありました。いくつか紹介させていただきます。 1. SASTツールのスキャン結果をGitHubの世界に持っていけない スキャン結果で対応優先度が高いものはGitHubのIssuesに登録 先ほど紹介したこの仕組み、自分が検証していた製品(Semgrep)には機能として存在していませんでした。そこでCIでスキャン結果をjsonファイルに出力し、そのjsonファイルを加工してIssueとして登録するシェルスクリプトを書きました。 このシェルスクリプトを使った解決方法は自分の同僚であり自分がとても尊敬している meiheiさん がPHPカンファレンス福岡2025で発表していた「隙間ツール開発」から着想を得ました。 speakerdeck.com 2. スキャン結果をどう加工して登録すればいいのか スキャン結果を出力したjsonファイルはそのまま使えませんでした。なのでjson結果を自然なIssueとして登録するための加工をしました。 次に話すのですが、GitHubの既存Issueを取得してすでに登録されているIssueを重複させない仕組みにしたかったのでタイトルを優先的に考えました。 最終的に下記にしました。 $severity='危険度' $path='対象ファイルの場所' $line='行' $col='列' $id='どんな脆弱性か' $TITLE="[$severity] in $path line: $line col: $col - Rule: $check_id" Issueのページで確認すると全体はこんな感じです。 Issueの例 「タイトルのファイルの行と列は不要じゃないか」と考える方がいらっしゃるかもしれませんが、1つのファイルで複数検出された場合、現状は1つしかIssueを作れません。ゆくゆくは「ファイルごとにIssueを作る」に改善した方が良いと考えています。 3. GitHub Issuesに同じスキャン結果が登録される GitHubに同じタイトルでいくつもIssueが作成可能です。これによって定期実行を行うと新規のIssueに加え、まだ対応し切れていないIssueを重複して作成してしまいます。 下記の画像は検証でデモを作成している間に生まれた同じ内容のIssue達です。 デモ開発中に見つけたIssueの重複 これを防ぐために既存のIssuesと同じタイトルかどうかを確認するようにしました。既存のIssues取得はGitHubのAPIでできます。 docs.github.com ※こちら2025年12月現在、注意書きがあるので気をつけてください。Issuesと一緒にプルリクエストも取得してしまうことがあるのでこれは利用者が対応する必要があります。 GitHub's REST API considers every pull request an issue, but not every issue is a pull request. For this reason, "Issues" endpoints may return both issues and pull requests in the response. You can identify pull requests by the pull_request key. 既存のIssuesと同じタイトルかどうかを確認 また上記ですが、これはSASTツールで検出された警告のリストの要素と既存Issuesのリストを突合する処理で実現しました。for文を2回まわして突合することも可能ですが、for文のネストは計算量が大きくなってしまうので 既存Issuesのリストを連想配列にして突合してみました。 for文を2回まわして突合 と 連想配列にして突合 のサンプルコードは下記になります。 #!/bin/bash list1 = ( " apple " " banana " " cherry " ) list2 = ( " banana " " cherry " " date " " fig " ) echo " === 二重ループ (O(n×m)) === " hit = 0 # リストに含まれているかのフラグ for a in " ${list1[ @ ]} " ; do for b in " ${list2[ @ ]} "; do if [[ " $a " == " $b " ]] ; then hit = 1 break fi done if [[ $hit -eq 1 ]] ; then echo " $a は list2 に含まれる " continue else echo " $a は list2 に含まれない " continue fi hit = 0 done # === 二重ループ === # apple は list2 に含まれない # banana は list2 に含まれる # cherry は list2 に含まれる #!/bin/bash list1 = ( " apple " " banana " " cherry " ) list2 = ( " banana " " cherry " " date " " fig " ) # list2 をハッシュセット化 declare -A set for b in " ${list2[ @ ]} " ; do set[" $b "]= 1 done echo " === 連想配列 === " # list1 の各要素がセットにあるか確認 for a in " ${list1[ @ ]} " ; do if [[ -n " ${set[$a]} " ]] ; then echo " $a は list2 に含まれる " continue else echo " $a は list2 に含まれない " continue fi done # === 連想配列 === # apple は list2 に含まれない # banana は list2 に含まれる # cherry は list2 に含まれる 4. 意思決定の情報が記録されず未来に情報を残せない これは実装に関係ない話しです。 PoCを進める中で追加で決めたい内容は出てきます。自分は導入することだけに集中してしまい、「なぜこの構成になっているのか?」、「これは誰が決めたのか?」の情報を残さないことに危機感を感じました。今回は広範囲に影響する変更になりうるのできちんと文章に残しました(検索ができればSlackに残しても良いと思います)。 意思決定ログ(Notionのテーブル) これは過去に別プロジェクトでやってよかったと感じたものを輸入しました。 おわりに 駆け足でしたが約6ヶ月のSASTツールPoCの内容を共有させていただきました。 振り返るとPoCの中で普段の機能開発とは違う能力が求められたこと、今まで触ってこなかったツールに詳しくなれたのは良い経験になったと考えています。 BASEでは機能開発はもちろん非機能要件について考えたり既存システムの改善について考える課題があります。また、その課題に挑戦する機会もあります。ご興味あればぜひ採用情報をご覧ください。 binc.jp 明日はPay ID所属の noji さんによるフロントエンドのビルド周りに関する記事です!
BASE ADVENT CALENDAR 2025 DAY.14 はじめに 本記事は BASE アドベントカレンダー 2025 の 14 日目の記事です。 BASE BANK Dept で フルサイクルエンジニア をしている 02 です。 2025年4月、BASEは新しい振込申請機能「最速振込」をリリースしました。最短10分、土日祝日を含む365日対応での入金が可能になり、ショップオーナーさんのキャッシュフロー改善に大きく貢献しています。 本記事では、最速振込の実装で使用したスキーマ変更とMySQL INSTANT DDLを活用したマイグレーションについて解説します。なお、テーブル名・カラム名は説明用に簡略化しています。 プロジェクト管理やリーダーシップの観点、振込申請や最速振込の詳細については、以前の記事「 最速振込の舞台裏:プロジェクトのリードの実践と学び 」で紹介しました。よろしければそちらもご覧ください。 既存のテーブル設計課題と新しいカラム 最速振込を追加する前、BASEの振込申請には通常振込、お急ぎ振込、定期振込の3種類がありました。当時、振込種別は複数のテーブルを参照して判定していました。 通常振込:振込手数料テーブルのお急ぎ振込フラグがOFF お急ぎ振込:振込手数料テーブルのお急ぎ振込フラグがON 定期振込:定期振込ログテーブルにレコードが存在 しかし、この設計には課題がありました。 振込種別を判定するために複数テーブルをJOINする必要がある フラグでは2種類しか表現できず、お急ぎ振込か否かしか判断できない 新しい振込種別を追加するたびに判定ロジックが複雑化する これらの課題を解決するため、振込申請テーブルに振込種別カラムを追加しました。 transfer_type varchar ( 20 ) COMMENT ' 振込種別(通常振込: normal、お急ぎ振込: express、定期振込: scheduled、最速振込: instant) ' これにより、振込種別の判定が振込申請テーブルの1カラムで完結します。カラム追加には、MySQLのINSTANT DDLを活用しました。 MySQL INSTANT DDLの活用 INSTANT DDLとは MySQL 8.0.12で導入されたALTER TABLEのアルゴリズムです。従来のCOPYやINPLACEと異なり、メタデータの更新のみで操作が完了するため、非常に高速です。 アルゴリズム 動作 特徴 COPY テーブルをコピーして入れ替え テーブルロックが発生、最も遅い INPLACE その場で変更(内部的には再構築) DML操作は可能だが、時間がかかる INSTANT メタデータのみ更新 非常に高速、テーブルサイズに依存しない INSTANT DDLは行データの再配置や再構築を行わず、メタデータだけを更新します。 結果として、アプリケーション視点では接続断やタイムアウト、強制リトライが生じず、読み書きトラフィックを止めないまま変更を完了できます。これが本記事で述べる「完全無停止」の根拠です。 MySQL 8.0.29以降、INSTANT DDLは以下の操作に対応しています。 カラムの追加(任意の位置に追加可能)、削除 カラム名の変更 ENUM/SETへの要素を追加 DEFAULTの追加、変更、削除 仮想カラムの追加、削除 テーブル名の変更 インデックスのデータ構造(BTREEやHASH)の変更 ただし、以下のような制限があります。 INSTANTのアルゴリズムに対応していない構文との併用はできない ROW_FORMAT=COMPRESSED のテーブルでは使用できない FULLTEXT インデックスを含むテーブルでは使用できない INSTANT DDLの操作回数には上限があり、テーブルごとに64回(超過時はOPTIMIZE TABLEでリセット)まで 詳細については、MySQLの公式ドキュメントをご参照ください。 https://dev.mysql.com/doc/refman/8.0/en/innodb-online-ddl-operations.html#online-ddl-column-operations なぜINSTANT DDLを選んだのか 振込申請テーブルは数百万件のレコードを持つ、追加・更新頻度の高いテーブルです。従来のINPLACE DDLでは長時間のロックが発生するため、メンテナンス時間の確保か、 gh-ost を使用したカラム追加が必要だと考えていました。 しかし、INSTANT DDLを使用すればサービスへの影響を最小限に抑えられ、メンテナンス時間も不要です。さらに、マイグレーション手順を適切に設計すれば、DDLの実行だけでカラム追加が完結します。 本番相当のデータ量を持つ検証環境で検証した結果、無停止で実現できると確信しました。 INPLACE DDL: 3分ほど INSTANT DDL: 374ms マイグレーション手順と安全なカラム追加の戦略 新しいカラムを追加する際、単純にカラムを追加してアプリケーションを更新するだけでは不十分です。過去のレコードとの整合性が取れないため、以下の手順で段階的にマイグレーションを行いました。 なお、今回出てくるクエリは、例として名称は一部置き換えています。 Step 1: DEFAULT句ありでカラム追加 まず、振込種別カラム transfer_type を DEFAULT句 'normal' (通常振込)で追加します。これにより、既存レコードの transfer_type には自動的に 'normal' が設定されます。 ALTER TABLE 振込申請テーブル ADD transfer_type varchar ( 20 ) DEFAULT ' normal ' COMMENT ' 振込種別 ' , ALGORITHM=INSTANT, LOCK = DEFAULT ; 'normal' (通常振込)をデフォルト値として選んだのは、通常振込が最も多く使われている振込種別だからです。Step 4で過去データをマイグレーションする際、更新するレコード数を最小限に抑えられます。最もレコード数の多い通常振込を更新対象から除外できるため、この方法が効率的だと判断しました。 Step 2: アプリケーションの対応 振込申請作成時に振込種別カラムを設定するようアプリケーションを更新します。 通常振込作成時: transfer_type = 'normal' お急ぎ振込作成時: transfer_type = 'express' 定期振込作成時: transfer_type = 'scheduled' Step 3: DEFAULTを検知用の値に変更する 次にDEFAULT句を検知用の値である 'unsupported' に変更します。 ALTER TABLE 振込申請テーブル ALTER transfer_type SET DEFAULT ' unsupported ' , ALGORITHM=INSTANT, LOCK = DEFAULT ; この変更後、 transfer_type = 'unsupported' のレコードが発生しないことを監視します。もし発生した場合は、対応漏れがあることを意味するため、アプリケーションコードを修正します。 Step 4: 過去データのマイグレーション レコードを監視して対応漏れがないことを確認したら、過去のレコードを関連テーブルの情報をもとに更新します。 -- お急ぎ振込の過去レコード(振込手数料テーブルのお急ぎ振込フラグを参照) UPDATE 振込申請テーブル LEFT JOIN 振込手数料テーブル ON 振込申請テーブル.振込申請id = 振込手数料テーブル.id SET 振込申請テーブル.transfer_type = ' express ' WHERE 振込手数料テーブル.お急ぎ振込フラグ = 1 ; -- 定期振込の過去レコード(定期振込ログテーブルの存在を参照) UPDATE 振込申請テーブル LEFT JOIN 定期振込ログテーブル ON 振込申請テーブル.id = 定期振込ログテーブル.id SET 振込申請テーブル.transfer_type = ' scheduled ' WHERE 定期振込ログテーブル.id IS NOT NULL ; Step 1でDEFAULT句を追加した際、既存レコードの transfer_type には自動的に 'normal' が設定されています。そのため、通常振込の過去レコードについてはUPDATE文を実行する必要はありません。 Step 5: 参照ロジックの切り替え 最後に、振込種別の判定ロジックを新しいカラム transfer_type を参照するように切り替えます。また、任意のタイミングでDEFAULT句を削除します。 ALTER TABLE 振込申請テーブル ALTER transfer_type DROP DEFAULT , ALGORITHM=INSTANT, LOCK = DEFAULT ; こういった手順で振込申請テーブルに振込種別カラムを追加しました。 おわりに 最速振込の実装では、MySQL INSTANT DDLを活用することで、更新頻度の高い数百万行のテーブルへ、メンテナンスなしでスキーマ変更を実現できました。 MySQL 8.0.29以降を使用している方は、ぜひINSTANT DDLの活用を検討してみてください。 BASE BANK Deptでは、プロダクト開発をリードできるエンジニアを募集しています。興味のある方は、ぜひ採用情報をご覧ください! binc.jp 明日は、BASEアドベントカレンダーは @capi さんです!お楽しみに!
こんにちは!CSE Group でエンジニアをしている上野です。 この記事は BASE AdventCalender の13日目の記事です。 12日目は kagano さんの GitHub Copilot の Custom Instruction でのコードレビューについての記事でした。この 1 年は AI に関する話題、特に Coding Agent の話題がたくさんありましたね。日々モデルも機能も進化していて、今どこの AI は何ができるんだっけ?と迷子になってしまっているので、私自身参考になりました。 さて、BASE AdventCalender 13日目のこの記事でも AI についての話ですが、開発業務以外の業務改善について、この1年間の取り組みをお話します。 はじまりから PoC 期 はじまり 2025年に入る前から各個人や部署毎で生成 AI のサービスを積極的に試したり導入してみたり、ということはされていましたが、2025年の頭から、会社としてしっかりと生成 AI を使っていこうという方針が打ち出されました。 私の所属する CSE は社内の業務改善をエンジニアリングで支援するチーム *1 ですが、このときの方針としては CSE に依頼しよう、ではなく各部門で業務を効率化できるようにしていこう、という流れでした。しかし、CSE としてなにかしようというものではなかったのですが、いずれはなにか依頼があるであろうということを見越し、CSE でも準備をしていくことにしました。 そこでまずは生成 AI を利用した簡単なアプリケーションを実装したり、SaaS などを検討したりしてみよう、という事になりました。 いくつか実装するアプリケーションの案はあったのですが、 ナレッジが整備されている 問い合わせの Slack チャンネルでも問い合わせが多く、ある程度の効果が見込める という2点から人事・労務の質問回答 Bot を作成することにしました。 AI Chat Bot の PoC 各種サービスの検討 作成するものが決まったため、次にいくつか SaaS などを比較検討をしました。 また、汎用的に使用できるということで Dify と、Amazon Bedrock も比較対象としました。 そこでのなかで挙がったそれぞれのメリット、デメリットなどを紹介します。 検討対象 メリット デメリット SaaS 製品 簡単な設定ですぐ構築できる ダッシュボードなど、必要な機能も利用可能 Chat Bot 以外でやりたいことがでてきた際にできないことも多い 価格は比較的高い Dify 色々な人がワークフローを構築できる 会社で使用する場合、SaaS版ではなくセルフホスト版を使用することになり、メンテナンスコストがかかる 社員全員に開放する場合、Notionにあるナレッジのセキュリティが課題 Amazon Bedrock Agent、KnowledgeBase 柔軟に構築が可能 コストも低い キャッチアップが必要 構築の工数は高くなる 上記のメリット・デメリットを勘案し、長期的な視点では技術力を持っている必要があること、単純な Chat Bot だけではなく今後業務に組み込まれていくであろうことをから、Amazon Bedrock で構築していくことに決定しました。また、Chat Bot への問い合わせのインターフェースは Slack としました。これは「ナレッジが Notion にあるのであれば、構築をせず NotionAI でよいのでは」という考えもあったものの、NotionAI で各個人が人事・労務の情報を確認してしまうと、ハルシネーションによる誤った情報だった場合人事担当がキャッチできないこと、オープンな場で質問することで他の人に見えないことで情報が個人で閉じてしまうことがあるため、Slack のオープンな場での問い合わせをするようにしました。(当然人事関連の問い合わせは非公開であるべきものもあるため、そういうものは既存のクローズドな問い合わせ窓口を利用してもらうよう案内しました。) 実装 実装するアプリケーションは以下のようなアーキテクチャとなりました。また、今回は Notion のデータを Bedrock KnowledgeBase に取り込みましたが、PoCということで継続的に更新するなどの仕組みは作らず、ダウンロードをして S3 に配置し、そのバケットを Bedrock KnowledgeBase で読み込むという単純なものにしました。 PoC 結果 PoCの結果、PoC 期間の約1ヶ月間で、問い合わせの件数が46件、正答率が 70%(32件/46件)、誤答の分類としては ナレッジの記事の内容が曖昧だった(ナレッジの課題) 質問内容について明記されていない、記事がなかった(ナレッジの課題) ナレッジの画像に情報がありAIが参照できなかった(技術的課題) 別の記事の内容が参照されてしまった(技術的課題) 古い記事の内容が参照されてしまった(ナレッジの課題) その他(ハルシネーションなど) 学び PoC の結果から以下のような教訓、学びが得られました。 少し曖昧であったり、抽象的な書き方の記事でも、人が読むとある程度行間を読んだり、入社時の研修で案内されていたり質問ができていたため補完されていたが、AI は前提知識がなにもないため誤った解釈をしてしまう事がある。 プロンプトや KnowledgeBase の RAG の設定はあまり凝ったことはしていないがある程度の正答率が得られ、生成 AI の力を改めて感じた。 過去の記事を参照してしまうなど、AI活用にはまずデータの整備が大事だと痛感した。 CS の問い合わせ改善プロジェクト はじまり PoC を進めていた頃と並行して、CS チームから「今後 CS の業務を AI で改善するだけでなく、AI 前提の業務に変革したい」という相談がありました。その中で、まずは工数がかかっている「問い合わせを一部委託しているパートナーからのエスカレーションの対応を AI で一次受けする AI」(以下エスカレーション AI) の依頼がありました。 こちらについても NotionAI などで代替できないかなどの検討を行ったところ、契約の関係上NotionAI を利用できず、また AI への一次エスカレーションの内容を弊社社員も確認できるようにしておきたいという要件、PoC で構築した仕組みと同等のものを転用できるということで、Slack をインターフェースに、ナレッジを参照して回答するAIを実装しました。 実装について Slack から AI への問い合わせ部分実装は PoC の仕組みとほぼ同じですが、PoC ではなく日々ナレッジが更新されていくため、Notion のデータを日次で S3 にアップロードし、その後 Bedrock KnowledgeBase のデータソースを再同期するという仕組みを構築しました。 また、当時 Notion のインテグレーションの作成はワークスペースオーナーのみができましたが、インテグレーションを記事にコネクトする操作 (つまりインテグレーションに読み込む許可をする操作) は任意のユーザーが、そのユーザーがアクセスできる任意のページ・データベースに可能でした。これにより、もし誤って社外秘の情報をコネクトしてしまった場合、AI がその情報を読んでしまい、意図せず情報が流出してしまうという課題がありました。( 現在はワークスペースオーナーのみがコネクトを許可する設定が実装され、この課題は解決されています。 ) そのため、Notion の記事連携機能部分では、Notion のホワイトリスト方式でデータベース ID、ページ ID を設定し、Pull Request でのレビューを必須とすることで意図しない記事の連携を制御するようにしました。 アーキテクチャは以下のようになりました。 効果 エスカレーション AI の実装により、以下のような効果が得られました。 約200件/週 のエスカレーションの20~30%削減ができた。 また、可能性として「BASE 側の工数が削減されたが、パートナー側でナレッジを確認したりなどの工数が発生しており、負担が移っただけではないか」という事も考えられましたが、CS のマネージャーに詳細をヒアリングをしたところ、そうではなく純粋にこの効果を得られた、とのことでした。 ヘルプページ改善プロジェクト はじまり 前述のエスカレーション AI が無事本番運用された後、次は CS で工数のかかっていたヘルプページの AI による改善の相談があり、そのプロジェクトが開始しました。 ヒアリングをする中で、以下のような状況や課題が見えてきました。 状況 PRD から仕様書や SOP の生成は NotionAI である程度自動作成ができていた 元々PRD、仕様書、SOP はすべて Notion で管理されており親和性が高かった しかしヘルプページは zendesk にしか存在していなかった 課題 ヘルプページが Notion になく、仕様書や SOP の作成後、どのヘルプページを更新すべきか検索が難しく、工数がかかり更新漏れもあった そこで、まずはAIで更新するなどの前に、Notion にヘルプページをもってきて、SSoT (Single Source of Truth) とする、その後 SOP や仕様書とヘルプページを紐づけることで、まずは管理ができるようにするよう提案し、その後紐づけたデータを NotionAI が参照し、内容を作成するという形で進めました。 技術的な部分 ここでは NotionAI を用いたため実装は zendesk の API を実行する Lambda など多くはありませんが、Notion 上での設定などを簡単に紹介します。 ドキュメントがすべて Notion にあるためすべて情報を Notion に集約し、 AI は NotionAI を使用 zendesk のヘルプページはセクション ID が必須なため、セクションの情報も同期するように構築 仕様書、SOP、ヘルプページのデータベースで、それぞれリレーションを設定し、紐づいているドキュメントを NotionAI が読み込み記事を生成 ヘルプページの完全自動公開は NotionAI の仕様上現状は難しいが、プロンプトを管理して最低限の操作で生成するように構築 ヘルプページについてはユーザーの目に触れる部分のため必ず人が最終チェックをしてから公開するようにしている 学び このプロジェクトは最近のもののため、具体的な数字としての効果はまだ得られていませんが、進める中で以下のような教訓や学びを得られました。 AI でなにかをやりたいという要望でも、しっかりと業務や課題を洗い出して課題を解決するというのは大事 AWS Bedrock Agent Core など、気になる技術はたくさんあるものの、それにこだわらずに、既存のツールで工夫することも時には大事 おわりに 大変だったこと この1年間業務改善に生成 AI を適用するという取り組みを進めてきましたが、(きっと同じことをしている多くの人が感じているであろう)大変なこともありました。そのうちの全てではないですがいくつか紹介します。 生成 AI のモデルや各社の機能拡充の対応速度が早く、実装してすぐ不要になる機能があったり、前提が変わったりしてしまうこと モデルだけでなく色々な機能(AI ワークフローや各種ドキュメントシステムとのナレッジ連携など)も次々でてきて、今実装しているこの機能は不要になってしまうのではないか、という不安はいつもつきまとっていました。 そのうえでどれにベットするのか、というのを考えるのが非常に難しかったです。(複数人が使用する業務システムなので、頻繁に「やっぱりこっちに変えます」ということはできないため。) この記事では、この1年間、BASE の CSE チームが生成 AI の業務改善への適用の取り組みを紹介しました。まだまだこの記事には書ききれなかった紆余曲折ややったことなどもありますし、もちろん昨日の記事のように BASE のアプリケーション開発での AI の取り組みなどもあります。興味を持っていただいたら、採用情報をご覧ください! 採用情報 | BASE, Inc. - BASE, Inc. 明日は 02 さんの「数百万行でも怖くない!MySQL INSTANT DDLで「完全無停止」カラム追加」の記事です!以前 gh-ost の記事 もありましたが、それとの違いや技術選定についてなど気になりますね!お楽しみに! *1 : CSE チームについての説明は こちらの記事 や、今年のアドベントカレンダー17日目の記事をご覧ください。
はじめに この記事はBASEアドベントカレンダーの12日目の記事です。 devblog.thebase.in BASEのカートチームでバックエンドエンジニアをしている、かがの( @ykagano )です。 他チームのコードも含めてレビューをする機会が増えてきたので、コードレビューの話をしようと思います。 コードレビューの流れ 普段自分が行っているコードレビューの流れは下記表の通りです。 GitHub Copilot Code Review では個別コメントの形でレビューしてくれるのですが、コード自体の品質を複数の観点で評価をしてもらいたいことから、別途VSCodeで複数のレビュー観点を与えた上でコードレビューを行っています。 今回はこの「VSCodeのGitHub Copilotに独自観点を与えて対象PRのコードレビューを依頼する」の部分を解説します。 No. 概要 補足 1 descriptionで概要を掴む 2 PRにプロジェクト用のlabelが付いているか確認をする Findy Team+で対象プロジェクトを計測しているため 3 GitHub Copilot Code Reviewが実施済みか確認する 実施済みでなければレビュアーに追加する 4 VSCodeのGitHub Copilotに独自観点を与えて対象PRのコードレビューを依頼する 詳細は後述 5 その間にコードをざっと読む 6 GitHub Copilotに依頼したコードレビューの応答結果を確認する 7 応答結果を踏まえてコードの気になった部分をあらためて読む 8 リリース影響の動作確認ができているか確認する FeatureFlagを使わないリリースの場合、既存の動作確認ができているか確認する 9 気になった点があればコメントする この時、コメントのtypoのようなnitsが一点だけならコメントしませんが、imoがあれば一緒に報告します 10 問題なければApproveする MCP ServerとCustom Instructionsの準備 VSCodeに GitHub Copilot Chat の拡張機能をインストールしていることを前提とします。 まずGitHub MCP ServerをVSCodeにインストールします。 下記GitHub MCP ServerからInstall MCP Serverボタンを押してVSCodeにインストールしてください。 github.com インストール直後に「PAT(Personal Access Token)or App token」の入力が求められるので、いずれかを入力してください。 これでGitHub MCP Serverが使用できるようになりました。 次に Custom Instructions を準備します。 プロジェクトの .github/instructions 配下に codereview.instructions.md ファイルを作成します。 Gistにアップロードしましたので内容はこちらのリンクからご確認ください。 https://gist.github.com/ykagano/0db4debc7339a93038858b5ec677dc8e codereview.instructions.md の最下部に記載のコードレビューの手順を抜粋します。 # コードレビューの手順 1. [ ] まずは変更が加えられたファイルの一覧を確認してください。 2. [ ] 次に変更差分を取得して、どのような対応がされているかを解説してください。 3. [ ] 変更差分について、コードレビューガイドラインの項目について○△×で評価して表にしてください。 4. [ ] テストクラスに実装されているテストケースを列挙してください。不足しているテストがあれば指摘してください。 5. [ ] 同じネームスペース内に存在する既存実装を参照して参照したクラス名を教えてください。また、それらのクラスと比較して過度に異なる実装をしている場合は指摘してください。 6. [ ] 気になった箇所について、詳細な説明と改善案を提案してください。 コードレビューは上記手順で実行されます。 ではここからはVSCodeでCopilotにPRをコードレビューしてもらいます。 GitHub CopilotのCustom Instructionによるコードレビュー VSCodeでCopilot Chatを開きます。 チャットの左上にある「コンテキストの追加」を選択すると、入力エリアが開くので「手順…」を選択します。 (画像は VSCode より引用) 「codereview」を選択します。 その後以下のプロンプトをチャットに入力します。 https : //github.com/[org]/[repo]/pull/[num] をレビューしてください つまりPRのURLを貼って「レビューしてください」と言うだけです。 するとGitHub MCP Serverを通して取得したコード差分をCopilotがチェックし、Custom Instructionsで与えた観点に沿ったレビュー結果が表示されます。 ここではサンプルコードをCopilotに作成してもらい、一時的に作成したPRに対してレビューしてみます。 まずPRの概要と変更内容が解説されます。 「コードレビューガイドライン」に沿って○△×で評価してくれます。 テストケースが列挙され、既存実装と比較されます。 気になった箇所と改善点を列挙してくれます。 最後にPR作成者への質問があれば列挙され、総評が出力されます。 これらの情報はコードをレビューする上で、有益な情報になっていると思います。 参考にさせていただいた記事 こちらの Custom Instructions は、以下の記事を参考に作成しています。 コードレビュー結果を○△×の表で評価する方法を使わせていただきました。 fintan.jp 多数のコードレビューガイドラインを用意されていたので非常に参考になりました。 zenn.dev また作成にあたり @OgasawaraYuki さんにご協力いただきました(@OgasawaraYuki さんはアドベントカレンダー21日目に登場予定です)。 ありがとうございました! おわりに Copilotによってある種複眼でのチェックが単独で行えるようになりました。 自分の眼で確認したレビュー品質の下限を担保できる上に、レビュー速度も向上しました。 BASEではこのようにCopilotと協働いただけるエンジニアを募集しています。 binc.jp 明日は @UenoKazuki さんの記事です、お楽しみに!
はじめに この記事はBASE Advent Calendar 2025の11日目の記事です。 devblog.thebase.in BASE プロダクト開発チームの komaki です。 私は文字を読むことがかなり苦手です。 仕事中はテキストでのコミュニケーションが多いし、プロジェクトやライブラリなどの様々なドキュメントなど、文字を読む機会はたくさんあります。 苦手とか関係なく毎日何かしらの文章に向き合わないと仕事になりません。 そんな環境のなかで、自分がどれくらい文字を読むことが苦手かというと 読みたいと思って開いた記事でも、最初にするのはスクロールバーのチェック。スクロールバーが長いと、その時点で読むのをやめることが多々あります。 あと漫画は好きなんですが、1冊読むのに1時間以上かかったり、時間がかかるのを想像して読まないことも多々あります。 という感じですが、働いている以上文字を読むことを避けることはできません。 仕事で必要なものは時間をかけてでも読みますが、それももっとはやくキャッチアップしたいと常々考えています。 これまでどうにか克服したいと思って色々試してきましたが、この記事では自分自身の振り返りも兼ねて、工夫していることや試してみてダメだったことを紹介しようと思います。 工夫していること 全部読むのは無理と割り切り、優先順をつける この割り切りを前提にするだけで、読むハードルがかなり下がりました。 読みたいものはたくさんありますが、仕事以外では1つか2つ読めたらいいやと思うようにしています。 毎朝目を通すブラウザウィンドウで記事を開いておく 朝だと頭が整理されているのか、読む気力が湧くことがあります。 必ず目に入る位置に置いておき、湧いてくるかもしれない気力に委ねます。 スマホで読む気がなくなったら PC に切り替える 同じ文章でも PC の大画面だと最後まで読めることがあります。 おそらくスクロールバーが短いことにより、精神的な負荷が軽減されていると思っています。 あと正確に測ったわけじゃないですが、PC の大画面で見るとスマホで読むよりかなり早く読めている気がしています。 Slack に投げてリマインダーにセットする Slack はおそらく自分が一番使うアプリなのでリマインダーのセットもしやすいです。 よく使うので PC でもスマホでも目に入りやすい場所に置いています。 そして自分は通知バッジを常に消しておきたい派です。 Slack でリマインダーをセットすることで、バッジを消すために読む行動につながるようになりました。 読まずに消してしまうこともありますが、それは優先度が低いということで気にしないでいます。 毎日、5分だけ本を読む習慣をつける 5分で終わる日もあれば、稀に1時間以上読める日もあります。 でも「5分でいい」と決めることで、読み始めるハードルが下がりました。 通勤時間をインプット時間として使う 集中時間と短い休憩を繰り返すポモドーロテクニックにトライしたこともありますが、だんだん休憩が長くなってしまってダメでした。 どうやら自分は物理的な制約がないと集中が続かないようです。 その点、電車の中はやれることが限られるし、降車駅までしか読めないというのが自分にとってちょうど良かったです。 ミーティング資料は事前に目を通しておく 最近はミーティング資料は事前に共有してもらうことがほとんどですが、過去にはミーティング中に資料を読む時間を設けることがありました。 そういうのはだいたい読み終わらずにまだ読めてません!って延長してもらってました。 そうならないようにミーティング資料は事前に目を通しておきます。 資料が共有されてない場合も、読んでおくべき資料があるか確認しておきます。 試してみてダメだったこと Todo アプリや標準リマインダーの活用 Slack 以外のアプリも色々試しましたがあまり上手くいきませんでした。 Slack を使うタイミングが多く常に目に入る位置に置いているので、リマインダーをセットするのも簡単にできました。 それ以外のアプリは、作業スペースの問題もあって常に目に入る位置に置けないので自分から見に行かないといけないし、複数アプリから通知が来たりバッジがついてしまうストレスのほうが大きかったです。 速読 時間がかかってしまうことで諦めてしまっていたので、速く読めるようになると苦手じゃなくなるかなと思って試しましたが、全然ダメでした。 頭の中で読まない、文字じゃなく絵や文章の塊でとらえる、目を早く動かす、などのコツがありますが全然できなかったです。 斜め読み、要点だけ読む 本をたくさん読む人や読むのが早い人は、大体これをしている気がします。 あとは速読に書いたコツが、自然できている人もいるみたいでした。 全体を読まないと何か見落としているんじゃないかと不安になるので、この方法も続きませんでした。 今思っていること こうして振り返ってみると、読むスピード遅いのに全部読まないといけないというのを解決したいと思いました。 読む状況に自分を追い込むやり方でなんとか向き合ってきましたが、来年は読むスピードや全部読まないといけない問題にもう少し向き合っていきたいと思っています。 読むこと自体が好きになれると、もっと楽しくインプットできていいんですが。 おわりに 文字を読むのが苦手でも工夫すればなんとかなる、振り返ってみてそんな実感がありました。 これからも試行錯誤は続けつつ、いつか読むことそのものが少しでも好きになれたらいいなと思っています。 この記事で書いた内容が、少しでも誰かのヒントになれば嬉しいです。 BASE では、今後のプロダクトの成長をリードしていただけるエンジニアを募集しています。 ご興味があれば、ぜひ採用情報をご覧ください。 binc.jp 明日の BASE アドベントカレンダーは @ykagano さんの記事です。お楽しみに!
はじめに この記事はBASEアドベントカレンダーの9日目の記事です。 devblog.thebase.in 基盤グループの @okinaka です。最近は、メール配信基盤の構築を担当しています。 今回は LocalStack の EventBridge Scheduler にある制約と、その対処法についてお話しします。 LocalStack と AWS EventBridge Scheduler 私が担当しているメール配信基盤は、AWS のサービスを組み合わせて作られています。 開発には Docker 上で AWS サービスをエミュレートした LocalStack を活用していて、私のお気に入りのツールです。特に Lambda 関数は、AWS サービスとの連携を前提としているため、ローカルでの動作確認には必須と言っていいかもしれません。 それに加えて最近のお気に入りの一つに AWS EventBridge Scheduler というサービスがあります。 EventBridge というサービスがありますが、それとは別のものです。 AWS EventBridge Scheduler には以下の特徴があります。 フルマネージドのサーバーレスなスケジューラーです。 AWSサービスや標準HTTP/Sエンドポイントを自動的に起動するスケジュールタスクを簡単に作成・管理できます。 Lambda、SQS、SNS、Step Functionsなど、200以上のAWSサービスを直接ターゲットとして呼び出すことができます。 メール配信では、日時を指定してメール配信するスケジュール機能として採用することにしました。一度限りのスケジュールを設定するのにとても有用です。(定期実行にも対応しています) LocalStack にある制約 ありがたいことに LocalStack は、EventBridge Scheduler にも対応しています。 LocalStack は、よくできたエミュレーターですが完全に本物のAWS の挙動に対応しているわけではありません。初めのうちは喜んで開発を進めていたのですが、実装を進めているうちに以下の制約があることに気づきました。 EventBridge Scheduler in LocalStack only provides mocked functionality. It does not emulate actual features such as schedule execution or target triggering for Lambda functions or SQS queues. (LocalStack の EventBridge Scheduler はモック機能のみを提供します。スケジュール実行や Lambda 関数や SQS キューのターゲットトリガーといった実際の機能はエミュレートされません。) https://docs.localstack.cloud/aws/services/scheduler/#current-limitations 肝心のスケジュール実行ができないなんて困ってしまいました。ただ、これで諦めてしまうのはもったいないです。 開発環境なので、実装方法はこだわらなくても動いてくれればよいので、足りない部分を補うような仕組みを用意してみました。 制約の対処方法 LocalStack は EventBridge (rule の方) にも対応しているので、これで Lambda 関数を定期実行することでスケジュール実行の代わりをさせます。 今回は、ターゲットとして SQS のキューに Input の内容を送る仕組みを作ってみます。 構成 (シーケンス図) 本来は EventBridge Scheduler にスケジュール作成すれば、SQS に送ってくれるのですが、LocalStack では、間に EventBridge と Lambda を挟む構成になっています。 本来のアプリから Scheduler にスケジュールを作成 EventBridge Rule が毎分 Lambda を起動 Lambda は Scheduler から情報を取得し、期限超過なら SQS へ投入後、スケジュールを削除 実装 Go 言語の Lambda 関数コードの例です。 このコードは一回限りの実行 ( at 式 )のみに対応しています。( cron や rate などの繰り返しには未対応です) package main import ( "context" "errors" "strings" "time" "github.com/aws/aws-lambda-go/lambda" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/scheduler" "github.com/aws/aws-sdk-go-v2/service/sqs" ) var ( schClient *scheduler.Client sqsClient *sqs.Client ) func init() { // aws クライアントの初期化 cfg, err := config.LoadDefaultConfig(context.TODO()) if err != nil { log.Fatalf( "unable to load SDK config, %v" , err) } schClient = scheduler.NewFromConfig(cfg, func (o *scheduler.Options) { o.BaseEndpoint = aws.String( "http://localhost.localstack.cloud:4566" ) }) sqsClient = sqs.NewFromConfig(cfg, func (o *sqs.Options) { o.BaseEndpoint = aws.String( "http://localhost.localstack.cloud:4566" ) }) } func handleRequest(ctx context.Context) error { var maxResults int32 = 10 var nextToken * string // 有効なすべてのスケジュールを取得 for { resp, err := schClient.ListSchedules(ctx, &scheduler.ListSchedulesInput{ MaxResults: &maxResults, NextToken: nextToken, }) if err != nil { return err } for _, sch := range resp.Schedules { // スケジュールの詳細を取得 s, err := schClient.GetSchedule(ctx, &scheduler.GetScheduleInput{ Name: sch.Name, GroupName: sch.GroupName, }) if err != nil { return err } // スケジュールの必須フィールドが存在することを確認 if s.ScheduleExpression == nil || s.ScheduleExpressionTimezone == nil || s.Target == nil || s.Target.Arn == nil { return errors.New( "schedule is missing required fields" ) } loc, err := time.LoadLocation(*s.ScheduleExpressionTimezone) if err != nil { return err } t, err := timeFromAt(*s.ScheduleExpression, loc) if err != nil { return err } // スケジュールの実行時間が過ぎている場合、ジョブを実行 if t.Before(time.Now()) { // ジョブの実行 (SQS にメッセージを送信) queueUrl := queueUrlFromArn(*s.Target.Arn) _, err = sqsClient.SendMessage(ctx, &sqs.SendMessageInput{ QueueUrl: &queueUrl, MessageBody: s.Target.Input, }) if err != nil { return err } // ジョブの実行後、スケジュールを削除 _, err = schClient.DeleteSchedule(ctx, &scheduler.DeleteScheduleInput{ Name: s.Name, GroupName: s.GroupName, }) if err != nil { return err } } } if resp.NextToken == nil { break } nextToken = resp.NextToken } return nil } // "at(2025-12-24T12:00:00)" のような at 式から時間を抽出する関数 func timeFromAt(expr string , loc *time.Location) (time.Time, error ) { expr = strings.TrimSpace(expr) if !strings.HasPrefix(expr, "at(" ) || !strings.HasSuffix(expr, ")" ) { return time.Time{}, errors.New( "not an at expression" ) } body := strings.TrimSuffix(strings.TrimPrefix(expr, "at(" ), ")" ) t, err := time.ParseInLocation( "2006-01-02T15:04:05" , body, loc) if err != nil { return time.Time{}, err } return t, nil } // ARN からキュー名とリージョンを抽出し、QueueUrl を生成する関数 func queueUrlFromArn(arn string ) string { parts := strings.Split(arn, ":" ) if len (parts) < 6 { return "" } region := parts[ 3 ] queueName := parts[ len (parts)- 1 ] // LocalStack用のURL return "http://sqs." + region + ".localhost.localstack.cloud:4566/000000000000/" + queueName } func main() { lambda.Start(handleRequest) } LocalStack を利用するための Docker の compose.yml の例です。 services : localstack : container_name : "${LOCALSTACK_DOCKER_NAME:-localstack-main}" image : localstack/localstack ports : - "127.0.0.1:4566:4566" # LocalStack Gateway - "127.0.0.1:4510-4559:4510-4559" # external services port range environment : - DEBUG=1 # トラブルシューティングに役立つため、DEBUGログをonに設定 - SERVICES=events,lambda,scheduler,sqs volumes : - "${LOCALSTACK_VOLUME_DIR:-./volume}:/var/lib/localstack" - "/var/run/docker.sock:/var/run/docker.sock" LocalStack の環境を整えるための初期化スクリプト( init.sh ) の例です。 Lambda 関数のビルド&デプロイと EventBridge (rule) の設定を行います。 LocalStack の設定には awslocal コマンドを使用します。 #!/bin/bash set -ex # Lambda 関数の名前 func_name =localstack-schedule-executor # Lambda 関数のビルドとパッケージング GOARCH =arm64 GOOS =linux CGO_ENABLED = 0 go build -o bootstrap main.go zip ${func_name} .zip bootstrap # LocalStack 上にデプロイ awslocal lambda create-function \ --function-name ${func_name} \ --architectures arm64 \ --runtime provided.al2023 \ --handler bootstrap \ --zip-file fileb:// ${func_name} .zip \ --role arn:aws:iam::000000000000:role/lambda-role \ --timeout 30 # create-function 実行完了まで待つ sleep 5 # EventBridge ルールの作成とターゲットの設定 awslocal events put-rule \ --name schedule-execution-rule \ --schedule-expression ' rate(1 minute) ' awslocal lambda add-permission \ --function-name ${func_name} \ --statement-id schedule-execution-permission \ --action ' lambda:InvokeFunction ' \ --principal events.amazonaws.com \ --source-arn arn:aws:events: ${AWS_DEFAULT_REGION} :000000000000:rule/schedule-execution-rule awslocal events put-targets \ --rule schedule-execution-rule \ --targets ' [{"Id":"1","Arn":"arn:aws:lambda: ' ${AWS_DEFAULT_REGION} ' :000000000000:function: ' ${func_name} ' "}] ' # SQS のキューを作成 (確認用) awslocal sqs create-queue --queue-name test-queue 実行してみます。EventBridge Scheduler に値をセットして様子を見ます。(例では日本時間の 12/24 12:00 に設定) $ docker compose up -d $ sh init.sh $ awslocal scheduler create-schedule \ --name test-schedule \ --schedule-expression ' at(2025-12-24T12:00:00) ' \ --target ' {"RoleArn": "arn:aws:iam::000000000000:role/schedule-role", "Arn":"arn:aws:sqs:us-east-1:000000000000:test-queue", "Input": "test" } ' \ --flexible-time-window ' { "Mode": "OFF"} ' \ --schedule-expression-timezone ' Asia/Tokyo ' 実際に SQS キューにメッセージが入るのかを確認します。 $ awslocal sqs receive-message --queue-url http://sqs.us-east-1.localhost.localstack.cloud:4566/ 000000000000 /test-queue { " Messages " : [ { " MessageId " : " 73db45fd-e1b1-4376-bdaf-348e1a6411cb " , " ReceiptHandle " : " NWVjMDAyZDMtNjlhYi00ZGVlLWE3MjAtNjQ5ZTc1ODlhOGJkIGFybjphd3M6c3FzOnVzLWVhc3QtMTowMDAwMDAwMDAwMDA6dGVzdC1xdWV1ZSA3M2RiNDVmZC1lMWIxLTQzNzYtYmRhZi0zNDhlMWE2NDExY2IgMTc2NDI0MzIyNC4xNDMzNTgy " , " MD5OfBody " : " 098f6bcd4621d373cade4e832627b4f6 " , " Body " : " test " } ] } 完全なエミュレートではありませんが、これで必要な機能を実現できました。 やったね! おわりに LocalStack の足りない機能を、既存のものを組み合わせて補えることが面白いなと思い紹介しました。いずれは公式でサポートされることになるとは思いますが、それまでのつなぎとして参考になれば幸いです。 明日は、BASEアドベントカレンダーは @FujiiMichiro さんの記事です。お楽しみに! BASE株式会社ではエンジニアを採用募集中ですのでご興味あれば下記の採用ページをご覧ください。 binc.jp
はじめに この記事は BASE アドベントカレンダー8日目の記事です。 devblog.thebase.in ネットショップ作成サービス BASE のプロダクト開発チームでエンジニアリングマネージャー(EM)をしている髙嶋です。 「開発生産性」という言葉は、一見共通言語のようで非常にブレやすく、定義も難しいものです。その辺については、昨年のアドベントカレンダーの記事で弊社開発担当役員の藤川も触れています。 devblog.thebase.in 今年はその開発生産性というビッグワードにいきなりフォーカスするのではなく、まずはそれを分解した「開発量」を増やそうと開発組織一丸となって取り組んできた1年でした。何がユーザーにとっての価値となるかはデリバリーしてみないと分からないゆえに、開発スピードを上げていかなければならないという前提があると考えています。そのため、まずはいわゆるレベル1生産性と呼ばれるような足元のアウトプットをしっかりと増やしていこうというものです。 私たちのチーム(数十名規模)でも、その方針に沿う形でハイスループットな開発組織になることを目指し、様々な取り組みをしてきました。この記事では、その1年の歩みについてご紹介したいと思います。ざっくりこの1年でのタイムラインを示すと以下の通りです。 内製ツールで計測基盤を構築する 各チームごとに振り返りの場で計測結果を分析し、改善活動へとつなげる 開発組織外へのレポートフローを作成し、組織横断で現状および起こった変化に対する目線を合わせられるようにする 改善活動のスピードと質をさらに高めるために外部ツールを導入する それでは、それぞれについて行間を埋めながら話を進めていこうと思います。 まずは計測して振り返る とにもかくにも計測基盤がないことには、チームで同じものを見て会話をするといった取っ掛かりを作ることができません。手始めとして内製ツールを利用し、スプレッドシート上で開発スタッツに関するデータを見られるようにするところから始めました。BASE のプロダクト開発組織全体としての数値、あとは各チームごとの数値を、下図のような形式で参照できるようにしました。 これを材料に各チームごとに振り返り会のような場でボトルネックを探ってもらい、改善活動を推し進めていくといった流れです。つまり計測する仕組みと、それを活用する仕掛けを用意したという格好です。プルリクのレビューを最優先にする、プルリクのサイズを適度に小さくするといったことに代表されるような、地道なアクションを一歩ずつ進めていくことで、その結果は着実に数値上でも表れていきました。 開発組織外とのコミュニケーションと組織状況の把握 自動取得できる開発スタッツ系以外の項目も加えて、月次で各種数値を Notion 上に蓄積し、全体のトレンドを把握できるようにしました。さらにそれを月次事業報告として開発組織外にもレポートするフローを作り、開発組織外との目線合わせもできるような体制を構築していきました。開発組織外にも適切に情報を届けることは、全社レベルでの組織運営観点からのフィードバックを得られるようにしたり、非エンジニアも巻き込んだ改善活動に取り組みやすい体制にしたりするために、非常に重要なことだと考えています。 ※PD Div:Product Dev Division という BASE のプロダクト開発組織の略称 加えておおよそのトレンドや開発 PJ との因果関係といったものも大まかには把握できるようになり、組織として次に目指す方向性を検討する材料の一つにできていると感じます。1年という期間を通じてモニタリングしてきたことで、開発組織としてのリズムをより解像度高く捉えられるようになったことには大きな意味がありました。 内製ツールから外部 SaaS ツールへ 年初からそうした取り組みを始めて半年が経過しようかという頃、改善が進んできたからこそ、内製ツールにおける運用だと以下のような課題が目立つようになってきました。 取得できるデータに限りがあって課題の深堀りがしづらく、改善アクションの精度向上が難しい メトリクスの悪化に気付くきっかけ(アラート)がなく、対処が後手に回りやすい チームのスタッツを相対的に評価する指標や基準がない ツールのメンテナンスコストが継続的にかかり、対応も属人的になってしまっている こうした課題を解決し、改善活動のスピードと質をさらに高めることを目的に、Findy Team+ の導入を決定しました。いきなり有償ツールを入れるのではなく、改善が進んでそれをより発展させるためにツールを入れるという順番になったことは、とても良かったポイントの一つだと考えています。ただしここで一つ懸念としてあったのが、実際に各チームで活用していこうとすると、多機能であるがゆえにどこからどう使えばいいか迷いやすくなってしまうのではないかということです。そのため現場任せにせず、組織として意思を持って活用を進めるために、最初は推奨する活用フローと機能スコープを提示することにしました。 今では基本的な使い方が定着したことで、各チームの課題や開発スタイルに応じてチューニングをしたり、より応用的な活用ができないかといった検討も進めているところです。ちなみにいくつか計測している指標の中でも、サイクルタイムはコミュニケーションの効率化指標として注目して追ってきた項目です。それが下図のように右肩下がりとなってからは安定した状態を維持できていることからも、一定成果が出ている状態にあると言えるのではないかと考えています。 ※Findy Team+ 画面より引用 実は2年前にも取り組んだことはあった いわゆる「開発生産性を上げよう」といった取り組みは、実は2年前の2023年にも取り組んだことがありました。やっていることの内容自体は、その当時と今回とで実はそこまで大きな差分はないのかもしれません。前回は半年程度で立ち消えとなってしまいましたが、今回は1年以上継続しており、来年もその発展をさせていこうという状況です。 では一体何が違ったのでしょうか。それは結局のところ、今回の場合は開発組織全体としての方針が、組織図上の先から先まで張り巡らされていたという前提が大きかったのではないかと考えています。私の立場で言うと一開発部署の一中間管理職となるわけですが、自部署だけで声を挙げて取り組んでいこうとするのではなく、まず会社としての意思が先にあり、それにアラインする形で取り組むんだということで覚悟が違った部分はあったと思います。もちろん自部署だけでスコープを区切って物事を進めやすくするといった手段はよくある話かと思いますし、今回も日々の活動としてはそのような形となっています。しかしながら大前提となる方針が会社レベルで先に示されたことで、自部署内での取り組みに対しても助けになったのは間違いありません。 まずは開発スタッツの計測から始めてみようとしたのが前回だったのですが、探索フェーズと考えればそれ自体がダメだったとは思っていません。ただし組織として目指したい方向性や日々変化する組織状況に対する解像度がマネージャー各人の中でもまだ低かったこともあり、改善が一定進んだときに「さて、ここからどうしよう」となってしまったのかなと思っています。12月4日の @tanden の記事でも、そうした悩みについては書かれています。 devblog.thebase.in 今回は幹がしっかりとあり、それゆえ中長期的な活動にすることを見据えて、計測基盤一つをとっても意思を持って整備しにいったことが今につながっていると考えています。 おわりに 開発量向上を旗印に1年をかけて様々な取り組みを進めてきましたが、組織として前進した部分は素直に認めつつ、伸びしろがあるのも間違いはないので今後も着実にレベルアップを図っていきたいと思っています。また本文脈においても生成 AI とどう付き合っていくかは無視できない観点の一つですが、ただ量が出せればいいというわけではなく、当然ながらそこには質や成果が伴ってくることも重要です。目先の数値にとらわれすぎず、エンジニア一人ひとりが納得感を持って前向きに開発に取り組める状態を作ること、一方で自分たちを客観的に見て内省するための仕組みと機会を用意し、より良いチームとなって顧客への価値提供サイクルを早めていくことが求められるのだろうと思います。 BASE では、今後のプロダクト成長を支える技術戦略や開発基盤の構築をリードしていただけるエンジニアを募集しています。 ご興味があれば、ぜひ採用情報をご覧ください。 binc.jp 明日の BASE アドベントカレンダーは @okinaka さんの記事です。お楽しみに!
この記事はBASEアドベントカレンダー 2025 の 7 日目の記事です。 エンジニアの右京です。BASE では今年、表示速度の改善を目標にすべてのショップへ Cloudflare を導入しました。これは、その過程や技術面の簡単な解説です。 記事は前後半になっており、この記事は後半で、Cloudflare Workers を利用したコンテンツのキャッシングの話題となります。 前半はこちら: Cloudflare でショップページをちょっとだけ速くしてみた - 導入/SSL for SaaS 編 ショップページのレスポンス速度を改善したい レスポンス速度を改善するにあたって、Cloudflare Workers を利用して前段でコンテンツをキャッシュするアプローチが有効なことは事前にイメージがついていました。この方針について特に以下の記事が参考になりました: zenn.dev しかし、現状のショップページが前段でキャッシュされることを想定して作られているわけではありません。例えば在庫数は、アクセス毎にサーバーサイドで都度計算を行い HTML として書き出しているため、長くキャッシュを持ってしまうと「商品ページは在庫がある表示なのにカートに商品が入らない」といったことが起こってしまいます。他には、時間経過によって販売状態が変化する商品であったり、抽選販売への応募期間といった時刻が関係するものも、現状の実装では長くキャッシュすることができません。 一方で、実際の購入フローでは必ずカートでの購入操作がある上、厳密な在庫や時刻に関する処理はカートで行われるので、ショップページに在庫数などがリアルタイムに反映され続ける必要というのは実はそこまでありません。そこで、数秒であればこのズレは納得できる範囲だろうと判断し、まずは小さく始めることができ、かつ大きな効果が期待できる「商品ページを数秒間マイクロキャッシングする」を実装することにしました。 最終的にはほとんど静的な作りにし、長くキャッシュを持つことで高速なレスポンスにするという目標はありつつも、まずはアクセススパイク時のインフラ面への負荷を抑えることを主軸としていきます。 Cloudflare Workers の設計と実装 まず、Cloudflare を利用する上での大前提として「Cloudflare ありきの設計にはせず、何かあった場合は外せるようにする」ということを設定しました。これには Workers 単体での障害程度であれば Workers を外すことでサービスを維持できるように、最悪 Cloudflare をやめることになってもサービスを維持できるように、という想いがあります。 ストレージの選択 当初想定していた Workers KV ではなく Cache API を採用することにしました。Cache API は前半でも登場した Cloudflare の Cache (あいまいさ回避のため以後 Cf Cache と呼ぶ)を Workers から操作できる API で、同一 DC 内であれば高速な書き込みと読み出しが行えます。 developers.cloudflare.com Cf Cache を Workers から扱うもう一つの方法として fetch を行う際に独自のフィールドを持った Request を利用する、というものがあります。 developers.cloudflare.com Cache API と fetch を比較した場合、性能だけを見ると fetch の方が次の 2 点で優秀です: Cache API では Tiered Caching が働かない fetch は同一のリクエストと判定できる場合リクエストをまとめてくれる(Request Collapsing) その上で今回 Cache API を選択したのは、その柔軟さにあります。Cache API は Cf Cache に乗せる API と Origin(BASE の Web アプリケーション本体) へのリクエストが分かれているので、キャッシュする前に Header を加工したいようなケースで有効になります。 また、これは自分の調査検証不足もあると思うのですが、 Request Collapsing を利用するにはレスポンスがキャッシュできる前提が必要であるような挙動をします。シークレット EC 機能を実現する際に、Request Collapsing を利用するとどうしてもどこかがキャッシュされてしまったため、Cache API を利用することにしました。 次に Workers KV を見送った理由ですが、書き込み制限と反映の遅延にあります。 KV は同一キーには 1 秒間に一度しか書き込みできず、かつその仕組み上反映が最大で 60 秒遅延します。 developers.cloudflare.com この特性から、今回の「数秒のマイクロキャッシング」には適していない判断としました。ただし KV を完全に利用していないわけではなく、後述する X-Webapp-Version のストアには KV を利用しています。コンテンツの更新頻度が頻繁ではないデータを、長期間に渡って信頼できるソースとして扱うことに向いているようです。 そして、KV ではなく Cf Cache を使う最大の利点が「 Cache-Control を元々うまく扱える」というところで、コアとなる仕組みはこれを活用した設計になっています。 Cache-Control を利用したキャッシング よく max-age=86400 などが指定されている Response Header で、コンテンツをキャッシュする際の挙動を指示するためのものです。このディレクティブにはいくつか CDN 向けのものがあります。 www.cloudflare.com CDN 向けのものに s-maxage というディレクティブがあり、これが今回のコアとなっています。 s-maxage は端的に言えば CDN 用の max-age で、例えば s-maxage=2 であれば 2 秒間 CDN にキャッシュできる、ということを示しています。 Cf Cache はこれを扱えるので、Header に Cache-Control: s-maxage=2 を持つ Response を Cache API から put することで、 2 秒間生存するキャッシュを作ることができます。作られたキャッシュは match で取り出せるので、これらを合わせると次のようなコードで Workers でのキャッシュを実現できます: export default { async fetch ( request , env , ctx ) { const cache = caches . default ; const cacheKey = new Request (( new URL ( request . url )) . toString () , request ) ; const cachedResponse = await cache . match ( cacheKey ) ; if ( cachedResponse ) { return cachedResponse ; } const newResponse = await fetch ( request ) ; // Cache-Control: s-maxage=2 ctx . waitUntil ( cache . put ( cacheKey , newResponse . clone ())) ; return newResponse ; } } Origin がキャッシュしたいページで Cache-Control: s-maxage=2 を返すと、Workers でこのコードを通って 2 秒間コンテンツがキャッシュされます。この s-maxage と合わせて 3 種類の Cache-Control を Origin が返すことでキャッシュをコントロールしています: s-maxage=N - コンテンツを N 秒間キャッシュする private - CDN にキャッシュできないことを示すディレクティブ、公開だがキャッシュしたくないページ、未対応のページで使用 no-store - CDN にもローカルにもキャッシュしない、シークレット EC で使用 上記のコードは一見すべてのレスポンスをキャッシュするように見えますが、 Cache API は Cache-Control がキャッシュできないことを指示していたり、 Set-Cookie が含まれている場合にはそのコンテンツをキャッシュしません。実際のコードでは put する条件として s-maxage を含んでいることを条件にしてはいますが、このままでも Origin がキャッシュ可能なレスポンスを返さない限りは何もしないようになっているので、安心して利用することができます。 この実装をコアとして、Origin との整合性を担保するための仕組みと、 Cache Stampede を緩和するための機能を加えています。 キャッシュキーの設計 ショップページでは一つの URL からユーザーの環境や設定に合わせた複数のレスポンスが返されるため、 Request Header や Cookie からキャッシュに利用するキーを計算することで、URL に対して複数のキャッシュを紐づけています。このキーにはキャッシュの世代管理のための値も含まれていて、Origin がデプロイされた場合にキャッシュのパージを行うのではなく、利用するキャッシュの参照を切り替える方式を取っています。 ざっくりと次のようなキーになっています: shop . example . com / items / 1234 ? webapp_version = xxxx - yyyy - zzzz & accept_language = ja , en & i18n_language = ja & i18n_currency = JPY webapp_version はキャッシュの世代管理のための値で Origin から取得します。Origin にはデプロイ毎に一意の値が割り振られており、ショップページのすべてのレスポンスと専用の API に X-Webapp-Version という独自の Response Header を含んでいます。 X-Webapp-Version: d8643bc9-02ae-49db-b37f-81c26e77cb39 Workers から定期的に API をコールして最新の値を取得していて、Workers ではこの値に一致するキャッシュのみを有効なものとして扱っています。また、各ページのレスポンスにもこれを含んでおくことで、早い段階でのデプロイの検知や、Blue / Green デプロイ中で Origin が新旧を混合で返す状態でも古いキャッシュを作成しないようにしたり、ということに役立てています。 accept_language には BASE でサポートしている日本語と英語にあわせて、Request Header の Accept-Language を ja,en もしくは en,ja に丸めた値が入ります。 Accept-Language をそのまま使用してもよいですが、種類が増えキャッシュヒットレートが下がってしまうので Workers で丸めています。 i18n_language と i18n_currency はユーザーが選択した言語と通貨の情報で、Cookie に入っています。 Origin ではこの Cookie の値が accept_language よりも優先され、指定された言語と通貨で HTML をレンダリングするため、キャッシュを細かく分ける必要があります。 基本的には静的な作りにしていく方針ですが、言語通貨のようにどうしてもユーザーによってレスポンス内容を変えたい場合はキャッシュキーを拡張して対応します。 X-Fresh-For stale-while-revalidate な動作を実現するための仕組みで、 s-maxage と組み合わせて使用します。コンテンツが新鮮な時間を表す Response Header で、任意の値が Origin から返されます。 Cache-Control: s-maxage=10 X-Fresh-For: 2 キャッシュから取得した Response の Age とこの値を比較し、指定されていた分の時間が過ぎていたら、ユーザーにはキャッシュが古いことを表す STALE 状態でキャッシュを返し、バックグラウンドでキャッシュの更新をします。上記であれば、最大 10 秒間キャッシュし 2 秒を超えた時点でアクセスがあれば更新を行う、という動作になります。 キャッシュが切れた際にオリジンへのアクセスが再度集中してしまう、 Cache Stampede を緩和する仕組みとして導入しました。キャッシュ時間を s-maxage のみの場合よりも遥かに長くすることができ、 STALE している間に次のキャッシュを作ることで、キャッシュが完全にない状態を減らすことが目的です。 概念としては次のようなコードで実装されています(このコードは動作しません)。さっきは Cache API と fetch からの Cf Cache 利用を比較していましたが、ここではこの 2 つを組み合わせていることがポイントです。 STALE 状態のキャッシュは「キャッシュできるコンテンツである」という前提があることになるので、安全にリクエストをまとめることができます。 const cachedResponse = cache . match ( cacheKey ) ; if ( isStale ( cachedResponse )) { ctx . waitUntil (() => { const revalidateKey = cacheKey + `&revalidate= ${ cacheResponse . header . get ( 'X-Cache-Id' )} ` ; const newResponse = fetch ( request , { cf : { cacheKey : revalidateKey , cacheTtl : 1 } }) ; newResponse . headers . set ( 'X-Cache-Id' , uuid ()) ; cache . put ( cacheKey , newResponse ) ; }) ; } return cachedResponse ; キャッシュされてから時間が経ったものを検知すると、アクセスに対しては古いレスポンスを返しつつ、裏で更新を行っています。この時に再検証用のキャッシュキーを別途作り、それを使って Request Collapsing の利用を目的とした fetch を呼び出し、その結果を Cache API で実際に使用するキャッシュとして改めて put します。このような実装にすることで、複数の再検証リクエストが一つにまとまり、Origin へ到達するリクエストを削減することが可能になりました。 2025/12/15 追記: この利用方法の場合、 Request Collapsing は fetch のレスポンスが Cf-Cache-Status: MISS の場合に動作するようです。 MISS ではなく Cf-Cache-Status: EXPIRED となる場合、リクエストがまとめられていないことがあります。実際に動作しているものは Response にユニークな Id を割り振ったものをキャッシュし、更新リクエストに含めています。上記のコードも修正済みです。 Cache-Control には stale-while-revalidate ディレクティブがあり、これを利用したいと考えていたのですが、Cache API ではこれを利用できないという制約があり独自に実装するような形になりました。例えば s-maxage=2, stale-while-revalidate=10 の場合、キャッシュとしては STALE になりつつも 12 秒間生存してほしいのですが、Cache API の場合は 2 秒でキャッシュが蒸発してしまいます。キャッシュ時間自体を伸ばすためには s-maxage を伸ばす必要があり、このような形になりました。 developers.cloudflare.com Origin 側の変更点 Workers だけではキャッシュが動作しないようになっているので、ここまでに解説してきた各種 Response Header を Origin が返すように改修を行いました。CDN でのキャッシュを禁止する Cache-Control: private をすべてのページで返すことを基本としつつ、キャッシュしたいページでは次のように返します: Cache-Control: s-maxage=6 X-Webapp-Version: d8643bc9-02ae-49db-b37f-81c26e77cb39 X-Fresh-For: 2 キャッシュ動作に関して Origin は Response Header を追加しただけで、これによって動作がなにか変わることはありません。Workers がなくなっても動き続ける設計を達成できたように思います。 また、商品の特性によってキャッシュ時間をコントロールすることも可能なので、例えば販売前→販売開始のようにステータスが遷移する時刻をまたぐ場合直前には s-maxage を短く設定するようにしています: Cache-Control: s-maxage=1 X-Webapp-Version: d8643bc9-02ae-49db-b37f-81c26e77cb39 ただし、さすがに既存機能のコードをまったく変更せずに、とはいかなかったので事前にいくつか以下のような調整を行っていました: 言語通貨設定が CakePHP の Session 機能に依存していたため、 Plain な Cookie での実装へ変更し、Cloudflare Workers でも読み出せるように 商品の閲覧履歴を CakePHP の Session 機能から localStorage を用いたものに変更 一部ログイン状態によってラベルやメニューが変更される箇所の改修、元々非ログイン状態の表示に統一したい認識があったため、これにあわせて変更 query として付与させる referrer 情報を事前にサーバー側で処理するコードがあったため、クライアント側で処理ができるように調整 効果 まず、レスポンス速度についてです。キャッシュの導入以前の商品ページは、利用している拡張機能やアクセスの状況にもよりますが、Chrome DevTools で確認する限りでは大体 600ms ~ 2s 程度のサーバー応答待ち時間(Waiting for server response の値)がありました。 キャッシュが有効な場合はこの値が大きく改善され、100ms ~ 150ms 程度で安定するようになります。平均的に 1 秒を超えてくるようなショップだと、1/10 程度になったことになります。ただしあくまでキャッシュが存在する前提なので、すべてのアクセスでこの恩恵を受けられるわけではありません。 では、キャッシュがどの程度働いているかをとある日のアクセススパイクを含む 30 分で見てみます。縦軸がキャッシュヒット率(%)、横軸が時刻、赤が全体のキャッシュヒットレート、緑が HIT 、青が STALE でそれぞれ返した割合です。 12:00 頃にアクセスが集中し、キャッシュヒット率が 80-90% 付近まで跳ね上がっています。具体的な数字で言えば、商品ページ毎にざっくり 50,000 程度のアクセスがあり、そのうち 40,000 を HIT 、5,000 を STALE で返しているようなイメージ感で、高いものだと 90% のリクエストキャッシュから返しています。このログには Bot も含まれているのですべてが人間に向けて返されたものではありませんが、Origin への到達を 90% キャッシュで捌けていると考えるとそれなりに効果があるように思えます。 このタイミングで商品ページにアクセスすると 100ms 程度でレスポンスが返ってくるので、全体でみるとちょっとだけショップページが速くなったことになります、なりませんか? おわりに ということでショップページがちょっとだけ速くなった話でした。ショップページは改良の余地が多くがあり、Cloudflare の活用もまだまだこれからです。こういった領域に興味が湧きましたら採用情報もぜひご覧ください。 binc.jp 明日は @takashima です、お楽しみに!
この記事はBASEアドベントカレンダー 2025 の 6 日目の記事です。 エンジニアの右京です。BASE では今年、表示速度の改善を目標にすべてのショップページへ Cloudflare を導入しました。これは、その過程や技術面の簡単な解説です。 記事は前後半になっており、この記事は前半で、Cloudflare を導入〜直後までの話題となります。 モチベーション ショップページの表示が遅いことに尽きます。サービスが大きくなり、機能が増えていく中で処理が増え、速度が犠牲になってしまうのはある程度は仕方ないことだとは思います。とはいえレスポンスを返し始めるまでに 1 秒以上かかるようなケースもザラにあり、そこにアクセスのスパイクが重なると 10 秒以上返ってこない、オートスケーリングや手動でのスケーリングが都度必要… と、成り立ってはいるものの「良い」とは言えない状態でした。 一方でオーナーが利用する管理画面とは異なり、ショップページは在庫などの一部のデータを除けば、利用者によって変わる表示も極一部で、ほとんど静的サイトのような作りになっています。そこで、Edge Worker でのコンテンツのキャッシュがスピード面でもインフラ面でも効果が期待できるだろうということで Cloudflare の導入検討を始めました。 vs CloudFront BASE は基本的に AWS に乗っかって動いているので、 Cloudflare の特に Workers への対抗馬として CloudFront + Lambda@Edge / Functions があります。今回検討していた時点では技術面では以下の点を考慮して、採用を見送りました: 独自ドメインの証明書の問題 CloudFront をショップページとして扱う場合、証明書を CloudFront に配置する必要がある 具体的な数値は出せないが、現状のドメインをすべてカバーするためには複数のディストリビューションを管理する必要があり、現実的ではない コンテンツをキャッシュすることを前提とした設計 CDN なので当たり前といえばそうだが、あくまでキャッシュされているコンテンツに対する操作という印象 BASE では特定の会員のみが利用できるシークレット EC 機能を同じ仕組みの上で提供しているので、キャッシュ前提となるのが扱いづらい KV やストレージの自由度 コンテンツのキャッシュ以外にも何かしらのメタ情報はストアできる必要があるだろうという前提があった CloudFront KeyValueStore があるものの、Edge Worker からは読み取り専用 Cloudflare の Workers KV と比べるとどうしても取り回しが難しいように感じた 現在ではどうかというと、AWS 側でもこれらを解決するようなソリューションが発表されており、状況が変わっていることに注意が必要です。 aws.amazon.com Cloudflare を導入する 当然ですが、 Cloudflare が BASE のアプリケーション(Origin と呼ぶ)よりもエンドユーザーに近い位置で動作する必要があります。そのため、これまで Internet → Origin だった経路を、 Internet → Cloudflare → Origin に変更する必要があります。ここで問題になるのがショップページのドメインです。 ショップページのドメインには 2 つのパターンがあります: BASE の管理ドメインのサブドメイン base.shop / base.ec / theshop.jp のようなドメインを BASE が管理 これのサブドメインとして、例えば example.base.shop でショップページを配信 オーナーの持ち込み独自ドメイン 独自ドメイン App で CNAME を利用して任意のドメインでショップページを配信 前者の場合は特に問題はなく、経路変更を行うだけで済みます。詳細な内容はそれぞれの都合で異なると思うので割愛しますが、Origin に Cloudflare からアクセスされるサブドメインを新たに用意しておき、DNS 設定を切り替えることで経路が次のように変更されます: [導入以前] Internet ──▶ example.base.shop(Origin) [切替後] Internet ──▶ example.base.shop(Cloudflare) ──▶ from-cloudflare.base.shop(Origin) from-cloudflare の部分をユーザーが作ったり上書きできないようにしておく必要はありますが、大した問題ではないでしょう。このタイプのドメインは特にメンテナンスを必要とすることなく、無停止で移行していきました。 問題は後者のオーナーの持ち込みドメインです。前述のように CNAME で管理されており、オーナーが設定した DNS では CNAME cname.thebase.in となっています。Cloudflare を通すという理由でこれをすべてのオーナーに変更してもらうのは現実的ではないので、なんらかの方法で設定を維持したままドメインが Cloudflare へ解決される必要があります。ここで登場するのが SSL for SaaS です。 Cloudflare SSL for SaaS developers.cloudflare.com qiita.com ドキュメントの説明にもある通り、カスタムドメインをサポートする機能です。簡単に言ってしまうと、 持ち込みドメインをBASE 管理下にあるドメインと同様に from-cloudflare.* へ転送する機能です。 developers.cloudflare.com Custom Hostname を作成し、 SSL 証明書が発行された状態で cname.thebase.in が Cloudflare を向くようになると、持ち込みドメインが Cloudflare を通過してから Origin へ到達するようになります。 この切り替えは停止メンテナンスで行ったのですが、メンテナンス中にすべてのアクティブな独自ドメインを Custom Hostname として登録し、証明書の発行を終えるのは現実的ではなかったため、事前に Pre-validation という仕組みを使って移行の準備を進めていました。 developers.cloudflare.com Pre-validation を使うと、現行のアプリケーションを稼働させたまま Cloudflare 側に SSL 証明書を配置しておくことができます。BASE では HTTP Tokens の方を使用していて、Custom Hostname を作成すると Cloudflare の Dashboard もしくは API から検証用の http_url と http_body を得ることができます。 { " result ": [ { " id ": " 24c8c68e-bec2-49b6-868e-f06373780630 ", " hostname ": " app.example.com ", // ... " ownership_verification_http ": { " http_url ": " http://app.example.com/.well-known/cf-custom-hostname-challenge/24c8c68e-bec2-49b6-868e-f06373780630 ", " http_body ": " 48b409f6-c886-406b-8cbc-0fbf59983555 " } , " created_at ": " 2020-03-04T20:06:04.117122Z " } ] } https://developers.cloudflare.com/cloudflare-for-platforms/cloudflare-for-saas/domain-support/hostname-validation/pre-validation/ より引用 ドキュメントでは ownership_verification_http というフィールドでそれが返ってきていますが、このフィールドは少し時間が経過すると使用することができず、そのタイミングまでにレスポンスを準備できない場合は ssl フィールドに含まれる http_url と http_body を使う必要がありました。 { " result ": { " ssl ": { " status ": " pending_validation ", " http_url ": " http://app.example.com/.well-known/acme-challenge/uVKCkPGJZt2PewfPPQxSRe8QTDDyq_DzOLZ4AK5T1Vg ", " http_body ": " uVKCkPGJZt2PewfPPQxSRe8QTDDyq_DzOLZ4AK5T1Vg.LNFwUG0womdgXgxKtKU4B6bqUXvBkIouc5BNejjQTh0 " , }, } } 定期的にこの URL に Cloudflare からアクセスがあり、 http_body の内容を返すことができれば検証に成功し、Custom Hostname が有効になって SSL 証明書が配置されます。簡単に図にすると以下のようになり、一時的に SSL 証明書が 2 つになります: [Origin] app.example.com [Internet] ────────────────▶ 元々ある SSL 証明書 ◀──────────────── アプリケーションの動作 create apps.example.com ◀──────────────── ドメインの登録や更新をトリガーに作成 [Cloudflare] /.well-known/acme-challenge/uVK... ────────────────▶ ◀──────────────── uVK.... apps.example.com の SSL 証明書が事前に発行される 移行準備中 ──────────────────────────────────────────────────────────────────────────── 切り替え後 [Internet] [Origin] │ app.example.com 元々あった SSL 証明書は不要に │ │ ▼ from-cloudflare.* [Cloudflare] ────────────────▶ アプリケーションの動作 SSL証明書 切り替えメンテナンスの事前準備として、すべてのドメインを事前に Cloudflare に登録、 SSL 証明書が発行された状態にしておき、実際のメンテナンスでは NS の切り替えのみにすることで、比較的短い時間での切り替えが完了しました。 また、これ以降は SSL 証明書の管理を Cloudflare が行ってくれるようになります。自前での更新が不要になるので、これも利点の一つと言えるでしょう。 切り替え直後のトラブル 過去の動作との不整合で一部個別対応が必要なケースはあったものの、特に大きな問題はなく切り替えを終えることができました。ただ、少し想定外だったことが 2 つあったので、それを書いてこの記事は締めようと思います。 Cloudflare を通った時点でデフォルトのキャッシュが動作する 何も設定をしていない場合、一般的にキャッシュできるとみなされるアセットのキャッシュが自動的に始まります。デフォルトの挙動は以下で確認することができます: developers.cloudflare.com 殆どの場合は困らないと思いますが、動的に JS を作成したり、ビルド済み JS をアプリケーションから配信している場合は注意が必要です。影響は軽微だったものの一部動的に生成されているものがあり、これが原因で不具合が発生しました。 この設定は Cache Rules を作成することで上書きすることができます。切替時はすべてを Bypass する設定にしておくのが無難に思いました。 developers.cloudflare.com O2O 管理している Cloudflare より前に別の Cloudflare がいる状態です。Cloudflare のアイコンがオレンジなので Orange-to-Orange というらしいです、可愛いですね。 developers.cloudflare.com この状態が存在することを認識していないと、「キャッシュをしていないはずなのに Cloudflare がキャッシュを返している」という状態になったときに混乱します、しました。 これは持ち込まれるドメインの DNS が Cloudflare の場合に起こります。CNAME を設定する際に自身の Cloudflare でも Proxy をするという設定があり、これを使うとオーナー側の Cloudflare でコンテンツをキャッシュできるようになります。こうなると、こちらが管理できる範囲よりユーザーに近い場所でキャッシュが起こってしまい、基本的には手が出せない状態になってしまうので、個別でなにかしらの解決をする必要があります。 おわりに 明日は続けて Cloudflare Workers でのコンテンツのキャッシュについて書きます。よろしくお願いします!
はじめに この記事はBASE Advent Calendar 2025の5日目の記事です。 devblog.thebase.in こんにちは!Pay IDのEngineering Sectionでエンジニアリングマネージャーを務めている岡部( @rerenote )です。今回はPay ID…ではなく、社内の有志で活動している iikanji-conference-toudanチームによる「技術イベント・カンファレンスのスポンサー活動」について、今年の取り組みをまとめてご紹介します。 iikanji-conference-toudanチームとは? iikanji-conference-toudanチームは技術イベント・カンファレンスへBASEから登壇する人たちを応援するために立ち上げられました。社内の有志メンバーで構成されています。 登壇資料のレビュー相談をはじめ、こういう技術イベントがあったよ、こういう発表がおもしろかったよなどのカジュアルトーク、スポンサーブース出展時にはブース企画なども行なっています。 2025年のスポンサー活動一覧 「登壇するメンバーをもっと後押ししたい!」という思いから、技術イベント・カンファレンスへのスポンサー協賛活動に取り組みました。協賛活動を通してBASEメンバーのトークを見つけてもらうきっかけ作りにもなっています。 協賛したのはBASEで採用しているPHP、TypeScriptのカンファレンス。今年はこの2つの技術領域でコミュニティとの繋がりを深める一年になりました。カンファレンス運営のみなさま、参加者のみなさま、このような場をいただき本当にありがとうございました。 各イベントの詳細は、以下のレポートにまとめています。登壇者コメントや参加メンバーによるレポート、ブース企画についての内容もありますので読んでいただけたら嬉しいです。 PHPerKaigi 2025(3/21-3/23) devblog.thebase.in PHPカンファレンス小田原 2025(4/12) devblog.thebase.in TSKaigi 2025(5/23-5/24) devblog.thebase.in PHP Conference Japan 2025(6/28) devblog.thebase.in おわりに 私は過去に個人で勉強会を主催したり、技術イベントやカンファレンスのスタッフとして数年活動していた背景があり、技術コミュニティやカンファレンスという場そのものがとても好きです。iikanji-conference-toudan に参加している一番の理由は、そうした好きな場所に、少しでも貢献できたら嬉しいと思っていることが大きいのかもしれません。 BASEでは会社全体でOSSやコミュニティを応援する文化があり、このような活動を行えることにいつも感謝しています。 2026年もイベント・カンファレンスでの登壇を応援したり、スポンサー活動を通してコミュニティを盛り上げていければと思っておりますので、どうぞよろしくお願いいたします。 プロダクト開発も好きだけど技術コミュニティも好き!という方でBASEに興味を持っていただいた方は、エンジニア募集中ですので採用情報もぜひ覗いてみてください! binc.jp 明日のBASEアドベントカレンダーは @yaakaito さんの記事が登場する予定です!お楽しみに!
はじめに この記事はBASEアドベントカレンダーの4日目の記事です。 devblog.thebase.in EC作成サービスBASEのプロダクト開発チームでエンジニアリングマネージャー(EM)をしている @tanden です。 私たちのチームではこの1年ほど、開発組織のケイパビリティをどう可視化し、継続的に改善していくかについて考え方の整理と運用に取り組んできました。「今の組織はどこが強みで、どこに伸びしろがあるのか?」を共通の視点で語れるようにするため、SPACEのようなフレームワークにヒントを得ながら、組織を立体的に捉えるための「補助線」を引くことを目指しました。 この記事では、私たちが整理した「開発組織のケイパビリティ可視化」のコンセプトと運用方法について紹介します。まだ取り組みの途中ですが、組織運営のヒントになれば幸いです。 ケイパビリティ可視化の取り組み振り返り EC作成サービス BASE のプロダクト開発組織ではこれまで、Four Keys を開発チームのケイパビリティ指標として捉え、Google Apps Script などを用いてプルリクエストのマージ数やリードタイムを計測し、改善につなげてきました。 マージ数やリードタイムは今も継続して追いかけている重要な指標で、計測と振り返りをセットで運用することで一定の成果を得られています。 一方で、複雑さが増すプロダクト開発・運用の実態を捉え、組織として継続的に改善サイクルを回していくには、これらの指標だけでは不十分ではないか——そんな考えが徐々に生まれていきました(課題感に共感いただける方もいるのではないでしょうか)。 そんな中で、これまでの取り組みを振り返りから、主に以下の3点が課題として挙がりました。 組織としての方向性や全体像が曖昧だった 本来であれば「どんな組織を目指すのか」という前提に沿って指標を選ぶべきでしたが、その背景や全体像を描き切れていませんでした。 プルリクエストの指標だけでは複雑な開発を捉えきれない マージ数やリードタイムは重要ですが、プロダクト開発を多面的に理解し、改善サイクルを回すには要素がやや不足していました。 一定の改善ラインを越えると伸びしろが見えづらくなる プルリクエスト関連の指標は、ある程度最適化されるとそれ以上の改善余地が小さくなり、チームの状態把握はできても、次のアクションにつながりにくくなっていました。 ケイパビリティ可視化のコンセプトと目的 そこで私たちは、これまでの反省を踏まえつつ、開発組織のケイパビリティ可視化の枠組みづくりに取り組むことにしました。 「汎用的な生産性や開発力指標は存在しない」という前提のもと、上記3つの課題を反転させるため、以下のコンセプトのもと進めました。 個別指標の妥当性よりも、開発組織の能力の全体像と方向性を描いてから指標の位置づけを明らかにする 複数指標を利用して組織の能力や状態を多面的に測れるようにし、定期的に計測指標を入れ替えることで改善のきっかけを提供し続ける 指標を達成できるかどうかよりも、改善後の組織やチーム・個人の理想状態をイメージできるかどうかを重視する 上のコンセプトを満たしつつ、対話を通じた未来へのアクションプラン作りと改善の実行・計測のサイクルを回すこと目的に、ケイパビリティ可視化の取り組みを進めました。 『サーベイフィードバック入門 p.28』(中原淳著)を参考に作成 開発組織の4つのケイパビリティ 開発組織のケイパビリティ可視化を進めるために、まず開発組織のあるべき姿を描いていく必要があります。開発組織のあるべき姿を描くために、この取り組みの中では Evidence Based Management(EBM) という考え方を参考に、全社やプロダクト開発組織において定義されているミッション・ビジョン・バリューと整合させる形で、開発組織のケイパビリティを4つの分野に整理しました。 サービス価値の維持 ユーザーが今現在価値を感じて利用しているサービス上の機能提供を維持し続けること。 全社におけるミッションやビジョンとの整合性 「人生のオーナーを増やす社会基盤」(基盤=いつ・いかなる状況でも利用できる信頼性が高いもの) プロダクト開発組織のミッションやビジョンとの整合性 「ショップが成長していくことを支えるサービスであり続ける」 「トラフィックを適切に受け止めて、決済を無事に完了させる」 素早い価値提供 新しい価値を提供するまでに必要な時間を限りなく短くすること。 全社におけるミッションやビジョンとの整合性 価値提供スピードについての言及はないが、スタートアップとしてのスピード感で価値提供・価値創出を行い、社会基盤の構築を目指すことは暗黙的な前提 プロダクト開発組織のミッションやビジョンとの整合性 ハイスループットなプロダクト開発組織を目指す 新しい価値の実現 まだ実現できていない価値(機能や体験)をテクノロジーの力で実現すること。 全社におけるミッションやビジョンとの整合性 「あたらしい決済で、あなたらしい経済を」 プロダクト開発組織のミッションやビジョンとの整合性 「多様なニーズに応える機能改善をし続ける」 「サービスの成長にチャレンジしていく」 参考として、BASEグループとプロダクト開発組織のMissionやVision、Foundationがまとまっている会社紹介資料を掲載させていただきます。 手前味噌ですが素敵なMission / Vision / Foundationなのでご覧いただけると嬉しいです。 speakerdeck.com speakerdeck.com 組織・ワークエンゲージメント 組織体制(組織)やワークエンゲージメントは上の3つのケイパビリティを支える土台として位置づけます。仮に上の3つのケイパビリティを高いレベルで実現できるスキルや経験を有していたとしても、ネガティブな雰囲気で溢れ疲弊した組織では、もてる力を十分に発揮することはできません。 プロダクト開発に直接関わるケイパビリティだけでなく、組織に関する指標やワークエンゲージメントの可視化と改善も目指しています。 組織における実際の運用 4つのケイパビリティ分野における理想像(未来づくり)を起点として、可視化と対話の流れを作るために、組織が日々運用しているOKRと結びつけることをまず目指しました。 開発組織として達成したい目標(Objective)がどのケイパビリティ領域に紐づいているのかを明確にすることで、以下のつながりを意識しやすくなります。 いま自分たちは組織として何を伸ばそうとしているのか / どんな課題があるのか その施策をやる理由は何なのか KPIが示す変化はどの能力の成長を意味しているのか 事業目標とOKR(施策含む)、ケイパビリティ領域のマッピングは、以下の画像のようなイメージになります。 このように事業目標、OKR、ケイパビリティを重ね合わせることで、それらが一本の線でつながり、チームの前進をより的確に捉えられるようになります。 実際にやってみてどうだったのか 実際に運用を始めてみると、上記以外のメリットや取り組みの難しさもでてきました。ここでは簡単にご紹介できるものに限りますが、いくつか共有させてください。 施策の偏りに気づける 大規模なプロダクトを日々運用する上で、システムのパフォーマンス改善や利用パッケージのアップデートなど、今あるシステムを維持するだけでもやるべきことは山積みです。EC作成サービスBASEの運用においても同様で、「サービス価値の維持」分野の施策が多くなりがちでした。一方で、そこばかりに力を割いてしまうと、Four Keys計測の取り組みが後回しになるなどの弊害が生じます。 そのため、OKR施策を検討する際には、まず1年間の最重要領域を決め、その領域に優先的に施策の枠を確保し、残りを他の領域に割り当てるようにしています。 ちなみにFY2025では「素早い価値提供」分野を最重要領域と定めて取り組みを進めてきました。詳細については、12月8日のアドベントカレンダーでEMの @takashima から共有させて頂く予定です。 「新しい価値の実現」分野は難しい 「新しい価値の実現」分野は、定義にある通り「まだ実現できていない価値(機能や体験)をテクノロジーの力で実現すること」を目指すことになります。この分野は、普段の開発とは異なる技術スタックやそれらを使ったUI/UXの検証など、開発研究・R&Dの要素を含みます。成果が不確実な試行錯誤が必要であり、難易度も高いため、どうしても後回しになってしまっているのが現状です。 とはいえ、AI活用をはじめ、やるべきことはたくさんあります。プロダクト開発組織として引き続き取り組んでいきたいテーマです。 おわりに プロダクト開発組織のケイパビリティ可視化の取り組みについて紹介しました。4 つのケイパビリティ領域を定義し、それらを OKR と結びつけることで、組織として何を伸ばそうとしているのか、施策の意図や成果がどの能力に紐づくのかをより明確に捉えられるようになったのではないかと感じています。開発組織の目標管理や成長を考えるための新しい「補助線」として、読者の方の参考になれば幸いです。 まだ道半ばではありますが、これからも組織の未来づくりと可視化、そして対話のサイクルを重ねながら、プロダクトと開発組織の継続的な成長を目指していきたいと考えています。 BASEでは、今後のプロダクト成長を支える技術戦略や開発基盤の構築をリードしていただけるテックリード候補を募集しています。 ご興味があれば、ぜひ採用情報をご覧ください。 open.talentio.com 明日のBASEアドベントカレンダーは @rerenoteさんからの記事です。お楽しみに!
はじめに この記事は🎄🎅 BASE PRODUCT TEAM BLOG Advent Calendar 2025 🎅🎄の3日目の記事です。 devblog.thebase.in こんにちは! BASE 株式会社 Pay ID 兼 BASE PRODUCT TEAM BLOG 編集局メンバー の @zan_sakurai です。 私の所属する Pay ID では一部のアプリケーションでGoを採用しており、日々Goらしいコードを書くことを意識して開発を行っています。 読者のみなさまは「Goらしさ」という言葉を聞いたことがある、もしくは使ったことはありますか...? 私も日々の開発シーンで聞いたことも使ったこともありますが、実際に「Goらしさとは何か?」と問われると、私も正直言葉に詰まってしまいます...。 とはいえ「Goらしさ」とは曖昧なままではあるのもよろしくないので、 本記事では一旦「Goらしさ」/「Goらしいコード」とは、Goの言語仕様だけでなく、Goの設計思想や慣習に沿ったコードを指すこととして、 Go の特徴の一つである interface を題材に、今回は「Goらしい」 interface の書き方について掘り下げてみたいと思います。 「Goらしい」 interface を書く Go の interface の特徴は多岐にわたりますが、 今回は Go Wiki: Go Code Review Comments#Interfaces に記載されている内容を掘り下げていこうと思います。 具体的なコード例はGo Code Review Commentsがとても良くまとまっているので、そちらもぜひ御覧ください。 ステップ1: interface が本当に必要になるまで書かないのが「Goらしい」 Go Wiki: Go Code Review Comments の Interfaces の章には以下のように記されています。 Do not define interfaces before they are used: without a realistic example of usage, it is too difficult to see whether an interface is even necessary, let alone what methods it ought to contain. 引用元: Go Wiki: Go Code Review Comments#Interfaces 端的にまとめてしまうと必要性が出てきたらinterfaceを定義せよ、という旨です。 私のようなJavaなどのOOP言語からGoに来た人は、最初に抽象型を定義してから具体的な実装を作る傾向があるかもしれませんが、 Goでは具体的な実装を行ってから必要性が出てきた時に初めてinterfaceを定義するのが「Goらしい」書き方のようです。 過度な抽象化による複雑化、いわゆる「インターフェース汚染」などと巷では呼ばれていますが、このようなことを避けるために、interfaceを書くタイミングに慎重になることが推奨されているようです。 Go Code Review Comments の例を参考に、ステップ1を踏まえたコードを書いてみました。 package consumer import "interfaceidioms/step1/producer" // 何らかの処理を行う構造体 // producerパッケージのThingerを内部に持つ type ThingerConsumer struct { t producer.Thinger } // 何らかの処理を行うメソッド func (tc ThingerConsumer) DoSomething(input producer.Input) bool { return tc.t.Thing(input) } // いわゆるファクトリ関数 // consumer.NewThingerConsumer(producer.NewThinger()) のような使われ方が想定される. func NewThingerConsumer(t producer.Thinger) ThingerConsumer { return ThingerConsumer{t: t} } package producer // 何らかの入力データを表す構造体 type Input struct { Content string } // 何らかの処理を行う構造体 type Thinger struct { // snip... } // 何らかの処理を行うメソッド func (t Thinger) Thing(input Input) bool { return input.Content == "" } // いわゆるファクトリ関数 func NewThinger() Thinger { return Thinger{} } 特に何の変哲もない?コードですが、ステップ1を踏まえたコードになっています。 「Goらしい」interfaceを書く上での最初のステップは、 本当にinterfaceが必要になるまで書かないこと です。 ステップ2: 消費する側に interface を定義すると 「Goらしい」 ステップ1で 本当にinterfaceが必要になるまで書かないこと を述べましたが、実際にinterfaceを書く必要性が出てきた場合、どう書くと「Goらしい」のでしょうか? またまた Go Wiki: Go Code Review Comments の Interfaces の章からの引用ですが、以下のように記されています。 Go interfaces generally belong in the package that uses values of the interface type, not the package that implements those values. The implementing package should return concrete (usually pointer or struct) types: that way, new methods can be added to implementations without requiring extensive refactoring. 引用元: Go Wiki: Go Code Review Comments#Interfaces これも端的にまとめてしまうと、interfaceは実装を提供する側ではなく、消費する側のpackageに定義すべき、という旨が記載されています。 この点も私のようなJavaなどのOOP言語からGoに来た人は、実装を提供する側でinterfaceを宣言したくなるかもしれませんが、 Goでは消費する側でinterfaceを定義するのが「Goらしい」書き方のようです。 Go では明示的にimplementsを宣言せず、Duck Typing的に暗黙的にinterfaceを満たすので、 ステップ1で直接呼びだしている箇所も後からinterfaceに置き換えるリファクタリングは容易です。 また、消費する側が使うメソッドだけをinterfaceに含めば良いので、小さなinterfaceを定義できます。(いわゆるインターフェース分離の原則。) 小さなinterfaceですので、テスト時に必要な振る舞いだけを持つinterfaceを満たすモック実装を用意するのも容易です。 Go Code Review Comments の例を参考に、ステップ2を踏まえたコードを書いてみました。 package step2goodconsumer import "interfaceidioms/step1/producer" // step1で使ったproducerパッケージの変更はなく、そのままstep2goodconsumer側でinterfaceを定義できる. // Thinger interface を定義 // これがいわゆる消費者側のinterface. type Thinger interface { Thing(input producer.Input) bool } // 何らかの処理を行う構造体 // producerパッケージのThingerを内部に持つ type ThingerConsumer struct { // interface型を使うように変更 t Thinger } // 何らかの処理を行うメソッド func (tc ThingerConsumer) DoSomething(input producer.Input) bool { return tc.t.Thing(input) } // いわゆるファクトリ関数 // DIなりでよしなに入れ替えよう. func NewThingerConsumer(t producer.Thinger) ThingerConsumer { return ThingerConsumer{t: t} } 実際のstep1との差分も少なく、step2goodconsumer側の判断でproducerの変更なく、容易に interface を使った書き方にリファクタリングできることがわかります。 $ diff -u --label "step1/consumer.go" --label "step2/consumer.go" \\ step1/consumer/consumer.go step2goodconsumer/consumer.go --- step1/consumer.go +++ step2/consumer.go @@ -1,11 +1,18 @@ -package consumer +package step2goodconsumer -import "interfaceidioms/step1/producer" +import "interfaceidioms/step1/producer" // step1で使ったproducerパッケージの変更はなく、そのままstep2goodconsumer側でinterfaceを定義できる. + +// Thinger interface を定義 +// これがいわゆる消費者側のinterface. +type Thinger interface { + Thing(input producer.Input) bool +} // 何らかの処理を行う構造体 // producerパッケージのThingerを内部に持つ type ThingerConsumer struct { - t producer.Thinger + // interface型を使うように変更 + t Thinger } // 何らかの処理を行うメソッド @@ -14,7 +21,7 @@ } // いわゆるファクトリ関数 -// consumer.NewThingerConsumer(producer.NewThinger()) のような使われ方が想定される. +// DIなりでよしなに入れ替えよう. func NewThingerConsumer(t producer.Thinger) ThingerConsumer { return ThingerConsumer{t: t} } 「Goらしい」interfaceを書く上での次のステップは、 消費する側に interface を定義する です。 番外編: Accept interfaces, return structs / Accept Interfaces, Return Concrete Types Accept interfaces, return structs という言葉を聞いたことはありますか? いわゆる interfaceを受け入れ、具体的な型を返す、というidiomです。 この際に改めて原典を探ってみましたが、詳しく言及しているようなものはあまりないように見受けられました。 もし原典をご存知の方がいらっしゃいましたら、ぜひ教えてください。(以下触れている箇所) https://go-proverbs.github.io/ https://github.com/go-proverbs/go-proverbs.github.io/issues/37 https://github.com/google/styleguide/blob/gh-pages/go/decisions.md ステップ1とステップ2を踏まえた上で、ですとproducer側でinterfaceを定義するケースはまだないので、自ずと Accept interfaces, return structs になるのかと思います。 余談 今回はあえて、producer側で定義するinterfaceについては触れませんでした。 実際に https://github.com/golang/go で標準ライブラリのコードを見てみると、producer側でinterfaceを提供しているケースも多々あります。 続編を書こうと思いますので、ご期待いただけますと幸いです。 さいごに 最後に改めて「Goらしい」 interface を書くステップをまとめます。 本当にinterfaceが必要になるまで書かないこと 消費する側に interface を定義する 読者のみなさまが「Goらしさ」に触れるきっかけの一助になれば幸いです。 参考資料 Go Wiki: Go Code Review Comments Effective Go https://go-proverbs.github.io/ https://github.com/go-proverbs/go-proverbs.github.io/issues/37 https://github.com/google/styleguide/blob/gh-pages/go/decisions.md 宣伝 Pay ID ではエンジニアを募集中です!ご興味があれば採用情報もぜひご覧ください! open.talentio.com 明日は、BASEアドベントカレンダーは @tanden さんの記事です。お楽しみに!
はじめに この記事はBASEアドベントカレンダーの2日目の記事です。 devblog.thebase.in こんにちは、 BASE Feature Dev1 Group で PHPer をしている @meihei です。今日は Gopher です。 この記事では、外部サービスの Webhook を AWS Lambda (Function URLs) で受け取り、SQS にいれる設計と実装、そして、それら全体が正常に稼働しているかを監視するやり方について書きます。 1. 前提とアーキテクチャ 前提として BASE が連携する外部サービスでイベントが発生した際、その通知を Webhook 経由で受け取り、必要なユースケースを後続ワーカーが実行できるようにつなぐ仕組みを設計します。 技術要件 外部サービスの中には EventBridge の Partner Event Source を利用して直接イベントを受信できるものもあります。しかし、今回の対象サービスは EventBridge 連携に対応していないため Webhook を利用しています。Webhook の正当性については、署名ベースの検証によって確認しています。 また、Webhook を受信するエンドポイントは BASE の PHP アプリケーションサーバーでは処理せず、サーバーレス環境(Lambda)で受信する方針としています。後続のワーカーは、既存のコンテナ基盤上で稼働する PHP プロセスによって実行されます。 ビジネス要件 今回は、BASE から外部サービスへ API 経由で商品連携を行った後に、商品審査ステータスの変更が発生し、その審査ステータスを受け取って BASE 側の連携ステータスを更新するユースケースについて解説します。 ここで求められるビジネス要件は以下のとおりです。 許容遅延 :リアルタイム性は必須ではありませんが、できる限り早く反映されることが望ましい。 処理漏れへの対応 :処理漏れは設計上許容せず、万が一発生した場合は必ず SQS Dead Letter Queue (以下、DLQ)に退避させ、後続の運用フローで確実に回収できるように。 二重通知への対応 :外部サービスから同一イベントが複数回送られたとしても、後続ワーカー側で冪等性を担保し、重複処理を許容。 通知順序のズレ :順序入れ替わりは発生するものとして扱い、最終的に正しい状態へ同期されていれば問題なし。 これらの要件から最終的に以下のような構成になりました。 SQS は Standard Queue を使用し、外部サービスのシークレットキーなどは AWS Systems Manager のパラメータストアを利用しています。 2. インフラ(Terraform+lambroll) 本構成で必要となるAWSリソースは以下の通りです。 *1 IAM: Lambda 実行用の Role と Policy Lambda: Webhook を受信する Lambda Function(Function URLs) SQS: イベントを後続ワーカーへ受け渡すための Main Queue と Dead Letter Queue Systems Manager & KMS: 外部サービスのシークレットキーを保管するためのParameter Store と KMS Key BASE ではインフラ構築に Terraform を、Lambda のデプロイには lambroll を利用しています。これらは別々のリポジトリで管理されていて、アプリケーションエンジニアでも安全かつ容易に AWS リソースを管理でき、Lambda のコードは継続的デリバリーが可能な体制になっています。 Terraform 編 Terraform は一般的な AWS 構築を行いますが、Lambda だけは異なります。 aws_lambda_function のリソースは Lambda を作成する時と、削除する時だけに使用し、最初にダミーファイルをデプロイします。その後は source_code_hash や runtime , environment などの情報に差分を検出しても変更を適用しないように設定しています。これらは lambroll 側で行います。 また、 BASE では Terraform をモジュール化して管理しており、インフラエンジニアや SRE でなくても、必要なパラメータを入力すれば標準的な AWS リソースを安全に作成できるようになっています。 例えばこんな感じです。 // lambda.tf module "lambda_hogehoge_integration_webhooks" { source = "../modules/lambda" function_name = "hogehoge-integration-webhooks" ... } // sqs.tf module "hogehoge_integration_webhooks" { source = "../modules/sqs_dead_letter" name_sqs = "hogehoge-integration-webhooks" dead_letter_queue_arn = module.hogehoge_integration_webhooks_dead.sqs_arn ... } module "hogehoge_integration_webhooks_dead" { source = "../modules/sqs" name_sqs = "hogehoge-integration-webhooks-dead" ... } module からアタッチするポリシーも設定出来るので、送信先 SQS への sqs:SendMessage 、シークレットを格納している SSM Parameter Store への ssm:GetParameter 、そして Parameter Store 経由で Secrets Manager のシークレットを参照するための kms:Decrypt だけをアタッチして、最小権限になるようにします。 また、Lambda Function URL の設定は lambroll 側ではなく Terraform 側で定義しました。 lambroll 編 lambroll は、AWS Lambda に特化したシンプルなデプロイツールです。Terraform でインフラ(関数そのものや IAM ロール、Function URL など)を作成しつつ、アプリケーションコードのビルドとデプロイは lambroll に任せることで、役割を分離しています。 github.com まず、Terraform で作成した既存の Lambda 関数を、Lambda 専用リポジトリ側から管理できるように初期化します。 lambroll init --function-name hogehoge-integration-webhooks --download このコマンドにより、対象の関数設定を取得し、 lambroll 用の設定ファイル( function.json )が手元に生成されます。 { " Architectures ": [ " arm64 " ] , " Environment ": { " Variables ": { " APP_KEY ": " {{ must_env `APP_KEY` }} ", " APP_SECRET_NAME ": " {{ must_env `APP_SECRET_NAME` }} " } } , " Handler ": " bootstrap ", " Runtime ": " provided.al2023 ", ... } function.json では Terraform で ignore の設定をしている環境変数やランタイムを指定します。 コードを書き換えた後の Go のビルドとデプロイは次のように実行します。 go build -v -o ../build/bootstrap lambroll deploy --src="build" この一連のフローは GitHub Actions 上から自動でビルドとデプロイが走るようになっています。 3. 実装(Go) Lambda のコードは Go 言語で実装しています。後続ワーカーはドメインロジックを担うため BASE のアプリケーションと同じ PHP を採用していますが、Lambda 側ではビジネスロジックを持たないため採用言語に強い制約はありません。 今回は外部サービスの公式ドキュメントが Go のサンプルを提供していたこと、社内で Go の利用実績があることから、Lambda は Go で実装する方針としました。 次のようなディレクトリ構成で進めています。 hogehoge-integration-webhooks/ build/ bootstrap src/ go.mod go.sum main.go Makefile function.json .env まず、 init 関数では Lambda 起動時に一度だけ実行される初期化処理として、SQS クライアントの生成とシークレットの取得を行います。ここで作成したクライアントやシークレットはグローバル変数として保持し、各リクエスト処理で再生成しないようにしています。 func init() { ... // SQSクライアントを作成 sqsClient = sqs.NewFromConfig(cfg) // SSM Parameter Store から APP_SECRET を取得 ssmClient := ssm.NewFromConfig(cfg) withDecryptionc := true param, err := ssmClient.GetParameter(context.TODO(), &ssm.GetParameterInput{ Name: &appSecretName, WithDecryption: &withDecryptionc, }) appSecret = *param.Parameter.Value ... } main 関数では lambda.Start(handler) を呼び出し、実際のリクエスト処理は handler 関数に集約しています。 handler では署名の検証と SQS へのメッセージ送信だけを担当させ、ビジネスロジックは持たないようにしています。署名検証に失敗した場合は 404 を返すことで、認証まわりの情報を外部に漏らさないようにしています。 func handler(ctx context.Context, request events.LambdaFunctionURLRequest) (events.LambdaFunctionURLResponse, error ) { // 事前検証 ... // 署名を検証 if !verifyWebhookSignature(request.Body, authHeader, appKey, appSecret) { return events.LambdaFunctionURLResponse{ StatusCode: 404 , }, nil } // リクエストBodyをそのままSQSに送信 _, err := sqsClient.SendMessage(ctx, &sqs.SendMessageInput{ QueueUrl: &sqsQueueURL, MessageBody: &request.Body, }) if err != nil { log.Printf( "Failed to send message to SQS: %v" , err) return events.LambdaFunctionURLResponse{ StatusCode: 500 , }, err } // 成功の場合空で返す return events.LambdaFunctionURLResponse{ StatusCode: 200 , }, nil } 今回の Lambda は「受け取った Webhook を検証し、そのまま SQS に流す」ことだけに専念しており、後続のワーカーがビジネスロジックを実行する前提のため、リクエスト Body をそのまま SQS に送信するシンプルな実装としています。 もしビジネス要件の異なる複数種類の Webhook を単一のエンドポイントで受け取る必要がある場合は、この handler 内でイベント種別(リクエスト Body 内の値)などに応じて送信先のキューを振り分ける構成にすると、FIFO Queue が使用可能後になったり、後続のワーカーを用途ごとに分離しやすくなります。 4. 監視・運用 前提として BASE では New Relic でも AWS サービスのインフラの観測を行っています。 New Relic では以下の様なダッシュボードを用意し、ひと目でサービスの状態が把握出来るようになっており、万が一異常が起きてもアラートを設定していて slack へ通知されるようにしています。 ダッシュボード編 この連携の状態を把握するための「入り口」として New Relic のダッシュボードを用意しています。ダッシュボードを開けば、構成・ステータス・関連リソースへの導線がひと目で分かるようにすることを意識しています。 まず、ダッシュボードの一番上には全体構成が分かる図を配置します。New Relic では Mermaid 記法でダッシュボード内に図を記述できるため、 Lambda Function URL → Lambda → SQS → ワーカー というイベントの流れを示した簡単な構成図を載せています。 参考: Mermaid記法でダッシュボードにアーキテクチャ図など視覚的な表現が可能に! #AWS - Qiita 次に、この連携に関わる周辺情報へのリンクをまとめた Markdown テキストを置きます。具体的には、対象 Lambda 関数や SQS キューの AWS コンソールへのリンク、関連する GitHub リポジトリ、ドキュメントなどを並べておき、運用時にここからすぐに辿れるようにしています。 その下には、主要なメトリクスをサービスの流れに沿って並べます。入口となる Lambda については Invocations 、 Duration 、 Errors 、 Throttles を New Relic 経由で可視化し、エラーやスロットルの有無をすぐ確認できるようにしています。SQS については、 SentMessages / ReceivedMessages / DeletedMessages といったトラフィックの傾向に加えて、 ApproximateAgeOfOldestMessage 、 ApproximateNumberOfMessagesNotVisible 、 ApproximateNumberOfMessagesVisible を配置し、キュー滞留や詰まり具合を一目で判断できるようにしています。 (LatencyのSLOはうまく計測出来ておらず、0となってしまっている…) これらを「入口 → キュー → 下流ワーカー」の順番で並べることで、どのレイヤーで異常が起きているかを直感的に追えるダッシュボード構成としています。 アラート設定と運用 New Relic アラートでは、各レイヤーで重要なメトリクスを監視対象として、異常を最速で検知し、slack へ送るようにしています。 Lambda では Errors と Throttles を監視し、失敗が発生した場合は Slack へ即時通知されるようにしています。Lambda の失敗時は、外部サービス側の Webhook リトライが発生しているかの確認を行います。 SQS 側では ApproximateAgeOfOldestMessage と ApproximateNumberOfMessagesVisible をアラート条件に設定し、キューの滞留や処理遅延が発生した場合に通知します。これにより、下流ワーカーの負荷増大を早期に把握できます。 さらに、DLQ にメッセージが入った場合は、件数が1件以上であることをトリガーとして Slack に通知しています。 5. まとめ 今回の構成は、Webhook を受け取り、後続ワーカーへ処理を引き渡すことを目的としたシンプルなアーキテクチャでした。入口となる Lambda Function URL は軽量で扱いやすく、ビジネスロジックを後続ワーカー側へ集約したことで責務が明確になり、機能追加や修正にも柔軟に対応できる構造になっています。 今回の事例が、今後の設計や運用を進める際の参考になれば幸いです! BASE ではアプリケーションエンジニアが設計からインフラ・監視まで幅広く活躍することが出来ます。興味があれば採用情報もぜひご覧ください! binc.jp 明日は、BASEアドベントカレンダーは @zan_sakurai さんの記事です。お楽しみに! *1 : Cloudwatch Logsもありますが、ここでは省略して書いています。
この記事は BASEアドベントカレンダー の一日目の記事です。 こんにちは!BASE株式会社で開発担当の役員をしている、えふしんです。 僕も今、BASEグループ全体を視野に「AIを経営資源としてどうアップグレードするか」を日々考えています。 2025年の締めくくりにふさわしく、 今日は“生成AIの憂鬱”について書いてみたいと思います。 AIツールが乱立する時代に、企業は何を選び、どこから撤退するべきか 2024年から2025年にかけて、企業のIT環境は一気に騒がしくなりました。 Slack に AI がつき、Notion に AI がつき、 Google に Gemini が載り、 Microsoft は Copilot を標準にし、 そして OpenAI は ChatGPT Enterprise を大々的に展開し始めています。 どのサービスも「今度こそ、これ一つで生産性が劇的に上がります」と主張する。しかし、企業の中に身を置いていると、そんなに単純な話ではないわけです。 Enterprise SaaSは、Enterpriseプランなどの高額なプランにAI拡張機能を載せています。これは数年後のAIプロダクトの風景では多分ありえない光景で、「あたりまえのAI時代」の前段階が故に高付加価値機能として置かれていると考えます。 もちろん、これはAI発展のストーリーの1ページで、OSSだって最初からOSSが生まれてくるのではない。プロプライエタリな製品を大企業がお金を払って普及させてくるからこそ、クローンとしてOSS化して多くの人が使えるようになり世界が拡がるというのは、ここ数十年のITの歴史ではないでしょうか。Linuxを使うユーザーが、インストールされている素敵なライブラリ一つ一つに適切なアウトカムを認識して使っているなんてことはないわけです。 だからと言って、この流れを無視して、ある意味フリーライドできる時が来るまで待つのも違うと思ってしまうわけです。これぞAIバブル。 この流れをどう乗りこなしていくか。未来と現状のギャップ。発展途上の過渡期だからこそ、“生成AIの憂鬱”が広がっています。 AIの導入はワクワクより「疲労」のほうが先に来る Slack AI を買うべきか? Notion AI の方が効果が高いのか? Copilot や Gemini はどうする? ChatGPT Enterprise を入れるべきか? 気づけば、どのSaaSも「AI検索できます」「自然言語で仕事ができます」と言い始め、企業はコア実装である生成AIが類似なものであろう複数のAI機能に同時に課金するような状況に追い込まれている。 こうした“AI疲れ”は、技術的な問題というより、意思決定する側の負担が大きすぎるところに原因がある。導入判断だけでなく、撤退判断がもっと難しい。 導入時には派手に謳われた生成AI機能も、いざ組織に入ってしまうと、多少期待外れだったとしても「使い慣れたツールをやめたくない」という声が必ず出る。生成AIが汎用的が故に、特定のユースケースが恩恵を受けてしまったところから、引き剥がすのは難しい。 その結果、企業の中には“サンセットできないツール”が静かに積み上がっていく。 意思決定者は、ここまで予測し、ある意味非情とも言える決断を覚悟しないと入れられません。それ故に撤退基準の仕組み化が必要という考え方もありますね。 なぜAIツールがこんなに乱立してしまうのか 理由は単純で、各サービスが「社内で最初に質問される場所」を狙っているからだと考えます。 Slack は会話の中心にいる。 Notion はナレッジの中心にいる。 Google はドキュメントの中心、Microsoft はメールと会議の中心。 そして OpenAI は「生成AIそのものの中心」にいる。 それぞれが「うちをAIの入口にしてほしい」と主張し、結果として企業側は、複数の“AIフロントエンド”と向き合わざるを得なくなる。 いずれ企業毎の重要な情報が集まっているコアとなるAIが中心となり、周辺のフロントエンドにさまざまなツールが存在する構造になるかもしれないが、今はまだ早い。 AI導入の本質は、生産性やコスト削減だけではない よくAI導入のメリットは「業務効率化」「コスト削減」と語られる。 もちろんそれも重要だが、2025年現在の企業にとって、もっと重要な価値がある。 それは 新人のレバレッジではないでしょうか。 生成AIを使うと、まだ経験が浅いメンバーでも、会話ログからプロジェクトの背景を掴んだり、文章の下書きを素早く用意したり、技術的な不明点を自分で解消したりできる。 結果として、組織は「人を増やさずに回る領域」を増やせる。 (人員削減という言葉は使わないが、チームが肥大化しないという“健全さ”を保つためにAIは効く) この“レバレッジ”は、AI導入における一番の価値だと思っています。 ソーシャルではシニアの方が恩恵を受けて、新人が採用されなくなったり入り込む余地がなくなるんじゃないかと言われているが、僕は必ずしもそうだとは思っていません。 「生成AIの憂鬱」を超えるために、企業がすべきこと AIツールは導入しただけでは価値になりません。組織文化と技術構造が噛み合って初めて成果につながります。 導入でも撤退でも重要なのは、「どのレイヤーでどのAIが本当に必要か」 を冷静に見極めること。 Slackでのキャッチアップには Slack AI が向いている。 会社ナレッジには Notion AI が向いている。 横断的な調査や下読みには ChatGPT Enterprise という選択肢もあるが、何をどこまで統合するかは組織の成熟度次第。 AIの時代は、ツールが増えること自体よりも、“どれを選び、どれを手放すか”の判断 が企業を悩ませる。 引き続き悩み続けます。皆さんのご意見もお聞きしてみたいです。 おわりに 生成AIによって、働き方も役割もこれから大きく変わっていきます。AIのセンスと自分の専門性を掛け合わせて、新しい価値をつくれる時代です。 BASEグループでも、そんな環境に一緒に取り組んでくれる仲間を探しています。もし興味があれば、採用情報もぜひ覗いてみてください。 binc.jp