TenHub のアーキテクチャ設計について、サービス構成・通信フロー・設計判断の背景を整理します。
全体構成
マイクロサービスアーキテクチャを採用し、各サービスは gRPC(Protocol Buffers)で通信します。外部からのリクエストは nginx → Gateway を経由し、適切なサービスにルーティングされます。
クライアント
↓ HTTPS
nginx (SSL終端 / レート制限)
├── / → Frontend (Next.js :3000)
├── /uploads/ → Gateway (:8080) — 静的ファイル配信
├── /api/ → Gateway (:8080) — REST API
└── /api/logs/ → Gateway (:8080) — SSE ストリーミング
↓ gRPC
┌─────────────────────────────────────┐
│ Gateway (:8080) │
│ REST ↔ gRPC 変換 / 認証 / CORS │
└─────────┬───────┬───────┬───────┬───┘
↓ ↓ ↓ ↓
Auth Wiki Profile AI
:50051 :50052 :50053 :50054
↓ ↓
MySQL Redis
:3306 :6379
サービス詳細
Gateway(REST ↔ gRPC 変換)
外部からの HTTP リクエストを受け取り、各マイクロサービスへの gRPC 呼び出しに変換する。認証・CORS・レート制限のミドルウェアもここに集約。
ミドルウェアチェーン:
CORS → RequestID → Auth → AI Rate Limit → Router
| ミドルウェア | 役割 |
|---|---|
| CORS | Access-Control-Allow-Origin 等のヘッダー設定、preflight 対応 |
| RequestID | X-Request-ID を生成・伝搬し、ログのトレーサビリティを確保 |
| Auth | Cookie / Authorization ヘッダーから JWT を抽出し、Auth サービスで検証 |
| AI Rate Limit | 未認証ユーザーの AI 利用を制限(同時1リクエスト、1日5回/IP) |
Gateway は 4 つの gRPC クライアントを起動時に確立する:
AUTH_ADDR = auth:50051
WIKI_ADDR = wiki:50052
PROFILE_ADDR = profile:50053
AI_ADDR = ai:50054
Auth(認証)
ユーザー登録・ログイン・トークン検証を担当。JWT RS256(非対称鍵)で署名する。
- 秘密鍵: Auth サービスのみが保持し、トークンの署名に使用
- 公開鍵: Auth サービスが
VerifyTokenRPC で検証に使用 - トークン有効期限: 24 時間
- パスワード: bcrypt でハッシュ化して保存
Gateway 認証パターンを採用しており、Gateway が全リクエストの JWT を Auth サービス経由で検証する。内部サービス(Wiki / Profile / AI)は認証ロジックを持たず、Gateway が context に設定した userID を信頼する。
Wiki(記事管理)
記事の CRUD、カテゴリ管理、いいね・保存機能、アクセス分析を担当。CQRS パターンで読み書きを分離している(詳細は後述)。
主な RPC(19個):
- 記事 CRUD: Create / Get / List / Update / Delete
- カテゴリ: ListCategories / CreateCategory / DeleteCategory
- エンゲージメント: ToggleLike / SaveArticle / UnsaveArticle 等
- 分析: RecordPageView / GetAnalyticsSummary
Profile(プロフィール管理)
プロフィール情報とポートフォリオの CRUD を担当。
AI(RAG / Agent / ナレッジグラフ)
記事検索(BM25 / Vector / Hybrid / Graph)、RAG による質問応答、ReAct Agent、ナレッジグラフ構築を担当。
主な RPC(7個):
- SearchArticles — 記事検索(4 種の検索エンジン)
- AskQuestion — RAG 質問応答
- AskWithAgent / AskWithAgentStream — Agent モード(ストリーミング対応)
- GetKnowledgeGraph — グラフ可視化用データ取得
- GetRelatedArticles — 関連記事の取得
AI サービスは Wiki サービスに gRPC で記事データを取得し、Ollama(ローカル LLM)で推論を行う。
CQRS(コマンド・クエリ責務分離)
Wiki サービスでは、書き込み(Command)と読み取り(Query)を異なるリポジトリに分離している。
書き込みパス(Command → MySQL)
Gateway → Wiki.Create() → CommandRepository → MySQL INSERT
→ Redis DEL "articles:list"
記事を作成・更新・削除するたびに MySQL に書き込み、Redis のキャッシュを即時無効化する。
読み取りパス(Query → Redis → MySQL)
Gateway → Wiki.List() → QueryRepository → Redis GET "articles:list"
├─ HIT → JSON デコードして返却
└─ MISS → MySQL SELECT → Redis SET (TTL: 10分)
読み取りは Redis を優先し、キャッシュミス時のみ MySQL にフォールバック。Redis 障害時も MySQL から直接読み取れるため、可用性を維持できる。
キャッシュ無効化戦略
| 操作 | 無効化対象 |
|---|---|
| Create | articles:list |
| Update | articles:list + article:{id} |
| Delete | articles:list |
記事変更時は AI サービスのナレッジグラフキャッシュも非同期で無効化する(5 秒タイムアウト、失敗しても記事操作はブロックしない)。
JWT RS256 認証フロー
1. ユーザーがログイン
→ Gateway → Auth.Login()
→ bcrypt でパスワード照合
→ 秘密鍵で JWT 署名(RS256)
→ トークンを Cookie にセット
2. 保護されたAPIにアクセス
→ Gateway の Auth ミドルウェア
→ Cookie / Authorization ヘッダーから JWT 抽出
→ Auth.VerifyToken() で公開鍵検証
→ userID を context に設定
→ 内部サービスに転送
ルート保護ポリシー:
| ルート | 認証 | 備考 |
|---|---|---|
| POST /api/user/register, /login | 不要 | — |
| GET /api/articles(公開) | 不要 | userID があれば非公開記事も取得可 |
| POST /api/articles | 必要 | — |
| /api/ai/* | 不要 | 未認証はレート制限あり |
| いいね・保存 | 不要 | フィンガープリントベース |
| /api/logs/* | 必要 | 管理者向け |
AI レート制限
未認証ユーザーの AI 利用を制限し、Ollama の負荷を守る。
// 同時実行制限: バッファ付きチャネル(セマフォ)
anonSemaphore chan struct{} // AI_ANON_MAX_CONCURRENT (default: 1)
// 日次制限: IP ハッシュベース(SHA256 の先頭16文字)
daily map[string]dailyCounter // AI_ANON_DAILY_LIMIT (default: 5/day)
- 認証済みユーザー: 制限なし(ミドルウェアをバイパス)
- 未認証ユーザー: 同時 1 リクエスト + 1日 5 回まで
- レスポンスヘッダーで残り回数を通知:
X-RateLimit-Remaining
Docker ネットワーク
全サービスは Docker Compose の内部ネットワークで接続される。サービス名がそのまま DNS ホスト名になる。
# サービス間通信の例
gateway → auth:50051 # Docker DNS で自動解決
wiki → db:3306 # MySQL
wiki → cache:6379 # Redis
ai → wiki:50052 # 記事データ取得
ai → host.docker.internal:11434 # ホスト上の Ollama
起動順序制御
depends_on + condition で依存関係を制御:
db (healthy) → auth, wiki, profile
cache (healthy) → wiki
wiki (started) → ai
auth, wiki, profile, ai (started) → gateway
gateway (started) → frontend
frontend (started) → nginx
service_healthy は MySQL の mysqladmin ping や Redis の redis-cli ping で判定。
nginx の役割
nginx はリバースプロキシとして、以下を担当:
- SSL 終端: Let's Encrypt 証明書による HTTPS
- レート制限:
limit_req_zoneによる IP ベースの制限(API: 30r/s、一般: 30r/s) - セキュリティヘッダー: HSTS、X-Frame-Options、X-Content-Type-Options
- ルーティング: パスに基づく振り分け(frontend / gateway / SSE)
- SSE 対応:
/api/logs/streamはバッファリング無効、タイムアウト 3600 秒
VPS デプロイ
インフラ構成
とにかく安く小さく始めたるなら Lighthouseはおすすめです!
ローカル開発
↓ git push (develop → main)
GitHub Actions (test.yml)
↓ テスト通過
GitHub Actions (deploy.yml)
↓ Docker image build & push
GHCR (GitHub Container Registry)
↓ docker compose pull(手動)
VPS (Tencent Cloud Lighthouse)
└── /opt/tenhub/
├── docker-compose.prod.yml
├── .env.production
├── scripts/
│ ├── health.sh
│ └── backup.sh
└── backups/
CI/CD パイプライン
自動化されている部分:
develop/mainへの push →test.ymlが Go テスト・Frontend ビルド・Compose 構文検証を実行mainへの push →deploy.ymlがテスト通過後、7つの Docker image(auth / wiki / profile / ai / gateway / migrator / frontend)を GHCR へ push- テスト結果は Slack に通知
手動で実行する部分:
VPS への自動デプロイはしない設計。deploy/Makefile 経由で SSH 越しに操作する。
# image 更新 & サービス再起動
make -C deploy deploy-quick
# DB マイグレーション
make -C deploy migrate-up
# ログ確認
make -C deploy deploy-logs
# ヘルスチェック
make -C deploy health
内部的にはすべて SSH → docker compose -f docker-compose.prod.yml --env-file .env.production ... のパターン。
DB マイグレーション
goose を使った Go 製マイグレーターを専用 Docker image として管理。
docker compose run --rm migrator up
→ tenhub-migrator up
→ main.go: os.Args[1] = "up"
→ goose.Up(db, "/migrations")
→ deploy/migrations/*.sql を番号順に適用
→ goose_db_version テーブルで適用済みを管理
マイグレーションファイルは deploy/migrations/ に goose 形式(-- +goose Up / -- +goose Down)で配置。image ビルド時に /migrations ディレクトリへ焼き込まれる。
SSH セキュリティ
deploy/Makefile の setup-ssh-hardening ターゲットで以下を設定:
- SSH ポート変更: デフォルト 22 からカスタムポートへ(Cloud Firewall で事前許可が必要)
- パスワード認証無効化: 鍵認証のみ
- root ログイン禁止
Makefile の接続変数(SSH_HOST / REMOTE_DEPLOY_PATH / SSH_HARDENED_PORT)はセキュリティのため deploy/.env.local(Git 管理外)で管理する。
バックアップ
deploy/scripts/backup.sh が cron で毎日 3:00 に実行:
| 対象 | 方法 | 保持期間 |
|---|---|---|
| MySQL | mysqldump --single-transaction → gzip | 30 日 |
| Redis | BGSAVE → docker cp dump.rdb → gzip | 30 日 |
| アップロードファイル | Docker volume を alpine コンテナ経由で tar.gz | 30 日 |
| JWT 鍵 | ファイルコピー | 30 日 |
| .env.production | ファイルコピー | 30 日 |
S3 アップロードはオプション対応(S3_BUCKET 環境変数で有効化)。バックアップ完了・失敗は Slack 通知。
ヘルスチェック
deploy/scripts/health.sh が cron で 5 分おきに実行:
- コンテナ確認: nginx / frontend / gateway / auth / wiki / profile / ai / db / cache の 9 サービスが Running か
- HTTP 確認:
curlで nginx 経由のレスポンス(200 / 301 / 302 / 304)を確認 - ディスク使用率: 80% 超で NG
- メモリ使用率: 90% 超で NG
異常検出時は Slack / Discord に通知。
VPS リソース最適化
2GB メモリの VPS 向けに以下を設定:
- スワップ: 2GB(
vm.swappiness=10でなるべく実メモリを使う) - MySQL チューニング:
innodb_buffer_pool_size=256M/max_connections=50/ スロークエリログ有効 - Docker ログ制限: 各コンテナ
max-size: 10m×max-file: 3(最大 30MB/コンテナ) - Docker 定期清掃: 毎週日曜 4:00 に
docker system prune -f
Proto-first 開発
サービス間の契約を .proto ファイルで先に定義し、Go / TypeScript のコードを自動生成する。
proto/
├── auth/auth.proto — AuthService (4 RPCs)
├── wiki/wiki.proto — WikiServices (19 RPCs)
├── profile/profile.proto — ProfileService (8 RPCs)
└── ai/ai.proto — AIService (7 RPCs)
新しい機能を追加する際は:
.protoにメッセージ型と RPC を定義protocでコード生成(Go: サーバー/クライアント、TypeScript: 型定義)- 生成されたインターフェースを実装
これにより、サービス間の型不一致やインターフェースのずれを防ぐ。
設計判断のトレードオフ
| 判断 | メリット | トレードオフ |
|---|---|---|
| gRPC | 型安全、効率的なバイナリ通信、ストリーミング対応 | ブラウザから直接呼べない → Gateway で REST 変換が必要 |
| CQRS | 読み取り性能の最適化、書き込みと読み取りの独立スケーリング | 結果整合性(キャッシュ TTL の間は古いデータの可能性) |
| JWT RS256 | DB アクセスなしで検証可能、サービス間でトークン共有 | トークン失効が即時反映されない(有効期限まで有効) |
| Gateway 認証 | 認証ロジックの一元管理、内部サービスの簡素化 | Gateway が単一障害点になる |
| Docker Compose | シンプルな構成管理、ローカルと本番で同一構成 | 単一ホストに制約される(スケーリングには k8s が必要) |
| Ollamaローカル | 外部 API 依存なし、コストゼロ、プライバシー確保 | CPU 推論のため応答速度に制約(0.6B モデル) |
| 手動デプロイ | 変更内容を確認してから反映、事故防止 | デプロイ速度は自動化より遅い |
| GHCR | GitHub との統合が容易、無料枠あり | Docker Hub より知名度が低い |