2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

S3 + CloudFront で React SPA を公開する(AWS CDK / TypeScript)

Posted at

はじめに

この記事では、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 にアクセスできるようにする仕組み

データの流れ

  1. ユーザーがWebサイトにアクセス
  2. Route53 でドメイン名を CloudFront のIPアドレスに変換
  3. CloudFront が最寄りのエッジサーバーからコンテンツを配信
  4. ファイルがキャッシュにない場合のみ 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: viewerProtocolPolicyREDIRECT_TO_HTTPS に設定されているか確認。

まとめ

この構成により、以下のメリットが得られます:

  • セキュア: S3 は非公開、CloudFront OAI で安全配信
  • 高速: 世界各地のエッジサーバーでキャッシュ配信
  • SPA対応: クライアントサイドルーティングが正常動作
  • 運用しやすい: CDK でインフラをコード管理
  • コスト効率: サーバーレスで従量課金
2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?