./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 目次抽出