📊

GCPで作るサーバーレスNPSシステム

2024/12/22に公開

はじめに

この記事はGMOメディア株式会社 Advent Calendar 2024の22日目の記事です。

https://qiita.com/advent-calendar/2024/gmo-media

顧客満足度を測る指標として広く使われているNPS(Net Promoter Score)。
今回はGCPのサーバーレス構成を活用して、NPSデータを収集・保存するシステムを構築しました。

Cloud Run FunctionsとCloud SQLを組み合わせることで、運用コストを抑えつつスケーラブルなシステムを実現できたので、その実装方法をご紹介します。

NPSとは

Net Promoter Score(NPS)は、顧客ロイヤルティを測定するための指標です。

標準的なNPSでは「この製品・サービスを友人や同僚に薦める可能性はどのくらいですか?」という質問に対して、0-10の11段階で回答してもらいます。

回答者は点数によって3つのグループに分類されます。

  • 推奨者(Promoters): 9-10点
  • 中立者(Passives): 7-8点
  • 批判者(Detractors): 0-6点

NPSは以下の式で計算します。

NPSは以下の式で計算します。

NPS = 推奨者の割合(%) - 批判者の割合(%)

結果は-100から+100の間となり、一般的に+50以上が優秀な水準とされます。
ただし、日本では海外と比較して全般的に低めの傾向があるとされています。(らしい)

システムアーキテクチャ

本システムは完全なサーバーレス構成を採用し、以下のGCPサービスを使用しています。

使用したサービス

  1. Cloud Run functions(以前はCloud Functions)

    • フロントエンドからのNPSデータを受け取るAPIエンドポイント
    • Node.jsランタイムを使用
    • HTTPトリガーで起動
  2. Cloud SQL (MySQL)

    • NPSデータの永続化
    • スコア、コメント、タイムスタンプなどを保存
  3. Serverless VPC Connector

    • Cloud Run functionsからCloud SQLへの安全な接続を実現
    • プライベートなネットワーク内での通信を確保
  4. Secret Manager

    • データベース認証情報の安全な管理
    • 環境変数との連携

システムフロー

  1. フロントエンドからCloud Run functionsにデータを送信
  2. Cloud Run functionsがVPC Connector経由でCloud SQLにデータを保存

Cloud Run functionsの実装

バックエンドはCloud Run functionsを使用し、セキュアなAPIエンドポイントを実装するよう心掛けました。

npsHandler
const functions = require('@google-cloud/functions-framework');
const mysql = require('mysql2');

// DB接続設定
function connectToDatabase() {
  return mysql.createConnection({
    host: process.env.SAMPLE_DB_HOST,
    user: process.env.SAMPLE_DB_USER,
    password: process.env.SAMPLE_DB_PASSWORD,
    database: 'sample_nps_db'
  });
}

// Cloud Function
functions.http('npsHandler', async (req, res) => {
  // CORSヘッダー設定
  res.set('Access-Control-Allow-Origin', '*');
  res.set('Access-Control-Allow-Methods', 'POST, OPTIONS');
  res.set('Access-Control-Allow-Headers', 'Content-Type');

  if (req.method === 'OPTIONS') {
    res.status(204).send('');
    return;
  }

  const connection = await connectToDatabase();
  
  try {
    const { score, comment } = req.body;
    
    // バリデーション
    if (typeof score !== 'number' || score < 0 || score > 10) {
      return res.status(400).json({ error: 'Invalid score format' });
    }

    if (comment && typeof comment !== 'string') {
      return res.status(400).json({ error: 'Invalid comment format' });
    }

    // データ挿入
    const now = new Date().toISOString().slice(0, 19).replace('T', ' ');
    const query = `
      INSERT INTO NpsResult 
      (score, comment, createdAt, updatedAt) 
      VALUES (?, ?, ?, ?)
    `;
    
    const [result] = await connection.promise().query(
      query, 
      [score, comment, now, now]
    );

    res.status(201).json({ id: result.insertId });
  } catch (error) {
    console.error('Error details:', error);
    res.status(500).json({ error: 'An unexpected error occurred' });
  } finally {
    connection.end();
  }
});

CloudSQLの作成

Cloud SQLインスタンスはGoogle Cloud コンソールから作成します。社内向けシステムということを考慮し、最小限のスペックで構成しました。

https://cloud.google.com/sql/docs/mysql/create-instance?hl=ja

評価結果を保存するテーブルは、Google Cloud コンソール上のインポート機能を使用してSQLファイルから作成しました。

https://cloud.google.com/sql/docs/mysql/import-export/import-export-sql?hl=ja#import_data_to

createTable.sql
-- CreateTable
CREATE TABLE `NpsResult` (
    `id` INTEGER NOT NULL AUTO_INCREMENT,
    `score` INTEGER NOT NULL DEFAULT 0,
    `comment` VARCHAR(255) NOT NULL DEFAULT '',
    `createdAt` TIMESTAMP(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0),
    `updatedAt` TIMESTAMP(0) NOT NULL,

    PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

Cloud Run functionsとCloud SQLの接続設定

Cloud RunfunctionsからCloud SQLに安全に接続するためには、サーバーレスVPCアクセスコネクタを使用します。VPCコネクタを使用することで、以下のメリットがあります。

  • プライベートネットワークでの通信が可能
  • パブリックインターネットを経由しないセキュアな接続
  • Cloud SQL内部IPアドレスへの直接アクセス

VPCコネクタの作成

まず、以下のコマンドでVPCコネクタを作成します。

gcloud compute networks vpc-access connectors create vpc-connector \
  --network default \
  --region asia-northeast1 \
  --range 10.8.0.0/28

https://cloud.google.com/vpc/docs/serverless-vpc-access?hl=ja

Cloud Run Functionsのデプロイ設定

作成したVPCコネクタを使用してCloud Run Functionsをデプロイします。

gcloud functions deploy nodejs-http-function \
  --gen2 \
  --runtime=nodejs22 \
  --region=REGION \
  --source=. \
  --entry-point=npsHandler \
  --trigger-http 
  --vpc-connector projects/${PROJECT_ID}/locations/asia-northeast1/connectors/vpc-connector \
  --set-secrets SAMPLE_DB_USER=SAMPLE_DB_USER:latest,SAMPLE_DB_PASSWORD=SAMPLE_DB_PASSWORD:latest

https://cloud.google.com/functions/docs/create-deploy-gcloud?hl=ja

各オプションの説明としては、以下の通りです。

  • --gen2:第2世代のCloud Run Functionsを使用
  • --runtime nodejs20:Node.js 20を実行環境として指定
  • --trigger-http:HTTPリクエストで関数を実行できるように設定
  • --region asia-northeast1:デプロイするリージョンを東京に指定
  • --source:現在のディレクトリをソースコードとして指定
  • --entry-point:関数のエントリーポイントとなる関数名を指定
  • --vpc-connector:作成したVPCコネクタを指定
  • --set-secrets:Secret Managerから環境変数として設定する値

実装のポイント

実装にあたって、以下の点で安全性と保守性を重視しました。セキュリティ面だけでなく、データの整合性やエラーハンドリングなど、本番運用を見据えた実装を心がけています。

セキュリティ面

  • VPC Connectorを使用したプライベートな通信
  • プリペアードステートメントによるSQL Injection対策
  • stringifyObjectsによるオブジェクト注入攻撃の防止
  • 入力値の厳密なバリデーション

データの整合性

  • 入力値の適切なバリデーション
  • スコアの範囲チェック(0-10)
  • 必須パラメータの存在確認
  • データ型の検証

エラーハンドリング

  • 適切なHTTPステータスコードの返却
  • try-catch-finallyによる例外処理
  • DB接続の確実なクローズ処理

セキュリティ面に関しては以下の記事を参考にさせていただきました!

https://blog.flatt.tech/entry/node_mysql_sqlinjection

API利用例

ここまでで構築したNPSデータ基盤は、以下のようなReactコンポーネントから利用できます。
実際のプロジェクトではこのような形でフロントエンドを実装し、NPSデータを収集しています。

import React, { useState } from 'react';

interface NPSFormProps {
  onSubmit?: (data: { score: number; comment: string }) => void;
}

const NPSForm: React.FC<NPSFormProps> = ({ onSubmit }) => {
  const [score, setScore] = useState<number | null>(null);
  const [comment, setComment] = useState('');

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();

    if (score === null) return;

    try {
      await fetch('YOUR_CLOUD_FUNCTION_URL', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ score, comment }),
      });
      alert('ご回答ありがとうございました');
    } catch (error) {
      console.error('Error submitting NPS:', error);
      alert('エラーが発生しました');
    }
  };

  return (
    <div>
      <h2>NPSアンケート</h2>
      <form onSubmit={handleSubmit}>
        <div>
          <p>このサービスを友人や同僚に推奨する可能性はどのくらいですか?</p>
          {[...Array(11)].map((_, i) => (
            <button
              key={i}
              type="button"
              onClick={() => setScore(i)}
              className={score === i ? 'selected' : ''}
            >
              {i}
            </button>
          ))}
        </div>
        
        <div>
          <p>ご意見・ご感想をお聞かせください</p>
          <textarea
            value={comment}
            onChange={(e) => setComment(e.target.value)}
            maxLength={200}
          />
        </div>

        <button type="submit" disabled={score === null}>
          送信
        </button>
      </form>
    </div>
  );
};

このコンポーネントでは以下のような実装を行っています。

  • 標準的なNPSの質問形式
  • 0-10の評価スケール
  • 簡単なバリデーション(スコア必須)
  • 基本的なエラーハンドリング

運用コストについて

サーバーレスアーキテクチャを採用したことで、なるべくコストを抑えながら必要な機能を実現できました。

  • Cloud Run FunctionsとCloud SQLは従量課金制で、使用量に応じた料金体系

    • 小規模な利用(月間数千リクエスト程度)であれば、月額1000円程度で運用可能
    • 200万回/月までの無料枠あり(今回は無料枠範囲内でした)
  • Cloud SQLはインスタンスのスペックで料金が変動

    • 現時点では、~$70ぐらいはかかってしまいます。(ここもっと下げたい)
    • 仮に特定のスプレッドシートに書き込むなどすればDBコストは0円になる。
    • が、今後のことやデータ管理を考慮して、素直にRDBを選択しました。

今後の展望

今回の実装では、シンプルな評価スコアとコメントという基本的なデータ構造で実装しましたが、さらなる拡張性を考えると以下のような展望があります。

  1. データ構造の拡張
    現在はスコアとコメントという正規化されたシンプルな構造ですが、JSON型を活用することで、より柔軟なデータ構造を実現できると考えています。
  • 質問項目の動的な追加や変更が容易に
  • 部門や製品ごとに異なる質問セットの管理
  • 回答形式の多様化(選択式、複数回答、スケールなど)
  1. 機能の拡張
  • 投稿時にSlackへ通知する機能の追加
  • 部門別・製品別の分析機能の強化
  1. NPS結果の可視化
  • Looker Studioなどを用いて実際のNPSを計算する

まとめ

GCPのサーバーレスアーキテクチャを活用することで、効率的なNPSシステムを構築できました。特にCloud Run FunctionsとCloud SQLの組み合わせは、運用コストを抑えながらスケーラブルなシステムを実現する上で効果的でした。

必ずしも外部サービスを利用する必要はなく、GCPのサーバーレスサービスを適切に組み合わせることで、要件に合った柔軟なシステムを構築できることが分かりました。

おまけ

収集したデータの可視化を考えた際に、無料で使えるLooker Studioなどが便利ですが、読み込みに時間がかかったりUIを組み立てないといけないので少し面倒ですよね。色々漁ってるとCloud SQL Studioというものを発見!

https://cloud.google.com/sql/docs/mysql/manage-data-using-studio?hl=ja

テーブルを選択ユーザーとパスワードを入力すれば、以下のようなUIでSQLクエリが実行できるようになります!クエリを押すと表示されているクエリが自動作成されたりと便利。

が、画像を見て分かる通り(IAMによりますが)テーブル削除やレコード削除(DELETE)などが実行できるので怖いです。試すとしてもサービス立ち上げ時の初期段階などぐらいですかね。でも、無料(らしい)のでデータ閲覧程度であればとても便利だと思いました!

GMOメディアテックブログ

Discussion