はじめに
GMOコネクトの永田です。
「政府OSSの源内(genai-web)は堅牢そう。ただ社内ポリシー上、LLM推論をクラウドに通すのは避けたい」というニッチな相談を受けました。
genai-web本体は無改修のまま、ExAppプラグインモデルで社内DGX Sparkに推論だけ逃がす構成に落としどころを見つけたので、その記録です。Bedrock経由で十分な組織が大半ですが、本記事は「LLMプロバイダ委託だけは切り離したい」少数派向けです。
先にまとめ
- genai-web本体は無改修。ExAppプラグイン仕様に従い、外部AIアプリとして社内LLMを登録
- Bedrock Guardrails圏外を Microsoft Presidio + 自前 Vault で補完
- 既存 WireGuard 資産を流用(前回記事の続編)
- 社内利用は数名程度の規模感で月次運用中
第1部: genai-webのセキュリティ設計を読み解く
genai-webは2026年4月にデジタル庁がOSS化した政府向け生成AI基盤で、AWS社製のGenU(Generative AI Use Cases)をベースに堅牢化と機能追加を加えたものです。CDKで一通りの構成がコード化されており、設計を読むと「政府レベルのセキュリティ要件をAWS上でどう組むか」の実例として参考になります。
1.1 強みは2軸で整理できる
リポジトリを読み込んで、セキュリティ関連の主要な強み10個を「一般Web共通かLLM特化か」「AWSサービス機能かアプリ独自実装か」の2軸で分類しました。
| 一般 Web サービス共通 | LLM 利用サービスならでは | |
|---|---|---|
| AWS サービス機能 | CMEK + 暗号消去(KMS) VPC隔離 + VPC Endpoints WAF + CloudFront + Geo制限 DynamoDB PITR + TTL + CMEK |
― |
| AWS 機能 + 設定判断 | Cognito 脅威検出FULL + SAML + IP DENY 監査ログ別アカウント日次転送 ※ |
Bedrock Guardrails の日本向けチューニング |
| アプリ独自実装 | ― | マルチテナント ExApp プラグインモデル Cross-account Bedrock |
| IaC 運用パターン | cdk-nag 抑制理由の明示化 | ― |
※ 監査ログ転送は DDB Export + EventBridge + Cross-account sts:AssumeRole という AWS 機能の組み合わせで、Lambda コード自体は API を叩くだけの薄い接着剤レベル。
整理してみて見えてきたのは、強みの大半はAWSサービス機能の組み合わせ + 設定判断であり、「アプリ独自実装」と呼べる強みの中で一般Web共通の項目はゼロという構造でした。アプリ独自実装は LLM 特化の領域(ExApp プラグイン / Cross-account Bedrock)に集中しており、ここが genai-web の真の独自価値、と読み替えることができます。
1.2 一般Webサービス共通の対策(AWSサービス機能で完結)
第1部の主役ではないので表で要点だけ。各項目はAWSの推奨パターンそのものです。
| 項目 | 実装の要点 | 参考 |
|---|---|---|
| CMEK + 暗号消去 | DDB / S3 / Logs / SNS / SQS / Cognito を 同一CMK で暗号化、削除で復元不可 | Well-Architected SEC08-BP02 Enforce encryption at rest |
| VPC + VPC Endpoints | Private subnet + NAT egress only、DDB/Secrets Manager は AWSバックボーン経由 | Well-Architected SEC 5: How do you protect your network resources? |
| WAF + CloudFront + Geo | IPv4/v6/国コードの AND条件、ログ1年保持KMS暗号化 | AWS Prescriptive Guidance: Restrict access based on IP address or geolocation by using AWS WAF |
| Cognito + SAML + IP DENY | SAML有効時セルフサインアップ自動無効化、NotIpAddress でDENY |
Amazon Cognito: Advanced security with threat protection |
| 監査ログ別アカウント転送 | DDB日次エクスポート → Cross-account Lambda転送 | DynamoDB Export to S3 |
「政府レベル」と聞くと身構えますが、要素技術はAWSの標準的な堅牢化パターンの集合です。同じ設定パターンは他のWebアプリにも横展開できます。
1.3 LLM特化の独自価値(genai-web ならではの領域)
genai-web独自の価値は次の2点に集約されます。
Bedrock Guardrails の日本向けPIIチューニング (guardrail.ts)
piiEntitiesConfig: [
{ action: 'BLOCK', type: 'AGE' },
{ action: 'BLOCK', type: 'AWS_ACCESS_KEY' },
{ action: 'BLOCK', type: 'CREDIT_DEBIT_CARD_NUMBER' },
{ action: 'BLOCK', type: 'EMAIL' },
{ action: 'BLOCK', type: 'IP_ADDRESS' },
{ action: 'BLOCK', type: 'PHONE' },
// ...計17種、ただし NAME / DRIVER_ID / CA_* / US_* / UK_* は意図的に除外
]
NAME や DRIVER_ID は日本仕様で機能しないため誤検知抑制目的で意図的に除外、というコメントが付いています。マイナンバー・日本のパスポート等のBedrock非対応種別は別途regexで補完する設計思想ですが、現時点で genai-web OSS にはこの補完層は実装されておらず、ExApp 側で持つ前提になっています。第3部のハマり2でこの空白を埋める実装を入れます。
マルチテナント ExApp プラグインモデル
外部AIアプリをJSONスキーマ(text / select / file 等)で登録すると、genai-webが入力フォームを自動生成し、HTTP経由でAIアプリ側を呼び出します。チーム単位RBAC、Secrets Manager によるAPIキー管理、HMAC匿名化ユーザーID、202応答時のSQSポーリング等が組み込まれていて、外部AIアプリの追加・差し替えを安全に行えます。第3部で実物に乗ります。
第2部: 「LLM推論」の委託先だけを切り離す
2.1 委託先は二階建て
genai-webをAWSにデプロイすると、データを預ける委託先は2種類に分かれます。
| 委託先 | 内容 |
|---|---|
| AWS(IaaS / PaaS) | Lambda 実行、DDB保存、KMS、Cognito、CloudFront 等のWeb層全体 |
| LLM プロバイダ | Bedrock経由の Anthropic Claude / Amazon Nova / Mistral 等 |
契約主体・運用主体・セキュリティ評価が完全に別物で、FISC外部委託リスク管理基準でも個別に管理が想定されています(参考: FDUA生成AIガイドライン)。
2.2 Bedrockの契約は十分強い
Bedrockは AWS Service Terms により入出力が学習に使われず、通信もAWSバックボーン内で完結し、保存もゼロです。通常はここで十分です。本記事はこの先に進みたい少数派向けの内容です。
2.3 それでも切り離したい組織
実務上、次のような事情で「LLMプロバイダ委託だけは避けたい」となる組織があります。
- 委託先管理コスト(契約レビュー・年次評価・監査対応)を減らしたい
- 契約上のリスクではなく、契約外のリスクまで物理的に排除したい
- 既存の社内規定が「LLM as a Service 利用禁止」と書かれており、規定改定が間に合わない
ポリシーの問題なので、技術的に正しいかという議論ではなく、組織の事情として存在するケースです。
2.4 本記事のスコープ
本記事は「AWS の IaaS / PaaS 委託は許容するが、LLMプロバイダ委託だけは切り離したい」組織向けです。クラウド全停止が要件の組織は対象外です。
第3部: ローカルLLMをExAppとして繋ぐ
3.1 全体アーキテクチャ
WG serverとDGX Sparkの接続は前回記事で構築済みなので、その上にgenai-webからの呼び出し経路を載せた形です。
3.2 ExAppプラグインモデルが綺麗に効く
genai-webのExApp仕様は「HTTPSエンドポイント + APIキー + 入力フォーム定義JSON」というシンプルな契約です。
| ExAppが受け持つ範囲 | genai-web側で受け持つ範囲 |
|---|---|
POST /invoke で推論 |
UI生成、認証、ログ保存、課金、監査 |
GET /status/{id} で状態確認 |
非同期ポーリング、タイムアウト管理 |
| 自分の業務範囲のガードレール | Bedrock経由部分のGuardrails |
genai-web本体には一切手を入れないため、git pullでバージョン追随できる状態を維持できます。これが今回の構成で一番大きい利点でした。
3.3 FastAPI最薄実装(VPC内 Lambda → WG server → DGX Spark)
genai-web の CDK は vpcIdForInvokeExApp コンテキストで InvokeExApp Lambda を 既存VPC に配置できる仕組みを持っています。前回記事で作った WireGuard 用 VPC をそのまま指定すれば、Lambda はインターネットを経由せずに WG server のプライベート IP へ直接到達します。
WG server 上の FastAPI(350行程度)は実質的に「プロトコル変換 + PIIガード + 非同期ジョブ管理」を兼ねた一枚仕立てのリバースプロキシです。
- プロトコル変換: genai-web の ExApp 仕様(
POST /invoke+202+status_url)↔ Ollama の OpenAI 互換 API(/v1/chat/completions) - PIIガード: ハマり2 で扱う Presidio + 自前 Vault、または regex baseline
- 非同期ジョブ管理:
asyncio.create_task+ dict ベースのジョブ状態保持 - HTTPS 終端: ハマり3 で扱う自己署名 cert で 8443 を待ち受け
要点だけコードを抜粋します。
@app.post("/invoke")
async def invoke(req: InvokeRequest, x_api_key: str | None = Header(default=None, alias="x-api-key")):
_require_api_key(x_api_key)
request_id = str(uuid.uuid4())
# 即座に202で返し、推論はasyncio.create_taskで裏で回す
asyncio.create_task(_run_inference(request_id, req, x_user_id))
return JSONResponse(
status_code=202,
content={"request_id": request_id, "status": "PENDING",
"status_url": f"/status/{request_id}"},
)
裏側のワーカーは「入力ガードレール → Ollama呼出 → 出力ガードレール」の3段で、それぞれの段で問題が起きたら status を書き換えるだけのシンプルな構造です。プロセスを分けてマイクロサービス化する選択肢もありましたが、責務が小さく1ファイルで全体把握できる範囲に収まったため、一枚に寄せています。
ハマり1: API Gateway 29秒制限
genai-webのInvokeExApp Lambdaは API Gateway経由で呼ばれるため、29秒の統合タイムアウトを超えるとクライアント側が打ち切ります。長文推論や大きいモデルだとローカルLLMでも余裕で超えます。
genai-webのExApp仕様は元から非同期モードを想定していて、POST /invoke が 202 + status_url を返すと、genai-web側のPollExAppStatus LambdaがSQS backoffで定期的に状態取得しに来てくれます。FastAPI側はfastapi-async-job のような既製品を使わず素朴に asyncio.create_task で実装しました。シングルプロセス前提(uvicorn --workers 1)でジョブ状態をdictで持つだけです。
ハマり2: 念のためのPIIフィルタをExAppに持たせる判断
InvokeExApp Lambdaはユーザー入力をそのままExAppにforwardするだけで、Bedrock Guardrailsを経由しません。genai-webの設計上、ExApp側にガードレールを持たせる前提になっているためです。ドキュメント上は明示されておらず、ソースコメント止まりでした。各クラウドのExAppテンプレート(genai-ai-api)も実装としてはガードレールが入っていません。
ただし正直に言うと、本構成ではLLM推論がローカル DGX Spark で完結しているため、委託先管理ポリシーの観点だけで言えば PII フィルタは必須ではありません。PIIが流れる先が社内資産だけなので、社内DBに流れるのと同じ位置付けです。
それでも次の副次的な理由で薄く入れることにしました。
- DynamoDB ChatLogs への PII 永続化を抑える(運用者・監査者の閲覧時の最小権限)
- LLM ハルシネーションで無関係な氏名・メアドが出力に混入するケースの保険
- 将来 Bedrock 併用や別プラットフォーム移行時の保険
「ガードレール必須だから入れた」ではなく「念のため入れた」というスタンスです。ExApp境界のフィルタとして4ツールを比較しました。
| ツール | 主目的 | 可逆Masking | 備考 |
|---|---|---|---|
| Microsoft Presidio | PII検出+匿名化(汎用) | ✅ encrypt operator | 他3ツールが内部で利用 |
| LLM Guard | LLM入出力スキャナ | ✅ Vault パターン | 内部でPresidio利用 |
| NeMo Guardrails | 会話フロー・トピック制限 | ✗ BLOCK中心 | 用途違い |
| Guardrails AI | LLM出力 validator | ✗ 検証中心 | Maskには不向き |
4ツールいずれも内部でPresidioを呼んでおり、日本PII recognizer(マイナンバー / 運転免許 / 銀行口座等)はどのツールを選んでもPresidioのPattern recognizerを書く作業に帰結します。LLM Guardが機能要件にフィットし第一候補でしたが、transformers連鎖で2GB近い依存になるため、結局Presidio直接 + 自前Vaultで実装しました。Vaultは30行程度です。
@dataclass
class Vault:
mapping: dict[str, str] = field(default_factory=dict)
counter: dict[str, int] = field(default_factory=dict)
def reserve(self, entity_type: str, original: str) -> str:
self.counter[entity_type] = self.counter.get(entity_type, 0) + 1
placeholder = f"[{entity_type}_{self.counter[entity_type]}]"
self.mapping[placeholder] = original
return placeholder
def restore(self, text: str) -> str:
out = text
for placeholder in sorted(self.mapping.keys(), key=lambda s: -len(s)):
out = out.replace(placeholder, self.mapping[placeholder])
return out
実装中に踏んだPython正規表現の罠も書いておきます。\b は Unicode mode でひらがな・カタカナ・漢字も「word char」扱いになるため、連絡先は090-1234-5678です のような日本語直接隣接ケースでJP_PHONE推定が発火しません。(?<!\d)...(?!\d) のlookaroundに置き換えて解決しました。
ハマり3: 自己署名cert + Lambda Node.js TLS
genai-webのExApp登録UIは https:// 必須で、http:// は弾かれます。社内向け検証段階でACM管理の正規証明書を出すのも面倒だったので、自己署名証明書 + uvicornのTLS終端で対応しました。
ただしLambda Node.jsはデフォルトで自己署名を拒否するので、Lambda環境変数に NODE_TLS_REJECT_UNAUTHORIZED=0 を投入する必要があります。CDKデプロイのたびにドリフトで戻るので、post-deploy-tls-bypass.sh というスクリプトでLambda環境変数のpatchを毎回当て直す運用にしました。本番化するならACM正規cert + Route53が筋ですが、検証段階としてはこれで十分としました。
第4部: 結果と考察
4.1 ローカルLLM呼び出し化まとめ
| 観点 | Bedrockのみ | ローカルLLM統合後 |
|---|---|---|
| LLM推論データの経路 | AWSバックボーン内で完結 | 社内 WireGuard tunnel 内で完結 |
| LLMプロバイダへの委託 | あり(契約で管理) | なし(経路上に存在せず) |
| Bedrock Guardrails | 適用 | 圏外 → Presidio + 自前 Vault で補完 |
| genai-web本体のセキュリティ | 全機能維持 | 同左 |
4.2 トレードオフ
- 運用負荷増: WG server + DGX Spark の死活監視、モデル更新作業が自社管理に
- フェイルオーバーなし: DGX Spark が落ちたら停止
- 最新モデル追随の遅れ: Bedrock側の新モデル即時反映は享受できない
- ガードレールの自前管理: regex / Presidio Pattern を自分で更新する必要がある
これらを許容できる組織だけが選ぶ構成です。
4.3 結語
genai-webの設計を読み解いた限り、Bedrock経由で運用しても十分堅牢で、多くの組織はそのままで足ります。ただ「Web層は政府レベルOSSの堅牢性をそのまま借りつつ、LLM推論層だけ自社内に閉じる」という分岐が、ExAppプラグインモデルのおかげで本体無改修で実現できる、というのは想像以上に綺麗な設計でした。
委託先管理ポリシー上「LLMプロバイダ委託だけは避けたい」少数派の組織にとっては、現実的な選択肢になりうる構成だと思います。
まとめ
- genai-webのセキュリティ設計の半分はAWSサービス機能の組み合わせで、AWS Best Practiceの集合体としても参考になる
- LLM特化の独自価値は「日本向けGuardrails設定」と「ExAppプラグインモデル」の2点
- ExAppプラグインを使えば本体無改修のままLLM推論層だけクローズドにできる
- Bedrock Guardrails圏外はPresidio + 自前Vaultで念のため補完(ローカルLLMだけなら必須ではないが副次理由で薄く入れた)
- Bedrockで足りる組織はそれが最適。本記事はあくまでニッチな委託先管理要件向け
最後に、GMOコネクトではサービス開発支援や技術支援をはじめ、幅広い支援を行っておりますので、何かありましたらお気軽にお問合せください。