0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

🔒 APIキーを守りながら作る!AWS AmplifyとDeno Deployで実現する最新サーバーレスAIチャット構築術 🚀

Posted at

🚀 サーバーレス実装の選択肢を広げよう!AWS AmplifyとDeno Deployで作るセキュアなAPIバックエンド

こんにちは、@YushiYamamotoです!前回の記事では、Netlify FunctionsとCloudflare Workersを使ったサーバーレス実装について解説しました。

今回は残りの2つの選択肢、「AWS Amplify + Lambda」と「GitHub Pages + Deno Deploy」について詳しく見ていきましょう。どちらも強力なサーバーレス環境を提供してくれますが、それぞれに特徴があります。

APIキーを公開せずにフロントエンドアプリケーションを構築する方法として、これらのサービスがどのように役立つのか、実際のコード例を交えながら解説していきます。それでは早速始めましょう!

AWS Amplify + Lambda でAPIキーを安全に管理する 🔐

AWS Amplifyは、フロントエンド開発者がAWSのバックエンドサービスを簡単に利用できるようにするためのフレームワークです。AmplifyとLambdaを組み合わせることで、APIキーを安全に管理しながらサーバーレスアプリケーションを構築できます。

AWS Amplifyの主な特徴 ✨

  • 統合開発環境: フロントエンドとバックエンドを同じプロジェクトで管理
  • TypeScriptサポート: 型安全なコードを書ける
  • 認証機能: Amazon Cognitoとの統合
  • CI/CD: GitHubなどと連携した継続的デプロイ
  • スケーラビリティ: AWSインフラの強力なスケーリング機能

実装手順

1. 環境のセットアップ

まずはAWS Amplify CLIをインストールし、初期化します:

# Amplify CLIのインストール
npm install -g @aws-amplify/cli

# Amplifyの初期化
amplify init

プロジェクト名やフレームワークなどの質問に答えると、Amplifyプロジェクトが初期化されます。

2. Lambda関数の追加

次に、APIリクエストを処理するLambda関数を追加します:

amplify add function

対話式のプロンプトで以下のように設定します:

  • 関数名: chatFunction
  • ランタイム: NodeJS
  • テンプレート: Hello World

これにより、amplify/backend/function/chatFunction/src/index.jsが作成されます。このファイルを編集して、OpenAI APIを呼び出す処理を実装します:

const axios = require('axios');

exports.handler = async (event) => {
  try {
    // リクエストボディの解析
    const body = JSON.parse(event.body);
    const userMessage = body.message;
    
    // OpenAI APIにリクエスト
    const response = await axios.post(
      'https://api.openai.com/v1/chat/completions',
      {
        model: 'gpt-3.5-turbo',
        messages: [
          {role: 'system', content: 'あなたはラク子さんというキャラクターです。親しみやすく、丁寧な口調で回答してください。'},
          {role: 'user', content: userMessage}
        ],
        max_tokens: 150,
        temperature: 0.7
      },
      {
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`
        }
      }
    );
    
    return {
      statusCode: 200,
      headers: {
        "Access-Control-Allow-Origin": "*",
        "Access-Control-Allow-Headers": "*"
      },
      body: JSON.stringify({ 
        response: response.data.choices[0].message.content 
      }),
    };
  } catch (error) {
    console.log('エラー:', error);
    return {
      statusCode: 500,
      headers: {
        "Access-Control-Allow-Origin": "*",
        "Access-Control-Allow-Headers": "*"
      },
      body: JSON.stringify({ error: '内部サーバーエラーが発生しました' }),
    };
  }
};

3. 環境変数の設定

Lambda関数に環境変数を追加します:

amplify function update
# 関数を選択し、環境変数を追加するオプションを選びます
# OPENAI_API_KEY=your_api_key_here を追加

4. API Gatewayの追加

Lambda関数をAPIとして公開するために、API Gatewayを追加します:

amplify add api

以下のように設定します:

  • APIタイプ: REST
  • リソース名: chatapi
  • パス: /chat
  • Lambda関数: chatFunction
  • 認証タイプ: API Key

5. フロントエンドコードの修正

フロントエンドからAPIを呼び出すコードを実装します。まず、Amplifyライブラリをインストールします:

npm install aws-amplify

次に、Amplifyの設定とAPI呼び出しのコードを実装します:

import { Amplify, API } from 'aws-amplify';
import awsconfig from './aws-exports';
Amplify.configure(awsconfig);

async function sendMessage(message) {
  // ローディングメッセージを表示
  const loadingId = addBotMessage('考え中...');
  
  try {
    // API Gatewayを通じてLambda関数を呼び出し
    const response = await API.post('chatapi', '/chat', {
      body: {
        message: message
      }
    });
    
    // ローディングメッセージを削除
    document.getElementById(loadingId).remove();
    
    // ボットの応答を表示
    addBotMessage(response.response);
  } catch (error) {
    document.getElementById(loadingId).remove();
    addBotMessage('すみません、エラーが発生しました。後でもう一度お試しください。');
    console.error('Error:', error);
  }
}

6. デプロイ

最後に、Amplifyプロジェクトをデプロイします:

amplify push

これにより、Lambda関数とAPI Gatewayがデプロイされます。さらに、フロントエンドをAmplify Hostingでホストする場合は:

amplify add hosting
amplify publish

AWS Amplify + Lambdaのアーキテクチャ図

+----------------+      HTTPS       +----------------+      AWS      +----------------+
|                ||                ||                |
|  フロントエンド  |                  |  API Gateway   |              |  Lambda関数    |
|  (Amplify      |                  |                |              |                |
|   Hosting)     |                  |                |              |                |
+----------------+                  +----------------+              +----------------+
                                                                          |
                                                                          | 環境変数
                                                                          v
                                                                    +----------------+
                                                                    |                |
                                                                    |  OPENAI_API_KEY|
                                                                    |                |
                                                                    +----------------+

GitHub Pages + Deno Deploy でモダンなサーバーレス環境を構築 🦕

GitHub Pagesは静的サイトのホスティングに最適で、Deno Deployはモダンなサーバーレス環境を提供します。この組み合わせは、特に小規模なプロジェクトや個人開発に最適です。

Deno Deployの主な特徴 ✨

  • モダンなJavaScript/TypeScript環境: ES ModulesやTypeScriptをネイティブサポート
  • グローバルエッジネットワーク: 世界中のエッジロケーションでコードを実行
  • 高速な起動時間: V8 Isolateベースの軽量実行環境
  • GitHub連携: GitHubリポジトリと直接連携可能
  • 無料枠が充実: 個人開発や小規模プロジェクトに最適

実装手順

1. Denoのインストール

まずはDenoをインストールします:

# macOS/Linux
curl -fsSL https://deno.land/install.sh | sh

# Windows (PowerShell)
irm https://deno.land/install.ps1 | iex

2. Deno Deployのセットアップ

Deno Deployで使用するサーバーレス関数を作成します。server.tsファイルを作成し、以下のコードを記述します:

// server.ts
import { serve } from "https://deno.land/std@0.140.0/http/server.ts";

// 環境変数からAPIキーを取得
const OPENAI_API_KEY = Deno.env.get("OPENAI_API_KEY");

async function handler(req: Request): Promise {
  // CORSヘッダー
  const headers = {
    "Content-Type": "application/json",
    "Access-Control-Allow-Origin": "*",
    "Access-Control-Allow-Methods": "POST, OPTIONS",
    "Access-Control-Allow-Headers": "Content-Type",
  };

  // OPTIONSリクエスト(プリフライト)の処理
  if (req.method === "OPTIONS") {
    return new Response(null, { headers });
  }

  if (req.method !== "POST") {
    return new Response(JSON.stringify({ error: "Method Not Allowed" }), {
      status: 405,
      headers,
    });
  }

  try {
    const body = await req.json();
    const userMessage = body.message;

    const openaiResponse = await fetch("https://api.openai.com/v1/chat/completions", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "Authorization": `Bearer ${OPENAI_API_KEY}`,
      },
      body: JSON.stringify({
        model: "gpt-3.5-turbo",
        messages: [
          {role: "system", content: "あなたはラク子さんというキャラクターです。親しみやすく、丁寧な口調で回答してください。"},
          {role: "user", content: userMessage}
        ],
        max_tokens: 150,
        temperature: 0.7,
      }),
    });

    const data = await openaiResponse.json();
    return new Response(
      JSON.stringify({ response: data.choices[0].message.content }),
      { headers }
    );
  } catch (error) {
    return new Response(
      JSON.stringify({ error: "内部サーバーエラーが発生しました" }),
      { status: 500, headers }
    );
  }
}

serve(handler);

3. ローカルでのテスト

作成したサーバーをローカルで実行してテストします:

deno run --allow-net --allow-env server.ts

これでhttp://localhost:8000にサーバーが起動します。

4. Deno Deployへのデプロイ

Deno Deployにデプロイするには、まずdeployctlをインストールします:

deno install -A jsr:@deno/deployctl --global

次に、Deno Deployのウェブサイトにアクセスし、新しいプロジェクトを作成します。そして、以下のコマンドでデプロイします:

deployctl deploy --project=your-project-name server.ts

または、GitHubリポジトリと連携してデプロイすることもできます:

  1. Deno Deployのダッシュボードで「New Project」をクリック
  2. GitHubリポジトリを選択
  3. デプロイ設定を構成(エントリーポイントとしてserver.tsを指定)
  4. 「Deploy」をクリック

5. 環境変数の設定

Deno Deployのダッシュボードで、プロジェクトの「Settings」タブから環境変数OPENAI_API_KEYを設定します。

6. GitHub Pagesのセットアップ

フロントエンドをGitHub Pagesでホストするために、リポジトリに.github/workflows/deploy.ymlファイルを作成します:

name: Deploy to GitHub Pages

on:
  push:
    branches: [ main ]

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Setup Node.js
        uses: actions/setup-node@v2
        with:
          node-version: '16'

      - name: Install dependencies
        run: npm ci

      - name: Build
        run: npm run build

      - name: Deploy
        uses: JamesIves/github-pages-deploy-action@4.1.4
        with:
          branch: gh-pages
          folder: dist

7. フロントエンドコードの修正

フロントエンドからDeno Deployのエンドポイントを呼び出すコードを実装します:

async function sendMessage(message) {
  // ローディングメッセージを表示
  const loadingId = 'loading-' + Date.now();
  addBotMessage('考え中...', loadingId);
  
  try {
    // Deno Deployにリクエスト
    const response = await fetch('https://your-project-name.deno.dev', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ message: message }),
    });
    
    const data = await response.json();
    
    // ローディングメッセージを削除
    document.getElementById(loadingId).remove();
    
    // ボットの応答を表示
    if (data.error) {
      addBotMessage('すみません、エラーが発生しました。後でもう一度お試しください。');
    } else {
      addBotMessage(data.response);
    }
  } catch (error) {
    // ローディングメッセージを削除
    document.getElementById(loadingId).remove();
    addBotMessage('すみません、エラーが発生しました。後でもう一度お試しください。');
    console.error('Error:', error);
  }
}

function addBotMessage(text, id = null) {
  const messageElement = document.createElement('div');
  if (id) messageElement.id = id;
  messageElement.classList.add('message', 'bot-message');
  messageElement.textContent = text;
  document.getElementById('messages').appendChild(messageElement);
  // 最新のメッセージが見えるようにスクロール
  document.getElementById('messages').scrollTop = document.getElementById('messages').scrollHeight;
  return id;
}

GitHub Pages + Deno Deployのアーキテクチャ図

+----------------+      HTTPS       +----------------+      HTTPS      +----------------+
|                ||                ||                |
|  GitHub Pages  |                  |  Deno Deploy   |                 |   OpenAI API   |
|  (静的ホスティング)|                  |  (サーバーレス関数)|                 |                |
|                |                  |                |                 |                |
+----------------+                  +----------------+                 +----------------+
                                           |
                                           | 環境変数
                                           v
                                    +----------------+
                                    |                |
                                    |  OPENAI_API_KEY|
                                    |                |
                                    +----------------+

AWS Amplify vs Deno Deploy: どちらを選ぶべき? 🤔

それぞれのサービスには長所と短所があります。以下の比較表を参考にして、プロジェクトに最適な選択をしましょう:

特徴 AWS Amplify + Lambda GitHub Pages + Deno Deploy
学習曲線 やや急(AWSの知識が必要) 比較的緩やか
スケーラビリティ 非常に高い 高い
コールドスタート 数百ミリ秒〜数秒 数十ミリ秒
料金体系 従量課金(無料枠あり) 無料枠が充実
エコシステム 豊富(AWSサービス連携) シンプル
TypeScriptサポート 良好 ネイティブサポート
デプロイ速度 数分 数秒
開発体験 統合環境(CLI) モダンでシンプル

おすすめの使い分け

  • AWS Amplify + Lambda

    • 大規模なプロジェクト
    • 複雑なバックエンド処理が必要
    • 他のAWSサービスと連携したい
    • エンタープライズレベルのセキュリティが必要
  • GitHub Pages + Deno Deploy

    • 小〜中規模のプロジェクト
    • 高速な開発サイクルを重視
    • モダンなJavaScript/TypeScriptを使いたい
    • コストを最小限に抑えたい

パフォーマンスとコスト最適化のポイント 💰

どちらの方法を選んでも、パフォーマンスとコストを最適化するためのポイントがあります:

1. キャッシュの活用

頻繁に同じ質問が来る場合は、レスポンスをキャッシュすることでAPIコールを削減できます:

// Deno Deployでのキャッシュ実装例
const cache = new Map();
const CACHE_TTL = 3600000; // 1時間(ミリ秒)

async function handler(req: Request): Promise {
  // 前略...
  
  try {
    const body = await req.json();
    const userMessage = body.message;
    
    // キャッシュキーの生成(単純な例)
    const cacheKey = userMessage.toLowerCase().trim();
    
    // キャッシュチェック
    const cachedResponse = cache.get(cacheKey);
    if (cachedResponse && (Date.now() - cachedResponse.timestamp) ();

function checkRateLimit(ip: string): boolean {
  const now = Date.now();
  const limit = rateLimits.get(ip) || { count: 0, resetTime: now + 60000 };
  
  // 1分間に10回までのリクエストを許可
  if (now > limit.resetTime) {
    limit.count = 1;
    limit.resetTime = now + 60000;
    rateLimits.set(ip, limit);
    return true;
  }
  
  if (limit.count >= 10) {
    return false;
  }
  
  limit.count++;
  rateLimits.set(ip, limit);
  return true;
}

async function handler(req: Request): Promise {
  const headers = {/* CORSヘッダー */};
  
  // クライアントIPの取得(実際の実装はサービスによって異なる)
  const ip = req.headers.get("x-forwarded-for") || "unknown";
  
  // レート制限のチェック
  if (!checkRateLimit(ip)) {
    return new Response(
      JSON.stringify({ error: "レート制限を超えました。しばらく待ってから再試行してください。" }),
      { status: 429, headers }
    );
  }
  
  // 通常の処理を続行...
}

実装例:Deno Deployで作るラク子さんチャットAPI 👩‍💻

では、Deno Deployを使って実際にラク子さんチャットAPIを実装してみましょう。以下は完全な実装例です:

// rakuko_chat_api.ts
import { serve } from "https://deno.land/std@0.140.0/http/server.ts";

// 環境変数からAPIキーを取得
const OPENAI_API_KEY = Deno.env.get("OPENAI_API_KEY");

// シンプルなメモリキャッシュ
const cache = new Map<string, { response: string; timestamp: number }>();
const CACHE_TTL = 3600000; // 1時間(ミリ秒)

// レート制限用のマップ
const rateLimits = new Map<string, { count: number; resetTime: number }>();

// レート制限をチェックする関数
function checkRateLimit(ip: string): boolean {
  const now = Date.now();
  const limit = rateLimits.get(ip) || { count: 0, resetTime: now + 60000 };
  
  // 1分間に5回までのリクエストを許可
  if (now > limit.resetTime) {
    limit.count = 1;
    limit.resetTime = now + 60000;
    rateLimits.set(ip, limit);
    return true;
  }
  
  if (limit.count >= 5) {
    return false;
  }
  
  limit.count++;
  rateLimits.set(ip, limit);
  return true;
}

// ラク子さんのシステムプロンプト
const RAKUKO_SYSTEM_PROMPT = `
あなたはらくらくサイトのイメージキャラクター「ラク子さん」です。
以下の特徴を持つキャラクターとして応答してください:

- 20代前半の女性
- 明るく親しみやすい性格
- 丁寧だが堅苦しくない口調(「です・ます」調)
- 時々「らく〜」という口癖を使う
- Web制作に関する知識が豊富
- ユーザーを応援する姿勢
- 絵文字を適度に使用する

らくらくサイトはWeb制作会社が運営するサイトで、HTML/CSS/JavaScriptなどの技術情報を発信しています。
`;

async function handler(req: Request): Promise<Response> {
  // CORSヘッダー
  const headers = {
    "Content-Type": "application/json",
    "Access-Control-Allow-Origin": "*",
    "Access-Control-Allow-Methods": "POST, OPTIONS",
    "Access-Control-Allow-Headers": "Content-Type",
  };

  // OPTIONSリクエスト(プリフライト)の処理
  if (req.method === "OPTIONS") {
    return new Response(null, { headers });
  }

  // POSTメソッド以外は拒否
  if (req.method !== "POST") {
    return new Response(
      JSON.stringify({ error: "Method Not Allowed" }),
      { status: 405, headers }
    );
  }

  try {
    // クライアントIPの取得
    const ip = req.headers.get("x-forwarded-for") || "unknown";
    
    // レート制限のチェック
    if (!checkRateLimit(ip)) {
      return new Response(
        JSON.stringify({ error: "レート制限を超えました。しばらく待ってから再試行してください。" }),
        { status: 429, headers }
      );
    }
    
    // リクエストボディの解析
    const body = await req.json();
    const userMessage = body.message;
    
    // 入力検証
    if (!userMessage || typeof userMessage !== "string" || userMessage.trim() === "") {
      return new Response(
        JSON.stringify({ error: "メッセージを入力してください" }),
        { status: 400, headers }
      );
    }
    
    // 長すぎる入力を制限
    if (userMessage.length > 500) {
      return new Response(
        JSON.stringify({ error: "メッセージは500文字以内にしてください" }),
        { status: 400, headers }
      );
    }
    
    // キャッシュキーの生成
    const cacheKey = userMessage.toLowerCase().trim();
    
    // キャッシュチェック
    const cachedResponse = cache.get(cacheKey);
    if (cachedResponse && (Date.now() - cachedResponse.timestamp) < CACHE_TTL) {
      console.log("Cache hit:", cacheKey);
      return new Response(
        JSON.stringify({ response: cachedResponse.response, cached: true }),
        { headers }
      );
    }
    
    // OpenAI APIにリクエスト
    const openaiResponse = await fetch("https://api.openai.com/v1/chat/completions", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "Authorization": `Bearer ${OPENAI_API_KEY}`,
      },
      body: JSON.stringify({
        model: "gpt-3.5-turbo",
        messages: [
          { role: "system", content: RAKUKO_SYSTEM_PROMPT },
          { role: "user", content: userMessage }
        ],
        max_tokens: 200,
        temperature: 0.7,
      }),
    });
    
    // APIレスポンスのチェック
    if (!openaiResponse.ok) {
      const errorData = await openaiResponse.json();
      console.error("OpenAI API Error:", errorData);
      return new Response(
        JSON.stringify({ error: "AI応答の取得中にエラーが発生しました" }),
        { status: 502, headers }
      );
    }

    const data = await openaiResponse.json();
    const responseText = data.choices[0].message.content;
    
    // レスポンスをキャッシュに保存
    cache.set(cacheKey, {
      response: responseText,
      timestamp: Date.now()
    });
    
    // 成功レスポンスを返す
    return new Response(
      JSON.stringify({ 
        response: responseText,
        model: data.model,
        usage: data.usage
      }),
      { headers }
    );
  } catch (error) {
    console.error("Server Error:", error);
    return new Response(
      JSON.stringify({ error: "内部サーバーエラーが発生しました" }),
      { status: 500, headers }
    );
  }
}

// サーバーの起動
console.log("ラク子さんチャットAPIサーバーを起動中...");
serve(handler);

デプロイと運用のベストプラクティス 🚀

サーバーレスアプリケーションを本番環境で運用する際のベストプラクティスをいくつか紹介します:

1. モニタリングとロギング

サーバーレス環境でのデバッグは難しいため、適切なモニタリングとロギングが重要です:

  • AWS Lambda: CloudWatchでログとメトリクスを監視
  • Deno Deploy: 組み込みのログ機能を活用し、必要に応じて外部サービス(Sentry、Datadog)と連携
// Deno Deployでの構造化ログの例
function logInfo(message: string, data?: any) {
  console.log(JSON.stringify({
    level: "info",
    timestamp: new Date().toISOString(),
    message,
    data
  }));
}

function logError(message: string, error: any) {
  console.error(JSON.stringify({
    level: "error",
    timestamp: new Date().toISOString(),
    message,
    error: error.message,
    stack: error.stack
  }));
}

// 使用例
try {
  // 処理
} catch (error) {
  logError("API呼び出し中にエラーが発生しました", error);
}

2. 段階的デプロイ

本番環境に変更を適用する前に、ステージング環境でテストすることをお勧めします:

  • AWS Amplify: 環境変数を使い分けた複数の環境(開発、ステージング、本番)
  • Deno Deploy: プロジェクトごとに開発用とプロダクション用のインスタンスを分ける

3. 自動テスト

サーバーレス関数のテストは、ローカル環境で行うことができます:

// Denoでのテスト例(test.ts)
import { assertEquals } from "https://deno.land/std@0.140.0/testing/asserts.ts";

// モック関数
const mockFetch = async (url: string, options: any) => {
  return {
    ok: true,
    json: async () => ({
      choices: [{ message: { content: "テスト応答" } }],
      model: "gpt-3.5-turbo",
      usage: { total_tokens: 10 }
    })
  };
};

// テスト
Deno.test("チャットAPIが正しく応答を返すこと", async () => {
  // グローバルfetch関数をモックに置き換え
  const originalFetch = globalThis.fetch;
  globalThis.fetch = mockFetch as any;
  
  // テスト対象の関数を呼び出し
  const response = await fetch("http://localhost:8000", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ message: "テストメッセージ" })
  });
  
  const data = await response.json();
  
  // アサーション
  assertEquals(data.response, "テスト応答");
  
  // 元のfetch関数を復元
  globalThis.fetch = originalFetch;
});

AWS Lambdaの場合は、AWS SAM Local を使用してローカルでテストできます。

4. セキュリティ対策

サーバーレスアプリケーションのセキュリティを強化するためのポイント:

  • 入力検証: すべてのユーザー入力を検証する
  • 最小権限の原則: 関数に必要最小限の権限のみを付与する
  • シークレット管理: 環境変数やシークレット管理サービスを使用する
  • 依存関係の脆弱性スキャン: 定期的に依存パッケージをスキャンする
// 入力検証の例
function validateInput(input: any): { valid: boolean; error?: string } {
  if (!input || typeof input !== "object") {
    return { valid: false, error: "無効なリクエスト形式です" };
  }
  
  if (!input.message || typeof input.message !== "string") {
    return { valid: false, error: "メッセージは文字列である必要があります" };
  }
  
  if (input.message.length > 500) {
    return { valid: false, error: "メッセージは500文字以内にしてください" };
  }
  
  // 悪意のあるプロンプトのパターンをチェック
  const suspiciousPatterns = [
    "system:",
    "ignore previous instructions",
    "forget your role"
  ];
  
  for (const pattern of suspiciousPatterns) {
    if (input.message.toLowerCase().includes(pattern)) {
      return { valid: false, error: "不適切な入力が含まれています" };
    }
  }
  
  return { valid: true };
}

サーバーレス実装の発展的なトピック 🔍

1. WebSocketsとリアルタイム通信

長時間実行されるチャットセッションでは、WebSocketsを使用したリアルタイム通信が有効です:

  • AWS: API Gateway + WebSocket API + Lambda
  • Deno Deploy: 標準のWebSocketサポート
// Deno DeployでのWebSocket実装例
import { serve } from "https://deno.land/std@0.140.0/http/server.ts";

const clients = new Map();

function handleWebSocket(socket: WebSocket) {
  const clientId = crypto.randomUUID();
  clients.set(clientId, socket);
  
  socket.onopen = () => {
    console.log(`クライアント接続: ${clientId}`);
  };
  
  socket.onmessage = async (event) => {
    try {
      const data = JSON.parse(event.data);
      
      // メッセージ処理
      // ...
      
      // 応答を送信
      socket.send(JSON.stringify({ type: "response", response: "応答メッセージ" }));
    } catch (error) {
      console.error("メッセージ処理エラー:", error);
      socket.send(JSON.stringify({ type: "error", error: "メッセージの処理中にエラーが発生しました" }));
    }
  };
  
  socket.onclose = () => {
    console.log(`クライアント切断: ${clientId}`);
    clients.delete(clientId);
  };
  
  socket.onerror = (error) => {
    console.error(`WebSocketエラー: ${clientId}`, error);
    clients.delete(clientId);
  };
}

serve((req) => {
  const upgrade = req.headers.get("upgrade") || "";
  
  if (upgrade.toLowerCase() === "websocket") {
    const { socket, response } = Deno.upgradeWebSocket(req);
    handleWebSocket(socket);
    return response;
  }
  
  // 通常のHTTPリクエスト処理
  // ...
});

2. ストリーミングレスポンス

OpenAI APIはストリーミングレスポンスをサポートしており、これを活用することで、より自然な会話体験を提供できます:

// Deno Deployでのストリーミングレスポンスの実装例
async function handleStreamingChat(req: Request): Promise {
  try {
    const body = await req.json();
    const userMessage = body.message;
    
    // OpenAI APIにストリーミングリクエスト
    const response = await fetch("https://api.openai.com/v1/chat/completions", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "Authorization": `Bearer ${Deno.env.get("OPENAI_API_KEY")}`,
      },
      body: JSON.stringify({
        model: "gpt-3.5-turbo",
        messages: [
          { role: "system", content: RAKUKO_SYSTEM_PROMPT },
          { role: "user", content: userMessage }
        ],
        max_tokens: 200,
        temperature: 0.7,
        stream: true // ストリーミングを有効化
      }),
    });
    
    // ReadableStreamを作成してクライアントに返す
    const stream = new ReadableStream({
      async start(controller) {
        const reader = response.body?.getReader();
        if (!reader) {
          controller.close();
          return;
        }
        
        const decoder = new TextDecoder();
        let buffer = "";
        
        try {
          while (true) {
            const { done, value } = await reader.read();
            if (done) break;
            
            buffer += decoder.decode(value, { stream: true });
            
            // データチャンクを処理
            const lines = buffer.split("\n");
            buffer = lines.pop() || "";
            
            for (const line of lines) {
              if (line.startsWith("data: ") && line !== "data: [DONE]") {
                const data = JSON.parse(line.substring(6));
                const content = data.choices?.delta?.content || "";
                if (content) {
                  controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify({ content })}\n\n`));
                }
              } else if (line === "data: [DONE]") {
                controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n"));
              }
            }
          }
        } catch (error) {
          console.error("ストリーミングエラー:", error);
          controller.error(error);
        } finally {
          controller.close();
        }
      }
    });
    
    return new Response(stream, {
      headers: {
        "Content-Type": "text/event-stream",
        "Cache-Control": "no-cache",
        "Connection": "keep-alive",
        "Access-Control-Allow-Origin": "*",
      },
    });
  } catch (error) {
    console.error("エラー:", error);
    return new Response(
      JSON.stringify({ error: "内部サーバーエラーが発生しました" }),
      { status: 500, headers: { "Content-Type": "application/json" } }
    );
  }
}

まとめ:どのサーバーレス実装を選ぶべきか 🤔

今回紹介した4つのサーバーレス実装方法(Netlify Functions、Cloudflare Workers、AWS Amplify + Lambda、GitHub Pages + Deno Deploy)はそれぞれに特徴があります。プロジェクトの要件に合わせて最適な選択をしましょう。

初心者におすすめの選択肢

もし今回が初めてのサーバーレス実装なら、Netlify Functions または GitHub Pages + Deno Deploy がおすすめです。これらは学習曲線が緩やかで、すぐに始められます。

大規模プロジェクトにおすすめの選択肢

エンタープライズレベルのプロジェクトや、将来的に大規模な拡張を予定している場合は、AWS Amplify + Lambda または Cloudflare Workers が適しています。特にAWSは豊富なサービス連携が魅力です。

コスト効率を重視する場合

コスト効率を最も重視するなら、GitHub Pages + Deno Deploy が最適です。両方とも無料枠が充実しており、小〜中規模のプロジェクトであれば、ほとんどコストをかけずに運用できます。

パフォーマンスを重視する場合

エッジでの高速な処理を重視するなら、Cloudflare Workers または Deno Deploy が優れています。どちらもグローバルなエッジネットワークを持ち、低レイテンシーを実現します。

最後に:サーバーレス実装のこれから 🔮

サーバーレス技術は急速に進化しており、今後もさらに使いやすく、強力になっていくでしょう。特にAIとの連携においては、APIキーを安全に管理しながら、高性能なアプリケーションを構築できる点が大きな魅力です。

今回紹介した方法を活用して、ぜひ自分だけのAIチャットアプリケーションを作ってみてください。技術的な課題があれば、コメント欄でお気軽に質問してくださいね!

それでは、楽しいサーバーレス開発ライフを!👨‍💻✨


最後に:業務委託のご相談を承ります

私は、業務委託エンジニアとしてWEB制作やシステム開発を請け負っています。最新技術を活用し、レスポンシブなWebサイトやインタラクティブなアプリケーション、API連携など、幅広いニーズに対応可能です。

「課題解決に向けた即戦力が欲しい」「高品質なWeb制作を依頼したい」という方は、ぜひお気軽にご相談ください。一緒にビジネスの成長を目指しましょう!

👉 ポートフォリオ

🌳 らくらくサイト

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?