はじめに
この記事では、React で作成したシングルページアプリケーション(SPA)を AWS 上で安全かつ高速に配信する方法を解説します。
「なぜこの構成が必要なの?」
- React アプリは静的ファイル(HTML/CSS/JS)なので、サーバーを立てるより軽量で安い
- 世界中のユーザーに高速でアプリを届けたい
- セキュリティを確保しつつ、運用しやすい構成にしたい
基本概念の説明
SPA(シングルページアプリケーション)とは?
- 1つのHTMLページで動作するWebアプリ
- ページ遷移時もブラウザでJavaScriptが動的にコンテンツを変更
- 例:
/about
というURLでも実際はindex.html
を表示し、JSでコンテンツを切り替え
使用するAWSサービス
- S3(Simple Storage Service):ファイル保存庫。React のビルド成果物を保存
- CloudFront:CDN(コンテンツ配信ネットワーク)。世界各地のサーバーからファイルを高速配信
- OAI(Origin Access Identity):CloudFront だけが S3 にアクセスできるようにする仕組み
データの流れ
- ユーザーがWebサイトにアクセス
- Route53 でドメイン名を CloudFront のIPアドレスに変換
- CloudFront が最寄りのエッジサーバーからコンテンツを配信
- ファイルがキャッシュにない場合のみ S3 から取得
各リソースの詳細設定
S3 バケット設定(既存バケットを使用)
📁 S3バケット
├── index.html # メインのHTMLファイル
├── assets/
│ ├── main.a1b2c3.js # ハッシュ付きJSファイル
│ ├── style.d4e5f6.css # ハッシュ付きCSSファイル
│ └── logo.png
└── favicon.ico
重要な設定
- Public Access Block: ON(一般公開しない)
- 静的サイトホスティング: 無効(CloudFront 経由でのみアクセス)
- バケットポリシー: OAI からの読み取りのみ許可
CloudFront Distribution 設定
基本設定
- Origin: S3バケット(OAI使用でセキュア)
-
Default Root Object:
index.html
(ルートアクセス時に表示) - Viewer Protocol Policy: HTTPS 強制リダイレクト
キャッシュ戦略
- index.html: 短期キャッシュ(更新を素早く反映)
- assets/*: 長期キャッシュ(ハッシュ付きファイル名のため安全)
SPA 対応(重要!)
React Router 等でクライアントサイドルーティングを使用する場合、/about
というURLに直接アクセスしても該当ファイルが存在しないため404エラーになります。これを index.html
にリダイレクトして JS でルーティング処理させます。
CDK 実装例
必要なライブラリのimport
import { Duration } from 'aws-cdk-lib';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import { S3Origin } from 'aws-cdk-lib/aws-cloudfront-origins';
リソース実装
// 既存 S3 バケットを参照
// 注意:事前にバケットを作成しておく必要があります
const bucket = s3.Bucket.fromBucketName(
this,
'SiteBucket',
'YOUR_BUCKET_NAME' // ここを実際のバケット名に変更
);
// OAI作成 & S3読み取り許可
// CloudFront からのみ S3 にアクセス可能にする
const oai = new cloudfront.OriginAccessIdentity(this, 'OAI', {
comment: 'React SPA OAI'
});
bucket.grantRead(oai); // OAI に読み取り権限を付与
// キャッシュポリシー定義
// HTMLファイル用:短期キャッシュ(更新をすぐ反映)
const cacheHtml = new cloudfront.CachePolicy(this, 'CacheHtml', {
cachePolicyName: 'ReactSPA-HTML-Cache',
comment: 'HTML files - short cache',
minTtl: Duration.seconds(0),
defaultTtl: Duration.seconds(0),
maxTtl: Duration.seconds(1),
enableAcceptEncodingBrotli: true,
enableAcceptEncodingGzip: true,
});
// アセットファイル用:長期キャッシュ(パフォーマンス重視)
const cacheAssets = new cloudfront.CachePolicy(this, 'CacheAssets', {
cachePolicyName: 'ReactSPA-Assets-Cache',
comment: 'Asset files - long cache',
minTtl: Duration.hours(1),
defaultTtl: Duration.days(365),
maxTtl: Duration.days(365),
enableAcceptEncodingBrotli: true,
enableAcceptEncodingGzip: true,
});
// セキュリティヘッダ設定
const securityHeaders = new cloudfront.ResponseHeadersPolicy(this, 'SecurityHeaders', {
responseHeadersPolicyName: 'ReactSPA-Security-Headers',
comment: 'Security headers for React SPA',
securityHeadersBehavior: {
// HTTPS 強制(1年間)
strictTransportSecurity: {
accessControlMaxAge: Duration.days(365),
includeSubdomains: true,
preload: true,
override: true
},
// MIME タイプスニッフィング防止
contentTypeOptions: { override: true },
// iframe での埋め込み禁止
frameOptions: {
frameOption: cloudfront.HeadersFrameOption.DENY,
override: true
},
// リファラー情報制限
referrerPolicy: {
referrerPolicy: cloudfront.HeadersReferrerPolicy.NO_REFERRER,
override: true
},
// XSS 攻撃防御
xssProtection: {
protection: true,
modeBlock: true,
override: true
},
},
});
// CloudFront Distribution 作成
const distribution = new cloudfront.Distribution(this, 'FrontendCdn', {
comment: 'React SPA Distribution',
defaultRootObject: 'index.html',
// デフォルト動作(全てのパス)
defaultBehavior: {
origin: new S3Origin(bucket, {
originAccessIdentity: oai
}),
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
cachePolicy: cacheHtml, // HTML ファイルは短期キャッシュ
responseHeadersPolicy: securityHeaders,
compress: true, // Gzip/Brotli 圧縮有効
},
// 特別な動作(アセットファイル)
additionalBehaviors: {
'assets/*': {
origin: new S3Origin(bucket, {
originAccessIdentity: oai
}),
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
cachePolicy: cacheAssets, // アセットは長期キャッシュ
responseHeadersPolicy: securityHeaders,
compress: true,
},
},
// SPA ルーティング対応(重要!)
// 404/403 エラーを index.html で処理してクライアントサイドルーティングを機能させる
errorResponses: [
{
httpStatus: 403,
responseHttpStatus: 200,
responsePagePath: '/index.html',
ttl: Duration.seconds(0)
},
{
httpStatus: 404,
responseHttpStatus: 200,
responsePagePath: '/index.html',
ttl: Duration.seconds(0)
},
],
});
デプロイ後の作業
React アプリのビルド & アップロード
# React アプリをビルド
npm run build
# S3 にアップロード
aws s3 sync build/ s3://YOUR_BUCKET_NAME --delete
# CloudFront キャッシュを削除(HTMLファイルの更新を反映)
aws cloudfront create-invalidation \
--distribution-id YOUR_DISTRIBUTION_ID \
--paths "/index.html"
運用のベストプラクティス
1. デプロイ戦略
- HTML ファイル: 毎回 CloudFront Invalidation 実行
- アセットファイル: ハッシュ付きファイル名なら Invalidation 不要
2. 監視・ログ
// CloudWatch でメトリクス監視
const alarm = new cloudwatch.Alarm(this, 'HighErrorRate', {
metric: distribution.metricOriginLatency(),
threshold: 1000,
evaluationPeriods: 2,
});
3. セキュリティ強化(本格運用時)
- CSP(Content Security Policy)ヘッダの追加
- WAF(Web Application Firewall)の導入
- アクセスログの分析
よくあるトラブルシューティング
Q1: /about
に直接アクセスすると404エラーになる
A: errorResponses
の設定が正しく適用されているか確認。SPA では全てのルートを index.html
で処理する必要があります。
Q2: CSS/JS ファイルが更新されない
A: ハッシュ付きファイル名を使用しているか確認。または手動で Invalidation を実行。
Q3: HTTPS でアクセスできない
A: viewerProtocolPolicy
が REDIRECT_TO_HTTPS
に設定されているか確認。
まとめ
この構成により、以下のメリットが得られます:
- セキュア: S3 は非公開、CloudFront OAI で安全配信
- 高速: 世界各地のエッジサーバーでキャッシュ配信
- SPA対応: クライアントサイドルーティングが正常動作
- 運用しやすい: CDK でインフラをコード管理
- コスト効率: サーバーレスで従量課金