アーキテクチャ¶
Rikako は 1 つのリポジトリに iOS アプリ / 4 種類の Web / 2 つの API / データ が同居する モノレポ。AWS 上では dev(197865631794)と prod(211125415945)の 2 アカウントに、 ほぼ同じ構成を並べている。
図が大きくなるため関心ごとに分けている。
1. クライアントと配信ドメイン¶
graph LR
subgraph クライアント
iOS["iOS アプリ<br/>化学版 / 4択IT"]
Browser[ブラウザ]
end
subgraph "CloudFront + S3(静的配信)"
LP["rikako.org<br/>LP(素の HTML)"]
Web["it. / chemistry.<br/>問題集 Web(Next.js)"]
Portal["account.<br/>アカウントポータル"]
AdminFE["admin.<br/>管理画面"]
ImageCDN["image.<br/>問題画像"]
ContentCDN["content.<br/>静的 JSON"]
DocsCDN["docs.<br/>MkDocs(prod のみ)"]
end
subgraph API
PublicAPI["api.<br/>公開 API"]
AdminAPI["admin./api<br/>管理 API"]
end
iOS --> ContentCDN
iOS --> ImageCDN
iOS --> PublicAPI
Browser --> LP
Browser --> Web
Browser --> Portal
Browser --> AdminFE
Web --> PublicAPI
Web --> ContentCDN
Portal --> PublicAPI
AdminFE --> AdminAPI
- iOS アプリは問題データを公開 API から取らない。
content.の静的 JSON を読む (コンテンツ配信)。公開 API は回答送信・学習記録・お知らせなど動的なものだけ。 - 問題集 Web も静的。Next.js の静的エクスポートで、ビルド時に
content.の JSON を焼き込む。web/1 つをNEXT_PUBLIC_SITEで it / chemistry に出し分ける。 - dev は LP / Web / ポータル / 管理画面が Basic 認証(CloudFront Function)。prod は 管理画面のみ Basic 認証で、他は一般公開。
- prod の
docs.rikako.orgだけ dev に対応物が無い。
2. バックエンド¶
graph TB
APIGW["API Gateway HTTP API<br/>api.rikako.org"]
Lambda["Lambda + Web Adapter<br/>公開 API"]
AdminCF["CloudFront + Basic Auth<br/>admin.rikako.org"]
AdminFURL["Lambda Function URL<br/>AWS_IAM + OAC"]
AdminLambda["Lambda + Web Adapter<br/>管理 API"]
ImageS3["S3 rikako-images-*"]
ContentS3["S3 rikako-content-*"]
SSM["SSM Parameter Store<br/>DATABASE_URL / OPENAI / SLACK"]
OpenAI["OpenAI API<br/>問題チャット"]
DB[("Neon PostgreSQL<br/>ap-southeast-1")]
APIGW --> Lambda
AdminCF --> AdminFURL --> AdminLambda
Lambda --> DB
AdminLambda --> DB
Lambda -.->|起動時に解決| SSM
AdminLambda -.->|起動時に解決| SSM
Lambda --> OpenAI
AdminLambda -->|Presigned URL| ImageS3
AdminLambda -->|publish| ContentS3
シークレットの扱い: Lambda 環境変数には実値を持たず
ssm:/path形式の参照だけを設定し、 起動時にapp/internal/secrets.Resolveが SSM から実値を取得してos.Setenvで展開する (Python の slack_notifier も同じ規約)。aws lambda update-function-codeのレスポンス経由で シークレットが露出することを防ぐ(Issue #199)。DB 接続: 公開 API / 管理 API の Lambda は Neon の pooled endpoint を使う。datasync と マイグレーションは direct。詳細は runbook。
3. 認証¶
普段は登録なしの匿名利用で、機種変更などでデータを引き継ぎたいときだけ メールログイン(Cognito User Pool)する(Issue #283)。
匿名時の端末識別子はクライアントで出どころが違う。iOS は Cognito Identity Pool の
Identity ID、Web とポータルは crypto.randomUUID() で生成した UUID(Identity Pool は
呼ばない)。どちらも X-Device-ID ヘッダで送り、サーバーは identity_id として扱う。
graph LR
subgraph 通常利用
IOS1[iOS] -->|GetId| CIP[Cognito Identity Pool]
CIP -->|Identity ID| IOS1
WEB1["Web / ポータル"] -->|"crypto.randomUUID()"| LS[localStorage]
IOS1 -->|X-Device-ID| API[公開 API]
WEB1 -->|X-Device-ID| API
API --> DB[("users / user_answers")]
end
subgraph ログイン
App2["iOS / Web / ポータル"] -->|SignUp / InitiateAuth| CUP[Cognito User Pool]
CUP -->|メール確認コード| SESx[SES]
CUP -->|JWT| App2
App2 -->|"Authorization: Bearer + POST /account/link"| API2[公開 API]
API2 -->|端末識別子に sub を紐づけ| DB
end
- 端末識別子(
X-Device-ID)の出どころはクライアントで違う。iOS は Cognito Identity Pool の Identity ID を Keychain に永続化する。Web とポータルは Identity Pool を呼ばず、crypto.randomUUID()で生成した UUID を localStorage に持つ(src/lib/deviceId.ts)。 サーバーはidentity_idとして受け取るだけなので、どちらでも同じように扱える - メールログインは iOS / Web / ポータルのいずれからでもできる。Web は
src/lib/cognito.tsと(auth)配下で User Pool を直叩きしている POST /account/link: ログイン中の Cognito ユーザーに、いま使っている端末識別子の 学習記録を紐づける- サーバー側は JWT 検証のみ(
app/internal/auth/)。JWKS は kid 単位でキャッシュ(TTL 1 時間) - 環境変数が未設定なら認証をスキップする(ローカル開発・CI 用)
設計の詳細は メールログイン設計 を参照。
4. データの流れ¶
問題データは YAML が source of truth。DB を経由して静的 JSON になり、CDN から配信される。
graph LR
YAML["data/*.yml<br/>(リポジトリ)"] -->|datasync apply| DB[("Neon")]
Admin["管理画面"] -->|CRUD| DB
DB -->|"POST /api/publish"| S3["S3 rikako-content-*"]
S3 --> CF["content.rikako.org"]
CF --> iOS[iOS アプリ]
CF -->|ビルド時に焼き込み| WebBuild["問題集 Web のビルド"]
web はビルド時に焼き込むため、公開内容を変えたら /publish の後に web を再デプロイする
必要がある。詳しくは データ同期 と runbook。
画像は別系統で、API が返す images にフル URL(https://image.rikako.org/<uuid>.png)が入り、
クライアントが直接 CloudFront から取得する。
5. デプロイと CI¶
graph TB
PR[Pull Request] --> Plan["terraform plan(dev)<br/>datasync plan<br/>Go / Web / iOS のテスト"]
Plan -->|tfcmt| Comment[PR コメント]
Main["main へ push"] --> DevAuto["dev の自動デプロイ<br/>API / 管理API / 管理画面 / LP / Web / ポータル<br/>+ terraform apply"]
Manual["手動 dispatch"] --> ProdApprove["production environment の承認"]
ProdApprove --> ProdDeploy["prod のデプロイ・apply・マイグレーション"]
- dev:
mainへの push で、変更のあった領域だけが自動デプロイされる(各ワークフローのpathsで判定。ワークフローファイル自身もpathsに含める) - prod: アプリのデプロイ・Terraform apply・マイグレーションは手動 dispatch +
productionenvironment の承認ゲート。ただしドキュメント(docs.yml)は例外で、mainへの push で prod アカウントのrikako-docsへ承認なしに自動デプロイされる - Terraform: dev は
apply-terraform-dev.ymlが自動 apply。prod はapply-terraform-prod.yml(plan → 承認 → apply) - 認証: GitHub Actions OIDC。AWS のアクセスキーは持たない
- ECR:
rikako-api/rikako-admin-apiは shared アカウント(579039992557)にあるが、 その IaC は別リポジトリaws-iacが管理する。このリポジトリは pull で参照し、push はrikako-ecr-pushロールを使うだけ
コンテナのデプロイは「ECR に build & push → Lambda のコード更新 → ヘルスチェック」の 3 段。
6. 監視とバックアップ¶
graph LR
Alarm[CloudWatch Alarm] --> SNS["SNS rikako-alerts"]
SNS --> Notifier["Lambda slack_notifier<br/>(Python)"]
Logs["エラーログ<br/>サブスクリプションフィルタ"] -->|直接 invoke| Notifier
Notifier --> Slack[Slack]
Backup["backup-db-prod.yml<br/>(毎日 cron)"] --> BackupS3["S3 rikako-db-backup-*"]
- アラームは SNS 経由、エラーログのサブスクリプションフィルタは slack_notifier を直接 invoke する
- prod には CloudWatch ダッシュボード(
cloudwatch.tf)がある - DB バックアップは prod のみ。手順は DBバックアップとリストア
7. インフラ構成¶
| リソース | 用途 | 環境 |
|---|---|---|
| Lambda + Web Adapter | 公開 API / 管理 API | dev / prod |
| API Gateway HTTP API | 公開 API(api.dev.rikako.org / api.rikako.org) |
dev / prod |
| Lambda Function URL + CloudFront (OAC) | 管理 API(admin.<env>/api) |
dev / prod |
| S3 + CloudFront | LP / 問題集 Web(it・chemistry)/ ポータル / 管理画面 | dev / prod |
| S3 + CloudFront (OAC) | 画像 CDN(image.) |
dev / prod |
| S3 + CloudFront | コンテンツ CDN(content.) |
dev / prod |
| S3 + CloudFront | ドキュメント(docs.rikako.org) |
prod のみ |
| Neon PostgreSQL | データベース(ap-southeast-1) | dev / prod |
| Cognito User Pool / Identity Pool | メールログイン / 匿名認証 | dev / prod |
| SES | Cognito のメール送信(DKIM・DMARC を Cloudflare に登録) | dev / prod |
| SSM Parameter Store | シークレット(database-url / openai-api-key / slack-*-webhook-url ほか) |
dev / prod |
| SNS + Lambda (Python) | CloudWatch アラーム・エラーログ → Slack | dev / prod |
| S3 | DB バックアップ | prod のみ |
| Cloudflare | DNS(rikako.org ゾーン) |
共通 |
| ECR | コンテナレジストリ(IaC は aws-iac 管理) |
shared |
| S3 | Terraform State | dev / prod |
8. Terraform モジュール¶
モジュールはリソースのラッパー。環境レベルで組み合わせて使う(例: dev/image_cdn.tf が
s3 + cloudfront)。フロントエンド系のディストリビューションは要件が個別なため、
モジュールを介さず環境側に直接書いている。
| モジュール | 内容 |
|---|---|
modules/s3 |
S3 バケット + パブリックアクセスブロック |
modules/cloudfront |
CloudFront ディストリビューション + OAC |
modules/lambda |
Lambda + IAM Role + CloudWatch Logs + Function URL (optional) + SSM 読み取りポリシー (optional) |
modules/api_gateway |
API Gateway HTTP API + カスタムドメイン + スロットリング |
modules/cognito |
Cognito User Pool + Client |
modules/cognito_identity |
Cognito Identity Pool + IAM Role (unauthenticated) |