シンクロ・フード エンジニアブログ

飲食店ドットコムを運営する、株式会社シンクロ・フードの技術ブログです

数週間かかっていた gem バージョンアップの手動テストを Claude Code で自動化した話

はじめに

こんにちは。第一開発G アプリケーション基盤チームの寸田です。

アプリケーション基盤チームは複数のサービスにまたがる部分の開発を担っており、その中で私は主に Ruby / Rails のバージョンアップを担当しています。

シンクロ・フードでは全社的に業務における AI 活用を進めており、開発部でも今年から全エンジニアに Claude Code を配布しています。ただ「配って終わり」ではなく、活用ノウハウの共有や、日々の開発業務の中で AI でできそうなことは AI に任せるといった取り組みを継続して行っています。

その結果、Claude Code によって開発速度は上がった一方で、レビューやテストにかける時間が新たなボトルネックになるという変化が見えてきました。本記事では、こうした AI 活用の取り組みの一例として、特に負荷の大きかった gem バージョンアップの手動テスト を自動化した事例を紹介します。

開発フローを AI 前提で見直す

Claude Code の配布後、私たちはコードを書く工程だけでなく、レビュー・テスト・調査といった周辺工程も AI に任せられないかを順に検討してきました。その中でも費用対効果が大きいと判断したのが、長年エンジニアの手を取られてきた「手動テスト」の領域です。なかでも代表的なのが、gem バージョンアップに伴うテストでした。

なぜ gem バージョンアップのテストが辛いのか

弊社では主に Ruby / Rails を使ってサービス開発をしています。Ruby / Rails はバージョンの更新頻度が高く、それに追従するためには依存している多数の gem も合わせてバージョンを上げていく必要があります。

gem のバージョンアップは、RSpec による自動テストが通るだけでは「問題ない」と言い切れないケースがあります。代表例は次のとおりです。

  • 外部システムに依存する処理 … テストではモック・スタブしている部分が、本番の外部サービスとの組み合わせで初めて壊れることがある
  • 本番環境固有の挙動 … 本番特有の設定・データ・インフラ構成でしか再現しない問題があり、テスト環境では気づけない
  • JavaScript が絡む処理 … ブラウザ上での実際の動作は、リクエストスペックなどサーバーサイドのテストだけでは検証しづらい

例: CarrierWave のバージョンアップ

具体例として、ファイルアップロードに使っている CarrierWave gem のバージョンアップが挙げられます。

CarrierWave gem は、

  • Amazon S3 へのファイルアップロード
  • 画像の場合はリサイズ処理

などを担っています。つまり 外部システム(Amazon S3)に依存 しており、アップロードやリサイズが実際の環境で正しく動くかは、自動テストだけでは担保しづらく、結局は人手による動作確認に頼ることになります。

また、弊社特有の課題として、影響範囲が複数のサービスにまたがりやすい、という点もあります。弊社では共通機能を Rails エンジンとして社内 gem に切り出し、それを複数のアプリケーションから利用しています。そのため、ある gem のバージョンアップによる影響が複数のサービスに及びやすく、それぞれを担当するサービスチームをまたいでしまうことも珍しくありません。影響箇所をひとつひとつ手動でテストしていく必要があり、各サービスチームにテストを依頼してから完了するまでに数週間かかる こともあります。これが gem バージョンアップの最大の辛みでした。

手動テストの自動化

そこで、この手動テストを AI(Claude Code)で自動化することにしました。手動テストの実行基盤としては Playwright を採用し、Playwright のテストを Claude Code に書かせることにしました。バージョンアップする gem の利用箇所をソースコードから洗い出し、その箇所を確認する Playwright のテストを生成してもらう、という流れです。

ただ、単純に「ソースコードをもとにテストを書いて」と指示しただけでは、意味のないテストが生成されてしまうことが多くありました。たとえば、画面を開くだけで肝心の処理を通っていなかったり、確認したい機能とは関係のない操作をテストにしてしまったりして、「一見テストはあるが、肝心の箇所をまったく検証できていない」という状態になりがちだったのです。

そこで重要になるのが、テスト対象のコードがちゃんとテストされているかを AI 自身に確認させることです。具体的には、テスト実行時のカバレッジを計測し、テスト対象の箇所がカバーされているかを判断させています。なお、カバレッジの計測には coverband gem を利用しています。

まとめると、以下のような手順でテストを追加しています。

  1. テスト対象(バージョンアップする gem の利用箇所)をソースコードから洗い出す
  2. テスト対象の Playwright のテストをコードから生成し、実行する
  3. カバレッジの計測結果を見て、ちゃんとテストでカバーされているかを確認する
  4. カバーされていない箇所のテストを修正する

どうしてもソースコードだけではテストを生成できない場合があるため、ステップ3〜4(カバレッジの確認と不足箇所の修正)は最大 3 回まで繰り返すようにしています。

スキルとして組み立てる

この一連の流れは、Claude Code のスキルとして実装しています。スキルは以下の3つから構成されています。

  • 手動テスト一覧を作るスキル … ソースコードを調査し、確認すべき項目をテンプレート形式で洗い出す
  • 1つの対象の Playwright のテストを生成するスキル … 指定された箇所をカバーするテストを生成し、実行 → カバレッジ検証 → 不足なら修正、というループを回す
  • オーケストレーターとなるスキル … 上記2つを束ね、対象をグループに分けて、テスト生成スキルを 複数エージェントで並列に 走らせる

処理の流れをフローチャートで示すと以下のようになります。

処理の流れのフローチャート

結果と課題

実際に CarrierWave gem のバージョンアップで試したところ、これまで手動でテストしていた多くの利用箇所を、自動化したテストでカバーできることが確認できました。一方で、現時点では AI が機能への到達方法を検出できず、カバーしきれないことがあります。この場合はソースコード以外のコンテキストを渡す必要があるようですが、こうした改善は今後の課題となります。

おわりに

今回は開発部における AI 活用の一例を紹介させていただきました。弊社ではこの他にも AI による生産性向上に取り組んでいます。 今回の記事が、AIでの開発生産性向上に取り組んでいる方々の参考になれば幸いです。

RubyKaigi2026に参加した弊社メンバーで感想を話し合いました

自己紹介

大津: 新卒4年目の大津です。Ruby歴は入社前のインターンから触り始めたので今年で5年目になります。RubyKaigiの参加は今回が初めてです。

岡塚: 新卒3年目の岡塚です。自分は入社してから触り始めたので今年でRuby歴3年目になります。自分もRubyKaigiの参加は今回が初めてです。

湯の川温泉近くの函館アリーナで行われました

印象に残ったセッション

From Formal Specification to Property Based Test

大津: まず自分が印象に残ったのは、「From Formal Specification to Property Based Test」という形式仕様をもとに、Property Based Testを半自動生成させる発表です。

岡塚: 形式仕様という概念が自分は初めて聞くものだったんですが、どういうものなんですか?

大津: 自然言語ではなく、形式仕様記述言語という論理的な記述が行える言語で記述した仕様のことです

// 【ドメインモデル】 本と利用者という概念を定義
sig Book {}
sig User {
    borrowed: set Book
}

// 【業務ルール(不変条件)】 1人が同時に借りられるのは3冊まで
fact BorrowLimit {
    all u: User | #u.borrowed <= 3
}

// 【検証したい性質】 2人のユーザーが同じ本を持っていることは絶対にない
assert NoDoubleBorrow {
    no disj u1, u2: User | some u1.borrowed & u2.borrowed
}

// 本5冊・ユーザー5人までのあらゆる組み合わせを試して、
// 「2人のユーザーが同じ本を同時に持っている」状況が作れないことを確認
check NoDoubleBorrow for 5

岡塚: どのような場面で役に立つのでしょうか?

大津: 多くの現場でそうかと思いますが、自然言語でAIに指示をして、機能とテストを実装してもらうという流れが多いかと思います。
ただ、それだと自然言語自体の曖昧さが機能に悪影響を及ぼしてしまったり、AIが生成したテストが本当にちゃんとテストできているかという信頼性が担保できなくなってしまうという背景があります。
そこを形式仕様で指示することによって、自然言語のような曖昧さなくAIに指示ができる。そしてその形式仕様から機械的にテストを生成することで、信頼性の高いテストが作れるから嬉しいよね、という発表でした。

上記について大津による略図

岡塚: 実現できたら確かにテストとしてすごく理想的で革新的だなという感想を持ちました。
大津さんは形式仕様というものについて既にご存知だったとお聞きしたんですけれど、どのような場面で使いましたか?

大津: 複雑な仕様の改修をする機会があり、変更内容が他の箇所に影響しないか事前に確認したい場面がありました。

なので形式仕様を使えば、そのようなところで出るバグが防げるんじゃないかと思って、試験的に導入してみたことがありました。現在も形式仕様で検証を行い、改善ポイントについて事業部と議論している段階です。とても便利ですよ。

岡塚: あ、もうそこまで使われているんですね。確かにそうですね。すごい複雑なドメインみたいなのって、サービスでもありがちというか、自分が普段触っているサービスもそういうところで使えるところを今聞いていてあったりするかな、と思ったりしていたので、ちょっと触ってみたいなって思いましたね。

大津: Alloyは導入も便利なんですよ。VS Codeの拡張機能があって、VS Codeでコードを書いて、その拡張機能を入れればビジュアライザーが出てくるので、別途インストールとかもいらず、すごい楽です。

先程のコード例を実行して実際に反例が出てくる様子

岡塚: あ、そうなんですね。いいですね。ちょっと後で触ってみようと思います。

大津: AIを使ったりもしたんですけど、自分が試した範囲ではAIもAlloyのコードを十分書いてくれますね。参入障壁は比較的低いと思います。ぜひ使ってみてください。

岡塚: 分かりました。ありがとうございます。

The Journey of Box Building

大津: もう1個印象に残ったのが「The Journey of Box Building」ですね。

岡塚: どのような発表でしたか?

大津: Ruby 4.0から実験的機能として導入されているRuby Boxについてのお話でした。Ruby Boxはどういうものかというと、複数のバージョンのgemや機能を別々のRuby Boxという箱のようなものに入れておき、呼び出し側で特定のRuby Boxを選んで実行するというものでした。

岡塚: どのような場面で役に立つのでしょうか?

大津: 例えばgemのバージョンアップでは、仕様変更による予期せぬエラーが懸念されると思います。それをRuby Boxという機能で防ぐという使い方が紹介されていました。

例えば、Ruby Box 1番にアップデートしたバージョンのgemと機能を入れておいて、Ruby Box 2番に元のバージョンのgemと機能を入れておくというふうにすると、Ruby Box 1番のアップデートした側で実行して失敗したときに、Ruby Box 2番の処理にフォールバックさせるといった切り替えをアプリケーション側で組むことで、安全に本番デプロイができるというような使い方が紹介されていました。

上記についての大津による略図

岡塚: Ruby Boxは以前から気になっていて、面白そうだなって思っていました。ライブラリの開発のための機能がメインなのかなとか思っていたんですけれど、Railsでも使える場面がありそうですね。
他の使い方はありますか?

大津: もう一つ面白いと思ったのが、さっき言ったRuby Box 1番、Ruby Box 2番を、本当にデプロイいきなりするのがちょっと怖いなとなったときに、Ruby Box 1番とRuby Box 2番が同じ値を返してくるかというテストを、1つの環境上でできるというところですね。ここはかなり面白いなと思いました。

上記についての大津による略図

岡塚: 社内で使えそうなところもありそうですね。

大津: そうですよね。gemのバージョンアップだけでなくRailsのバージョンアップなどでも使えそうだなと思いました。

岡塚: でもちょっとこれをやるためには、Ruby 4系まで上げる必要があるので、ちょっと先ですけれども。

大津: はい、そうですね笑

The AST Galaxy to the Virtual Machine Blues

岡塚: では、続けて私が興味を持った発表について話します。「The AST Galaxy to the Virtual Machine Blues」(https://atdot.net/~ko1/activities/2026_rubykaigi2026.pdf)についてなのですが、YARVを作った本人の笹田さんが、AST(Rubyのコードを構文解析した結果であるデータ構造)の直接実行の話をされるというところが面白そうだなと思い聞きに行きました。

大津: 発表スライドを拝見したんですが、今言語処理系が高速化とともに複雑化しているという問題があって、それをシンプルなまま高速化するというのは、メンテナンスなどの面で嬉しそうかなと思いましたね。岡塚さんはもともとVMに興味があって、この発表を聞いたんですか?

岡塚: そうですね。Rubyの言語処理系については興味があったもののVMがどう動いているかなどをあまり知らず、その辺りを少し知ることができればいいのかなというのがRubyKaigiに行ったモチベーションの一つではあったという気持ちでした。そしたら、YARV自体を作った人が、それについて再考するというような趣旨の発表をするというところを知ってとてもびっくりして、何の話をするのだろうと思い見に行きました。

大津: そうなんですね。岡塚さんはVMの方に興味があるとのことでしたが、今回の発表ではRuby1.9でVMが入る前のASTの直接実行に完全に戻る...という訳ではないものの、現代的な技術を使ってRuby1.9以前に似て非なることをするという感じなのですかね。

岡塚: おそらくそうなんじゃないかと思っています。

大津: なかなか難しそうな発表ですよね。

岡塚: 難易度が高い内容で、すべてを理解するには更なる学習が必要だと感じました。

大津: そうですよね。構文木からVMのバイトコードへの変換を飛ばして機械語に、というのは仕事では全然触れない範囲だと思うので、こういう発表で知れるのがいいですよね。

岡塚: そうですね。なんとかして、この辺のAST周りの話を追っていきたいなとは思うんですけれど。

大津: ぜひ来年もRubyKaigiに参加して、追っていきたいですね。

岡塚: この発表についてではないのですが、全体的に、数年前のRubyKaigiでの発表の続編のような印象のものが多かったですね。

大津: そうですね。今後もRubyKaigiに参加したりして、話にキャッチアップしていけるようになりたいですね。

岡塚: ですね。じゃあ、次行きますね。

Surviving Black Friday: 329 billion requests with Falcon!

岡塚: 「Surviving Black Friday: 329 billion requests with Falcon!」というセッションについてです。Black Fridayというのは、アメリカの感謝祭の翌日にあたり、オンラインショッピングのリクエスト数などが急増するシーズンらしいです。
そこに対応するにあたって、もともとUnicornを使っていたサーバーを独自の別サーバーであるFalconに置き換えるということをやっていたようで、独自開発したものに置き換えてしまうというところに技術力の高さを感じました。

大津: なるほど。

岡塚: 置き換えたところのサーバーであるFalcon自体についても興味深いと思っていたのですが、それ以上にためになったのが試行錯誤の過程の話です。サーバーを作って一気にデプロイしたかというとそうではなかったようで、致命的なものも含めて幾つかの不具合が潜むリスクがあったようでした。
そこをカナリアリリースやスケールテストを組み合わせてどうやって段階的に移行していくのかというような試行錯誤の部分の話があって、そこが興味深いと感じました。

大津: スライドを見ていてすごいハラハラする発表でした。だんだんBlack Fridayが近づいていく中でいろんなエラーが出て、解消しているという、間に合うのかというハラハラ感がすごいある発表かなと思ったんですが、会場の雰囲気はどうでしたか?

岡塚: 盛り上がっていたような気がします。発表の最後の方で、Black Fridayに実際に問題なくアクセスがさばけてましたという話があったのですが、その部分では会場からおおっ、と歓声が上がったりしていた気がします。

大津: これは達成感ありそうですよね。こんなすごい苦労して、結構改善していますからね。

岡塚: そうですね。席が全部埋まっていて立ち見になっているくらいの状態で、雰囲気盛り上がっていた感じでした。

大津: 人気の発表だったんですね。

岡塚: そうでした。

大津: 自分と岡塚さんのチームだとなかなかこういう大きいリリースあんまりないかなと思うんですが、活かせそうなところありましたか?

岡塚: 活かせそうなところがあるかと言われると、パッとは出てこないかもしれないです。でもそうですね、UnicornのサーバーをPumaに移行するタイミングなどで、事故の確率を最小にするためにどのように移行するか、という観点で活かせるかなと思っています。

大津: そうですね。移行してすごく遅くなってしまったなどになったら困りますからね...ちゃんとこの発表のように、計画を立てて監視して、対策を打って、というサイクルをちゃんと繰り返したいですね。

Ruby Releases Ruby

岡塚: 最後に紹介するのは「Ruby Releases Ruby」というセッションです。Rubyが普段どのように開発されているのかというのは本当に意識したことがなく、Rubyに詳しい方々がさまざまな取り組みをされているのだろう、という程度の認識でした。CIのサイクルの回し方、複数プラットフォームの動作担保、リリース速度向上の取り組みなど、開発の裏側を知れる内容で、ワクワクしました。

大津: このKeynoteは自分も現場で見ましたが、面白かったです。いろんなコミッターの関わり、関係性が見えて面白かったですね。

岡塚: そうですね。

大津: これについてうちの会社の場合にどのように活かせるかについて考えていたのですが、社内ツールなどで自動化され切れていない部分で活かせる気がしています。対応時に緊張するし、間違えたらどうしようみたいな。そういうのをCIとかでリリース、自動化できるとすごい嬉しそうですよね。

岡塚: 確かにそれはありそうですね。

大津: 今だと開発内容がリリースされた後に、営業部の方にリリースしましたっていうふうにSlackなどでお伝えいただいているんですけど、この辺も自動化できるかもですね。もう時代は自動化の時代ですからね。

岡塚: できたら業務チームの負担も減るのでいいですよね。以前だと自分の場合、ちょっと自動化したいと思いつつやる作業が面倒に感じていたのですが、今だとAIを使ってすぐにプログラムを書けるので、自動化に取り組むモチベーションは相対的に上がっています。

大津: そうですよね。なんなら、開発以外のメンバーでも、AIを使って自動化していますもんね。

岡塚: そうですね。そんな話もありましたね。

大津: 負けてられないですね。

岡塚: 頑張らないと。

大津: そんな感じですかね。

岡塚: はい!ありがとうございました。

RubyKaigi全体についての感想

大津: 最初は英語が理解できるのか、技術的な内容についていけるのか不安でした。しかし、Google翻訳の会話機能でのリアルタイム翻訳を使ったり、分からない単語が出てきたらすぐにAIに聞いたりすることで、問題なく楽しむことができました。セッション以外のイベントでも、新機能の使い道について話し合ったり、登壇者の方に詳しい内部実装をお聞きできたりと、実際に会場に行かないと受けられない刺激を受けることができました。

岡塚: 最初は『セッションはネットでも見られるし、現地で見ても楽しい程度かな』という気持ちでした。しかし、実際に行ってみると熱量や雰囲気が直に伝わってきて、かなり刺激になりました。

おわりに

一緒に参加した弊社メンバーと感想を話し合うことで、お互いに印象に残ったセッションや着眼点が違っていたことに気づき、一人で参加するよりも何倍も学びが深まったように感じます。
RubyKaigiのオーガナイザーの方々や、発表者の皆さんへの感謝でこの記事を締めたいと思います。
来年のRubyKaigi2027は宮崎県での開催だそうですね。非常に楽しみです!

複数DB間環境のRailsアプリのCIを自社版database_rewinderで(ちょっとだけ)速くする

はじめに

こんにちは、第二開発Gの岡塚(https://x.com/kyuuri1791)です。 前回エンジニアブログの記事を書いた時は第三開発Gに所属だったのですが、その後異動になって1年くらい経っています。時間が経つのが早い......
今のチームではモビマル店舗物件探しの開発をしていることが多いです。
今回は、普段自分が業務でやっている開発とは全く関係ないのですが、社内で使っているデータベースクリーナーを自作版に置き換えてちょっとだけCIが速くなったのでその話を書きます。

背景

本題に入る前に、社内の事情関係で若干ハイコンテキストな部分があるのでそこについての説明を書きます。
弊社のDB構成としては、1個の物理的なDBの中に複数の論理的なDBが配置されている構成になっています。レプリケーションしてR/W splittingするなどではなく、複数サービスがあってそれぞれでのDBが置かれているマルチテナンシー(?)構成です。
直近に立ち上げられたアプリケーションについては1サービス1DBに閉じる世界観で、もし別DBを触りたい場合はAPIなりなんなりを用意してデータを取りに行くような構成になっているのですが、昔から開発されているサービスに関しては歴史的な経緯から複数DB間でアソシエーションが貼られており、joinが発生するような状態になっています。

本題

ここからが本題です。
複数DB間でjoinする状態があるといろいろと困ることが出てくるのですが、その中の一つとしてCIの実行時間の問題があります。
RSpecでは特に理由がなければuse_transactional_fixtures=trueにしてトランザクショナルにテストを実行すると思います。(たぶん...)
が、複数DBに対してはdatabase.ymlでそれぞれでコネクションを作ってみにいくことになるので、use_transactional_fixtures=trueにして、

  1. コネクションAでdb1に対してなんらかのレコードを作成
  2. コネクションBでdb2に対してなんらかのレコードを作成
  3. 1と2で作ったレコードをjoinするコードをRSpecで実行する

とした場合コミット前なのでコネクション間でデータを互いに参照できず、3を行うようなRSpecがコケまくるという状態になってしまいます。そのため、

  • use_transactional_fixtures=falseにする(これによりRSpecがちょっと遅くなる)
  • database_rewinderの複数DB機能を使ってデータをクリーンアップする

という形に既存でなっていました。

考えたこと

これでCIが遅くなっているのはちょっとイヤだったので、なんとかして速くしたいなと思いました。やり方としては、いくつかありそうです。

  • Railsにモンキーパッチを当てて、CIでは無理やりコネクションを一つにする
  • CIではトランザクション分離レベルをread uncommittedに落とす
  • なんとかしてデータのクリーンアップ部分を高速化する

箇条書き1個目はやってみて挫折しました。箇条書き2個目は正直一番手っ取り早いですが、ちょっと怖いです。
箇条書き3個目はどうだろう...?と考えてみます。RSpecの実行ログを眺めていたところ、以下のようなログが流れていました。

delete from A.t1; delete from A.t2;
delete from B.t3;

database_rewinderのREADMEを見ると、

Also, database_rewinder joins all DELETE SQL statements and casts it in one DB server call.

と書いてあったので、ぱっと見だとdeleteは1リクエストでいいように思います。内部実装を読んでみると、1DBに対してcleaner的なクラスのオブジェクトが1個ずつ作られて、それぞれの単位でdeleteをまとめて発行しているみたいでした。multiple databasesといった場合それぞれでjoinが発生しているなどということは普通想定しないような気がするので、しかしそれはそうという気がします。それはそれとして、

delete from A.t1; delete from A.t2; delete from B.t3;

みたいにdelete文を1リクエストにまとめるようにすれば若干速くなりそうです。

実装

以下のようなスクリプトを書いてみました。

# frozen_string_literal: true

class MysqlSynchroDbRewinder
  class << self
    def init(conf = { host: '127.0.0.1',
                      username: 'dummy_user',
                      password: 'dummy_password' },
             logger = Rails.logger)
      @affected_tables = Set.new
      @logger = logger
      @cleaner_client = Mysql2::Client.new(
        **conf,
        flags: Mysql2::Client::MULTI_STATEMENTS,
        reconnect: true,
        init_command: 'SET FOREIGN_KEY_CHECKS = 0;SET UNIQUE_CHECKS = 0;',
      )

      # mysql2 経由で実行されたクエリは
      # - Mysql2::Client#query を通過する
      # - Mysql2::Client#prepare -> Mysql2::Statement#execute を通過する(prepared statementとして実行される)
      # の2つがありうるため両方をフックする
      # @note
      #  このコードを書いた現時点でのRails最新である8.1ではMySQL adapter限定でprepared statementがデフォルトで無効になっている
      #  なのでprepared statement用のprependをしなくても当分は困ることはない気もする(明示的にdatabase.ymlでprepared_statements: trueにしない限り)が対応しておく
      #  元々は7.2でデフォルトを有効にする予定だったが、既存のバグ周りで一悶着あってリバートされているという経緯らしい(https://github.com/rails/rails/pull/48370)
      raise '.init has already been called. Do not call init twice.' if Mysql2::Client <= Mysql2ClientSynchroDbExt

      Mysql2::Client.prepend(Mysql2ClientSynchroDbExt)
      Mysql2::Statement.prepend(Mysql2StatementSynchroDbExt)
    end

    # @param [Mysql2::Client] client
    # @param [String] sql
    def track_insert(client, sql)
      match = sql.match(/\A\s*INSERT(?:\s+IGNORE)?(?:\s+INTO)?\s+(?:\.*[`"]?([^.\s`"(]+)[`"]?)*/i)
      return unless match

      @affected_tables << "#{client.query_options[:database]}.#{match[1]}"
    end

    def clean!
      return if @affected_tables.empty?

      # 念の為100件ずつに分割して処理(max_allowed_packet対策)
      if @affected_tables.size > 100
        @logger.warn 'affected tables to clean has exceeded 100. you might be excessively inserting data in tests.'
      end
      @affected_tables.each_slice(100) do |tables_chunk|
        statements = tables_chunk.map { |t| "DELETE FROM #{t}" }.join(';')
        # トランザクションで囲むことでディスクへのfsync頻度を最小化し、IO待ち時間を減らす
        full_query = "START TRANSACTION;#{statements};COMMIT;"
        @cleaner_client.query(full_query)
        while @cleaner_client.next_result; end
        @logger.info(full_query)
      end

      @affected_tables.clear
    end
  end
end

module Mysql2ClientSynchroDbExt
  def query(sql, options = {})
    result = super(sql, options)
    MysqlSynchroDbRewinder.track_insert(self, sql) if affected_rows > 0 && sql.match?(/\A\s*INSERT/i)
    result
  end

  def prepare(sql)
    statement = super(sql)
    statement.instance_variable_set(:@_rewinder_sql, sql)
    statement.instance_variable_set(:@_rewinder_client, self)
    statement
  end
end

module Mysql2StatementSynchroDbExt
  def execute(*args)
    result = super(*args)
    sql = instance_variable_get(:@_rewinder_sql)
    client = instance_variable_get(:@_rewinder_client)
    if sql && client && (affected_rows > 0 && sql.match?(/\A\s*INSERT/i))
      MysqlSynchroDbRewinder.track_insert(client, sql)
    end
    result
  end
end

コードについてちょっと補足すると、Mysql2レベルでパッチを当てて、複数DBに対して発行されたinsertクエリを全部収集しています。使う側はdatabase_rewinderと同じノリで、RSpecのrails_helper.rbなどで以下のように指定します。

  config.before(:suite) do
    MysqlSynchroDbRewinder.init
  end

  config.after do
    MysqlSynchroDbRewinder.clean!
  end

これを複数DB間joinが起きているアプリケーションに対して入れたところ、最大で10%くらいCIが速くなりました。複数DB間joinが頻繁に起きているアプリケーションほど効果があり、あまり起きていないアプリケーションではそこまで効果がない。逆にCIが遅くなってしまっているということはなさそう、という感じです。

終わり

無事動いてくれるかなと思いながらマージしたのですが、導入してみて1ヶ月ちょい経っていて、現状特に問題なくCIで安定して動いてくれていそうだったので一旦よかったです。
こちらの記事が、どなたかの参考になれば幸いです。
ここまでお読みいただき、ありがとうございました。

AWS WAF レートリミット導入と誤検知対策:正規トラフィックを保護するアーキテクチャを設計した話

はじめに

こんにちは。開発部 SRE チームの柴山です。
技術構成の記事でもご紹介した通り、弊社ではWebアプリケーションの脆弱性保護を目的として Scutum を中心としたセキュリティ対策を実施してきました。
しかし、昨今のAI技術の急速な発展に伴い、悪意のある攻撃だけでなく、データ取得(AI学習等)を目的とした突発的なスクレイピングといった「予測不可能なトラフィックスパイク」に直面することが増えてきました。
これらはバックエンドのリソース逼迫を招き、正規のユーザー様にも影響を与えかねません。
こういった機械的なアクセスには、アクセス数を制限するレートリミットを設定するのが定石として挙げられますが、導入には様々な検討事項があり、単純に導入するだけでは誤検知によって正規のトラフィックさえも巻き添えにしてしまうリスクがあります。
インフラを保護できても、正規ユーザーまでブロックして機会損失を生んでしまっては本末転倒です。

弊社では「正規トラフィックを保護するためのアーキテクチャ設計」として、最終的に AWS WAF を採用しました。
本記事では、その採用に至った経緯や設計思想についてご紹介いたします。

技術選定

突発的なトラフィックスパイクを制御するにあたり、まずは「どこで制限をかけるか」という実装ポイントの比較検討を行いました。
弊社ではWebサーバー(Nginx) + AWS ALBという構成で、サービスを提供しています。導入のしやすさから、以下二つのアプローチを比較検討しました。

  1. マネージドサービスでの制御(AWS WAF の レートベース ルールステートメント)
  2. サーバーサイドでの制御(Nginx の limit_req + geo モジュール)

それぞれの特性と優位性について、弊社での運用環境をもとに評価した結果がこちらです。

評価軸 AWS WAF Nginx
初期リソースコスト △ 中 ◎ 低
運用保守コスト ◎ ほぼゼロ(フルマネージド) △ 運用保守のフローを要検討
GeoIP/Bot DB更新 ◯ 自動 △ 手動(要自前構築・保守)
導入時の安全性 (DryRun) ◎ 可能 (Countモード) △ 工夫が必要(OSS版)
DDoS対応 ◯ 優位(専用ルール有/一時的な適用が可能) △ 困難(分散型は対応不可)
ログの可視性 ◯ 優位(専用ダッシュボード/Log ツール連携) △ 自前構築(ログ転送・別途可視化のコストが発生)

表からもわかる通り、初期のリソースコスト面では Nginx に分がありますが、保守・運用の観点で AWS WAF が弊社の要件にマッチしていました。
その中でも以下の点は非常にメリットとして大きく、採用の決め手となりました。

  • フルマネージドの恩恵
    GeoIP データベースやBotリストを都度更新し続けるのは長期的に人的な運用コストがかかることになります。
    ここをAWS側にほぼ丸投げできるのは大きなメリットの一つです。
  • Countモードによる段階的導入
    表題通り、今回一番懸念していた点が正規ユーザーの誤ブロックです。
    AWS WAF にはブロックせずにルール評価だけを行う Countモード が存在するため、事前にルールの妥当性を精度高く検証することができます。

設計の壁

いざ、AWS WAF でレートリミットを導入しようと思いましたが、早速いくつかの壁にぶつかりました。
「どうやって閾値を決定しよう」「正規ユーザーの誤ブロックを防ぐには?」「これって一律に制限かけていいの?」
たとえば、レートリミットの閾値を「1IPアドレスあたり、5分間に100リクエストまで」と決め打ちしたとします。
それだけで単純に適用をすると、以下のような絶対に弾いてはいけない正規のトラフィックまで遮断してしまいます。

  • 優良Bot(クローラー): Googlebot 等の検索エンジンや外部連携先の巡回Bot
  • 正規ユーザーアクセス: 本命の絶対にブロックしてはいけないトラフィック
  • アプリ・サービス連携: 外部連携サービスからのWebhookや、自社アプリなどからの通信
  • 監視系: 外形監視など

これらのトラフィックを踏まえた上で、悪意のあるトラフィックだけをそぎ落とし、可能な限り正規のトラフィックを通す必要がありました。

アーキテクチャ

今回の肝である誤検知の対策として、フラットにレートリミットをかけるのではなく WAF の評価順位の仕様を利用した多段フィルターを設計しました。
AWS WAF は設定された優先順位(Priority)の小さい順にルールが評価されていきます。
全ての通信に対してフラットにレートリミットをかけるのではなく、「既知の脅威をブロック」->「正規トラフィックの救済」->「その他トラフィックへのレートリミット」という段階的にふるいへかけるアーキテクチャを構築しました。

  1. 前段フィルター: 既知の脅威をブロック
    最前段では明らかに不正なトラフィックをAWSが提供するマネージドルールなどを用いて遮断します。

  2. 中段バイパス: 正規トラフィックの保護
    ここが正規トラフィック保護の要になります。
    前段を通過したトラフィックに対して、前述した項目(優良Bot、アプリ・サービス連携、監視系等)を独自の複合的な条件で識別を行います。
    条件に合致した場合は Allow(明示的な許可)とし、この段階で以降のルール評価をスキップさせてバックエンドへのトラフィックを許可します。
    ここに引き算のルールを挟むことにより、死守すべき正規トラフィックが後段のレートリミットにかかることを防ぎます。

  3. 後段フィルター: レートリミットの適用
    最終的に残ったトラフィックの大量アクセスにのみレートリミットを適用します。
    ルールステートメントにより、特定の属性を持つ通信が閾値を超えた場合にのみ遮断をすることができます。

誤検知を防ぐための Tips

静的ファイルの考慮
Webページへの初回アクセス時、ブラウザはページにリンクされた大量の静的ファイル(画像、CSS、JS等)を同時にリクエストします。
これをレートリミットのカウントに含めると、正常なユーザーでも一瞬で閾値に達してしまうリスクがあります。
これらを考慮した閾値を設定するか、静的ファイルはCDN等でキャッシュさせて、静的ファイルをカウント対象外にすることが重要です。

閾値の決定について
レートリミットの閾値を「だいたい1分間にn回くらいだろう」と勘で決めるのは非常に危険です。
過去のアクセスログを分析し、以下のようなステップで閾値を決定しました。一例として共有させていただきます。

  1. 外れ値を除外してベースとなる値の算出
    まずサービスのアクセスログから各時間(分)あたりで上限の基準値として95パーセンタイルを抽出しました。
    分析対象のログにはすでに悪意のあるアクセスやスクレイピングなど異常なスパイク(外れ値)が含まれていました。
    そのまま最大値を基準にしてしまうと、閾値がかなり甘めに出てしまうことになります。
    外れ値除外の例
    上位5%のノイズを取り除くことによって、Botなどによる外れ値を極力排除した値を抽出することができました。

  2. 安全係数の適用と基準値の厳格化
    抽出したベース値に対し、トラフィックの属性(エンドポイント等)ごとに数値を比較し、より厳格な(小さい)値を採用しました。
    その上で、正常な突発的アクセスを許容して安全側に寄せるため、安全係数(1.x)を掛け合わせて最終的な閾値を決定しています。
    これにより、正規トラフィックの多少の突発的なアクセスは許容しつつ、Botによる攻撃だけを制限するような閾値を導き出すことができました。

導入

アーキテクチャと閾値の設計が完了し、いよいよ導入フェーズですが、いきなり本番環境で遮断を開始するのは危険です。
いくら念入りに計算をしていても、あくまで絵に描いた餅で正規トラフィックの誤ブロックリスクは依然として存在します。
そこで、技術選定の項で紹介したCountモードを活用し、以下のような4段階のフェーズに分けて慎重にリリースを行いました。

フェーズ 環境 モード 実施内容のハイライト
フェーズ1 テスト環境 Count -> Block IaC(Terraform)でのインフラ構築と、テストシナリオに基づく動作(遮断)検証。
フェーズ2 ステージング環境 Block 本番同等環境でのリハーサル。
正規系動作の確認と、意図した通信のみが遮断されるかの最終確認。
フェーズ3 本番環境 Count 【最重要】 本番環境へCountモードで適用。
およそ二週間かけて全曜日・時間帯の実際のトラフィックログを取得・精査し、机上の計算とズレがないかを確認
フェーズ4 本番環境 Block ログ精査で正規トラフィックの巻き込みがないことを確信した後、本番運用(Block)へ切り替え。

このリリース計画の中で最も重要なのが フェーズ3(本番環境でのCount運用) になります。
本番のトラフィックがWAFに流れ、CloudWatch Logs に出力された検出結果を2週間かけて分析しました。
ここでは「正規ユーザーを誤って検出してしまっていないか」「正常にレートリミットが効いているか」「連携や監視に考慮漏れの項目が無いか」の答え合わせを実施しました。
結果として、設計時には検討できていなかった「優良Bot」や「連携系のアクセス」、「状況に応じて遮断されてしまう正規ユーザー」の存在に気づくことができました。
もし最初からBlockモードで導入していた場合、これらの通信も遮断してしまい、サービス影響に繋がっていた可能性があります。

誤検知の可能性を事前に全て摘み取ることで、満を持して最終フェーズのBlockモードへの切り替えを実施しました。
その成果が、導入後に発生した突発的な大量アクセス時のメトリクスに表れました。

実際の検知事例

上記グラフの通り、トータルアクセス(赤線)が急増した際、レートリミットが即座に反応し、悪質なトラフィックのみを選択的にブロック(紫線)してくれています。
一方で、正規ユーザーの通信(緑線)はスパイクの影響を一切受けることなく一定に保たれていることが見て取れます。
無事、正常なトラフィックを保護したまま、悪質なトラフィックのみを遮断するレートリミット環境を本番稼働させることに成功しました。

まとめ

今回は、AWS WAFを用いたレートリミットの導入と、正規トラフィックを保護するためのアーキテクチャ設計についてご紹介しました。
単に「アクセスを一定数でブロックする」という安易な設定をせず、以下のステップを踏むことで、「正規トラフィックを保護するアーキテクチャの設計」を実現できました。

  1. アーキテクチャの工夫: WAFのPriority仕様を活用した「段階的なフィルター」と、静的ファイルの考慮。
  2. 統計に基づいた閾値設計: 勘に頼らず、過去のアクセスログのp95値から算出した「ノイズを極力排除した閾値」の決定。
  3. 安全なリリース戦略: 本番環境でのCountモード運用と、ログ精査による誤検知の事前排除。

Scutumに加えてAWS WAFを導入したことで、より堅牢な構成を実現できました。
細かなチューニング内容や今後の展望があれば、またこの場でご紹介させていただけたらと思います。
この記事が、もし今後レートリミットを導入しようと検討されている方の参考になれば幸いです。

AppsFlyerのUDLを使ったディファードディープリンク実装で学んだこと

シンクロ・フード開発部モバイルアプリチームの横山です。
普段は、飲食店と飲食店で働きたい求職者を繋ぐ「求人飲食店ドットコム」アプリのAndroid開発を担当しています。

今回は、アプリにディファードディープリンクを実装した経緯と、調査・設計・実装を通じて得た知見をまとめます。同じような機能を検討している方の参考になれば幸いです。

実装の背景

弊社アプリではすでにUniversal Links(iOS)とApp Links(Android)によるディープリンクを実装していましたが、これらはアプリがインストール済みのユーザーにしか機能しないという制約があります。リンクを踏んだ時点でアプリが入っていなければ、Webページへのフォールバックで終わってしまいます。

今回、求人飲食店ドットコムの求人詳細画面に「アプリで開く」ボタンを追加することになりました。このボタンはアプリをインストールしていないユーザーにも利用してもらう想定です。 つまり、「未インストールのユーザーをApp Store / Google Playへ誘導し、インストール後に目的の画面へ遷移させる」 という仕組みが必要でした。この課題を解決するのがディファードディープリンクです。

ディファードディープリンク

ディープリンクの種類と選定理由

ひとくちに「ディープリンク」といっても、実装方式はいくつかに分類されます。

  • Custom URL Schememyapp://path のようなカスタムスキームでアプリを起動する方式です。シンプルな反面、アプリが未インストールの場合はエラーになるためユーザー体験上の課題があります。また、同じスキームを別のアプリが乗っ取れてしまう「URLスキームハイジャック」のような脆弱性もあり、現在は積極的に使われるケースは少なくなっています。

  • Universal Links(iOS)/ App Links(Android):通常の https:// URLでアプリを起動できるOSレベルの仕組みです。アプリ未インストール時はそのままWebページとして開くため自然なフォールバックが実現でき、現在のスタンダードといえます。弊社アプリもこの方式をすでに導入していました。

  • サードパーティ製SDK(AppsFlyer / Adjust等):SDKが提供するリンク機能を利用する方式です。ディープリンクとしての機能に加えて、インストール前後の計測やアトリビューションにも対応しており、マーケティング施策と連携しやすいのが特徴です。今回はもともとAppsFlyerのOneLinkを利用していたため、AppsFlyerが提供するUDL(Unified Deep Linking)を採用する方針となりました。

UDL(Unified Deep Linking)とは

UDLはAppsFlyerが提供するディープリンク取得APIで、従来は別々のコールバックで制御していた「Direct Deep Linking」と「Deferred Deep Linking」のAPIを、1つのインターフェースに統合した仕組みのようです。

  • Direct Deep Linkingは、アプリがすでにインストールされている状態でリンクから起動するケースです。
  • Deferred Deep Linkingは、未インストールの状態でリンクを踏んだユーザーがストアからインストールし、初回起動した際に遷移先を復元するケースです。コールバックはアプリ起動後に非同期で返ってくるため、「いつデータが返ってくるか」というタイミングの揺らぎへの対応が、実装上の重要な課題となります。

UDLの調査

UDLはSDKの初期化とともにコールバックを購読し、非同期でパラメータを取得する仕組みです。そのため、「いつデータが返ってくるか」というタイミングの揺らぎを考慮し、実装前に以下のユースケースに沿って挙動を検証しました。

  • パラメータの伝搬: リンクに付与した情報がストア経由でも欠落せずに受け取れるか、また Direct / Deferred で受け取れる値に差異がないか
  • コールバックのタイミング: Direct / Deferred でそれぞれで、どのタイミングでデータが返ってくるか
  • 「15分制約」の挙動: インストールから起動までのタイムリミットがどの程度厳密に運用されているか

調査を通じて浮き彫りになった課題は、「コールバックの即時性は必ずしも保証されない」という点でした。ネットワーク環境や端末の状態によって、数秒以内にデータが取得できることもあれば、タイムアウトで情報が返ってこないケースの可能性もあるようでした。このため、実装側では「情報が取得できなかった場合」を前提としたハンドリングが必須ということが分かりました。

また、実機検証の結果、ストアを経由する際(Deferred)には利用できるパラメータに制約があることも分かりました。 取得可否の詳細は以下の通りです。

パラメータ取得可否の検証結果

キー Direct Deferred 備考
deep_link_value ✅ 取得可 ✅ 取得可 管理画面で設定可
deep_link_sub1 ✅ 取得可 ✅ 取得可 管理画面で設定可
deep_link_sub210 ✅ 取得可 ✅ 取得可 管理画面で設定不可
deep_link_sub11 ✅ 取得可 取得不可 -
custom_key ✅ 取得可 取得不可 -

上記の内容は執筆時点(2026年3月)の調査結果です。AppsFlyerの仕様変更により情報が更新される可能性があるため、実装の際は必ず最新の公式ドキュメントをご参照ください。

実装にあたって考えたこと

URLの命名規則

ディファードディープリンクで渡すURLは、一度決めたら変更しにくい性質上、設計には慎重になりました。 今回アプリに渡す必要がある情報は、画面識別子・エリア情報・求人詳細ID・職種情報の4つです。ただし、これらの情報の性質は均一ではありません。画面識別子・エリア情報・求人詳細IDは遷移先の画面を決定するために必須の情報である一方、職種情報は絞り込み条件として画面に渡す付加的な情報です。後者はパラメータの種類や組み合わせが変わりやすく、将来的に増減したり仕様が変わったりする可能性もあります。こうした性質の違いを踏まえ、どのようにURLに乗せるかを以下の3案で検討しました。

案1:パラメータ分割方式

  • AppsFlyerの標準フィールド(deep_link_valuedeep_link_sub110)に各値を個別に割り振る方式
  • パラメータの意味がURL上で明確になる反面、sub10 までという上限があり将来的にパラメータが増えた際に対応できなくなるリスクがある
  • 「sub1はエリア情報」「sub2は職種情報」といったマッピング定義を厳密に管理し続ける必要があり、運用コストの面でも懸念があった

案2:URL埋め込み方式

  • deep_link_value にWebの完全なURLをエンコードして丸ごと格納する方式
  • 個数制限を気にせず済み、WebのURL構造をそのまま流用できる点は魅力的だった
  • アプリ独自の画面(Webに対応するURLが存在しない画面)を開きたい場合にダミーURLの設計が別途必要になる点が課題として残った

案3:ハイブリッド方式(採用)

  • deep_link_value に画面識別子、deep_link_sub1 に職種情報を含むWebの完全URL、deep_link_sub2 にエリア情報、deep_link_sub3 に求人詳細IDを入れる方式
  • アプリは deep_link_value だけを見れば遷移先を即座に判断でき、必須の求人詳細IDとエリア情報はそれぞれ deep_link_sub2deep_link_sub3 で確実に受け取れる。一方、職種情報など付加的な絞り込み条件は、WebのURLにクエリパラメータとして存在しているため、そのURL自体を deep_link_sub1 に格納することで、OneLinkのパラメータを増やすことなく柔軟に渡せる
  • URL生成ロジックが若干複雑になるデメリットはあるものの、拡張性と可読性のバランスが最もよいと判断し採用

議論を行い最終的に決まったパラメータの構成ルールは以下のとおりです。

パラメータキー 役割 値の例
deep_link_value 画面識別子 shopDetail など
deep_link_sub1 職種情報を含むURL https://example.com/kanto/work/100245?param={職種情報}...
deep_link_sub2 エリア情報 kanto
deep_link_sub3 求人詳細ID 100245
deep_link_sub4〜10 未使用 -

非同期での情報取得のタイミング

実装で最も苦労したのは、ディファードディープリンクの情報を非同期で取得するタイミングの制御です。

UDLのコールバックはアプリ起動後に非同期で返ってくるので、アプリの初期化処理との兼ね合いが厄介でした。 スプラッシュ画面にタイムアウト時間を設けて取得できなければコールバックを破棄する案なども検討しましたが、最終的には2つのタイミングでコールバックを受け取る方針に落ち着きました。コールバック受信時点でのアプリの状態に応じて処理を切り替えることで、データの到着が早くても遅くても、スムーズに画面遷移を繋げられるようになりました。

パターン1:スプラッシュ画面中にコールバックが返ってくるケース

  • スプラッシュ画面表示中にUDLから deep_link_value 等を取得
  • 取得した情報を解析し必要情報を端末に保存
  • ウォークスルーをスキップし、指定された求人詳細画面へ直接遷移

パターン2:スプラッシュ画面中にコールバックが返ってこないケース

  • コールバックが間に合わなかった場合、通常通りウォークスルーへ遷移
  • 求人一覧(TOP)画面に到達したタイミングで改めてUDL情報の取得を購読し、deep_link_value 等を受け取る
  • 遷移確認ダイアログをユーザーに表示し、同意が得られれば指定された求人詳細画面へ遷移する

なお、リンクを踏んでから15分以上経過して起動した場合は、UDLによる情報復元は行われず通常のアプリ起動として処理されます。

処理

まとめ

今回の開発を通じて、チームとしてディファードディープリンクを含むディープリンク周りの実装経験を得ることができました。Universal LinksやApp Linksといったシンプルなディープリンクとは異なり、ディファードはSDKの非同期処理やOS・端末ごとの挙動の差異など、実際に手を動かして初めてわかる複雑さがありました。そういった泥臭い部分を含め、チームで一つひとつ検証しながら実装を進めたことは、今後の開発においても大きな財産になると感じています。

また、ネット上には、SDKの繋ぎ込み方の記事はあっても、「URLの構成をどう設計したか」や「起動後の具体的な処理」にまで踏み込んだ事例は意外と少ないと感じました。

この記事が、同じようにディープリンクの実装で悩み、試行錯誤しているエンジニアの方々にとって、解決のヒントや一助となれば幸いです。
最後までお読みいただき、ありがとうございました。

新卒として第四開発Gに参加してからの働き方

まえがき

こんにちは!シンクロフード開発部25卒の小林です!入社してから約1年、第四開発Gに配属されてから9ヶ月が経ちまだまだ勉強の日々ですが、楽しく開発に取り組んでいます!
私の所属する第四開発Gは、求人インテリア農業ジョブグルメバイトちゃんなどのサービスの開発を行っているチームです。

本ブログでは、IT業界やシンクロ・フードに興味がある未来の新卒の方に向けて、第四開発Gに参加後の新卒エンジニアの1日をお届けしたいと思います!
※チームごとに運用が違う場合があるので、あくまで私が所属する「第四開発G」の例ということをご留意ください。
開発Gに参加する前に実施されている、新卒研修に興味のある方は是非、以下の記事をご覧ください
tech.synchro-food.co.jp

1日の流れ

  • 9:00 出勤
  • 9:00 ~ 9:15 タスク整理
  • 11:30 ~ 12:00 第四開発G朝会
  • 12:00 ~ 12:15 雑談会
  • 12:30 ~ 13:30 お昼休憩
  • 14:30 ~ 15:30 第四開発G振り返り
  • 17:00 ~ 18:00 新卒振り返り
  • 18:00 退勤

ミーティング以外の時間は、開発業務をしています!

※上記のスケジュールは、振り返りMTGが集中している「金曜日」の例です。 月〜木曜日は朝会以外のMTGは少なく、午後いっぱいは開発に集中できることが多いです!
ミーティングの回数の平均は、約1日2回以下です。朝会の他にミーティングが一つある、というようなイメージですね。

出勤

私はいつも、9時~10時の間に出勤しています!
弊社はフルリモート制度と、フレックスタイム制が採用されているため、第四開発Gのメンバーの出勤時間もバラバラです。

コアタイムの11:00〜16:00に出勤していれば、フレキシブルタイムの7:00〜22:00の間で好きな時間に業務を行うことができ、月の労働時間が所定の時間を満たすように気をつければ出勤時間と退勤時間は自由に調整できます。

そのため、私は日によって勤務時間が変わることが多いです。
08:31 ~ 18:54 のように少し長く働いた日もあれば
10:52 ~ 17:58 のように短い日もあります。

下の写真は、とある日の第四開発Gの出勤時間です!朝早い人から遅い人まで様々です。
とある日の第四開発Gの出勤時間

メールや、Slackのメッセージを確認して、第四開発G朝会まで開発などの業務を行います!

タスク整理

第四開発GではNotionというツールを使ってタスク管理をしています!
画像は私のタスクの一例です!
タスクの粒度は様々で、文言修正のような小さめのタスクから、1つページを作成するような大きめのタスクまであります。

一週間のはじめに自分の開発工数がどのくらい残っているかを共有して、それに合わせてタスクを振られるのでタスクが多すぎて開発が間に合わない!ということはないので安心してください。
一週間で開発が終わらなかった場合は、残っている分を差し引いた工数のタスクが割り振られるので、その部分も安心です。

Notionで管理されていることでひと目見てタスクの状態がわかりやすく、タスクにメモを書いたりする事ができるのでタスクごとの進捗を管理しやすいです。
自分でタスクの場所を動かしたり、タスクの状態を変更できるので今自分は何をするべきなのかを把握しやすいです。
私はたくさんのタスクを抱えることが苦手なのですが、Notionでタスクを整理することでどのタスクをやればいいかがわかると一つずつタスクをこなしていけるのでとても便利に感じています。
Notionでタスクを整理

開発業務

基本的には複数の開発要件をもっていて、期日や進捗に合わせて開発を進めています!
Ruby on Railsを触ることが最も多く、次点でJavaScript、たまにJavaを触るような感じです。
現在は、冒頭で紹介した「求人インテリア」などの機能改修を担当しています。
集中して黙々と開発することが多いですが、わからなかったり詰まったりしたらSlackで相談しています。

例えば、テキストだけでは状況が伝わりにくい場合、下の画像のようにハドル(通話機能)を使って画面共有をしながら相談することもあります。
ハドル(通話機能)を使って画面共有をしながら相談

開発業務は大まかに以下の流れで行っています。

  1. タスク割り振り
  2. 調査&実装
    1. 大きめのタスクの場合実装方針を相談します
  3. レビュー
    1. 他のメンバーに見てもらえるので、ここでもアドバイス等をもらえます
  4. テスト
  5. 本番アップ

第四開発G朝会

第四開発G朝会は毎日行っています!
内容は以下のとおりです。

  • メンバーの今日のタスク確認
  • 共有事項の確認
  • エラー確認
  • 気になったことの共有、議論

この中でも特徴的なのは、「気になったことの共有、議論」です。
第四開発Gでは、気になったことを書くSlackチャンネルがあります。
そのチャンネルに普段の業務で気になったことを書き込んでおくことで、第四開発G朝会のタイミングでメンバーのみんなに共有をしつつ議論することができます。
リモート故にちょっとしたことを話しづらい、という問題を解消しています!

画像は実際に私が書き込んだものです!
「グルメバイトちゃん」で使用している特定の色について、微妙に異なる2色が混在しており、統一されていなかったので、影響範囲を確認つつ「どのタイミングでどう対応をすべきか」について話し合いました!
実際に私が書き込んだもの

雑談会

第四開発Gは、火曜日と金曜日に雑談会を開催しています!
メンバーからランダムに選ばれた3人で15分間雑談をします。
休日の過ごし方や、最近ハマっていることなどのプライベートな話から、時事的な話まで話題は様々です。
フルリモートでも、こういった雑談会をとおしてメンバーと交流を深められています!

お昼休憩

リモートワークなので、お昼休憩にちょっとした家事をする方も多いそうです。
私はお皿洗いをしています。

第四開発G振り返り

週に一回、第四開発Gでの振り返りを行っています!
チームで決める必要があることを話し合ったり、第四開発G朝会では時間が足りなくて議論しきれなかった議題などについて話し合います。
また、実装共有と言ってメンバーが開発した機能などの実装内容を共有することもあります。
どのような仕組みで実装しているかや、用いた技術、仕様について共有します。
気になった点や、わからなかったことは質問をして、他メンバーの開発内容も理解することを心がけています!

新卒振り返り

週に一回、新卒である私の振り返りを行っています!
この振り返りでは、私が一週間で行った開発について振り返っていきます。
私の他には新卒二年目のメンバー、チームリーダー、あと一人は月替りで他のメンバーが参加します!

実装の進め方だったり、技術的な話、一週間の振り返りをしてアドバイスをもらいます。
自分のうまくいかない部分を他のメンバーがどうやって対応しているかや、便利なツールの共有などをしています!
自分の開発の進め方や実装方法について色々な視点でアドバイスをもらえる機会があるのがとても成長に繋がっています。

例えば、以前は実装時の小さなミスが多かったのですが、「レビュー依頼の前だけじゃなくgitのコミット単位でもセルフレビューで確認をすると良いよ」とアドバイスをもらったことで小さなミスが大幅に減りました!

画像は開発部外の人とのやり取りで認識のズレがあったときの振り返りの議事録です。
先輩から対策についてのアドバイスをもらいました!
振り返りの議事録

また、KPTといって自分が一週間で成長できているかを確認する場もあります。
Keep(うまくできていて、続けて伸ばしたいこと)
Problem(うまくいかなかったこと)
Try(うまくいかなかったことを改善する方法)
の頭文字を取ったもので、振り返りで浮かんできた課題についてKPTを設定することが多いです。

画像は、開発部外の人とのやり取りで認識のズレがあったときのKPTの抜粋です。
他のメンバーがどう意識しているかアドバイスをもらえるので改善しやすいです!
KPTの抜粋

退勤

急ぎのタスクがないかを確認して、退勤します!
リモートワークなので、退勤後すぐにご飯を食べたりお風呂に入ることができます!

配属前後での心境の変化

配属直前や直後は、チームへの貢献度や自分の成長度合いなどに不安を抱いていました。しかし、新卒振り返りや、Slackでの相談しやすい環境があったり、今は焦らずいろんな知識を業務を通じて吸収していってほしいという言葉をもらって、今ではとても前向きに楽しく働いています!

結び

いかがでしたでしょうか?
今回は、シンクロ・フード第四開発Gに配属された新卒エンジニアの1日をご紹介しました。

配属当初は「リモートワークで質問できるかな?」「技術についていけるかな?」という不安もありましたが、
紹介したような「雑談会」や手厚い「振り返り」のおかげで、孤独を感じることなく楽しく開発できています。

シンクロ・フードには、年次に関係なく意見を言えるフラットな空気と、挑戦を後押ししてくれる環境があります。
私もまだまだ勉強中の身ですが、先輩たちの背中を追いかけて、早く一人前のエンジニアとして価値を届けられるよう頑張ります!

この記事を読んで、少しでもシンクロ・フードのエンジニアの働き方に興味を持っていただけたら嬉しいです。
最後までお読みいただき、ありがとうございました!

求人飲食店ドットコムの性能改善のために Datadog APM を導入しました

はじめまして、開発部の熊谷です。
今回は弊社のサービスである求人飲食店ドットコムの性能改善をするため、Datadog の Application Performance Monitoring(以下APM)を導入した際の話をしたいと思います。

docs.datadoghq.com

なぜAPMを導入したか

ここ数年、当チームでは開発・保守を担当している「求人飲食店ドットコム」の性能改善に対する関心が高まっていました。
主な動機は、Core Web Vitals の各指標を向上させることで検索順位やユーザー体験を改善したいという点にあります。
しかし、フロントエンドには PageSpeed Insights のような手軽な計測ツールがある一方、サーバーサイドのレスポンスタイムについては具体的なボトルネックを特定するためのモニタリング環境が整っていない状態でした。

そこで1年ほど前、開発プロセスの中で性能劣化を防ぐためローカル環境(各エンジニアの開発機)に rack-mini-profiler という Rails 用のプロファイラを導入しました。
github.com このツールは Controller や View ごとに発行されたSQLのクエリ数や実行時間を可視化できるため、これを使い新規開発や機能改修を行った際のレビュー前に「意図せぬクエリ増加(N+1問題など)が起きていないか」や「意図して追加した処理の実行時間が許容範囲内か」を開発者自身がチェックするフローを確立しました。
この取り組みによって明らかなパフォーマンス劣化をリリース前に検知できるようにはなりましたが、運用を続ける中で新たな課題も見えてきました。

最大の課題は、ローカル環境での最適化が必ずしも本番環境のパフォーマンス改善に直結しないという点です。
ローカル環境はWebサーバー、アプリケーションサーバー、DBや全文検索エンジンなどの各種サービスが単一のマシン内で完結するオールインワン構成ですが、本番環境では各サービスがネットワークを通じて連携しており通信レイテンシが発生します。また、サーバーのスペック差に加えて Rails の設定(config.eager_load など)も異なるため、同一のコードであっても処理時間の内訳やボトルネックとなる箇所が本番とは異なる可能性が高いという状態にあります。

また、運用の手間という観点でも課題がありました。rack-mini-profiler は詳細なプロファイリングには適していますが、あくまで単発のリクエストを分析するためのツールです。サービス全体を俯瞰出来るようなサマライズ機能もなく、蓄積されたデータを使った時系列での傾向把握なども出来ないため継続的なパフォーマンス監視として使うには不向きで、そこを人手でカバーするのは工数的に現実的ではないというのが問題でした。

APM導入

上述の背景から、本番環境の実態を正確に把握し継続的な改善をしていくための仕組みとして Datadog APM の導入を決定しました。
弊社ではすでにSREチームが各種インフラリソースの監視目的で運用している実績があり、アカウントの作成や Datadog エージェントの導入は済んでいる状態だったため、求人飲食店ドットコムへの導入はスムーズに進みました。
導入に関して Web アプリケーション側で必要だったのは Datadog エージェントへトレース情報を送信するための gem のインストールと、それに伴う十数行程度の設定のみでした。

ただしトレース情報を採取する頻度(サンプリングレート)の設定には注意を払いました。APM のトレース情報はスパンと呼ばれる単位で採取されますが、採取したスパンはその数と容量が一定数を超えると基本料金+超過分の従量課金となる料金体系であるため、サイトに対するリクエストすべてのスパンを採取していると高額になる恐れがあります。
そのためサイトへの平均リクエスト数などを元に基本料金内に収まるスパン数を事前に試算し、まずはリクエスト全体のうち10%のみのスパンを保存するように設定しました。念の為保存しているスパンの量をモニタリングしながら運用していますが、今のところ超過の傾向はないうえ分析に十分なサンプル数は確保できているのでこのレートで運用を続ける予定です。

APMの活用

まず求人飲食店ドットコムの中で特に重要ないくつかのページを対象として、それらに対するリクエストのレイテンシを時系列の折れ線グラフで表示するダッシュボードを作成し、毎週末に実施している振り返りのミーティングで前週のグラフと比較して明らかな劣化がないかを確認するという時間を試験的に設けてみました。

それからしばらくしたある週のミーティングで、サイト上で掲載している求人の詳細情報を表示するページのレイテンシがある日の本番リリースの後から定常的に悪化していることに気づきました。
緑の線のタイミング(本番リリース)を境に、前週のレイテンシ(赤の点線)と比較して当該週のレイテンシ(青の実線)のベースラインが明確に増加していることが一目で分かるかと思います。

ここまでなら Datadog APM ほど高機能なツールを使わずとも「パフォーマンス監視を定期的に行う」というルールを決めるだけでも実現できたかもしれませんが、ここからさらに Datadog APM の別の機能を使うことで原因となった開発要件の特定まで迅速に行うことが出来ました。
Datadog APM にはサーバー側の処理の総実行時間を処理の種類ごとに可視化する機能があります。それを使用して下記のように実行時間の合計を積み上げ面グラフとして表示することで、本番リリースのタイミングから全文検索エンジンである OpenSearch の処理(一番下の水色の層)が新たに増えていることが一目で分かり、それが手がかりとなって原因となった開発要件を短時間で特定することが出来ました。

運用の工夫

しかしこの運用では目視によるチェックが必要かつ定量的で明確な基準がないので、運用コストと属人性の観点でもう少し改良が必要だと感じました。そこでAPMモニターという機能を使い、異常検知アラートを設定することを試みました。

docs.datadoghq.com

異常検知アラートとは、蓄積された過去のメトリクスデータをもとに機械学習アルゴリズムが「予測される変動幅」を動的に算出し、その範囲を逸脱した場合にアラートを上げる機能です。
固定の閾値を設定する方式とは異なり時間帯や曜日ごとの傾向も考慮されるため、例えば「夜間はアクセスが減るが、日中は増える」といったサイト毎の特性も加味した柔軟な検知が出来る点が強力です。
下記の画像は動作確認時に発生させたときの通知内容ですが、現在ではこのように自動でSlackに通知が来るように設定をしているため即時に異常に気付けるような状態になっています。

今後について

本記事では、APM の導入からダッシュボードによる可視化、そして APM モニターによる異常の自動通知までを紹介しました。これにより、チームメンバー全員がフロントエンド・バックエンド問わずシステムの性能を定量的に把握出来る状態になったので、実際に性能改善活動を進めるための準備が整ったと言えます。

次のステップとして、Datadog Continuous Profiler の活用を進めています。

docs.datadoghq.com

APM がリクエストの流れを追うのに対し、Continuous Profiler はプログラム実行中の CPU やメモリの使用状況をメソッドや行単位で記録する機能です。これらを可視化したフレームグラフを読み解くことで、処理遅延の原因であるコード箇所をピンポイントで特定できるようになります。
現在は、実際にフレームグラフの分析手法を学習しながら、特定のエンドポイントのボトルネック改善にトライしている最中です。具体的な改善事例や成果を出すことが出来たら、また別の記事として共有したいと思います。