本記事は、本リポジトリをベースにした「セキュリティ情報ブログSPA」を、AWS上で本番運用できる形に仕上げるまでのプロセス記録です。動作/開発環境、モチベーション、設計・実装、そしてハマり所まで、手元で再現できる具体性を重視してまとめました。
サイトの作成
動作環境(インフラ構成)
- フロント(静的配信): S3 + CloudFront(Next.js
output: export
) - 動的API: API Gateway + Lambda(DynamoDB をデータストアに想定)
- 認証: Cognito User Pool + Hosted UI(OAuth2/OIDC: Authorization Code Flow)
- ドメイン/証明書: Route 53 + ACM(us-east-1, CloudFront用)
- セキュリティヘッダ/CSP: CloudFront Response Headers Policy(CSP, HSTS, CT-Options, Referrer-Policy, Permissions-Policy)
- ルーティング補助: CloudFront Function(クリーンURL →
index.html
付与) - ドメイン取得: お名前.comで取得し、初期費用を最小化(レジストラコストを抑制しつつ、Route53にネームサーバを委譲)
graph LR U[User Browser] CF[CloudFront / CSP HSTS] S3[S3 Hosting] API[API Gateway] L[Lambda] DB[(DynamoDB)] Cg[Cognito Hosted UI] R53[Route53] ACM[ACM us-east-1] U <--> CF CF --> S3 U --> API API --> L L --> DB U --> Cg R53 --- CF ACM --- CF
-
CloudFront の要点
- HTMLはキャッシュ無効(Managed-CachingDisabled)
- Function で
/path
→/path/index.html
へ書き換え(/_next/*
と拡張子付きは除外) - CSP の
connect-src
に Cognito/IdP/API を明示 - エラーフォールバック(403/404 →
index.html
)は無効化(JS/CSSのMIME誤配信を防止)
-
Cognito User Pool Client
AllowedOAuthFlowsUserPoolClient=true
,allowed_oauth_flows=["code"]
allowed_oauth_scopes=["openid","email","profile"]
supported_identity_providers=["COGNITO"]
callback_urls=["https://<domain>/auth/callback"]
,logout_urls=["https://<domain>"]
- OAuth クライアント種別: Public + PKCE(SPAはクライアントシークレットを安全に保持できないため。PKCEでコード横取り攻撃への耐性を確保)
開発環境(プロセスとツール)
進め方の全体像
- 要件/設計: Kiro で要件・設計書を先に固める
- 実装(大枠): 設計反映の骨組みを作成
- 実装(仕上げ): Cursor を用いたペアプロで差分修正・リファクタ
- フェーズ2 改修: CodexCLI + VSCode(WSL上でCodexCLIを実行)で集中的に対応
flowchart TB Kiro["要件/設計"] --> Repo["Git Repository"] Repo --> Cursor["Cursor:差分実装/ペアプロ"] Repo --> Codex["Codex CLI + VSCode(WSL)"] Codex --> Repo Cursor --> Repo
WSL 上で CodexCLI を動かす(例)
- 前提: Windows 11 + WSL2 + Ubuntu、VSCode Remote を利用
- 手順(例)
- WSLでリポジトリをクローン
- Node/NPM, AWS CLI をセットアップ
- CodexCLI をインストールして実行
# 1) 基本セットアップ
sudo apt update && sudo apt install -y curl unzip
# Node (必要に応じてNVM)
# AWS CLI
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip && sudo ./aws/install
# 2) リポジトリ操作
git clone <this-repo>
cd security_spa
# 3) CodexCLI (例: npm経由のインストール)
npm i -g @codex/cli
codex --version
モチベーション
- AWS上で SPA + OAuth2(Cognito)を用いた本番運用可能なセキュア構成をゼロから構築
- 実プロダクトを通じて、セキュリティ設計・CSP・キャッシュ戦略・認証/認可を体系的に学ぶ
- (追記)セキュリティ担当として、SPAの挙動をフロント/エッジ/バックエンドで実戦理解したかった
- (追記)OAuth 2.0 / OpenID Connect のトークン(Authorization Code with PKCE、ID/Access/Refresh)仕様を、サイト実装と検証を通じて手を動かして把握したかった
やったこと(ハイライト)
要件(抜粋)
- 記事一覧/詳細/検索が高速に動作する SPA
- 認証(ログイン/ログアウト、プロフィール表示)
- 管理者は記事の投稿/更新/削除が可能
- 本番環境における安全なCSPとキャッシュ無効化運用
設計書(抜粋)
- インフラ: Terraform による S3/CloudFront/Cognito/API Gateway/Lambda/Route53/ACM
- フロント: Next.js App Router, Tailwind、Amplify(Auth)
- API: Lambda(DynamoDB想定)+ API Gateway(JWTオーソライザー)
- 監視/運用: CloudFront 無効化、S3同期、E2E/Smoke テスト(Playwright)
躓きポイント(と対策)
- CloudFront の SPA フォールバックにより、JS/CSSへ
index.html
が返却 → MIMEエラー- 対策: 403/404 フォールバックを削除し、URI リライトは Function に限定
- Service Worker の旧資産キャッシュで更新反映が遅延
- 対策: 本番は既定で SW 無効(環境変数でON可)。無効化後は CF 無効化で即時反映
- CSP が厳しすぎて Cognito や API へ接続拒否
- 対策:
connect-src
にexecute-api
/cognito-idp
/ Hosted UI ドメインを追加
- 対策:
- Cognito Hosted UI が
invalid_request/unauthorized_client
- 対策: User Pool Client の
IDP/flows/scopes/Callback/Logout
を完全一致で再設定
- 対策: User Pool Client の
- API オーソライザーが ID トークン検証のため、Access トークンで 401
- 対策: フロントの HTTP クライアントは ID トークン優先で
Authorization: Bearer
- 対策: フロントの HTTP クライアントは ID トークン優先で
- Route53 の Hosted Zone 不一致
- 対策:
domain_name
の見直しと ACM(バージニア北部) の適用
- 対策:
OAuth 2.0 + OpenID Connect の仕様と本構成でのトークン取り扱い
採用フロー
- 本構成は SPA のためクライアントは Public。クライアントシークレットを安全に保持できないため、Authorization Code + PKCE を採用。
- 流れ(要約)
- ブラウザ → Cognito Hosted UI へリダイレクト(code_challenge 付き)
- 認証成功で
authorization_code
を/auth/callback
に付与 - ブラウザ側(Amplify)が
code_verifier
でトークンに交換(ID/Access/Refresh)
3種類のトークン
- ID Token(JWT, OIDC)
- 用途: 認証結果とプロフィール(
sub
,email
,email_verified
,cognito:groups
など)をフロントに伝える - 注意: 本来は「ユーザー情報表示」向け。APIの認可ヘッダとしては原則非推奨
- 検証:
iss
(発行者),aud
(クライアントID),exp
,iat
,nonce
(必要時)
- 用途: 認証結果とプロフィール(
- Access Token(JWT)
- 用途: APIアクセスのための権限トークン(
scope
,aud
はリソースサーバに合わせる) - 原則: API への
Authorization: Bearer
は Access Token を使用
- 用途: APIアクセスのための権限トークン(
- Refresh Token
- 用途: 長寿命の再発行用シークレット
- リスク: ブラウザ保管は漏えいリスクが高い。SPAでは「発行しない/短寿命/回数制限/メモリ保持」など運用でリスク低減
本構成でブラウザに返るトークンと保存
- Hosted UI →
/auth/callback
後、Amplify のfetchAuthSession()
がトークンを取得しセッションに保持 - 設定(例):
access_token_validity = 60分
,id_token_validity = 60分
,refresh_token_validity = 30日
- 保存: Amplify が Web Storage を利用(実運用では「メモリ優先」「localStorage回避」が望ましい。BFF+HttpOnly Cookie 方式がより安全)
このリポジトリでの一時方針(APIに送るトークン)
- API Gateway の JWT オーソライザーが ID Token 検証に寄っていたため、フロントの HTTP クライアントは一時的に ID Token を優先 して送出
- 望ましい最終形: API オーソライザーを Access Token 検証に切替え、フロントも Access Token を送出
- 理由: ID Token は「認証の証跡」、Access Token は「認可トークン」。役割分離がベストプラクティス
守るべき優先度(機密度)
- 高: Refresh Token > Access Token > ID Token: 低
- 対策例
- Refresh: できれば SPA には発行しない or 短寿命+回数制限。発行する場合はメモリ保持やローテーション
- Access/ID: XSS対策(CSP堅牢化/依存ライブラリ健全化)、Storage の最小化、不要時クリア
トークン検証の要点
- 署名: JWKs(
/.well-known/jwks.json
)でkid
による鍵選択→署名検証 - クレーム:
iss
(Cognito発行者URL),aud
(クライアントID),exp
(期限) - 権限/ロール: Cognito グループ(
cognito:groups
)を ID Token のクレームから抽出→フロント表示/ゲートに反映
JWTの文字列表現とJWS/JWEの違い(本構成)
- 形式:
header.payload.signature
を.
で連結した文字列。- それぞれ Base64URL エンコード(
+
→-
,/
→_
, パディング=
なし)
- それぞれ Base64URL エンコード(
- 構成
- header: 署名アルゴリズムや鍵ID
- payload: クレーム(
iss
,sub
,aud
,exp
,iat
,email
,email_verified
,cognito:groups
など) - signature:
base64url(header) + "." + base64url(payload)
に対する署名
例(概念図)
<base64url>{"alg":"RS256","typ":"JWT","kid":"abc123"}</base64url>.
<base64url>{
"iss": "https://cognito-idp.ap-northeast-1.amazonaws.com/<user-pool-id>",
"sub": "user-sub",
"aud": "<app-client-id>",
"exp": 1727000000,
"iat": 1726996400,
"email": "user@example.com",
"email_verified": true,
"cognito:groups": ["admin"]
}</base64url>.
<base64url>(RS256 signature)</base64url>
- JWS(JSON Web Signature)
- 署名付き(改ざん検知)。payloadは「平文」なので閲覧可能(秘匿ではない)。
- 本サイトの ID Token / Access Token は Cognito により JWS(RS256)で発行。
- 検証は Cognito の JWKs(
/.well-known/jwks.json
)でkid
を用いて公開鍵選択→署名検証。
- JWE(JSON Web Encryption)
- 暗号化(秘匿)されたJWT。5セグメント構成(
header.encryptedKey.iv.ciphertext.tag
)。 - 本サイトでは未使用(HTTPS + JWS で十分。秘匿が必要ならBFF/HttpOnly Cookieなど別方式を検討)。
- 暗号化(秘匿)されたJWT。5セグメント構成(
- Refresh Token について
- CognitoのRefreshは一般に「不透明(opaque)」な長い文字列(JWTでない)。
- 機密度が最も高い。SPAでは極力保持を避け、短寿命・回数制限・メモリ保持などでリスク低減が望ましい。
JWEを使うべきケースと注意点(SPA観点)
- 使うべきケース
- JWTペイロードに**秘匿すべきクレーム(PII/PHI/金融情報/機密属性)**をどうしても入れる要件がある
- 法規制や内部規程でトークン内容の秘匿が明示的に要求される(TLSだけでは不十分と判断される)
- 受信側が**サーバ(秘匿鍵を安全に保管可能)**であり、復号をサーバで完結できる
- SPAでの注意点
- JWEは復号鍵の秘匿が前提。ブラウザ(SPA)に復号用の秘密鍵を置くと漏えいするため本末転倒
- よって、SPAが直接JWEを復号する設計は基本的に不適。JWEはBFF/サーバ間での秘匿用途に適合
- 推奨の代替策
- JWTに機密クレームを入れない(最小クレーム)。必要情報はAPI経由で都度取得
- BFF + HttpOnly Cookie 方式でサーバがトークン管理し、ブラウザはCookieのみ(JS不可視)
- リファレンストークン(オペークトークン)を用い、クライアントは識別子だけを持ち、内容はサーバ側ストアで管理
- どうしても暗号化が必要なデータはアプリケーションペイロードを受信者公開鍵で暗号化(エンドツーエンド)し、トークンとは分離
なぜSPAはクライアントシークレットを安全に保管できないのか/Cookie方式との比較
SPA(Public + PKCE)で秘密を持てない理由
- 実行環境がユーザー側(ブラウザ)
- 配布されたJSバンドルは誰でも取得・解析可能(DevTools/拡張/ソースマップ)
- localStorage/sessionStorage/IndexedDB はJSから可読で、XSS成立時に窃取されうる
.env
はビルド時に埋め込まれ、配布物に含まれる(秘匿不可)
- 結論: SPAはクライアントシークレットを保持できないため、OAuthクライアントはPublicとし、PKCEでコード横取りを緩和するのが前提
Cookie(BFF/サーバ)方式との違い
-
概要
- BFF/サーバ: サーバ側でトークンを取得・保管し、ブラウザへは HttpOnly + Secure + SameSite Cookie を付与。ブラウザJSからトークンは不可視
- SPA(Public+PKCE): ブラウザJSがトークンを取得・保持し
Authorization: Bearer
でAPIへ送信
-
メリット/デメリット
- BFF/サーバ(Cookie)
- 長所: トークンがJSから不可視(XSS耐性向上)、HttpOnly/SameSiteでCSRF対策が取りやすい、トークンローテーション/失効制御がしやすい
- 短所: サーバ実装/運用コスト(スケーリング、ステート管理、追加費用)
- SPA(Public+PKCE)
- 長所: サーバ不要でS3+CloudFrontの静的配信で完結、コスト最小・スケール容易、実装がシンプル
- 短所: トークンがJS空間に存在しXSSリスク、Refresh Tokenの安全運用が難しい、CORS/CSPのチューニングが必須
- BFF/サーバ(Cookie)
-
本プロジェクトの選択理由
- 学習・検証フェーズでは、静的配信 + Cognito Hosted UI + PKCE が最小コストで迅速
- 将来的により強固な防御が必要なら BFF/Server(Cookie)方式 へ段階的移行(Access Token検証・HttpOnly運用)
-
SPA方式での実務的ガード(推奨)
- 厳格なCSP(script-src, connect-src 等)、依存ライブラリの健全性維持
- トークンは可能な限りメモリ保持(永続ストレージ最小化)、短寿命化、Refreshは無効/短期/回数制限
- API側はAccess Token検証へ移行し役割分離(ID=認証、Access=認可)
Permissions(Scopes) と Claims の詳細
用語整理
- Permissions(権限): OAuth 2.0 では通常、APIが許可する操作を表現するためにscopeで表す(例:
articles:read
,articles:write
)。 - Claims(クレーム): JWT(ID/Access)に含まれる属性情報(
iss
,sub
,aud
,exp
などの標準、email
やcognito:groups
等の拡張)。
本構成でのScopes(権限)
- 採用スコープ:
openid
,email
,profile
(OIDC標準。IDの取得を目的) - 将来拡張例:
- 読み取り:
articles:read
- 投稿/更新:
articles:write
- 管理操作:
admin:*
- 読み取り:
- 運用ポイント: スコープは「最小権限」で付与。API Gateway 側でスコープ検証を組み込むか、Lambdaオーソライザーで
scope
クレームを解釈して許可/拒否。
ID Token と Access Token の主なクレーム
- 共通(標準):
iss
(発行者),aud
(クライアントID),exp
(有効期限),iat
(発行時刻),sub
(一意ユーザーID) - ID Token: ユーザー属性の表明(
email
,email_verified
,name
等)、Cognito固有(cognito:groups
) - Access Token: 認可関連(
scope
,token_use: "access"
,client_id
,username
)
例: ID Token のpayload(抜粋)
{
"iss": "https://cognito-idp.ap-northeast-1.amazonaws.com/<user-pool-id>",
"sub": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
"aud": "<app-client-id>",
"exp": 1727000000,
"iat": 1726996400,
"email": "user@example.com",
"email_verified": true,
"cognito:groups": ["admin"],
"token_use": "id"
}
例: Access Token のpayload(抜粋)
{
"iss": "https://cognito-idp.ap-northeast-1.amazonaws.com/<user-pool-id>",
"sub": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
"client_id": "<app-client-id>",
"username": "user@example.com",
"scope": "openid email profile articles:read",
"exp": 1727000000,
"iat": 1726996400,
"token_use": "access"
}
本プロジェクトでのClaims活用
- フロント: ID Token の
cognito:groups
からadmin/user
を導出し、ナビ表示/管理UI出し分け。 - API: 現状はID Token検証に寄せているが、最終的にはAccess Token検証へ切替予定。必要に応じて
scope
で操作別のガードを実装。
ベストプラクティス
- ID Token は表示/識別用途。APIの
Authorization
にはAccess Tokenを使用。 - スコープは細分化し、最小権限で発行。管理操作は専用スコープやグループで厳格に制御。
- カスタムクレームに機密情報を載せない。必要な詳細はAPI呼び出しで都度取得。
- 署名/JWK検証(
kid
選択),iss/aud/exp
検証を必ず行う。
ネットワーク設計(Public / Private / HA / VPC)
方針
- 静的配信(S3/CloudFront)はVPC外(グローバル)で提供し、API経路はAPI Gatewayを正面に置く構成。
- LambdaはPrivateサブネット(複数AZ)に配置し、DynamoDB/S3はVPCエンドポイント経由(可能な限りNAT経由を削減)。
- 高可用性(HA)はマルチAZのサブネット設計とNAT GW冗長化で担保。
構成要素(例)
- VPC: 10.0.0.0/16
- AZ: ap-northeast-1a, ap-northeast-1c(例)
- Public Subnets: 10.0.0.0/24 (1a), 10.0.1.0/24 (1c)
- Internet Gateway(IGW)へデフォルトルート
- NAT Gatewayを各AZに配置(NAT単一障害点を回避)
- Private App Subnets: 10.0.10.0/24 (1a), 10.0.11.0/24 (1c)
- Lambda ENI配置
- 送信先は基本VPCエンドポイント、必要時のみ同AZのNAT GW経由
- VPC Endpoints(推奨)
- Gateway型: DynamoDB, S3
- Interface型: CloudWatch Logs, Secrets Manager, STS など(必要に応じて)
通信経路
- ブラウザ → CloudFront → S3(静的アセット)
- ブラウザ → CloudFront → API Gateway → Lambda(VPC)→ DynamoDB(Gateway Endpoint)
- Lambda の外向き通信は VPC Endpoint を優先し、外部のみ NAT GW を利用
EC2/ECSを使わずにLambdaを採用するメリットと向いている構成
なぜLambdaか(EC2/ECSとの比較)
- 運用負荷最小: OS/ミドル/パッチ/オートスケールの管理不要。コードに専念できる
- 自動スケーリング: リクエストに応じて自動拡縮。スパイク耐性が高い
- 従量課金: 実行時間とリクエスト数に応じて課金。アイドルコストがほぼゼロ
- 周辺統合が容易: API Gateway/Step Functions/S3/DynamoDB/EventBridge とシームレス連携
- セキュリティ境界: コンテナ/OS面の責任共有が小さく、最小権限IAM設計に集中できる
EC2/ECS は常時稼働・長時間処理・特殊ミドル要件・独自ネットワーク制御が必要なときに選択。Lambda は短時間・イベント駆動・サージがあるワークロードに適合。
このサイト構成とLambdaの相性
- 静的フロント(S3+CloudFront)× 短時間API: 記事取得/投稿などは短いI/O中心で、Lambdaと相性が良い
- イベント駆動: 記事のインデクシング・分析・画像処理などをS3/イベントで非同期実行可能
- マイクロサービス分割: 記事/カテゴリ/ユーザ/監視などドメインごとに小さな関数へ分離
- 今回ECSを採用しなかった理由: フロントはS3+CloudFrontの静的配信、APIはAPI Gateway→Lambdaで完結し、常時稼働のWebサーバ/コンテナが不要だったため(Webサーバの維持管理コストを削減)
向いているユースケース
- REST/GraphQL APIの短時間処理(~数百ms〜数秒)
- Webhook/バッチ/スケジュール実行(EventBridge/CloudWatch Events)
- S3/DynamoDBトリガでの非同期処理・サムネイル生成・エンリッチ
- ワークフロー(Step Functions)での段階処理・リトライ・補償
向いていない(ECS/EC2を検討)
- 長時間処理やストリーミング常駐が必要(Lambda最大15分)
- 低レイテンシ厳格かつ超高RPSでコールドスタートの影響が許容できない
- 特殊ミドルウェアやネイティブ依存、大容量ディスク/永続プロセスが必要
- 双方向通信の恒常接続(WebSocketはAPI Gatewayで可能だが運用制約がある)
コスト・パフォーマンスの勘所
- 低〜中トラフィック: Lambdaが有利(アイドルコストゼロ)
- 高トラフィック一定: ECS(Fargate)/EC2の常時稼働が単価で有利になる境界がある
- プロファイルしてから選択: 実測の継続時間・同時実行・メモリで月額比較
Lambdaの考慮点(対策)
- コールドスタート: Provisioned Concurrency、軽量ランタイム、バンドル最適化、VPCエンドポイントでENI遅延低減
- タイムアウト/再試行:
timeout
適正化、Idempotency設計、DLQ/リトライポリシー - 観測性: CloudWatch Logs + Tracing(X-Ray/OTel)、メトリクス/アラート整備
- ネットワーク: VPC内接続はENI生成に伴うレイテンシ増。必要なVPCEを作成しNAT経由を最小化
本プロジェクトは「静的SPA + 短時間API + スパイクあり」を想定しており、Lambdaの強み(自動スケール・従量課金・運用負荷軽減)を享受できる構成です。
SPAベストプラクティス適合性評価
合致している点
- 配信構成: S3 + CloudFront(CDN)で静的アセットを配信
- HTTPS強制とセキュリティヘッダ: HSTS, X-Content-Type-Options, Referrer-Policy などをCloudFrontで付与
- CSP:
script-src
/style-src
/connect-src
をホワイトリスト運用(Cognito/IDP/APIのみ許可) - ルーティング: CloudFront Functionで
/path
→/path/index.html
の書き換え。403/404でのSPAフォールバックを無効化してMIME誤配信を防止 - キャッシュ戦略: HTMLは
no-cache, no-store, must-revalidate
で即時反映。CloudFront無効化フローを運用 - 認証フロー: Publicクライアント + Authorization Code with PKCE(Hosted UI)
- トークン検証観点:
iss/aud/exp
検証前提、JWKsで署名検証(JWS)。機密クレームはトークンへ載せない方針 - 秘密管理: クライアントにシークレットを置かない(Publicクライアント)。環境変数注入で動作
- IaC/自動化: Terraformでインフラ定義、S3同期→CloudFront無効化の運用を確立
- 品質担保: Playwrightのスモーク/E2E(モバイルメニュー配慮)
要改善(今後の方針)
- API認可トークンの統一: 現状はAPIオーソライザー都合で一時的にID Tokenを送出。最終形はAccess Token送出に統一(役割分離)
- スコープ設計: OIDC標準(
openid email profile
)に加え、将来的にarticles:read/write
などリソース単位のscopeを導入し最小権限化 - トークン保管: Amplify既定のWeb StorageはXSS時に窃取リスク。可能ならメモリ保持優先、長寿命Refreshの利用抑制
- 静的アセットのキャッシュ: ハッシュ付き
/_next/static/*
は長期キャッシュ+immutableが理想(HTMLはno-cacheのまま)。現状のヘッダ適用範囲を再確認 - BFF検討: より厳密な防御が必要な場合はBFF+HttpOnly Cookieへ移行(JSからトークン不可視、CSRF対策同時適用)
- API側ガード:
scope
/group
(cognito:groups
)を用いたルート単位の許可制御をLambdaで明文化 - 監視/可観測性: X-Ray/OTel導入、アラート閾値(4xx/5xx, p95/p99)設定の強化
推奨アクション(短期)
- API GatewayのJWTオーソライザーをAccess Token検証へ切替 → フロントHTTPクライアントもAccess優先に変更
/_next/static/*
に長期キャッシュ(Cache-Control: public, max-age=31536000, immutable
)を適用(HTMLはno-cache維持)- トークンのメモリ保持(永続Storage最小化)と短寿命化を検討(必要に応じてRefresh無効/短期/回数制限)
推奨アクション(中期)
articles:*
スコープの導入とAPI側のscope検証- BFF方式のPoC(Auth Code with PKCE→サーバ取得、ブラウザはHttpOnly Cookie)
- 可観測性強化(トレーシング/メトリクス/ダッシュボード)
一般的なSPA(純CSR)との違いと本構成のメリット/デメリット
一般的なSPA(純CSR)の流れ
- ブラウザがJSバンドルを取得 → JSがAPIへ直接アクセス → 返却JSONからDOM構築
- ルーティングはフロント側、CDN/ストレージはシンプルに静的配信
本構成の主な違い
- Next.js を静的エクスポート(output: export)で採用し、CloudFrontでCSP/セキュリティヘッダを一括付与
- CloudFront FunctionでクリーンURLを
/index.html
へリライトし、SPAの404/403フォールバックを無効化(MIME誤配信対策) - 認証は Cognito Hosted UI + Authorization Code with PKCE(Public) に統一
- HTMLはno-cacheで即時反映、ハッシュ付き静的アセットは長期キャッシュ想定(将来最適化)
- Service Workerは本番既定オフ(更新反映遅延の事故を回避)
メリット(本構成)
- セキュリティ強化: 厳格CSP/HSTS/Permissions-Policy、JWS署名検証、PKCE採用、サーバシークレット不要
- 運用容易: S3+CloudFront+Lambda+API Gatewayでサーバ不要。デプロイはS3同期とCF無効化で完結
- 可搬性/コスト: 静的配信が中心で低コスト、Lambdaは従量でスパイクにも強い
- ルーティング健全性: 404/403で
index.html
を返さないため、JS/CSSのMIMEエラーを根本防止
デメリット/トレードオフ
- SSR/SSGの制約: 完全CSR寄りのため、初期描画/SEOでSSRほど強くない(静的エクスポートできる範囲に限定)
- キャッシュ運用の複雑さ: HTML no-cache、静的アセット長期キャッシュ、CF無効化の運用知識が必要
- トークン管理はブラウザ側: BFFでないためXSS時のリスクは相対的に高い(CSP/メモリ保持/短寿命で軽減)
- 高度なリアルタイムや長時間処理: Lambda/静的配信構成では設計上の工夫や別サービスの併用が必要
こう使い分ける
- 本構成: 「静的SPA + 短時間API + コスト/運用最小化 + 学習/検証」に最適
- さらにセキュア/高機能にするなら: BFF + HttpOnly Cookie、SSR/SSG混在、WebSocket/Streams対応サービスの併用を段階導入
付録:運用コマンド例
フロントのビルドと反映
npm run build
aws s3 sync out/ s3://<your-s3-bucket>/ --delete --no-cli-pager
aws cloudfront create-invalidation --distribution-id <CF_DIST_ID> --paths "/*" --no-cli-pager
必要なフロント環境変数(例)
NEXT_PUBLIC_APP_URL=https://<custom-domain>
NEXT_PUBLIC_COGNITO_USER_POOL_ID=
NEXT_PUBLIC_COGNITO_CLIENT_ID=
NEXT_PUBLIC_COGNITO_DOMAIN=
NEXT_PUBLIC_API_GATEWAY_URL=https://<api-id>.execute-api.ap-northeast-1.amazonaws.com/prod
おわりに
- SPA×OAuth2×CSP×キャッシュ戦略を一通り組み合わせると、「表示は速いが安全」な構成が作れます。
- 本記事の構成/手順は、静的SPAを本番運用する上での落とし穴(CSP/キャッシュ/フォールバック/Hosted UI設定)を回避する実践例として活用できます。
コメント