🔐 APIキーを守りながら作る！サーバーレスで実現する「ラク子さんAIチャット」完全ガイド 🚀

# 🚀 サーバーレスで実現！APIキーを守りながらラク子さんチャットをAI化する方法

こんにちは、@YushiYamamotoです！前回作成した「ラク子さんチャットアプリ」に、今回はAI機能を追加していきたいと思います。

https://qiita.com/YushiYamamoto/items/4428181bab1049173a7a


特に「APIキーを公開せずにサーバーレスで実装する方法」について詳しく解説します。
これは多くの開発者が直面する課題ですが、実はいくつかの効果的な解決策があるんです！

## なぜAPIキーを公開してはいけないのか？ ⚠️

AIサービスのAPIキーをクライアントサイドのコードに直接埋め込むと、以下のようなリスクがあります：

- **不正利用のリスク**: 悪意ある第三者がAPIキーを使用して、あなたのアカウントで大量のリクエストを行う可能性があります[6]
- **料金発生のリスク**: 従量課金制のAPIの場合、予期せぬ高額請求が発生する可能性があります[4]
- **セキュリティリスク**: APIキーを持つ人は、そのAPIを通じてサービスにアクセスできるため、データ漏洩のリスクがあります[6]

ブラウザのデベロッパーツールを開けば、JavaScriptコードや通信内容は簡単に閲覧できてしまいます。そのため、クライアントサイドでAPIキーを使用することは避けるべきなのです。

## サーバーレスでAPIキーを保護する方法 🛡️

サーバーレスアーキテクチャを使って、APIキーを安全に管理しながらAIチャット機能を実装する方法をいくつか紹介します。

### 1. Netlify Functions（または Vercel Functions）を活用する方法

Netlifyなどの静的サイトホスティングサービスは、サーバーレス関数を提供しており、APIキーを安全に保管できます。

#### 実装手順

1. プロジェクトの構造を以下のように設定します：

```
プロジェクトルート/
├── functions/
│   └── chat.js      # サーバーレス関数
├── index.html       # メインのHTMLファイル
├── style.css        # CSSファイル
├── script.js        # フロントエンドのJavaScript
└── netlify.toml     # Netlify設定ファイル
```

2. `netlify.toml`ファイルを作成します：

```toml
[build]
  functions = "functions"

[dev]
  functions = "functions"
  publish = "."
```

3. `functions/chat.js`ファイルを作成します：

```javascript
const axios = require('axios');

exports.handler = async function(event, context) {
  // POSTリクエストのみを許可
  if (event.httpMethod !== 'POST') {
    return { statusCode: 405, body: 'Method Not Allowed' };
  }

  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,
      body: JSON.stringify({ response: response.data.choices[0].message.content })
    };
  } catch (error) {
    console.log('エラー詳細:', error);
    return {
      statusCode: 500,
      body: JSON.stringify({ error: '内部サーバーエラーが発生しました' })
    };
  }
};
```

4. フロントエンドの`script.js`を修正します：

```javascript
function respondToMessage(message) {
  // ローディングメッセージを表示
  const loadingId = addBotMessage('考え中...');
  
  // Netlify Functionsにリクエスト
  fetch('/.netlify/functions/chat', {
    method: 'POST',
    body: JSON.stringify({ message: message }),
  })
  .then(response => response.json())
  .then(data => {
    // ローディングメッセージを削除
    document.getElementById(loadingId).remove();
    
    // ボットの応答を表示
    addBotMessage(data.response);
  })
  .catch(error => {
    document.getElementById(loadingId).remove();
    addBotMessage('すみません、エラーが発生しました。後でもう一度お試しください。');
    console.error('Error:', error);
  });
}
```

5. Netlifyダッシュボードで環境変数`OPENAI_API_KEY`を設定します。

### 2. Cloudflare Workersを使用する方法

![スクリーンショット 2025-03-08 22.21.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3637864/8f23a22c-cb9c-456b-8cb6-a530da37ef02.png)

Cloudflare Workersもサーバーレス環境を提供しており、APIキーを安全に管理できます。

#### 実装手順

1. Cloudflare Workersの開発環境をセットアップします：

```bash
npm install -g wrangler
wrangler login
wrangler init chat-api
cd chat-api
```

2. `wrangler.toml`ファイルを編集します：

```toml
name = "chat-api"
main = "src/index.js"
compatibility_date = "2023-05-18"

[vars]
# 開発環境用の変数（本番環境では管理画面で設定）
# OPENAI_API_KEY = "your-api-key"
```

3. `src/index.js`ファイルを作成します：

```javascript
export default {
  async fetch(request, env, ctx) {
    // CORSヘッダーを設定
    const corsHeaders = {
      "Access-Control-Allow-Origin": "*",
      "Access-Control-Allow-Methods": "POST, OPTIONS",
      "Access-Control-Allow-Headers": "Content-Type",
    };

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

    // POSTリクエストのみを許可
    if (request.method !== "POST") {
      return new Response("Method Not Allowed", { status: 405 });
    }

    try {
      const body = await request.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 ${env.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 response.json();
      
      return new Response(
        JSON.stringify({ response: data.choices[0].message.content }),
        {
          headers: {
            ...corsHeaders,
            "Content-Type": "application/json"
          }
        }
      );
    } catch (error) {
      return new Response(
        JSON.stringify({ error: "内部サーバーエラーが発生しました" }),
        {
          status: 500,
          headers: {
            ...corsHeaders,
            "Content-Type": "application/json"
          }
        }
      );
    }
  }
};
```

4. Workerをデプロイします：

```bash
wrangler secret put OPENAI_API_KEY
# プロンプトでAPIキーを入力
wrangler publish
```

5. フロントエンドの`script.js`を修正します：

```javascript
function respondToMessage(message) {
  // ローディングメッセージを表示
  const loadingId = addBotMessage('考え中...');
  
  // Cloudflare Workersにリクエスト
  fetch('https://chat-api.your-username.workers.dev', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ message: message }),
  })
  .then(response => response.json())
  .then(data => {
    // ローディングメッセージを削除
    document.getElementById(loadingId).remove();
    
    // ボットの応答を表示
    addBotMessage(data.response);
  })
  .catch(error => {
    document.getElementById(loadingId).remove();
    addBotMessage('すみません、エラーが発生しました。後でもう一度お試しください。');
    console.error('Error:', error);
  });
}
```

## サーバーレス実装のアーキテクチャ図 📊

サーバーレスでAIチャットを実装する場合のアーキテクチャは以下のようになります：

```
+----------------+      HTTPS       +-------------------+      HTTPS      +----------------+
|                ||                   ||                |
|  ブラウザ       |                  | サーバーレス関数    |                 |   AI API       |
|  (HTML/JS/CSS) |                  | (Netlify/Vercel/  |                 | (OpenAI など)  |
|                |                  |  Cloudflare)      |                 |                |
+----------------+                  +-------------------+                 +----------------+
                                           |
                                           | 環境変数として
                                           | APIキーを保存
                                           v
                                    +----------------+
                                    |                |
                                    |   APIキー      |
                                    |                |
                                    +----------------+
```

この構成では：

1. ユーザーがブラウザでメッセージを入力
2. フロントエンドJSがサーバーレス関数にリクエスト送信
3. サーバーレス関数が環境変数からAPIキーを取得
4. サーバーレス関数がAI APIにリクエスト送信
5. AI APIからの応答をサーバーレス関数が受け取り
6. サーバーレス関数からブラウザに応答を返す

これにより、APIキーはクライアントサイドに公開されることなく、安全に管理できます。

## 実際のラク子さんAIチャットの実装例 👩‍💻

では、前回作成したラク子さんチャットアプリにAI機能を追加してみましょう。今回はNetlify Functionsを使用します。

### 1. 必要なファイルの準備

まず、前回作成したHTMLファイルに少し修正を加えます：

```html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>ラク子さんAIチャット</title>
  <link rel="stylesheet" href="style.css">
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.1/css/all.min.css">
</head>
<body>
  <div class="container">
    <div class="chat-container">
      <div class="user-bar">
        <div class="avatar">
          <img src="https://rakuraku-site.prodouga.com/img/RakuRaku.avif" alt="ラク子さん">
        </div>
        <div class="name">
          <span>ラク子さん</span>
          <span class="status">AI搭載</span>
        </div>
      </div>
      <div class="conversation">
        <div class="conversation-container" id="messages">
          <!-- メッセージがここに表示されます -->
        </div>
      </div>
      <form id="message-form" class="conversation-compose">
        <input id="message-input" class="input-msg" name="input" placeholder="メッセージを入力" autocomplete="off" autofocus>
        <button class="send" type="submit">
          <i class="fa fa-paper-plane"></i>
        </button>
      </form>
    </div>
  </div>
  <script src="script.js"></script>
</body>
</html>
```

### 2. サーバーレス関数の作成

`functions/chat.js`ファイルを作成します：

```javascript
const axios = require('axios');

exports.handler = async function(event, context) {
  if (event.httpMethod !== 'POST') {
    return { statusCode: 405, body: 'Method Not Allowed' };
  }

  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: 'あなたはらくらくサイトのイメージキャラクター「ラク子さん」です。親しみやすく、丁寧な口調で回答してください。らくらくサイトはWEB制作会社が運営するサイトです。'
          },
          {role: 'user', content: userMessage}
        ],
        max_tokens: 200,
        temperature: 0.7
      },
      {
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`
        }
      }
    );

    return {
      statusCode: 200,
      body: JSON.stringify({ 
        response: response.data.choices[0].message.content,
        model: response.data.model
      })
    };
  } catch (error) {
    console.log('エラー詳細:', error.response?.data || error.message);
    return {
      statusCode: 500,
      body: JSON.stringify({ 
        error: '内部サーバーエラーが発生しました',
        details: process.env.NODE_ENV === 'development' ? error.message : undefined
      })
    };
  }
};
```

### 3. フロントエンドJavaScriptの修正

`script.js`ファイルを以下のように修正します：

```javascript
document.addEventListener('DOMContentLoaded', function() {
  const messageForm = document.getElementById('message-form');
  const messageInput = document.getElementById('message-input');
  const messagesContainer = document.getElementById('messages');
  
  // 初期メッセージを表示
  addBotMessage('こんにちは！らくらくサイトのラク子です。何かお手伝いできることはありますか？');
  
  messageForm.addEventListener('submit', function(e) {
    e.preventDefault();
    
    const message = messageInput.value.trim();
    if (message !== '') {
      // ユーザーメッセージを表示
      addUserMessage(message);
      messageInput.value = '';
      
      // ボットの返信を取得
      getAIResponse(message);
    }
  });
  
  function addUserMessage(text) {
    const messageElement = document.createElement('div');
    messageElement.classList.add('message', 'user-message');
    messageElement.textContent = text;
    messagesContainer.appendChild(messageElement);
    scrollToBottom();
  }
  
  function addBotMessage(text, id = null) {
    const messageElement = document.createElement('div');
    if (id) messageElement.id = id;
    messageElement.classList.add('message', 'bot-message');
    messageElement.textContent = text;
    messagesContainer.appendChild(messageElement);
    scrollToBottom();
    return id;
  }
  
  function scrollToBottom() {
    messagesContainer.scrollTop = messagesContainer.scrollHeight;
  }
  
  function getAIResponse(message) {
    // ローディングメッセージを表示
    const loadingId = 'loading-' + Date.now();
    addBotMessage('考え中...', loadingId);
    
    // Netlify Functionsにリクエスト
    fetch('/.netlify/functions/chat', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ message: message }),
    })
    .then(response => response.json())
    .then(data => {
      // ローディングメッセージを削除
      const loadingElement = document.getElementById(loadingId);
      if (loadingElement) loadingElement.remove();
      
      // ボットの応答を表示
      if (data.error) {
        addBotMessage('すみません、エラーが発生しました。後でもう一度お試しください。');
      } else {
        addBotMessage(data.response);
      }
    })
    .catch(error => {
      // ローディングメッセージを削除
      const loadingElement = document.getElementById(loadingId);
      if (loadingElement) loadingElement.remove();
// エラーメッセージを表示
      addBotMessage('すみません、エラーが発生しました。後でもう一度お試しください。');
      console.error('Error:', error);
    });
  }
});
```

### 4. Netlify設定ファイルの作成

プロジェクトのルートに`netlify.toml`ファイルを作成します：

```
[build]
  functions = "functions"
  publish = "."

[dev]
  functions = "functions"
  publish = "."
  
[[redirects]]
  from = "/*"
  to = "/index.html"
  status = 200
```

## ローカル開発環境でのテスト方法 🧪

Netlify CLIを使用すると、ローカル環境でサーバーレス関数をテストできます。

```
# Netlify CLIのインストール
npm install -g netlify-cli

# ローカル環境変数の設定（.envファイルを作成）
echo "OPENAI_API_KEY=your_api_key_here" > .env

# ローカル開発サーバーの起動
netlify dev
```

これにより、ローカル環境でもAPIキーを安全に管理しながら開発できます。

## デプロイ手順 🚢

### 1. GitHubリポジトリの作成

まず、プロジェクトをGitHubリポジトリにプッシュします：

```
git init
git add .
git commit -m "初期コミット：ラク子さんAIチャットアプリ"
git remote add origin https://github.com/yourusername/rakuko-ai-chat.git
git push -u origin main
```

### 2. Netlifyへのデプロイ

1. Netlifyにログインし、「New site from Git」をクリック
2. GitHubを選択し、先ほど作成したリポジトリを選択
3. ビルド設定はデフォルトのままで良い
4. 「Advanced settings」を開き、環境変数`OPENAI_API_KEY`を設定
5. 「Deploy site」をクリック

数分後、サイトがデプロイされ、AIチャット機能が利用可能になります！

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

### 1. APIリクエストの最適化

OpenAIのAPIは使用量に応じて課金されるため、以下の点に注意しましょう：

```
// コスト削減のためのパラメータ調整例
{
  model: 'gpt-3.5-turbo', // 最新モデルより安価
  max_tokens: 150,        // 応答の長さを制限
  temperature: 0.7,       // 創造性と正確性のバランス
  presence_penalty: 0.6,  // 繰り返しを減らす
  frequency_penalty: 0.5  // 同じ単語の繰り返しを減らす
}
```

### 2. キャッシュの活用

よくある質問に対する応答をキャッシュすることで、APIリクエスト数を削減できます：

```
// サーバーレス関数でのキャッシュ実装例
const CACHE_TTL = 3600; // 1時間（秒単位）
const cache = {};

exports.handler = async function(event, context) {
  const body = JSON.parse(event.body);
  const userMessage = body.message;
  
  // キャッシュチェック
  const cacheKey = userMessage.toLowerCase().trim();
  if (cache[cacheKey] && (Date.now() - cache[cacheKey].timestamp) / 1000  userLimit.resetTime) {
    userLimit.count = 1;
    userLimit.resetTime = now + 60000;
    userRateLimits[userId] = userLimit;
    return true;
  }
  
  if (userLimit.count >= 5) {
    return false;
  }
  
  userLimit.count++;
  userRateLimits[userId] = userLimit;
  return true;
}
```

## ラク子さんの個性を引き出すプロンプト設計 🎭

AIにキャラクター性を持たせるためのプロンプト設計も重要です：

```
const systemPrompt = `
あなたはらくらくサイトのイメージキャラクター「ラク子さん」です。
以下の特徴を持つキャラクターとして応答してください：

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

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

// APIリクエスト時に使用
{
  messages: [
    { role: 'system', content: systemPrompt },
    { role: 'user', content: userMessage }
  ],
  // その他のパラメータ...
}
```

## セキュリティ強化のポイント 🔒

### 1. CORSの設定

APIエンドポイントへの不正アクセスを防ぐため、CORSを適切に設定します：

```
// Netlify Functionsでの例
exports.handler = async function(event, context) {
  // 許可するオリジンを指定
  const allowedOrigins = [
    'https://your-site.netlify.app',
    'http://localhost:8888'
  ];
  
  const origin = event.headers.origin;
  const corsHeaders = {
    'Access-Control-Allow-Origin': allowedOrigins.includes(origin) ? origin : allowedOrigins,
    'Access-Control-Allow-Headers': 'Content-Type',
    'Access-Control-Allow-Methods': 'POST, OPTIONS'
  };
  
  // OPTIONSリクエスト（プリフライト）の処理
  if (event.httpMethod === 'OPTIONS') {
    return {
      statusCode: 204,
      headers: corsHeaders,
      body: ''
    };
  }
  
  // 以下、通常の処理...
  // ...
  
  return {
    statusCode: 200,
    headers: corsHeaders,
    body: JSON.stringify({ /* レスポンスデータ */ })
  };
};
```

### 2. 入力検証

ユーザー入力を適切に検証し、悪意のあるプロンプトを防ぎます：

```
function validateUserInput(input) {
  // 空の入力をチェック
  if (!input || input.trim() === '') {
    return { valid: false, reason: '入力が空です' };
  }
  
  // 長すぎる入力をチェック
  if (input.length > 500) {
    return { valid: false, reason: '入力が長すぎます（500文字以内）' };
  }
  
  // 悪意のあるプロンプトのパターンをチェック
  const suspiciousPatterns = [
    'system:',
    'ignore previous instructions',
    'forget your role'
  ];
  
  for (const pattern of suspiciousPatterns) {
    if (input.toLowerCase().includes(pattern)) {
      return { valid: false, reason: '不適切な入力が含まれています' };
    }
  }
  
  return { valid: true };
}
```

## 実際の動作イメージ 📱

完成したラク子さんAIチャットアプリは、以下のような会話ができるようになります：

```
ユーザー: こんにちは、ラク子さん！

ラク子さん: こんにちは！らく〜♪ らくらくサイトのラク子です😊
何かお手伝いできることはありますか？Webサイト制作のこと、
HTMLやCSSのことなど、お気軽にお尋ねくださいね！

ユーザー: JavaScriptでチャットボットを作るにはどうしたらいいですか？

ラク子さん: JavaScriptでチャットボットを作るなら、らく〜に始められる方法をご紹介します！😊

基本的な手順はこんな感じです：

1. HTMLでチャットUIを作成（メッセージ表示エリアと入力フォーム）
2. CSSでデザインを整える
3. JavaScriptで機能を実装

特に実装では以下のポイントがあります：
- ユーザー入力の取得と表示
- 応答生成のロジック
- メッセージの表示処理

簡単なAI機能を付けたい場合は、OpenAIのAPIなどを使うと良いですよ！
もっと詳しく知りたい部分はありますか？💻✨
```

## まとめ：サーバーレスAIチャットの可能性 🌟

今回は、APIキーを公開せずにサーバーレスでAIチャット機能を実装する方法を紹介しました。この方法のメリットは：

1. **セキュリティ**: APIキーが安全に保管される
2. **コスト効率**: サーバーレスなので使用量に応じた課金のみ
3. **スケーラビリティ**: トラフィック増加にも自動的に対応
4. **メンテナンス性**: インフラ管理の手間が少ない

これらの技術を組み合わせることで、安全で高機能なAIチャットアプリケーションを簡単に構築できます。

今後の発展としては、以下のような機能追加も考えられます：

- 会話履歴の保存と継続
- 音声入力・出力機能
- 画像認識機能の統合
- 多言語対応

皆さんも是非、自分だけのAIキャラクターチャットを作ってみてください！質問やコメントがあれば、お気軽にどうぞ。

それでは、楽しいコーディングライフを！👨‍💻✨



---

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

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

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

👉 [ポートフォリオ](https://yushiyamamoto.vercel.app/)

🌳 [らくらくサイト](https://rakuraku-site.prodouga.com/)