Contents
イントロ:Model Context Protocol(MCP)とは
Model Context Protocol(MCP)は、LLM 対話や RAG に必要なコンテキストを複数サービス間で安全かつ効率的に共有するための実務向け設計例です。Model Context Protocol は正式な業界標準ではなく、名称の重複や実装バリエーションが生じやすい点に注意が必要です。
この記事は、設計上の前提、メッセージスキーマ、デルタ・チャンクの扱い、署名と鍵管理、ブラウザ制約、最小限で動く実装例、運用/テスト計画までを網羅します。設計例としての妥当性と、実装で陥りやすい差異点やリスクを明確に示します。
設計上の前提と位置付け(Model Context Protocol の注意点)
ここでは MCP を使う前提と、導入時に明示すべきルールをまとめます。重要なのは「MCP は提案された運用パターンであり、導入チームが互換性ルールを厳密に決める必要がある」ことです。
非標準であることの明示
導入時に運用ドキュメントで次を明確にしてください。
- MCP は公式標準ではなく、社内仕様または契約仕様として取り扱うこと。
- 名前空間の衝突を避けるため、プロジェクト単位で prefix/kid や URL 名を確定すること。
バージョニングと互換性の原則
バージョン管理と互換性ルールは必須です。
- プロトコルはセマンティックバージョニングを採用します。
- ハンドシェイクで相互にサポートする capabilities を交換してダウングレードやフォールバックを制御します。
- 既知でないフィールドは無害に無視できる設計(後方互換)を採用します。
運用上の基本方針
運用時は次を守ってください。
- 仕様差異(seq 起点、timestamp 形式、署名方式)は導入前に厳密に合意すること。
- 実装は契約テスト(JSON Schema / Protobuf validator)でCIに組み込むこと。
- 公開APIと内部APIでセキュリティ要件(mTLS 等)を分けて定義すること。
Model Context Protocol:メッセージ種別と厳格スキーマ
ここではメッセージの共通フィールドと各種メッセージ(handshake/data/snapshot/delta/ack/error)の詳細スキーマ、プロトバインディング、代表的なエラーコードおよびシーケンスフローを示します。実装間で差異が出やすい箇所は明確に指定します。
共通フィールドの仕様(導入)
全メッセージが従う共通ルールを定義します。下記に沿うことで実装差異を減らします。
- version: セマンティック文字列(例 "1.0")。
- session_id: UUID(RFC4122 v4 推奨)文字列。
- message_id: UUID v4 推奨。冪等化に使います。
- seq: uint64。運用上の推奨は 1 を起点(初回アプリケーションメッセージは seq=1)。ハンドシェイク時に server が initial_seq を返します。
- timestamp: JSON では RFC3339 / ISO8601 UTC(例 "2026-05-09T12:34:56Z")。Protobuf では google.protobuf.Timestamp を使うこと。
- ttl: 整数(秒)。
- signature: JWS (JOSE) を推奨。署名対象フィールドの正規化ルールを決めること(後述)。
- payload: メッセージ固有の構造体。JSON と Protobuf のマッピングを明記すること。
JSON / Protobuf のマッピング(導入)
JSON と Protobuf 間の不整合を減らすためのルールです。
- JSON の type は小文字文字列("handshake" 等)を推奨し、Protobuf 側は enum を定義して文字列→enum のマッピングをサーバ/クライアント実装で明示します。
- timestamp: JSON は ISO8601 文字列、Protobuf は google.protobuf.Timestamp を使います。変換ルールを実装ドキュメントに明記してください。
- seq 起点:仕様では seq の起点を 1 に統一し、handshake の response に initial_seq を含めます。
- signature: JSON では signature.jws(compact)や detached JWS を用いることを推奨。Protobuf では signature.message_jws のように string を使います。
handshake
ハンドシェイクはセッション初期化、能力ネゴ、短命トークンの発行を行います。REST で行うことを推奨します。
- 要点(JSON 例):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
{ "version": "1.0", "session_id": null, "message_id": "uuid-v4", "type": "handshake", "payload": { "client": {"id":"agent-01","role":"assistant"}, "capabilities": ["snapshot","delta","compression:gzip"], "want_version": "1.0" }, "timestamp": "2026-05-09T12:34:56Z" } |
- サーバ応答例(REST):
|
1 2 3 4 5 6 7 8 |
{ "session_id": "sess-<uuid>", "token": "<JWT短命トークン>", "version": "1.0", "initial_seq": 1, "server_capabilities": ["snapshot","delta","compression:gzip"] } |
- Protobuf(抜粋):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
message HandshakePayload { repeated string capabilities = 1; string client_id = 2; string client_role = 3; string want_version = 4; } message HandshakeResponse { string session_id = 1; string token = 2; uint64 initial_seq = 3; repeated string server_capabilities = 4; } |
- 主要エラーコード(handshake):
- 1001: invalid_credentials
- 1002: unsupported_version
-
1003: capability_mismatch
-
シーケンス(テキスト):
- Client -> POST /handshake -> Server
- Server -> 200 {session_id, token, initial_seq}
- Client -> open WS (token) -> Server
- Client -> send messages starting seq=initial_seq
data
data メッセージは任意のアプリケーションデータを運びます。小さなペイロードは直接入れ、大きい場合はチャンク参照を使います。
- JSON 代表フォーマット:
|
1 2 3 4 5 6 7 8 9 10 11 12 |
{ "version":"1.0", "session_id":"sess-abc123", "message_id":"msg-0002", "seq": 1, "type":"data", "payload": {"doc": {"text": "こんにちは"}}, "timestamp":"2026-05-09T12:35:01Z", "ttl":3600, "signature":"<JWS compact>" } |
-
Protobuf では Envelope の oneof で DataPayload を定義します。payload は構造化型 or bytes(圧縮済み)を選べる oneof を使ってください。
-
主要エラーコード(data):
- 2001: invalid_schema
- 2002: payload_too_large
-
2004: compression_error
-
取り扱い注意:
- 大きなデータはチャンク化し、chunk_id/index/total と chunk-level checksum を付与すること(後述)。
snapshot
スナップショットはフルの状態転送に使います。初回同期や再同期時に利用します。
- payload は通常 chunkable。snapshot は base_seq を含めない(snapshot が新たな基準となる)。
-
snapshot を受けたら受信側はローカル状態を置き換えるか、受け入れ条件(version, capability)を確認します。
-
主要エラーコード(snapshot):
- 3001: snapshot_conflict
- 3002: storage_unavailable
delta
デルタは差分操作を表現します。相互運用性のため差分表現は仕様で選択します(下節で詳細)。
- delta の必須フィールド例:
- delta_id: 一意 ID
- base_seq: このデルタが適用されるベースの seq
- ops: JSON Patch(RFC 6902)配列 または CRDT/op ベース表現
- JSON 例(JSON Patch):
|
1 2 3 4 5 6 7 8 9 10 |
{ "type":"delta", "seq": 2, "payload": { "delta_id":"d-0001", "base_seq":1, "ops":[{"op":"replace","path":"/doc/0/text","value":"更新"}] } } |
- 主要エラーコード(delta):
- 3001: seq_mismatch
- 3002: base_seq_unknown
- 3003: conflict_detected
ack
ack は受信確認を返す標準構造です。ack で確実受信と処理結果を伝えます。
- ack payload 例:
|
1 2 3 4 5 6 7 8 9 10 |
{ "type":"ack", "seq": 0, "payload": { "acked_message_id":"msg-0002", "acked_seq": 1, "status":"ok" } } |
- status は "ok" / "nack"。nack の場合は error_code と message を含めます。
error
error はエラー通知用の汎用メッセージです。再送・スナップショット要求・切断などの次動作を明示します。
- error payload 例:
|
1 2 3 4 5 6 7 8 9 |
{ "type":"error", "payload": { "code": 3001, "message":"seq mismatch: expected 2, got 5", "retry_after": 2 } } |
- 全体的なエラーコード帯域例:
- 1000–1999: 認証/ハンドシェイク
- 2000–2999: データペイロード関連
- 3000–3999: 同期/シーケンス関連
- 4000–4999: 署名/整合性
- 5000–5999: サーバ内部エラー
Model Context Protocol:デルタ設計とチャンク/圧縮
デルタ表現、適用順序、競合解消、チャンク化・圧縮の運用指針を示します。特に差分アルゴリズムとチャンク再送は相互運用で差が出やすい箇所です。
差分表現の選択
差分は用途に応じて選びます。推奨は次の通りです。
- 単純な構造変更:JSON Patch(RFC 6902)をデフォルトにする。実装が容易で広くサポートされています。
- 競合が頻出しオフライン操作がある場合:CRDT(例 Automerge や Yjs)を検討してください。CRDT は複雑ですが最終的な収束性を提供します。
- オペレーションログ(op-based):順序付きの操作ログが必要な場合は op ベースで表現します。op に署名や actor-id を含めるとフォレンジックが容易になります。
適用順序と base_seq の扱い
デルタ適用の基本フローは次の通りです。
- delta.base_seq は「このデルタが期待する現在の seq」を示します。受信側は現在の seq と照合します。
- もし current_seq + 1 == delta.base_seq なら順次適用。
- もし gap(missing) がある場合は、欠落した seq の再送を要求するか、snapshot の再取得を行います。
- current_seq > base_seq(既に進んでいる)場合は、再生(rebase)またはコンフリクト処理を行います。実装はアプリケーション単位で方針を定義してください。
疑似コード(適用):
|
1 2 3 4 5 6 7 8 9 |
if delta.base_seq == current_seq: apply(delta.ops) current_seq += 1 else if delta.base_seq > current_seq: request_missing(delta.base_seq - current_seq) else: // rebase / conflict resolution handle_conflict(delta) |
競合解決アルゴリズム
競合解決は用途ごとに異なります。選択肢の例です。
- 最終更新優先 (LWW): フィールド毎に timestamp と actor-id で競合解を決める。実装が簡単だが意味的に誤りを生む場合がある。
- アプリケーション固有マージ: 重要なフィールドはカスタムマージ関数を用意する。
- CRDT: 競合耐性が高く複雑さを吸収する。オフライン変更や大量の concurrent updates がある場合に有効。
チャンク化・圧縮・整合性チェック
大きなペイロードは圧縮→チャンク化を基本にします。運用指針は次の通りです。
- デフォルト最大チャンクサイズ: 1 MiB 前後を推奨(可変)。実際はネットワーク MTU やレイテンシ制約で調整してください。
- チャンクフィールド:
- chunk_id: 一意のペイロード単位 ID(UUID 推奨)
- index: 0 ベースのチャンク順序番号
- total: チャンク総数
- chunk_checksum: SHA-256(base64)などのチャンク単位の整合性値
- payload: 圧縮済みバイト列(base64 エンコード)
- 再構築手順:
- 受信側は全チャンクを index に基づきバッファする。
- total 個受信できたらチャンクを連結し、チャンク合計の top-level checksum を検証する。
- 失敗したらエラー(4004: chunk_corrupt)を返し、該当チャンクの再送を要求する。
- 再送アルゴリズム:
- チャンク ACK を設ける(ack に受信済 index を含める)。
- ACK タイムアウト(例 5 秒)、指数バックオフ、最大再試行回数(例 5 回)。
- 再試行上限到達後は snapshot 再取得かセッション切断を選択する。
Model Context Protocol:セキュリティ、署名、ブラウザ制約
セキュリティ周りの設計を具体化します。特に署名の正規化、トークン検証、鍵管理、ブラウザ WebSocket の制約に注意してください。
認証とトークン検証
認証方式の推奨と検証要点です。
- server-to-server: mTLS または JWT(公開鍵方式 RS256/ES256)を推奨します。HMAC はクローズドなトラスト領域でのみ。
- ユーザー連携: OAuth2 authorization_code を推奨。短命アクセストークンとリフレッシュトークンの組み合わせを使います。
- JWT の必須クレーム検証:
- iss, aud, exp, iat を必ず検証すること。
- scope(または custom claim)で session:read / session:write 等を確認すること。
- 実装上の注意:
- 公開鍵は JWKS で配布し、kid を用いて鍵を選択すること。
- トークン失効(logout/rotation)はブラックリストまたは短命トークン+能力失効リストで実装すること。
署名の正規化と JOSE の採用
メッセージ整合性は JWS / JOSE を推奨します。推奨手順は次の通りです。
- 正規化: 署名対象は JSON の Canonicalization(例 RFC8785 / JCS)で正規化した後に JWS で署名します。signature フィールド自体は署名対象に含めないか、detached JWS を使います。
- 推奨アルゴリズム: RS256 または ES256(公開鍵方式)。HMAC はクローズドな信頼境界でのみ。
- 検証手順(受信側):
- JWS ヘッダから kid と alg を取得する。
- kid に対応する公開鍵を取得する(キャッシュし期限を設ける)。
- 署名検証を行い、成功したら署名されたフィールドの一致を確認する。
- timestamp と ttl を検証してリプレイを防止する。
ブラウザの WebSocket 制約と代替
ブラウザの標準 WebSocket API はカスタム Authorization ヘッダを送れません。実運用での代替手段を示します。
- cookie(推奨): HTTPS ハンドシェイクで HttpOnly, Secure, SameSite=strict のクッキーを設定し、WebSocket は同一ドメインで接続する。CSRF/同一生成元対策を併用すること。
- Sec-WebSocket-Protocol(サブプロトコル): サブプロトコル文字列に短命トークンを渡す手法。ブラウザはサブプロトコルを送信可能ですが、ヘッダに露出するため短命トークンに限定し、必ず TLS を使うこと。
- クエリパラメータ: URL に token を埋めるのは最もリスクが高い(ログに残る可能性など)。どうしても採用する場合は短命トークン+TLS+アクセスログのフィルタリングを徹底してください。
- 推奨組合せ: 初回は HTTPS で認証(/handshake)→ HttpOnly cookie をセット→ WebSocket 接続。これが最も安全でクロスブラウザ性が高い。
鍵管理・ローテーション・インシデント対応
鍵ライフサイクルとインシデント対応の骨子です。
- 鍵ローテーション:
- 新しい鍵を生成し、公開鍵(JWKS)を配布する。
- サービスを新しい鍵の署名 (kid 新規) に切替える。
- 旧鍵は一定の猶予期間を設けつつ失効させる(overlap を許容)。
- 鍵漏洩時のインシデント手順:
- 直ちに該当 kid の鍵を無効化(JWKS で削除または無効フラグ)。
- 影響トークンのリストアップと失効。短命トークンを推奨している場合は影響を最小化できます。
- セッションの強制再認証、フォレンジックログの保全、関連サービス通知、法的対応。
- 運用注意:
- KMS(Cloud KMS 等)を使って秘密鍵を管理し、アクセス制御・監査を有効にしてください。
- キー利用は最小特権で行い、運用ログは WORM 形式で保護することを検討してください。
実装例・運用とテスト(MCP 実務ガイド)
ここでは最小限動くサンプルと、運用で必須のテスト・SLO・監査計画を示します。実用コードは依存関係や鍵の取り扱いに注意してください。
実装例(Node.js / Python / Go:最小限の動作と検証)
以下のサンプルは教育目的の最小実装です。本番ではシークレット管理、監査ログ、例外処理、再試行制御、TLS/mTLS を必ず追加してください。
Node.js(Express + ws)の簡易サーバー(要点説明)
この Node サンプルはハンドシェイクで JWT を発行し、WebSocket 接続時に短命トークンを検証します。環境変数 JWT_PRIVATE_KEY / JWT_PUBLIC_KEY(PEM)を使用します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
// server.js // deps: npm i express ws jsonwebtoken uuid const express = require('express'); const http = require('http'); const WebSocket = require('ws'); const jwt = require('jsonwebtoken'); const { v4: uuidv4 } = require('uuid'); const app = express(); app.use(express.json()); const PRIVATE_KEY = process.env.JWT_PRIVATE_KEY; // PEM const PUBLIC_KEY = process.env.JWT_PUBLIC_KEY; // PEM if (!PRIVATE_KEY || !PUBLIC_KEY) throw new Error('JWT keys required'); app.post('/v1/mcp/handshake', (req, res) => { try { const clientId = req.body?.client?.id; if (!clientId) return res.status(400).json({error:'missing client.id'}); const sessionId = `sess-${uuidv4()}`; const token = jwt.sign( { sub: clientId, session: sessionId, scope: ['session:write'] }, PRIVATE_KEY, { algorithm: 'RS256', expiresIn: '5m', keyid: 'k1' } ); return res.json({ session_id: sessionId, token, version: '1.0', initial_seq: 1 }); } catch (e) { return res.status(500).json({ error: e.message }); } }); const server = http.createServer(app); const wss = new WebSocket.Server({ server, path: '/v1/mcp/ws' }); const sessions = new Map(); // session_id -> { lastSeq, seenMessageIds: Set } wss.on('connection', (ws, req) => { // トークン取得: query param または Sec-WebSocket-Protocol を試す const url = new URL(req.url, `http://${req.headers.host}`); const token = url.searchParams.get('token') || (req.headers['sec-websocket-protocol'] || '').split(',').slice(-1)[0]?.trim(); if (!token) { ws.close(4001, 'missing token'); return; } try { const payload = jwt.verify(token, PUBLIC_KEY, { algorithms: ['RS256'] }); const sessionId = payload.session; if (!sessionId) { ws.close(4003, 'invalid token'); return; } sessions.set(sessionId, sessions.get(sessionId) || { lastSeq: 0, seenMessageIds: new Set() }); ws.sessionId = sessionId; } catch (err) { ws.close(4002, 'invalid token'); return; } ws.on('message', (data) => { try { const env = JSON.parse(data.toString()); // 基本バリデーション if (!env.message_id || typeof env.seq !== 'number' || !env.type) { ws.send(JSON.stringify({ type: 'error', payload: { code: 2001, message: 'invalid envelope' } })); return; } const s = sessions.get(ws.sessionId); if (s.seenMessageIds.has(env.message_id)) { // 冪等処理: 既に処理済 ws.send(JSON.stringify({ type: 'ack', payload: { acked_message_id: env.message_id, acked_seq: env.seq, status: 'ok' }})); return; } // seq チェック(単純な増分) if (env.seq !== s.lastSeq + 1) { ws.send(JSON.stringify({ type: 'error', payload: { code: 3001, message: `seq mismatch: expected ${s.lastSeq + 1}` }})); return; } // 正常処理(ここで payload を保存・配布など) s.lastSeq = env.seq; s.seenMessageIds.add(env.message_id); ws.send(JSON.stringify({ type: 'ack', payload: { acked_message_id: env.message_id, acked_seq: env.seq, status: 'ok' }})); } catch (e) { ws.send(JSON.stringify({ type: 'error', payload: { code: 5001, message: e.message }})); } }); }); server.listen(8080); |
このサンプルは次の点を補強してください。鍵は KMS で保管し、トークン検証に時間外攻撃防止(回数制限)を追加してください。
Python(FastAPI)の最小サンプル(要点説明)
Python は PyJWT を利用した簡易な handshake と WebSocket を示します。依存: fastapi, uvicorn, pyjwt
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
# server.py # deps: pip install fastapi uvicorn pyjwt import os, time, json from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException import jwt app = FastAPI() PRIVATE_KEY = os.environ['JWT_PRIVATE_KEY'] PUBLIC_KEY = os.environ['JWT_PUBLIC_KEY'] @app.post("/v1/mcp/handshake") def handshake(payload: dict): client_id = payload.get("client", {}).get("id") if not client_id: raise HTTPException(status_code=400, detail="client.id required") token = jwt.encode({"sub": client_id, "session": f"sess-{int(time.time())}", "exp": int(time.time()) + 300}, PRIVATE_KEY, algorithm="RS256") return {"session_id": f"sess-{int(time.time())}", "token": token, "version":"1.0", "initial_seq": 1} @app.websocket("/v1/mcp/ws") async def ws_endpoint(websocket: WebSocket): await websocket.accept() try: # token を query param か Sec-WebSocket-Protocol で受け取る token = websocket.query_params.get("token") or websocket.headers.get("sec-websocket-protocol") if not token: await websocket.close(code=4001) return try: payload = jwt.decode(token, PUBLIC_KEY, algorithms=["RS256"]) except jwt.PyJWTError: await websocket.close(code=4002) return # メッセージ処理ループ while True: data = await websocket.receive_text() env = json.loads(data) # 最小検証 if "message_id" not in env or "seq" not in env: await websocket.send_text(json.dumps({"type":"error","payload":{"code":2001,"message":"invalid envelope"}})) continue await websocket.send_text(json.dumps({"type":"ack","payload":{"acked_message_id":env["message_id"], "acked_seq": env["seq"], "status":"ok"}})) except WebSocketDisconnect: return |
Go(gorilla/websocket)クライアント例(要点)
Go クライアントは Authorization ヘッダを送れます。ブラウザでは不可点に注意してください。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// client.go // deps: go get github.com/gorilla/websocket package main import ( "log" "net/url" "os" "github.com/gorilla/websocket" ) func main() { token := os.Getenv("MCP_TOKEN") u := url.URL{Scheme: "wss", Host: "localhost:8080", Path: "/v1/mcp/ws"} header := map[string][]string{"Authorization": {"Bearer " + token}} c, resp, err := websocket.DefaultDialer.Dial(u.String(), header) if err != nil { log.Fatalf("dial error: %v, resp: %v", err, resp) } defer c.Close() // 送受信の例... } |
テスト・検証・監査計画
運用前に以下を実施してください。セキュリティ試験を省略すると重大なリスクがあります。
- 単体テスト: シリアライズ/デシリアライズ、schema バリデーション、署名検証ロジック。
- 契約テスト: JSON Schema / Protobuf Validator を CI に組み込む。
- 統合テスト: モック LLM、モックベクトルDB で E2E を検証。
- 負荷テスト: k6 / Locust 等で同時接続、再接続、トークン消費シナリオを検証。
- セキュリティテスト: SAST、DAST、依存関係脆弱性スキャン、ペネトレーションテスト、fuzzing(差分・チャンク・署名周り)。
- 運用試験: キーローテーション、トークン失効、監査ログの WORM 保存を含む復旧試験。
SLO・アラート・インシデント対応(要約)
SLO の例とアラート優先度、初動手順です。実際の数値はサービス特性で調整してください。
- 例 SLO:
- 接続確立成功率(5 分窓): 99.9% 以上
- メッセージ ack レイテンシ(P95): <300ms
- 再接続率: <1% / 時間
- アラート優先度:
- P0: 全域障害(即時ページング)。Runbook: フェイルオーバー, snapshot 強制, SLA チェック。
- P1: 重大な機能劣化(ページング、調査 30 分以内)。
- P2: 小規模/非緊急(アサイン 24 時間以内)。
- キー漏洩時初動:
- 該当 kid を JWKS から削除・無効化。
- 短命トークンを早期失効させるための施策適用。
- 影響範囲評価、法務・関係者通知、フォレンジックログ保全。
まとめ
MCP は LLM コンテキスト共有のための実務設計例であり、導入には仕様合意と運用ルールの厳格化が不可欠です。設計上は seq 起点、timestamp 形式、署名方式、チャンク仕様、差分アルゴリズムを明文化してください。実装ではトークン検証、署名検証、入力バリデーション、鍵ローテーション、テスト(特にセキュリティ試験)を必須としてください。
- MCP は公式標準ではないため、名称の衝突や実装差異に注意すること。
- メッセージ共通ルール(version/session_id/seq/timestamp/signature)をチームで厳密に合意すること。
- 署名は JWS/JOSE を推奨し、署名対象の正規化ルールを定めること。
- ブラウザは Authorization ヘッダを送れないため cookie/subprotocol の代替策とリスク緩和を実装すること。
- デルタは JSON Patch をベースにしつつ、競合が多い用途では CRDT を検討すること。
- チャンクは index=0 ベース、total と checksum を必須にし、ACK/再送の明確なフローを設けること。
- 運用では鍵ローテーション、トークン失効テスト、ペネトレーションテスト、監査ログ(WORM)を実行すること。
参考実装や運用テンプレートは、本設計をベースに社内要件(法令、データ居住性、SLO)へ合わせてください。