💪

CloudFront Functions × KeyValueStore で店舗IDから口コミURLへ高速リダイレクト

に公開

はじめに

GENDAエンジニアで、GENDAグループ企業のカラオケBanBanを運営するシン・コーポレーションのCTOをやっております西尾です。
この記事は GENDA Advent Calendar 2025 の記事です。

https://qiita.com/advent-calendar/2025/genda

マーケティングオートメーション(以下MA)ツールを活用してカラオケ店舗来店直後に、店舗の Google Map 口コミへ誘導し口コミ数を増やすことで MEO 対策を強化したいという要望を、CloudFront Functions を活用して解決したお話しをさせていただきます。

TL;DR

  • MAツールには来店したカラオケ店舗の店舗IDのみが連携されているので、配信するメール内で店舗IDだけをパラメータに埋め込んでおき、CloudFront Functions で Google マップの口コミ投稿 URL にリダイレクトする仕組みを作った
  • 店舗ID → Google Maps の Place ID という対応関係は、CloudFront KeyValueStore に持たせることで、新店追加や修正を KVS の更新だけで完結させた
  • 実装では、CloudFront Functions のコードサイズ上限や KVS import のフォーマット制約などいくつかハマりどころがあったので、そのポイントを整理した

背景: braze を使用して来店後アンケート & Map 口コミ誘導をしたい

今回は、MAツールの1つである braze を使った施策が出発点でした。
braze の詳細は割愛させていただきますが、ユーザーの購買行動や属性に応じた柔軟なマーケティングオートメーションの実施が可能なツールとなっています。

今回の施策の概要は以下です。

  • 来店をトリガーに braze でメールを配信、メール内にアンケートへのリンクを載せる
  • メールから遷移して簡単なアンケート(CS、NPS など)に回答してもらう
  • アンケートの最後に「よければ Google Map の口コミもお願いします」と、各店舗の Google Map の口コミ投稿ページへ誘導したい

このとき、braze からは以下の情報が取れる前提でした。

  • 自社で管理している 店舗ID

一方で、Google Map の口コミ URL は以下のように、Google の Place ID をパラメータとして持ちます。

https://search.google.com/local/writereview?placeid=XXXXXXXXXXXXXXXXX

問題はここで、braze 側には「店舗ID → Place ID」の対応関係は連携されていないという点です。
自社のサーバーでは、店舗マスタで店舗 ID を管理しているのですが、Google Place ID までは保存しておりません。 braze に連携するデータにも Place ID は含まれていません。

したがって、「店舗 ID だけを持っているメール」と「Place ID が必要な Google Map の口コミ URL」の間をどこかで繋ぐ必要がありました。

この対応関係を解決する方法として、最初に思いつくのは、既存の DB に「Place ID カラムを追加」するという対応です。
あるいは「店舗 ID ↔ Place ID の対応表テーブル」を別途持つといったアプローチです。

ただ、今回はマーケ試作のために既存システムの schema 改修を入れるほどの話でもないと考えました。

一方で、将来的に店舗が増えたときに、マーケ / オペレーションサイドでも店舗や URL を更新しやすい形にし、かつアプリケーション本体のリリースサイクルとはなるべく切り離せたら良いなとも思っておりました。

そこで出てきた選択肢が店舗 ID → 口コミ URL を「リダイレクト」で解決するという方針です。

解決方針: 店舗ID 付き URL にアクセスしたら CloudFront Functions で口コミ URL へリダイレクト

最終的に採用した構成はざっくり以下です。

  1. braze でメール配信した店舗アンケート内に、店舗ID だけ埋め込まれた URL を載せる
    例: https://example.com/shops/622/review
    (example.com は CloudFront のディストリビューションに対応)

  2. CloudFront Functions で /shops/{店舗ID}/review をハンドリングしてCloudFront KeyValueStore に保存してある店舗 ID (key) → Google Map 口コミ URL (value)を lookup

  3. ヒットすれば口コミ URL に 302 リダイレクト、それ以外は店舗詳細ページへフォールバック
    例: https://karaoke-shin.jp/shop-list/622.html

以上のようなシンプルなリダイレクトレイヤーを、origin の前段に挟むイメージです。

CloudFront Functions & KeyValueStore のざっくり概略

CloudFront Functions とは

Cloudfront Functions は、CloudFront のエッジで動作する超軽量な javascript 実行環境です。

クエスト/レスポンスの加工など短い実行時間で実行可能な処理をエッジで実行します。

AWS のエッジで実行される別サービスである lambda@edge よりもさらに軽量 & 低レイテンシ & 低コストで実行できますが、コードサイズや cpu 時間などにより厳しめの制約があります。

CloudFront KeyValueStore とは

CloudFront KeyValueStore は、CloudFront Functions から参照できる低レイテンシの key-value ストアです。
エッジ関数から、ほぼローカル感覚で読める kv ストアになっています。

特徴としては、CloudFront Functions から kvs.get(key) のような形式で簡単に値を取得できます。
事前に s3 のファイルから import することでまとめて値を登録することが可能です。

今回のように、店舗 ID → 口コミ URL の対応関係をたくさん持ちたい、かつ更新頻度はそこまで高くない、でも新店追加時には柔軟に更新したい、そしてruntime ではただ読むだけでよい(書き込み不要)という要件と非常に相性が良かったため、オプションテーブルを増やす代わりに KeyValueStore で持つ方針にしました。

実装: CloudFront Function で kvs を参照してリダイレクト

/shops/{店舗ID}/review に来たリクエストをリダイレクトする CloudFront Function の実装イメージです。

import cf from 'cloudfront';

// コンソールで直接紐づける場合、IDの指定は不要
const kvs = cf.kvs('hogehoge');

function pad3(code) {
  // "7" → "007", "77" → "077", "622" → "622"
  if (code.length === 1) return '00' + code;
  if (code.length === 2) return '0' + code;
  return code;
}

// handler は CloudFront Functions のエントリポイント
async function handler(event) {
  var request = event.request;
  var uri = request.uri;

  // 先頭・末尾のスラッシュを削って分割
  var parts = uri.replace(/^\/+|\/+$/g, '').split('/');

  // /shops/{code}/review だけを対象にする
  if (parts.length === 3 && parts[0] === 'shops' && parts[2] === 'review') {
    var rawCode = parts[1];
    var paddedCode = pad3(rawCode);

    var targetUrl = null;

    try {
      // kvs から店舗 ID → 口コミ URL をlookup
      // 仕様上 string が返ってくる前提(存在しなければ null / 例外など)
      targetUrl = await kvs.get(paddedCode);
    } catch (e) {
      targetUrl = null;
    }

    // kvs にあれば口コミ url、なければ店舗ページへフォールバック
    var redirectUrl =
      targetUrl || 'https://karaoke-shin.jp/shop-list/' + paddedCode + '.html';

    return {
      statusCode: 302,
      statusDescription: 'Found',
      headers: {
        location: { value: redirectUrl },
        'cache-control': { value: 'no-cache' },
      },
    };
  }

  // それ以外のパスは素通し
  return request;
}

実装中にハマったポイント

ここからは実際に作業していてハマった部分と、どう対応したかのメモです。

1. 関数コードサイズ上限に引っかかった

最初は「店舗 ID → 口コミ URL のマップ」を すべて javascript のオブジェクトとして関数内に書く実装にしていました。

CloudFront Functions には 関数コードサイズの上限(約 40kb) があり、それを超えてしまったため、結果的に kv ストアを選択しました。

2. kvs import のフォーマットは決まっている

以下の形式でないと kv ストアは import できないです。

{
  "data": [
    {
      "key": "622",
      "value": "https://search.google.com/local/writereview?placeid=ChIJWeUhUEUEVzURICx6nhTj6EQ"
    },
    {
      "key": "077",
      "value": "https://search.google.com/local/writereview?placeid=ChIJ3VKDybjoHmARNigk0SAJEhY"
    },
    ,,,

3. function と distribution の紐づけ

CloudFront Function を書いただけでは動かず、対象の distribution の behavior に紐づける必要があります。

  • CloudFront console から対象 distribution を開く
  • 「behaviors」タブから該当の path パターン(/shops/* など)を選択
  • 「function associations」でevent type: viewer-requestを選択、function: 作成した cloudfront functionを選択して保存

これを忘れると、いくら function を更新しても実際のトラフィックには一切影響しません。
自分は忘れていました。

まとめ

CloudFront Functions × KeyValueStore の組み合わせによって、アプリケーション本体の db や schema を増やさずに店舗 ID から口コミ URL 取得するという柔軟な対応が可能になりました。

function コードは薄く保ちながら、データは kvs に押し出せるので、CloudFront Functions のコードサイズ制限も気にならず、新店追加も kvs の更新だけで完結するので、運用もシンプルになりました。

GENDA

Discussion