
Ruby on Rails
イベント
マガジン
技術ブログ
みなさんこんにちは!ワンキャリアでソフトウェアエンジニアを担当している渡邉(X: @PwatanabeMiki )です。現在は主にフロントエンドとSRE領域を担当しています。 今回は、複数チームを巻き込んで実施した「Sentryエラー退治(通知削減)と運用改善」の取り組みについてお話しします!
こんにちは。Findy Freelance開発チームの久木田です。 今回は、社内で運用している支払明細書PDFの生成基盤を、Lambda + Puppeteerから @react-pdf/renderer へ全面的に移行した話を書きます。最終的に処理時間はP50(中央値)で約27倍速くなり、メモリ消費も実測で約1/4まで落とせました。 これまでのPDF生成基盤と課題 対症療法でしのいだ期間 根本対応を決めた3つの背景 技術選定: 戻れる順に試す 移行で押さえておきたい実装ポイント 設計と実装のキモ テンプレート構造 esbuildで橋渡し どう変わったか 学び これまでのPDF生成基盤と課題 現在、システムから発行しているPDFはいくつかありますが、本記事では一例として支払明細書PDFに絞って紹介します。ファインディからフリーランスエンジニアへの支払明細として月次で一括発行しているPDFです。 支払明細書PDFは1件あたり1ページで、支払先、支払者、明細表、特記事項、ファインディの社印を配置した構成です。情報量としては小規模ですが、月次でフリーランスエンジニア全員分を一括発行する必要があり、1回のバッチで数百件規模のPDFを並列生成していました。Rails側からParallelライブラリで並列にLambdaを呼び出す構成です。 このPDFは、AWS Lambda上でPuppeteerを起動し、EJSテンプレートから組み上げたHTMLをヘッドレスのChromiumでレンダリングしてPDF化する構成で動いていました。フロントエンド向けのHTML/CSSをそのまま流用できるため初期実装は速かったのですが、運用が進むにつれ次の課題が顕在化しました。 コールドスタートが重く、Chromiumバイナリの起動だけで5秒前後を消費していた 大量生成で /tmp が枯渇し、一括処理で複数のレンダリングを並走させると、エフェメラルストレージが先に尽きてエラー終了する タイムアウトやエラーが恒常化し、数百件規模の一括出力は途中で停止して再実行が必要になる ボトルネックはChromiumの不安定さでした。Chromium起動のたびにプロファイル・キャッシュ・ソケットファイルが /tmp 配下に生成され、 browser.close() 後もディスク上に残存します。さらに、レンダリング中にChromiumがハングした場合は、一時ファイルを保持したままプロセスが停止します。その間にも新規リクエストでLambdaが起動するため、ウォームスタートで使い回されるインスタンスでは /tmp の残存ファイルが蓄積し、一定量を超えた時点で全体が停止してしまう挙動になっていました。 この蓄積に備えて、メモリと /tmp は常にバッファを含めて多めに割り当てる必要があり、1Lambdaあたり2-3GB相当の確保を継続していました。それでもハングと並列度のピークが重なる局面では詰まるため、設定値を引き上げては別の閾値で詰まるという状態が続いていました。 一括出力は今後数倍に増加する見込みであり、現状の構成のまま継続することは現実的ではありませんでした。 対症療法でしのいだ期間 課題を踏まえ、徐々に対策を施すことにしました。Lambdaのメモリ割り当て増、タイムアウト延長、エフェメラルストレージの拡張、並列度の調整など、設定値で吸収できそうな対策は一通りしました。短期的な改善にはなったものの、根本にあるのはChromiumをサーバーレス環境で動かすこと自体のコストと描画コストが入力規模に比例して伸びる構造です。設定の積み増しではレイテンシーもエラー率も思うように改善しませんでした。 根本対応を決めた3つの背景 根本対応に踏み切る判断は、次の3点が同時に揃った時点で下しました。 現状の課題: レイテンシーが悪化傾向、エラーも日次で観測される 将来の悪化見込み: 一括処理の件数は今後さらに増える計画があり、対症療法の余地がもう残っていない 対症療法の限界: 設定値の調整ではレイテンシーやエラー率の改善が頭打ちで、いずれの打ち手も効果が薄れてきた 課題が大きくなったため、対策を施しました。逆に言えば、どれか1つでも欠けていたら、もう少し対症療法を続けていたと思います。 技術選定: 戻れる順に試す 根本対応の方針として、大きく2案を比較しました。 A案: Lambdaを維持し、 @react-pdf/renderer でProgrammaticにPDFを組み立てる B案: LambdaからECSなど別のランタイムへ移行する A案は、いまのLambda構成を保ったままPDF生成方式だけを差し替える案です。 @react-pdf/renderer はJSXを書く感覚でPDFを組み立てるライブラリで、Chromiumのヘッドレスブラウザは利用しません。 react-pdf.org そもそも @react-pdf/renderer が候補に挙がったのは、ヘッドレスブラウザ以外でPDFを生成する方法を調べていたからです。継続的な利用料金が発生する外部サービスを使う選択肢は外し、OSSのプログラマティック生成ライブラリの中で、Reactと同じJSXで書ける @react-pdf/renderer を選びました。Reactはファインディの他プロダクトでも広く使われており、馴染みがあった点も決め手になりました。 B案はメモリやストレージの制約には強くなりますが、コスト構造が変わり、検証の立ち上げにも工数がかかります。Lambdaのタイムアウトに収まらない処理や、Chromiumの表現力(複雑なCSS・JavaScript描画など)をどうしても残したいケースではB案が候補ですが、今回のPDF生成はそのどちらにも当てはまりませんでした。 そのため、最終的には次の3つの理由からA案を選びました。 戻れる: Lambda構成のままPDF生成方式だけを差し替えるので、問題が出てもPuppeteer版に戻すことが容易 テスト可能: PDF生成ロジックがJSXで書けるため、単体テストや出力差分テストを書きやすい AIで移行コストが現実的になった: EJSテンプレートからJSXへの変換は、生成AIに任せられるレベルまで来ていた B案はA案が失敗した場合の代替案として残し、まずは戻れるA案を試すことにしました。 移行で押さえておきたい実装ポイント EJSからJSXへの書き換え自体は生成AIで一気に進められましたが、 @react-pdf/renderer の実装スタイルに合わせるために事前に押さえておきたい点がいくつかありました。 @react-pdf/renderer v4はESM-onlyで、tscのCommonJS出力からは読み込めないため、esbuildを入れてESMをCJSにバンドルした 日本語フォントは Font.register() で明示的に登録しないと文字化けする Puppeteerの scale: 0.8 相当が無いため、フォントサイズや余白を手で再計算した HTMLの <a> 自動展開は再現されず、URL部分だけ <Link> 化する小さなコンポーネントを自作した HTMLエスケープは自動化されていて、旧実装のエスケープ処理が不要になった(副産物) 特に大変だったのがJSXの空文字列の扱いです。 {stringValue && (...)} と書くと、空文字列がchildとしてそのまま流れ込み、 WARN Invalid '' string child outside <Text> component が大量に出ます。Reactの文法としては正しいのですが、 @react-pdf/renderer の <View> / <Page> 配下では {!!stringValue && (...)} と明示的にboolean化する書き方に揃える必要があります。さまざまなデータでPDFを作成していく過程で警告ログが出ていることに気付き、該当箇所をまとめて修正しました。 設計と実装のキモ ここからは、 @react-pdf/renderer をLambdaに載せていくときに考えた設計面のポイントを2つに分けてまとめます。 テンプレート構造 PDFそのものを1つのReactコンポーネントとして組み立てる構成にしました。リンクの自動展開のように共通で必要な要素は、専用の小さなコンポーネントとして切り出して再利用しています。 EJS時代は部分テンプレートをincludeで組み合わせる作りでしたが、JSXに移ってからはコンポーネントの組み合わせとして自然に再構成できました。 esbuildで橋渡し @react-pdf/renderer v4はESM-onlyですが、Lambda側はCommonJSで動かしています。tscの出力では直接読み込めなかったため、esbuildでESM→CJSのバンドルを作ってLambdaにデプロイする構成にしました。 // esbuild.config.js(抜粋) { entryPoints: ['src/index.ts'], bundle: true, platform: 'node', format: 'cjs', } 設定としてはシンプルですが、ここを通さないと依存関係の解決でつまずくことになるため注意が必要です。 どう変わったか 支払明細書PDFの一括作成処理について、移行前後をCloudWatchメトリクスで比較した実測値が次の通りです。表のP50 / P95 / P99は、実行時間を昇順に並べたときの中央値 / 95パーセンタイル / 99パーセンタイルを表します。 指標 Before After 倍率 実行時間 P50 約3,963 ms 約145 ms 約27倍高速 実行時間 P95 約4,707 ms 約212 ms 約22倍高速 実行時間 P99 約5,249 ms 約458 ms 約11倍高速 平均メモリ 約912 MB 約222 MB 約1/4 最大メモリ 約1,589 MB 約239 MB 約1/7 特に改善幅が大きいのはP50です。旧構成では実行時間そのものの遅さに加え、Puppeteer/Chromium由来のエラー(ブラウザの接続切れやハングなど)が起きると、一括処理の中で個別のPDF生成がLambdaのタイムアウトに到達し、リトライしても最後まで完成せずエラーとして残るケースがありました。表のP99の大きさにそれが現れています。移行後はこれらのエラー、エフェメラルストレージの逼迫、コールドスタートによる遅延がいずれも解消され、Lambda上での実行を意識する必要のない構成になっています。 学び 今回の移行で特に有効だったのは、判断軸として置いた「戻れる順に試す」です。Lambda構成を維持したままPDF生成方式だけを差し替えるA案は、もし行き詰まっても旧版のLambdaに切り戻す選択肢を残せました。ランタイムごと載せ替えるB案を最初に選んでいたら、検証のために抱えるリスクははるかに大きくなっていたはずです。 もうひとつは、生成AIの活用で技術選定の前提条件が変わったことです。A案はEJSテンプレートのJSXへの全件書き換えを伴います。AIなしで工数を見積もると、規模だけでA案は採用候補から外れていました。書き換えだけでなく、旧PDFと新PDFのレイアウト差分の特定と修正案の生成までAIに任せられたため、A案の工数は当初の想定より低く収まりました。 最後に、 @react-pdf/renderer を使った所感をまとめます。 メリットとして大きかったのは、Lambdaの割り当てメモリを大幅に減らせたことと、ヘッドレスブラウザを使っていたころよりテストが格段に書きやすくなったことです。PDFをバッファのまま受け取って中身を検証できるので、ブラウザを起動しない軽量な統合テストをCI上でも組めるようになりました。 一方で、HTML/CSSではなく <View> <Text> といったPDF専用のプリミティブをFlexboxで組み立てる、React Native寄りのコンポーネントモデルです。HTML/CSSしか経験が無い場合は、最初は書き方に戸惑う場面もあると思います。 ファインディでは一緒に会社を盛り上げてくれるメンバーを募集中です。興味を持っていただいた方はこちらのページからご応募お願いします。 herp.careers
はじめに JJUG CCCとは 登壇スライド 外部発信のモチベーション 登壇を通じて得られた気付き 振り返り はじめに 登壇直前に地元バスケクラブが準優勝し、かなりのダメージを負っていた楽楽債権管理チームの冨澤です。 2026年5月30日に行われたJJUG CCC 2026 Springで初登壇してきました。 本記事は、そのレポートとなります。 JJUG CCCとは JJUG CCCは、日本最大のJavaコミュニティイベントです。 日本Javaユーザグループ(JJUG) / Japan Java User Group (JJUG) が主催しており、今回は春に開催されたカンファレンスです。 (秋にもあります!2026年11月28日開催予定) ccc2026spring.java-users.jp 登壇スライド speakerdeck.com 外部発信のモチベーション なぜCIを速くしたいかは登壇スライドに書いているので、なぜ外部発信をしたのかについて少し書いておこうと思います。 1つ目は、取り組みを始めた当時、自分が調べた範囲ではJava関連プロダクトのCI時間削減の記事が少なかったからです。 RailsやGo、フロントエンド関連のCI時間削減の記事は多かったのですが、Java関連の記事はあまり見つかりませんでした。 そのため、成功でも失敗でも何かしら貢献ができるのではないかと考え、最初の取り組みを以下のテックブログに書きました。 tech-blog.rakus.co.jp ちなみにこのやり方は、Goのテスト実行における「パッケージ単位で実行を分け、パッケージ内のテストは必要に応じて t.Parallel() で並列化する」という考え方から着想を得ました。 2つ目は、外部からのフィードバックを得たかったからです。 これは今回JJUG CCCに登壇したかった理由でもあります。発表後や懇親会の場で社外の方とお話しする中で、新しいフィードバックや気付きを得られたらよいなと考えていました。 こうした機会は自分から動かないと得られないと思い、CfPを提出しました。 また、上記のテックブログの内容からさらに改善を行ったので、その取り組みも紹介したいと考えていました。 登壇を通じて得られた気付き 何名かの方とお話しする中で、共通して話題に挙がったのが「PRごとに毎回すべての単体テストを実行しているのか?」という点でした。 これは本当におっしゃるとおりで、今回の取り組みでCI時間を約50%削減できたものの、それでもまだテスト実行に約10分かかっています。 デグレの早期検知や安心材料としてすべての単体テストを実行しているのか、もっと速くできないか、すべて実行する必要は本当にあるのか。そうした会話を通じて、「そもそも何のためにこれをやっているのか?」という重要な問いに何度か立ち返ることができました。 自分たちのプロダクトにとって本当に大事なことは何か。逆に、何をトレードオフとして選ばないのか、あるいは優先度を下げられるのか。そうした視点の重要性に改めて気付くことができました。 例えば、より速くPRをマージしたいのであれば、変更の影響範囲に絞ってテストを実行し、夜間にすべてのテストを実行するという選択肢があります。逆に、速さよりもデグレの早期検知を重視するのであれば、毎回のPRですべてのテストを実行する判断になるかもしれません。 目の前の作業だけに没頭するのではなく、時折立ち止まって目的を見直す姿勢が大事なのだと改めて感じました。 振り返り CfP採択の結果連絡が来た時は、非常に嬉しかったです。 採択されたことが信じられず、社内の方にも本当に採択されたのか確認を取ったほどでした。 登壇直前までかなりバタバタしており、とても緊張していましたが、何名かの同僚が来てくれており非常に心強かったです。 登壇直後、次に発表される方からの質問や廊下での立ち話、懇親会での会話など、様々な場面で社外の方と交流することができました。「E2EやAPIテストなど他のテストとの役割分担はどうしてる?」「コーディングエージェントにテストをうまく書かせる工夫は?」「開発組織で取り組んでいる工夫は?」など、話題は多岐にわたり、どれもとても楽しい時間でした。 当初の想定どおり、たくさんの方と交流でき、そこで得たものを今後の業務に活かしていきたいと思います。 今後もこうした登壇の機会があれば、ぜひ続けていきたいです。 登壇者・参加者・企画・運営の皆さま、素敵な場をつくっていただき本当にありがとうございました!
動画
該当するコンテンツが見つかりませんでした










