0
1

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キヌを守りながら䜜るサヌバヌレスで実珟する「ラク子さんAIチャット」完党ガむド 🚀

Last updated at Posted at 2025-03-08

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

こんにちは、@YushiYamamotoです前回䜜成した「ラク子さんチャットアプリ」に、今回はAI機胜を远加しおいきたいず思いたす。

特に「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蚭定ファむル
  1. netlify.tomlファむルを䜜成したす
[build]
  functions = "functions"

[dev]
  functions = "functions"
  publish = "."
  1. functions/chat.jsファむルを䜜成したす
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: '内郚サヌバヌ゚ラヌが発生したした' })
    };
  }
};
  1. フロント゚ンドのscript.jsを修正したす
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);
  });
}
  1. Netlifyダッシュボヌドで環境倉数OPENAI_API_KEYを蚭定したす。

2. Cloudflare Workersを䜿甚する方法

スクリヌンショット 2025-03-08 22.21.14.png

Cloudflare Workersもサヌバヌレス環境を提䟛しおおり、APIキヌを安党に管理できたす。

実装手順

  1. Cloudflare Workersの開発環境をセットアップしたす
npm install -g wrangler
wrangler login
wrangler init chat-api
cd chat-api
  1. wrangler.tomlファむルを線集したす
name = "chat-api"
main = "src/index.js"
compatibility_date = "2023-05-18"

[vars]
# 開発環境甚の倉数本番環境では管理画面で蚭定
# OPENAI_API_KEY = "your-api-key"
  1. src/index.jsファむルを䜜成したす
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"
          }
        }
      );
    }
  }
};
  1. Workerをデプロむしたす
wrangler secret put OPENAI_API_KEY
# プロンプトでAPIキヌを入力
wrangler publish
  1. フロント゚ンドのscript.jsを修正したす
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ファむルに少し修正を加えたす

<!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ファむルを䜜成したす

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ファむルを以䞋のように修正したす

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制䜜を䟝頌したい」ずいう方は、ぜひお気軜にご盞談ください。䞀緒にビゞネスの成長を目指したしょう

👉 ポヌトフォリオ

🌳 らくらくサむト

0
1
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
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?