./knowledge-base/docs/AWS_ARCHITECTURE.md
# 🏗️ 時代考証システム — AWS アーキテクチャドキュメント
> 最終更新: 2026-03-17
> AWSアカウント: `903877990773` (ap-northeast-1)
---
## 目次
1. [システム概要](#1-システム概要)
2. [システム概要図](#2-システム概要図)
3. [AWSリソースマップ](#3-awsリソースマップ)
4. [CDKスタック構成](#4-cdkスタック構成)
5. [ネットワーク構成](#5-ネットワーク構成)
6. [フロントエンド配信フロー](#6-フロントエンド配信フロー)
7. [バックエンド処理フロー](#7-バックエンド処理フロー)
8. [Lambda関数一覧](#8-lambda関数一覧)
9. [AWSリソース詳細](#9-awsリソース詳細)
10. [セキュリティ設計](#10-セキュリティ設計)
---
## 1. システム概要
歴史資料の画像・PDFをAIで解析し、セマンティック検索を可能にするNHK内部向けシステムです。
| 区分 | 内容 |
| --------------- | ------------------------------------------------------- |
| ユーザー | NHK社内ユーザー(IPアドレス制限) |
| フロントエンド | React SPA (Vite) — S3 + CloudFront配信 |
| バックエンドAPI | API Gateway + Lambda (Python 3.11) |
| 検索エンジン | Amazon OpenSearch Service (kNN検索) |
| AI解析 | LiteLLM 経由 Claude Sonnet 4.5 |
| エンベディング | Amazon Bedrock Titan Text V2 |
| **CDK外管理** | **App Runner** (時代考証アシスト機能 — `/era-assist/*`) |
---
## 2. システム概要図
```mermaid
graph TB
subgraph Users["ユーザー(NHK社内)"]
Browser["🌐 ブラウザ"]
end
subgraph CDN["CDN / セキュリティ層"]
WAF_CF["WAF WebACL<br/>(CloudFront用)<br/>us-east-1"]
CF["CloudFront<br/>historical-research-dev.xmc.nhk.or.jp<br/>d1gb2qgx2q7oku.cloudfront.net"]
end
subgraph Frontend["フロントエンド (React SPA)"]
S3_Web["S3: historical-research-web"]
end
subgraph EraAssist["時代考証アシスト (CDK外管理)"]
AppRunner["App Runner<br/>zgwm746fji.ap-northeast-1<br/>.awsapprunner.com"]
WAF_AR["WAF WebACL<br/>(AppRunner用)<br/>ap-northeast-1"]
end
subgraph MainAPI["バックエンドAPI"]
APIGW["API Gateway<br/>historical-research-basic-api<br/>(prod)"]
Lambda_Upload["Lambda<br/>upload-handler"]
Lambda_Search["Lambda<br/>search-api"]
Lambda_Bulk["Lambda<br/>bulk-processor"]
Lambda_Maint["Lambda<br/>maintenance-handler"]
end
subgraph AsyncProcessing["非同期処理パイプライン"]
S3_PDF["S3: historical-research-pdfs"]
Lambda_PDF["Lambda<br/>pdf-splitter"]
SQS_Img["SQS<br/>image-analysis-queue"]
Lambda_Analyze["Lambda<br/>image-analyzer<br/>(VPC内)"]
SQS_TOC["SQS<br/>toc-extraction-queue"]
Lambda_TOC["Lambda<br/>toc-extractor<br/>(VPC内)"]
SQS_Emb["SQS<br/>embedding-queue"]
Lambda_Emb["Lambda<br/>embedding-generator<br/>(VPC内)"]
end
subgraph DataLayer["データ層"]
OpenSearch["OpenSearch<br/>historical-research-pages<br/>m5.large × 1"]
DynamoDB["DynamoDB<br/>processing-status"]
S3_Img["S3: historical-research-images"]
S3_Prompt["S3: historical-research-prompts"]
end
subgraph ExternalAI["外部AIサービス"]
LiteLLM["LiteLLM API<br/>api2.ai.dev.nhk.jp<br/>Claude Sonnet 4.5"]
Bedrock["Amazon Bedrock<br/>Titan Text V2"]
end
Browser -->|"HTTPS"| WAF_CF
WAF_CF --> CF
CF -->|"/* (SPA)"| S3_Web
CF -->|"/api/*"| APIGW
CF -->|"/era-assist/* (Lambda@Edge認証)"| AppRunner
CF -->|"/master/*"| S3_Prompt
CF -->|"/pdf-upload/* (Presigned URLプロキシ)"| S3_PDF
AppRunner -.- WAF_AR
APIGW --> Lambda_Upload
APIGW --> Lambda_Search
APIGW --> Lambda_Bulk
APIGW --> Lambda_Maint
Lambda_Upload --> SQS_Img
Lambda_Upload --> DynamoDB
Lambda_Upload --> S3_Img
Lambda_Bulk --> SQS_Img
S3_PDF -->|"ObjectCreated イベント"| Lambda_PDF
Lambda_PDF --> S3_Img
Lambda_PDF --> SQS_Img
Lambda_PDF --> SQS_TOC
Lambda_PDF --> DynamoDB
SQS_TOC -->|"SQSトリガー"| Lambda_TOC
Lambda_TOC --> LiteLLM
Lambda_TOC --> S3_Prompt
SQS_Img -->|"SQSトリガー"| Lambda_Analyze
Lambda_Analyze --> LiteLLM
Lambda_Analyze --> SQS_Emb
Lambda_Analyze --> DynamoDB
SQS_Emb -->|"SQSトリガー"| Lambda_Emb
Lambda_Emb --> Bedrock
Lambda_Emb --> OpenSearch
Lambda_Emb --> DynamoDB
Lambda_Search --> OpenSearch
Lambda_Search --> Bedrock
Lambda_Maint --> OpenSearch
Lambda_Maint --> DynamoDB
Lambda_Maint --> S3_Prompt
style EraAssist fill:#fff3cd,stroke:#ffc107
style ExternalAI fill:#e8f4f8,stroke:#0088cc
style CDN fill:#f0e6ff,stroke:#7c3aed
```
---
## 3. AWSリソースマップ
```mermaid
graph LR
subgraph us_east_1["us-east-1 (バージニア)"]
WAFCF["WAF WebACL<br/>historical-research-cloudfront-waf"]
ACM["ACM Certificate<br/>(CloudFront用カスタムドメイン)"]
EdgeAuth["Lambda@Edge<br/>EraAssistEdgeAuth"]
end
subgraph ap_northeast_1["ap-northeast-1 (東京)"]
subgraph vpc_block["既存VPC: vpc-08d84efb87d052cf9"]
subgraph private_a["Private Subnet A"]
OS["OpenSearch<br/>historical-research-pages<br/>m5.large.search / 20GB GP3"]
LambdaVPC1["Lambda (VPC内)<br/>image-analyzer<br/>embedding-generator<br/>search-api<br/>toc-extractor<br/>maintenance-handler"]
end
subgraph private_c["Private Subnet C"]
LambdaVPC2["(同上Lambdaの<br/>マルチAZ配置)"]
end
SG1["SG: ProcessingLambdaSG"]
SG2["SG: SearchLambdaSG"]
SG3["SG: OpenSearchSG"]
end
subgraph s3_buckets["S3 Buckets"]
S3W["historical-research-web"]
S3I["historical-research-images<br/>(バージョン管理有効)"]
S3P["historical-research-prompts<br/>(バージョン管理有効)"]
S3PDF["historical-research-pdfs"]
end
subgraph lambda_public["Lambda (VPC外)"]
LU["upload-handler<br/>512MB / 30s"]
LB["bulk-processor<br/>512MB / 15min"]
LD["dlq-processor<br/>256MB / 5min"]
LPDF["pdf-splitter<br/>3008MB / 15min"]
end
subgraph sqs_block["SQS キュー"]
SQ1["image-analysis-queue<br/>(VT: 15min)"]
DQ1["image-analysis-dlq<br/>(保持: 14日)"]
SQ2["embedding-queue<br/>(VT: 5min)"]
DQ2["embedding-dlq<br/>(保持: 14日)"]
SQ3["toc-extraction-queue<br/>(VT: 10min)"]
DQ3["toc-extraction-dlq<br/>(保持: 14日)"]
end
DDB["DynamoDB<br/>processing-status<br/>(PAY_PER_REQUEST / PITR)"]
APIGW2["API Gateway<br/>historical-research-basic-api"]
CF2["CloudFront Distribution<br/>d1gb2qgx2q7oku.cloudfront.net"]
WAF_AR2["WAF WebACL<br/>historical-research-apprunner-waf<br/>(Regional)"]
SM["Secrets Manager<br/>nhk_ai_api_key_lite_llm"]
subgraph apprunner_block["App Runner (CDK外管理)"]
AR["App Runner Service<br/>zgwm746fji.ap-northeast-1<br/>.awsapprunner.com"]
end
end
style apprunner_block fill:#fff3cd,stroke:#ffc107
style us_east_1 fill:#e6f0ff,stroke:#4a90d9
style ap_northeast_1 fill:#f0fff0,stroke:#2d8a2d
```
---
## 4. CDKスタック構成
本プロジェクトは2つのCDKスタックで構成されています。
```mermaid
graph TD
subgraph WafStack["WafStack (us-east-1)"]
W_IPSet["IP Set<br/>historical-research-allowed-ips"]
W_WebACL["WebACL<br/>historical-research-cloudfront-waf<br/>デフォルト: BLOCK / 許可IPのみALLOW"]
end
subgraph MainStack["HistoricalResearchStack (ap-northeast-1)"]
direction TB
M_Net["ネットワーク<br/>既存VPC + Security Groups"]
M_S3["S3 (4バケット)"]
M_DDB["DynamoDB"]
M_SQS["SQS (3キュー + 3DLQ)"]
M_OS["OpenSearch Domain"]
M_Lambda["Lambda (9関数 + 1 Layer)"]
M_APIGW["API Gateway"]
M_CF["CloudFront Distribution"]
M_WafAR["WAF Regional (AppRunner用)"]
M_CFF["CloudFront Functions (2個)"]
end
WafStack -->|"webAclArn を渡す"| MainStack
```
| スタック | リージョン | 主な理由 |
| ------------------------- | -------------- | ------------------------------------------- |
| `WafStack` | us-east-1 | CloudFront に紐付ける WAF は us-east-1 必須 |
| `HistoricalResearchStack` | ap-northeast-1 | メインリソース(東京リージョン) |
---
## 5. ネットワーク構成
```mermaid
graph TB
subgraph Internet["インターネット"]
User["NHK社内ユーザー"]
end
subgraph AWS_Edge["AWSエッジ (グローバル)"]
WAF_CF2["WAF (CloudFront用)<br/>IP制限: 社内NW + 固定IP"]
CloudFront["CloudFront"]
end
subgraph VPC["既存VPC: vpc-08d84efb87d052cf9 (ap-northeast-1)"]
subgraph PublicNet["(NAT Gateway 経由で外部アクセス)"]
direction LR
NAT["NAT Gateway"]
end
subgraph PrivateSubnetA["Private Subnet A<br/>subnet-0ebcb5a9bc54d1bd1 (ap-northeast-1a)"]
LambdaA["Lambda (VPC内)\nimage-analyzer\nembedding-generator\nsearch-api\ntoc-extractor\nmaintenance-handler"]
OpenSearchA["OpenSearch Node<br/>(Primary)"]
end
subgraph PrivateSubnetC["Private Subnet C<br/>subnet-03fa4782f20ba49ec (ap-northeast-1c)"]
LambdaC["Lambda (VPC内)\n(マルチAZ)"]
end
end
subgraph NHK_AI["NHK AI Platform (社内)"]
LiteLLM2["LiteLLM API\napi2.ai.dev.nhk.jp"]
end
User --> WAF_CF2
WAF_CF2 --> CloudFront
CloudFront --> PrivateSubnetA
LambdaA -->|"NAT経由"| NAT
NAT -->|"HTTPS"| LiteLLM2
LambdaA --> OpenSearchA
LambdaC --> OpenSearchA
style NHK_AI fill:#ffe6cc,stroke:#ff8000
```
### Security Groups
| SG名 | 用途 | インバウンド |
| ------------------------------------------ | ---------------------------------------------- | -------------------------------------------- |
| `historical-research-processing-lambda-sg` | image-analyzer / embedding-generator / toc-extractor | なし(アウトバウンドのみ) |
| `historical-research-search-lambda-sg` | search-api / maintenance-handler | なし(アウトバウンドのみ) |
| `historical-research-vpc-endpoint-sg` | VPCエンドポイント | SearchLambdaSGからTCP443 |
| `historical-research-opensearch-sg` | OpenSearch Domain | ProcessingLambdaSG・SearchLambdaSGからTCP443 |
| `sg-0faff8d9c30c94dc0` (既存) | NHK AI Platform | (既存ルールに準拠) |
---
## 6. フロントエンド配信フロー
```mermaid
sequenceDiagram
actor User as ユーザー (社内)
participant WAF as WAF (us-east-1)
participant CF as CloudFront
participant Edge as Lambda@Edge<br/>(EraAssistEdgeAuth)
participant CFF as CF Function<br/>(PathRewrite)
participant S3 as S3 (Web/Prompts/PDFs)
participant APIGW as API Gateway
participant AR as App Runner<br/>(CDK外)
User->>WAF: HTTPS リクエスト
WAF-->>User: IP不一致の場合 403 Block
WAF->>CF: 許可IPのみ通過
alt /* (SPA)
CF->>S3: GetObject (OAC署名付き)
S3-->>CF: index.html / assets
note over CF,S3: S3が403を返した場合 → /index.html にフォールバック
else /api/analyze または /api/jobs/*
CF->>AR: X-Origin-Secret ヘッダー付きで直接転送
AR-->>CF: AIアシストAPIレスポンス
else /api/*
CF->>CFF: PathRewrite (/api/ → /)
CFF->>APIGW: X-Origin-Secret ヘッダー付き
APIGW-->>CF: APIレスポンス
else /era-assist/*
CF->>Edge: Viewer Request (Cookie検証)
Edge-->>CF: 未認証の場合 / へリダイレクト
Edge->>AR: X-Origin-Secret ヘッダー付き転送
AR-->>CF: Next.jsレスポンス
else /master/*
CF->>S3: S3 Prompts バケットから取得
else /pdf-upload/*
CF->>CFF: PathRewrite (/pdf-upload/ → /)
CFF->>S3: S3 PDFs バケットへ直接 PUT (Presigned URL署名つき)
note over CF,S3: 企業プロキシ(i-FILTER)が直接S3 PUTをブロックするため<br/>CloudFront経由でプロキシする
end
```
### CloudFrontビヘイビア一覧
| パスパターン | オリジン | キャッシュ | 備考 |
| ------------------ | -------------- | ----------------- | ------------------------------------------------- |
| `/*` (default) | S3 Web | CACHING_OPTIMIZED | 403→/index.html フォールバック |
| `/api/analyze` | App Runner | CACHING_DISABLED | 時代考証アシストAPI(Lambda@Edge認証なし) |
| `/api/jobs/*` | App Runner | CACHING_DISABLED | 時代考証アシストジョブAPI(Lambda@Edge認証なし) |
| `/api/*` | API Gateway | CACHING_DISABLED | CF Function でパスリライト (`/api/` → `/`) |
| `/era-assist/*` | App Runner | CACHING_DISABLED | Lambda@Edge 認証(Okta Cookie検証) |
| `/_next/*` | App Runner | CACHING_OPTIMIZED | Next.js 静的アセット |
| `/master/*` | S3 Prompts | CACHING_OPTIMIZED | マスターデータ配信 |
| `/pdf-upload/*` | S3 PDFs (HTTP) | CACHING_DISABLED | CF Function でパスリライト + Presigned URL プロキシ|
> 💡 **CloudFrontはより具体的なパスを優先します。**
> `/api/analyze` と `/api/jobs/*` は `/api/*` より先にマッチするため、
> 時代考証アシストAPIへのルーティングが正しく機能します。
---
## 7. バックエンド処理フロー
### 7.1 PDFアップロード〜ページ分割
PDFファイルのアップロードは2段階で行われます。
```mermaid
sequenceDiagram
actor Client as フロントエンド
participant Upload as Lambda<br/>upload-handler
participant DDB as DynamoDB
participant CF as CloudFront<br/>/pdf-upload/*
participant S3PDF as S3<br/>pdfs (historical-research-pdfs)
participant Splitter as Lambda<br/>pdf-splitter
participant S3Img as S3<br/>images
participant SQS1 as SQS<br/>image-analysis-queue
participant SQSToc as SQS<br/>toc-extraction-queue
Client->>Upload: POST /api/process/pdf-upload-url
Upload->>DDB: ドキュメントエントリ作成
Upload-->>Client: 202 + Presigned PUT URL
Client->>CF: PUT /pdf-upload/pdfs/{key}<br/>(Presigned URLで直接アップロード)
CF->>S3PDF: PUT (パスリライト後)
S3PDF-->>Client: 200 アップロード完了
S3PDF->>Splitter: ObjectCreated イベント (pdfs/ プレフィックス)
Splitter->>S3PDF: PDFを読み取り
Splitter->>S3Img: ページ画像を保存
Splitter->>DDB: 各ページのエントリを作成 (PENDING)
Splitter->>SQS1: 各ページを画像解析キューへ
Splitter->>SQSToc: 目次抽出キューへ
```
### 7.2 目次抽出