【AWSハンズオン】第9回 フロントエンド用S3 + CloudFront構築

【AWSハンズオン】第9回 フロントエンド用S3 + CloudFront構築

S3の静的ウェブサイトホスティングとCloudFrontを組み合わせ、高速でセキュアなフロントエンド配信基盤を構築。Origin Access Control(OAC)により、S3バケットへの直接アクセスを制限し、CloudFront経由でのみコンテンツを配信する構成です。

AWS #AWS#初学者向け#ハンズオン

【AWSハンズオン】第9回 フロントエンド用S3 + CloudFront構築

サムネイル

S3の静的ウェブサイトホスティングとCloudFrontを組み合わせ、高速でセキュアなフロントエンド配信基盤を構築。Origin Access Control(OAC)により、S3バケットへの直接アクセスを制限し、CloudFront経由でのみコンテンツを配信する構成です。

更新日: 8/13/2025

今回作業対象のブランチ

前提条件

  • 第1回から第8回までの構築が完了していること
  • フロントエンド(React、Vue、Angular等)のビルド環境があること
  • こちらからDockerコンテナでの実行環境が準備できていること

フロントエンドのサンプルコードはこちらから

Terraform実行環境の起動

前回と同様に、Dockerコンテナ内で作業を進めます。

# コンテナを起動してbashに入る
docker-compose run --rm terraform

これ以降のコマンドは、すべてこのコンテナ内で実行します。

基礎知識

S3静的ウェブサイトホスティングとは

S3(Simple Storage Service)は、オブジェクトストレージサービスです。静的ウェブサイトホスティング機能を使用することで、HTML、CSS、JavaScript、画像などの静的ファイルをウェブサイトとして公開できます。

WEBアプリケーション開発者の視点から見ると

ローカル開発では一般的にdevサーバー(例えばlocalhost:3000)で確認しますが、クライアントサイドで動作するアプリの場合、本番環境では静的ファイルをどこかにホスティングする必要があります。S3は、これらのファイルを保存し、Web Serverとして機能させることができるサービスです。

主な特徴

特徴 説明
高可用性 99.999999999%(イレブンナイン)の耐久性
スケーラビリティ 自動的にスケールし、トラフィック増加に対応
低コスト 使用した分だけの従量課金
簡単な運用 サーバー管理不要

CloudFrontとCDN

CloudFrontは、AWSのCDN(Content Delivery Network)サービスです。世界中のエッジロケーションにコンテンツをキャッシュし、ユーザーに一番近い場所から配信します。

なぜCloudFrontが必要か

課題 CloudFrontによる解決
S3は対象リージョンのみ 世界中のエッジロケーションから配信
S3への直接アクセスはセキュリティリスク OACにより、CloudFront経由のみアクセス可能
HTTPSの設定が複雑 CloudFrontで簡単にHTTPS化
キャッシュ制御が困難 詳細なキャッシュポリシー設定可能

Origin Access Control (OAC)とは

OACは、CloudFrontからS3バケットへの安全なアクセスを提供する仕組みです。2022年に導入された新しい方式で、従来のOAI(Origin Access Identity)よりセキュアです。

OACの仕組み

  1. S3バケットのパブリックアクセスを完全にブロック
  2. CloudFrontにOACを設定
  3. S3バケットポリシーでOACからのアクセスのみ許可
  4. ユーザーはCloudFront経由でのみコンテンツにアクセス可能

今回作成するネットワーク構成

リソース 説明 数量
S3バケット 静的ファイルを保存 1個
S3バケットポリシー OACからのアクセスを許可 1個
CloudFrontディストリビューション CDN配信 1個
CloudFront OAC S3への安全なアクセス 1個
CloudFront関数 SPAのルーティング対応(オプション) 1個

Terraformコードの実装

変数の追加

フロントエンド用の変数を追加します。

# terraform/variables.tf に追加

variable "frontend_domain_name" {
  description = "フロントエンドのドメイン名(オプション)"
  type        = string
  default     = ""
}

variable "frontend_certificate_arn" {
  description = "CloudFront用のSSL証明書ARN(us-east-1リージョン)"
  type        = string
  default     = ""
}

variable "enable_cloudfront_logging" {
  description = "CloudFrontのアクセスログを有効にするか"
  type        = bool
  default     = false
}

variable "cloudfront_price_class" {
  description = "CloudFrontの価格クラス"
  type        = string
  default     = "PriceClass_100"  # 日本を指定してコスト削減
}

S3バケット

静的ファイルを保存するS3バケットを作成します。

# terraform/s3_frontend.tf

# フロントエンド用S3バケット
resource "aws_s3_bucket" "frontend" {
  bucket        = "${var.project_name}-${var.environment}-frontend"
  force_destroy = true # 実際の運用では外してもいい

  tags = {
    Name = "${var.project_name}-${var.environment}-frontend"
  }
}

# バケットのパブリックアクセスをブロック
resource "aws_s3_bucket_public_access_block" "frontend" {
  bucket = aws_s3_bucket.frontend.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

# バケットの暗号化設定
resource "aws_s3_bucket_server_side_encryption_configuration" "frontend" {
  bucket = aws_s3_bucket.frontend.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256"
    }
  }
}

# バケットのバージョニング設定
resource "aws_s3_bucket_versioning" "frontend" {
  bucket = aws_s3_bucket.frontend.id
  
  versioning_configuration {
    status = "Enabled"
  }
}

# CloudFrontアクセスログ用バケット(オプション)
resource "aws_s3_bucket" "cloudfront_logs" {
  count = var.enable_cloudfront_logging ? 1 : 0

  bucket        = "${var.project_name}-${var.environment}-cf-logs"
  force_destroy = true # 実際の運用では外してもいい

  tags = {
    Name = "${var.project_name}-${var.environment}-cf-logs"
  }
}

# Logバケットのライフサイクルルール
resource "aws_s3_bucket_lifecycle_configuration" "cloudfront_logs" {
  count = var.enable_cloudfront_logging ? 1 : 0

  bucket = aws_s3_bucket.cloudfront_logs[0].id

  rule {
    id     = "delete-old-logs"
    status = "Enabled"

    expiration {
      days = 30
    }
  }
}

S3バケット設定の説明

パラメータ 設定値 設定理由
bucket名 プロジェクト名-環境名-frontend グローバルで一意である必要があるため、識別しやすい命名規則を採用
block_public_acls true パブリックACLの設定を防ぎ、意図しない公開を防止
block_public_policy true パブリックなバケットポリシーの適用を防止
ignore_public_acls true 既存のパブリックACLを無視してセキュリティを強化
restrict_public_buckets true バケットのパブリックアクセスを完全に制限
sse_algorithm AES256 保存時の暗号化でデータを保護
versioning status Enabled 誤削除や上書きからの復旧を可能にする

CloudFront Origin Access Control

CloudFrontからS3への安全なアクセスを設定します。

# terraform/cloudfront.tf

# Origin Access Control
resource "aws_cloudfront_origin_access_control" "frontend" {
  name                              = "${var.project_name}-${var.environment}-frontend-oac"
  description                       = "OAC for ${var.project_name} frontend"
  origin_access_control_origin_type = "s3"
  signing_behavior                  = "always"
  signing_protocol                  = "sigv4"
}

OAC設定の説明

パラメータ 設定値 設定理由
origin_access_control_origin_type s3 S3オリジン用のOACであることを指定
signing_behavior always すべてのリクエストに署名を付与してセキュリティを確保
signing_protocol sigv4 AWS Signature Version 4を使用(最新かつ最もセキュア)

CloudFrontディストリビューション

CDN配信の設定を行います。

# terraform/cloudfront.tf

# CloudFront Distribution
resource "aws_cloudfront_distribution" "frontend" {
  enabled             = true
  is_ipv6_enabled     = true
  comment             = "${var.project_name} frontend distribution"
  default_root_object = "index.html"
  price_class         = var.cloudfront_price_class

  # エイリアス設定(カスタムドメインを使用する場合)
  aliases = var.frontend_domain_name != "" ? [var.frontend_domain_name] : []

  # オリジン設定
  origin {
    domain_name              = aws_s3_bucket.frontend.bucket_regional_domain_name
    origin_access_control_id = aws_cloudfront_origin_access_control.frontend.id
    origin_id                = "S3-${aws_s3_bucket.frontend.id}"
  }

  # デフォルトキャッシュ動作
  default_cache_behavior {
    allowed_methods  = ["GET", "HEAD"]
    cached_methods   = ["GET", "HEAD"]
    target_origin_id = "S3-${aws_s3_bucket.frontend.id}"

    forwarded_values {
      query_string = false
      cookies {
        forward = "none"
      }
    }

    viewer_protocol_policy = "redirect-to-https"
    min_ttl                = 0
    default_ttl            = 3600
    max_ttl                = 86400
    compress               = true
  }

  # 追加のキャッシュ動作(静的アセット用)
  ordered_cache_behavior {
    path_pattern     = "/static/*"
    allowed_methods  = ["GET", "HEAD"]
    cached_methods   = ["GET", "HEAD"]
    target_origin_id = "S3-${aws_s3_bucket.frontend.id}"

    forwarded_values {
      query_string = false
      cookies {
        forward = "none"
      }
    }

    viewer_protocol_policy = "redirect-to-https"
    min_ttl                = 0
    default_ttl            = 86400
    max_ttl                = 31536000
    compress               = true
  }

  # SPAのルーティング対応
  custom_error_response {
    error_code         = 404
    response_code      = 200
    response_page_path = "/index.html"
  }

  custom_error_response {
    error_code         = 403
    response_code      = 200
    response_page_path = "/index.html"
  }

  # 地理的制限(必要に応じて設定)
  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

  # SSL証明書設定
  viewer_certificate {
    cloudfront_default_certificate = var.frontend_certificate_arn == ""
    acm_certificate_arn            = var.frontend_certificate_arn != "" ? var.frontend_certificate_arn : null
    ssl_support_method             = var.frontend_certificate_arn != "" ? "sni-only" : null
    minimum_protocol_version       = "TLSv1.2_2021"
  }

  # ログ設定
  dynamic "logging_config" {
    for_each = var.enable_cloudfront_logging ? [1] : []
    content {
      include_cookies = false
      bucket          = aws_s3_bucket.cloudfront_logs[0].bucket_domain_name
      prefix          = "cloudfront/"
    }
  }

  tags = {
    Name = "${var.project_name}-${var.environment}-frontend-cf"
  }
}

CloudFront設定の説明

カテゴリ パラメータ 設定値 設定理由
基本設定 enabled true ディストリビューションを有効化
is_ipv6_enabled true IPv6サポートを有効化して将来性を確保
default_root_object index.html ルートアクセス時のデフォルトファイル
price_class PriceClass_200 コストと配信エリアのバランスを考慮
キャッシュ設定 viewer_protocol_policy redirect-to-https HTTPアクセスを自動的にHTTPSにリダイレクト
compress true gzip圧縮を有効化して転送量を削減
default_ttl 3600(1時間) 頻繁に更新されるHTMLファイル用の短めのキャッシュ
max_ttl(静的アセット) 31536000(1年) 変更頻度の低い静的ファイルは長期キャッシュ
SPA対応 custom_error_response 404→200 クライアントサイドルーティングに対応
セキュリティ minimum_protocol_version TLSv1.2_2021 最新のセキュリティ基準に準拠

S3バケットポリシー

CloudFrontからのアクセスのみを許可するポリシーを設定します。

# terraform/s3_frontend.tf に追加

# S3バケットポリシー
resource "aws_s3_bucket_policy" "frontend" {
  bucket = aws_s3_bucket.frontend.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "AllowCloudFrontServicePrincipal"
        Effect = "Allow"
        Principal = {
          Service = "cloudfront.amazonaws.com"
        }
        Action   = "s3:GetObject"
        Resource = "${aws_s3_bucket.frontend.arn}/*"
        Condition = {
          StringEquals = {
            "AWS:SourceArn" = aws_cloudfront_distribution.frontend.arn
          }
        }
      }
    ]
  })
}

バケットポリシー設定の説明

パラメータ 設定値 設定理由
Principal.Service cloudfront.amazonaws.com CloudFrontサービスプリンシパルを指定
Action s3:GetObject 読み取りのみ許可(最小権限の原則)
Resource バケット内の全オブジェクト(/*) すべてのファイルへのアクセスを許可
Condition AWS:SourceArn 特定のCloudFrontディストリビューションからのみアクセスを許可

出力の追加

作成したリソースの情報を出力に追加します。

# terraform/outputs.tf に追加

# S3関連の出力
output "frontend_s3_bucket_name" {
  description = "フロントエンド用S3バケット名"
  value       = aws_s3_bucket.frontend.id
}

output "frontend_s3_bucket_arn" {
  description = "フロントエンド用S3バケットARN"
  value       = aws_s3_bucket.frontend.arn
}

output "frontend_s3_bucket_domain_name" {
  description = "S3バケットのドメイン名"
  value       = aws_s3_bucket.frontend.bucket_domain_name
}

# CloudFront関連の出力
output "cloudfront_distribution_id" {
  description = "CloudFrontディストリビューションID"
  value       = aws_cloudfront_distribution.frontend.id
}

output "cloudfront_distribution_arn" {
  description = "CloudFrontディストリビューションARN"
  value       = aws_cloudfront_distribution.frontend.arn
}

output "cloudfront_distribution_domain_name" {
  description = "CloudFrontディストリビューションのドメイン名"
  value       = aws_cloudfront_distribution.frontend.domain_name
}

output "cloudfront_oac_id" {
  description = "CloudFront OAC ID"
  value       = aws_cloudfront_origin_access_control.frontend.id
}

リソースを作成

Terraformコンテナ内で以下のコマンドを実行します。

# terraformディレクトリへ移動
cd terraform

# 実行計画の確認
terraform plan

# リソースの作成
terraform apply

CloudFrontディストリビューションの作成には15〜20分程度かかります。

結果の確認

# S3バケット名の確認
terraform output frontend_s3_bucket_name

# CloudFrontドメイン名の確認(重要:アクセスURLとして使用)
terraform output cloudfront_distribution_domain_name

# CloudFrontディストリビューションIDの確認(デプロイ時に使用)
terraform output cloudfront_distribution_id

動作確認

AWS Management Consoleでの確認

S3 > バケット

作成されたS3バケットを選択し、以下を確認します。

カテゴリ 項目 設定値
アクセス許可 バケットポリシー CloudFrontからのアクセスのみ許可
パブリックアクセス すべてブロック
プロパティ バージョニング 有効
暗号化 AES256

CloudFront > ディストリビューション

作成されたディストリビューションを選択し、以下を確認します。

カテゴリ 項目 設定値
一般 料金クラス 北米と欧州のみを使用
ディストリビューションドメイン名 xxxxxx.cloudfront.net
オリジン オリジンタイプ S3
ビヘイビア ビューワープロトコルポリシー HTTP を HTTPS にリダイレクト

初期コンテンツのアップロード

サンプルコードを使わない場合は、簡単なHTMLファイルをアップロードします。

# テスト用HTMLファイルの作成
cat > index.html << EOF
<!DOCTYPE html>
<html>
<head>
    <title>AWS CloudFront Test</title>
</head>
<body>
    <h1>CloudFront配信テスト</h1>
    <p>S3 + CloudFrontの構築が完了しました!</p>
</body>
</html>
EOF

# S3にアップロード
aws s3 cp index.html s3://$(terraform output -raw frontend_s3_bucket_name)/

# CloudFrontのURLで確認
echo "https://$(terraform output -raw cloudfront_distribution_domain_name)"

ブラウザでCloudFrontのURLにアクセスし、HTMLが表示されることを確認します。

サンプルコードから実行

以下、サンプルコードのルートから実行

# 実行権限を付与
chmod +x scripts/deploy-frontend.sh

# 環境変数を設定
export S3_BUCKET_NAME=出力したS3バケット名
export CLOUDFRONT_DISTRIBUTION_ID=CloudFrontディストリビューションID

# デプロイ実行
./scripts/deploy-frontend.sh

キャッシュ

何にどのくらいのキャッシュを適用させるか、目安として以下をご確認ください。
もちろんプロジェクトにより全く違うこともあり得ますが…

ファイルタイプ キャッシュ期間 理由
HTML(index.html) 5分 頻繁に更新される可能性があるため短時間
JavaScript/CSS(ハッシュ付き) 1年 ファイル名にハッシュが含まれるため長期間
画像/フォント 1年 変更頻度が低いため長期間
JSON(manifest等) 5分 アプリケーション設定のため短時間

コスト管理

サービス 項目 料金(東京リージョン)
S3 ストレージ $0.025/GB・月
リクエスト(GET) $0.00037/1,000リクエスト
CloudFront データ転送(インターネットへ) $0.114/GB(最初の10TB)
HTTPリクエスト $0.0075/10,000リクエスト
無効化リクエスト 月1,000パスまで無料

トラブルシューティング

アクセスが403エラーになる場合

  1. S3バケットポリシーが正しく設定されているか確認
  2. CloudFront OACのIDが正しいか確認

キャッシュが更新されない場合

  1. CloudFrontの無効化を実行
  2. ブラウザのキャッシュをクリア
  3. Cache-Controlヘッダーの設定を確認

既存のロググループが原因でapplyできない

なんらかの理由でロググループが重複すると言うエラーが発生したりしなかったり…
おそらく前回削除しきれていなかっただけと仮定し、既存のロググループをimportします

terraform import 'aws_cloudwatch_log_group.vpc_flow_logs[0]' '/aws/vpc/sample-sys-dev'

次のステップ

次回は、CORS設定とセキュリティ強化を行います。

  • ALB Listener RuleのCORS対応
  • セキュリティヘッダーの実装
  • WAF(Web Application Firewall)の設定
  • CSRF対策の実装

検索

検索条件に一致する記事が見つかりませんでした