はじめに
前回の記事では、Kong Ingress Controller (KIC) を使ってOSS版Kong GatewayをKubernetes上に構築しました。
今回は、Kong Gatewayの機能の一つである AI Proxy Plugin を使用して、複数のLLM(Large Language Model)プロバイダーへの統一的なアクセスポイントを構築する方法を紹介します。
動機
Kong Solution Day 2026 Winterの登壇内容に、AI Gatewayの重要性について発表するものがありました。
生成AIの利用拡大に伴い、支出コストはどんどん上昇しています。
米国では、AI Gatewayの導入によりセキュリティ・可観測性などを担保しつつ、支出コストの削減を達成する企業が現れ始めているようです。
例えば、簡単な作業はより安価なLLMに任せ、難しい作業は最新モデルに実施させることで、支出コストを削減できます。AI Gatewayがその振り分けを担います。
Kong AI Gatewayを採用することで、LLMの利用にかかる支出コストを削減することができます。
また、他にも様々なメリットを享受できるとのことでした。
| 項目 | 内容 |
|---|---|
| 複数のAIツールの導入をシンプルにします | 共通のAIインターフェースを通じて、複数のLLMの実装・アクセスを簡単に切り替えることができます |
| 機密データを保護します | ・LLMに対する情報の入出力を制御するポリシーを設定し、プロンプト攻撃や機密データの流出を防ぎます ・LLMへのアクセスで利用するAPIキーの一元管理が可能となります |
| 社内のAI使用を適切に統制します | ・AIの利用を監視し、すべてのプロバイダやモデルに利用ポリシーを適用して、責任あるAI利用を社内で徹底します ・LLMの利用に独自のレート制限を制限を設定し、支出コストの管理を強化します |
Kong AI Gatewayの詳細は、以下のドキュメントを参照して下さい。
AI Proxy Pluginとは
AI Proxy Pluginは、Kong GatewayをAIモデルへのプロキシとして機能させるプラグインです。
OpenAI、Anthropic (Claude)、Google (Gemini)など、主要なAIプロバイダーに対応しています。
このプラグインを利用することで、以下のメリットが得られます。
| 項目 | 内容 |
|---|---|
| APIキーの一元管理 | クライアントアプリにAPIキーを持たせず、Gateway側で安全に管理できます。Vaultのようなキー管理システムとの連携も可能です。 |
| インターフェースの統一 | 各プロバイダーで異なるAPI仕様を、Kongが吸収して統一フォーマット(OpenAI互換など)でクライアントに提供できます |
| ガバナンスと可観測性 | AIへのリクエストを一箇所でログ記録、監視、レート制限できます |
実行環境
前回の記事で構築したKIC環境(Gateway APIモード)を引き続き使用します。
Kong Gatewayはバージョン3.7以上を利用しましょう。
Kubernetes: v1.33.1 (OrbStack)
Kong Gateway: 3.9
Kong Ingress Controller: 3.5
実装の概要
Kong Gatewayが /ai/* を公開し続けると、誰でもAPIキーを使ってLLMを実行できてしまいます。
検証が終わったら、作成したリソースは必ず削除して下さい。
今回は、以下の3つのAIモデルを利用できるチャットAPIを構築します。
- Google Gemini (Gemini 2.5 Flash)
- Anthropic Claude (Claude 3.5 Haiku)
- Sakura AI (さくらのクラウド 高火力, OpenAI互換)
クライアント(チャットUI)はKong Gateway (/ai/{provider}) にリクエストを送り、KongがそれぞれのAIプロバイダーに認証情報を付与して転送します。
APIキーの発行
各社のAPIキーは発行済みの前提で進めます。
実装手順
1. APIキーの管理 (Kubernetes Secret)
まず、各AIプロバイダーのAPIキーをKubernetesのSecretとして登録します。
本番環境ではHashiCorp Vaultなどの外部Secret管理システムとの連携が推奨されますが、今回はシンプルにKubernetes Secretを使用します。
kubectl create secret generic ai-keys -n kong \
--from-literal=GEMINI_API_KEY="<YOUR_GEMINI_API_KEY>" \
--from-literal=ANTHROPIC_API_KEY="<YOUR_ANTHROPIC_API_KEY>" \
--from-literal=SAKURA_API_KEY="Bearer ${YOUR_SAKURA_API_KEY}"
Kong GatewayがこのSecretを参照できるように、values.yaml で環境変数としてPodに注入する設定を行います。
gateway:
env:
ANTHROPIC_API_KEY:
valueFrom:
secretKeyRef:
name: ai-keys
key: ANTHROPIC_API_KEY
GEMINI_API_KEY:
valueFrom:
secretKeyRef:
name: ai-keys
key: GEMINI_API_KEY
SAKURA_API_KEY:
valueFrom:
secretKeyRef:
name: ai-keys
key: SAKURA_API_KEY
HelmChartを更新します。
helm upgrade --install kong kong/ingress -n kong -f values.yml
2. KongPluginリソースの定義
KICでは、KongPlugin カスタムリソースを使ってプラグインの設定を定義します。
各プロバイダーごとに ai-proxy プラグインの設定を作成します。
ここでは、環境変数からAPIキーを読み込むために {vault://env/VAR_NAME} 記法を使用しています。
Chat UIデプロイ先のNamespace ai-gateway を作成します。
kubectl apply -f - <<EOF
apiVersion: v1
kind: Namespace
metadata:
name: ai-gateway
labels:
shared-gateway-access: "true"
EOF
Google Gemini用設定
Secretから注入した環境変数名には KONG_ のプレフィックスが付与される点に注意して下さい。
参考:https://developer.konghq.com/ai-gateway/ai-providers/gemini/
apiVersion: configuration.konghq.com/v1
kind: KongPlugin
metadata:
name: ai-proxy-gemini
namespace: ai-gateway
plugin: ai-proxy
config:
route_type: "llm/v1/chat"
auth:
param_name: key
param_value: "{vault://env/KONG_GEMINI_API_KEY}"
param_location: query
model:
provider: gemini
name: "gemini-2.5-flash"
options:
max_tokens: 512
temperature: 1.0
Anthropic Claude用設定
Claudeの設定では、CORS対策としてブラウザからの直接アクセス許可ヘッダーを追加するため、request-transformer プラグインを併用します(後述のRoute設定で使用します)。
参考:https://developer.konghq.com/ai-gateway/ai-providers/anthropic/
apiVersion: configuration.konghq.com/v1
kind: KongPlugin
metadata:
name: ai-proxy-claude
namespace: ai-gateway
plugin: ai-proxy
config:
route_type: "llm/v1/chat"
auth:
header_name: "x-api-key"
header_value: "{vault://env/KONG_ANTHROPIC_API_KEY}"
model:
provider: anthropic
# https://platform.claude.com/docs/en/about-claude/models/overview
name: "claude-haiku-4-5-20251001"
options:
# https://platform.claude.com/docs/en/api/versioning
anthropic_version: "2023-06-01"
max_tokens: 512
temperature: 1.0
---
# CORS対策用ヘッダー追加プラグイン
apiVersion: configuration.konghq.com/v1
kind: KongPlugin
metadata:
name: add-anthropic-header
namespace: ai-gateway
plugin: request-transformer
config:
add:
headers:
- "anthropic-dangerous-direct-browser-access: true"
anthropic-dangerous-direct-browser-access ヘッダーについては、以下の記事が参考になりました。
さくらのAIエンジン用設定 (OpenAI互換)
さくらのAIエンジンの設定値は、各社の設定に準拠すれば基本OKなはず、です。
参考:https://developer.konghq.com/ai-gateway/ai-providers/openai/
apiVersion: configuration.konghq.com/v1
kind: KongPlugin
metadata:
name: ai-proxy-sakura
namespace: ai-gateway
plugin: ai-proxy
config:
route_type: "llm/v1/chat"
auth:
header_name: "Authorization"
# Secret作成時に Bearer を含めています
header_value: "{vault://env/KONG_SAKURA_API_KEY}"
model:
provider: openai
name: "llm-jp-3.1-8x13b-instruct4"
options:
upstream_url: "https://api.ai.sakura.ad.jp/v1/chat/completions"
max_tokens: 512
temperature: 0.1
3. HTTPRouteの定義
最後に、作成したプラグインをルートに適用します。
backendRefs にはダミーのサービス(ここでは chat-ui)を指定していますが、実際のリクエストは ai-proxy プラグインによってインターセプトされ、AIプロバイダーへ転送されます。
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: ai-routes
namespace: ai-gateway
spec:
parentRefs:
- name: kong
namespace: kong
rules:
# Gemini用のルーティング
- matches:
- path:
type: PathPrefix
value: /ai/gemini
filters:
- type: ExtensionRef
extensionRef:
group: configuration.konghq.com
kind: KongPlugin
name: ai-proxy-gemini
backendRefs:
- name: chat-ui
port: 80
# Claude用のルーティング
- matches:
- path:
type: PathPrefix
value: /ai/claude
filters:
- type: ExtensionRef
extensionRef:
group: configuration.konghq.com
kind: KongPlugin
name: ai-proxy-claude
- type: ExtensionRef
extensionRef:
group: configuration.konghq.com
kind: KongPlugin
name: add-anthropic-header
backendRefs:
- name: chat-ui
port: 80
# さくらのAIエンジン用のルーティング
- matches:
- path:
type: PathPrefix
value: /ai/sakura
filters:
- type: ExtensionRef
extensionRef:
group: configuration.konghq.com
kind: KongPlugin
name: ai-proxy-sakura
backendRefs:
- name: chat-ui
port: 80
4. Chat UIのデプロイ
即席のChat UIをNginxで公開します。このアプリケーションからKong Gatewayを介してLLMにアクセスします。
Chat UIのマニフェスト
apiVersion: v1
kind: ConfigMap
metadata:
name: chat-ui-code
namespace: ai-gateway
data:
index.html: |
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Kong AI Gateway Demo</title>
<style>
:root {
/* Digital Agency Design System inspired colors */
--color-primary: #005CA2;
--color-primary-hover: #004070;
--color-background: #F5F7FA;
--color-surface: #FFFFFF;
--color-text-main: #1A1A1A;
--color-text-sub: #666666;
--color-border: #D1D5DB;
--color-input-bg: #FFFFFF;
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--radius-md: 8px;
--radius-lg: 12px;
--font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Hiragino Sans", "Noto Sans JP", sans-serif;
}
* { box-sizing: border-box; }
body {
font-family: var(--font-family);
background-color: var(--color-background);
color: var(--color-text-main);
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
height: 100vh;
}
/* Header */
header {
background-color: var(--color-surface);
border-bottom: 1px solid var(--color-border);
padding: 1rem 1.5rem;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: var(--shadow-sm);
z-index: 10;
}
h1 {
font-size: 1.25rem;
font-weight: 700;
color: var(--color-primary);
margin: 0;
}
/* Model Selector */
.model-select-wrapper {
position: relative;
}
select {
appearance: none;
background-color: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: 0.5rem 2rem 0.5rem 1rem;
font-size: 0.9rem;
color: var(--color-text-main);
cursor: pointer;
transition: border-color 0.2s, box-shadow 0.2s;
}
select:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 2px rgba(0, 92, 162, 0.2);
}
/* Chat Area */
.chat-container {
flex: 1;
overflow-y: auto;
padding: 2rem;
display: flex;
flex-direction: column;
gap: 1.5rem;
scroll-behavior: smooth;
}
.message {
display: flex;
flex-direction: column;
max-width: 80%;
animation: fadeIn 0.3s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.message.user {
align-self: flex-end;
align-items: flex-end;
}
.message.ai {
align-self: flex-start;
align-items: flex-start;
}
.message-sender {
font-size: 0.75rem;
color: var(--color-text-sub);
margin-bottom: 0.25rem;
padding: 0 0.5rem;
}
.message-content {
padding: 1rem 1.25rem;
border-radius: var(--radius-lg);
line-height: 1.6;
font-size: 1rem;
position: relative;
word-break: break-word;
box-shadow: var(--shadow-sm);
}
.message.user .message-content {
background-color: var(--color-primary);
color: white;
border-bottom-right-radius: 2px;
}
.message.ai .message-content {
background-color: var(--color-surface);
color: var(--color-text-main);
border: 1px solid var(--color-border);
border-bottom-left-radius: 2px;
}
/* Typing Indicator */
.typing-indicator {
display: flex;
gap: 4px;
padding: 0.5rem 0;
}
.typing-dot {
width: 6px;
height: 6px;
background-color: var(--color-text-sub);
border-radius: 50%;
animation: bounce 1.4s infinite ease-in-out both;
}
.typing-dot:nth-child(1) { animation-delay: -0.32s; }
.typing-dot:nth-child(2) { animation-delay: -0.16s; }
@keyframes bounce {
0%, 80%, 100% { transform: scale(0); }
40% { transform: scale(1); }
}
/* Footer Input Area */
footer {
background-color: var(--color-surface);
border-top: 1px solid var(--color-border);
padding: 1.5rem;
box-shadow: 0 -2px 10px rgba(0,0,0,0.03);
}
.input-group {
max-width: 900px;
margin: 0 auto;
display: flex;
gap: 1rem;
position: relative;
}
input[type="text"] {
flex: 1;
padding: 1rem 1.25rem;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
font-size: 1rem;
transition: all 0.2s;
background-color: var(--color-input-bg);
}
input[type="text"]:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgba(0, 92, 162, 0.1);
}
button {
background-color: var(--color-primary);
color: white;
border: none;
border-radius: var(--radius-md);
padding: 0 1.5rem;
font-weight: 600;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.2s;
display: flex;
align-items: center;
gap: 0.5rem;
}
button:hover {
background-color: var(--color-primary-hover);
}
button:disabled {
background-color: var(--color-border);
cursor: not-allowed;
}
/* Responsive */
@media (max-width: 600px) {
.message { max-width: 90%; }
footer { padding: 1rem; }
.input-group { flex-direction: column; }
button { width: 100%; padding: 0.8rem; justify-content: center; }
}
</style>
</head>
<body>
<header>
<h1>Kong AI Gateway</h1>
<div class="model-select-wrapper">
<select id="modelSelect">
<option value="/ai/claude">Claude</option>
<option value="/ai/gemini">Google Gemini</option>
<option value="/ai/sakura">Sakura AI</option>
</select>
</div>
</header>
<div class="chat-container" id="chatBox">
<div class="message ai">
<div class="message-sender">AI Assistant</div>
<div class="message-content">
こんにちは!Kong AI Gatewayへようこそ。<br>
モデルを選択して、メッセージを入力してください。
</div>
</div>
</div>
<footer>
<div class="input-group">
<input type="text" id="userInput" placeholder="メッセージを入力..." onkeypress="handleEnter(event)">
<button id="sendBtn" onclick="sendMessage()">送信</button>
</div>
</footer>
<script>
const chatBox = document.getElementById('chatBox');
const userInput = document.getElementById('userInput');
const sendBtn = document.getElementById('sendBtn');
function handleEnter(e) {
if (e.key === 'Enter' && !sendBtn.disabled) sendMessage();
}
async function sendMessage() {
const message = userInput.value.trim();
if (!message) return;
const endpoint = document.getElementById('modelSelect').value;
// UI Update
appendMessage('User', message, 'user');
userInput.value = '';
setLoading(true);
try {
// AI Placeholder with Loading Animation
const loadingId = appendLoadingMessage();
const response = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
messages: [{ role: "user", content: message }]
})
});
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
const data = await response.json();
const aiText = extractContent(data);
updateMessage(loadingId, aiText);
} catch (error) {
appendMessage('System', 'エラーが発生しました: ' + error.message, 'ai');
} finally {
setLoading(false);
}
}
function extractContent(data) {
// Handle different provider response formats
if (data.choices && data.choices[0] && data.choices[0].message) {
return data.choices[0].message.content; // OpenAI format
}
if (data.content && data.content[0] && data.content[0].text) {
return data.content[0].text; // Anthropic format sometimes
}
if (data.content) return data.content; // Simple content
return JSON.stringify(data, null, 2); // Fallback
}
function appendMessage(sender, text, className) {
const div = document.createElement('div');
div.className = `message ${className}`;
div.innerHTML = `
<div class="message-sender">${sender}</div>
<div class="message-content">${formatText(text)}</div>
`;
chatBox.appendChild(div);
scrollToBottom();
return null;
}
function appendLoadingMessage() {
const id = 'msg-' + Date.now();
const div = document.createElement('div');
div.className = 'message ai';
div.id = id;
div.innerHTML = `
<div class="message-sender">AI</div>
<div class="message-content">
<div class="typing-indicator">
<div class="typing-dot"></div>
<div class="typing-dot"></div>
<div class="typing-dot"></div>
</div>
</div>
`;
chatBox.appendChild(div);
scrollToBottom();
return id;
}
function updateMessage(id, text) {
const div = document.getElementById(id);
if (div) {
const contentDiv = div.querySelector('.message-content');
contentDiv.innerHTML = formatText(text);
}
}
function setLoading(isLoading) {
userInput.disabled = isLoading;
sendBtn.disabled = isLoading;
sendBtn.innerText = isLoading ? '送信中...' : '送信';
if (!isLoading) userInput.focus();
}
function scrollToBottom() {
chatBox.scrollTop = chatBox.scrollHeight;
}
function formatText(text) {
// Simple XSS protection and newline handling
return text
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/\n/g, "<br>");
}
</script>
</body>
</html>
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: chat-ui
namespace: ai-gateway
spec:
replicas: 1
selector:
matchLabels:
app: chat-ui
template:
metadata:
labels:
app: chat-ui
spec:
containers:
- name: nginx
image: nginxinc/nginx-unprivileged:latest
ports:
- containerPort: 8080
volumeMounts:
- name: code
mountPath: /usr/share/nginx/html/index.html
subPath: index.html
volumes:
- name: code
configMap:
name: chat-ui-code
---
apiVersion: v1
kind: Service
metadata:
name: chat-ui
namespace: ai-gateway
spec:
selector:
app: chat-ui
ports:
- port: 80
targetPort: 8080
動作確認
Kong GatewayのIPアドレスに対してリクエストを送信し、AIからの応答が返ってくるか確認します。
AI Proxy Pluginは、リクエスト/レスポンスをOpenAIフォーマットに正規化してくれるため、どのプロバイダーに対しても同じJSON形式でリクエストできます。
# GatewayのIP (例: localhost)
GATEWAY_IP="localhost"
# Geminiへのリクエスト
curl -X POST http://$GATEWAY_IP/ai/gemini \
-H "Content-Type: application/json" \
-d '{ "messages": [{ "role": "user", "content": "Hello Gemini!" }] }'
# Claudeへのリクエスト
curl -X POST http://$GATEWAY_IP/ai/claude \
-H "Content-Type: application/json" \
-d '{ "messages": [{ "role": "user", "content": "Hello Claude!" }] }'
成功すれば、以下のようなOpenAI互換のJSONレスポンスが返ってきます。
{
"model": "gemini-2.5-flash",
"usage": {
"completion_tokens": 14,
"total_tokens": 505,
"prompt_tokens": 2
},
"created": 0,
"id": "xxx-xxx",
"choices": [
{
"index": 0,
"finish_reason": "stop",
"message": {
"role": "assistant",
"content": "I'm here and ready! How can I help you today?"
}
}
],
"object": "chat.completion"
}
次に、Chat UIからLLMへのアクセスを確認します。
ブラウザから http://localhost を開いて下さい。
以下、Geminiのレスポンスです。他のLLMも同様にアクセスできました。
Tips & トラブルシューティング
実装中に遭遇したいくつかの問題とその解決策を共有します。
1. Anthropic APIのCORSエラー
ブラウザから直接Kong経由でAnthropic APIを叩くと、401 Unauthorized と共に以下のエラーが発生します。
{
"type":"error",
"error": {
"type": "authentication_error",
"message": "CORS requests must set 'anthropic-dangerous-direct-browser-access' header"
}
}
これはAnthropicのセキュリティ仕様です。Kongの request-transformer プラグインを使用して、リクエストヘッダーに anthropic-dangerous-direct-browser-access: true
を付与することで解決できます。
2. モデル名の指定
プロバイダーによっては、APIで利用可能なモデル名が厳密に決まっています。404エラーなどが発生した場合は、curl でプロバイダーの v1/models エンドポイントを直接叩いて、利用可能なモデルIDを確認するのが確実です。
まとめ
Kong GatewayのAI Proxy Pluginを使用することで、複数のAIプロバイダーを統合し、統一されたインターフェースでアプリケーションから利用できるようになりました。
これにより、アプリケーション側は「どのAIを使うか」を意識せずに開発でき、インフラ側で認証やガバナンスを一元管理できる強力な構成が実現できます。
また、Enterprise版の機能の利用により、さらに高度なAI活用が可能となります。
以下は一例です。
| 項目 | 内容 |
|---|---|
| AI Semantic Cache | LLMへの問い合わせ結果をキャッシュし、類似リクエストに対して再利用することで、APIコスト削減と応答速度向上を実現します |
| AI Semantic Prompt Guard | 悪意あるプロンプト(プロンプトインジェクションなど)を検知・ブロックし、LLMアプリケーションの安全性を向上します |
| AI Egress Policy | 組織内のデータがLLMへ送信される際に、機密情報(PIIなど)が含まれていないかチェック・マスキングします(Data Loss Prevention) |
参考リンク

