🚀

CloudFront 署名付き URL でカスタムドメイン経由で S3 へアップロードする

に公開

はじめに

こんにちは。PKSHA Technology で SWE をしている須藤です。
前回の記事では、CloudFront 署名付き URL を使って複数の S3 バケットから単一カスタムドメインでダウンロードする方法を紹介しました。今回はその続編として、CloudFront 経由での S3 アップロードを解説します。

https://zenn.dev/pksha/articles/a737e75e7f4dcc

私たちのプロダクトでは、配信用のファイルは用途に応じて複数の S3 バケットに分けて管理しています。プロフィール画像などユーザーがアップロードするファイルも、同様にカスタムドメイン経由で処理したいというニーズがありました。そこで、CloudFront 経由で一時保管用の Upload Bucket にアップロードし、検証後に各配信用バケットへ振り分ける設計を採用しました。

本記事では、この設計パターンを解説します。CloudFront は本来 CDN ですが、署名付き URL と PUT メソッドを組み合わせることで、カスタムドメイン経由での S3 アップロードが可能になります。

CloudFront 経由でのアップロードに関する技術記事は意外と少なく、試行錯誤しながら設計しました。本記事が同様の課題を抱えている方の参考になれば幸いです。

本記事で扱わないこと
  • RSA 鍵ペアの生成および CloudFront Key Group の作成
  • AWS Certificate Manager (ACM) での SSL 証明書発行
  • Route 53 でのホストゾーン作成と DNS 設定

これらの詳細は前回の記事および AWS 公式ドキュメントをご参照ください。

全体アーキテクチャ

まず、Upload と Download を統合した全体像を示します。

Upload/Download 統合アーキテクチャ
Upload と Download を統合したアーキテクチャ

  • Upload 用 CloudFront は一時保管用の Upload Bucket へアップロードし、Lambda で検証後、用途に応じた Bucket へ振り分け
  • Download 用 CloudFront はパスベースルーティングで複数の配信用 Bucket から配信

Upload 用と Download 用の CloudFront に異なるサブドメインを使用することで、用途に応じた最適な設定が可能です。

なぜ CloudFront Distribution を分離するのか

Upload と Download では必要な設定が異なるため、Distribution を分離します。

設定項目 Upload Download
キャッシュ 無効 有効(パフォーマンス最大化)
許可メソッド PUT を含む GET/HEAD のみ
CORS ヘッダー転送 必須 不要な場合が多い

CloudFront 署名付き URL は HTTP メソッドを区別しない

Distribution を分離するもう 1 つの重要な理由は、CloudFront の署名付き URL が HTTP メソッドを区別しないことです。

S3 の署名付き URL は発行時に HTTP メソッドを指定するため、GET 用の URL では PUT できません。しかし、CloudFront の署名付き URL にはこの制約がありません。

Distribution を分離することで、Download 用 Distribution では PUT を許可せず、Upload 用 Distribution では GET でのダウンロードを想定しない、という明確な境界を設けられます。

CloudFront でアップロードを実現する仕組み

CloudFront は CDN として知られていますが、実は PUT メソッドも許可できます。

CloudFront の Behavior 設定で allowed_methodsPUT を追加することで、CloudFront 経由での S3 アップロードが可能になります。

# Terraform での設定例
default_cache_behavior {
  target_origin_id = "upload-bucket"

  allowed_methods = ["GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE"]
  cached_methods = ["GET", "HEAD"]  # 必須項目のため記載(PUT はキャッシュ対象外)

  # アップロードはキャッシュ不要
  default_ttl = 0
  min_ttl = 0
  max_ttl = 0

  # ...
}

なお、CloudFront の allowed_methods では選択可能な組み合わせが決まっているため、OPTIONS と PUT のみの許可はできません。

これにより、前回紹介したダウンロードと同様に、カスタムドメインでのアップロードが実現できます。

ダウンロードとの違い

設定項目 Download Upload
allowed_methods ["GET", "HEAD"] ["GET", "HEAD", "OPTIONS", "PUT", ...]
キャッシュ 有効 無効
CORS 不要な場合が多い 必須(ブラウザから直接アップロード時)

Upload Bucket による配信用 Bucket の保護

本記事で紹介する設計では、クライアントからのアップロード先を一時保管用の Upload Bucket に限定し、配信用 Bucket には直接書き込ませません。

CloudFront Upload アーキテクチャ
Upload Bucket を経由したセキュアなアップロードフロー

フローの概要

  1. クライアントが API Server に署名付き URL の発行をリクエスト
  2. API Server が CloudFront 署名付き URL を返却
  3. クライアントが CloudFront 経由で Upload Bucket に PUT
  4. CloudFront が Upload Bucket に PutObject
  5. S3 イベントで Lambda がトリガー
  6. Lambda が検証後、配信用 Bucket に移動

私たちのプロダクトで Upload Bucket を分離した理由

直接配信用 Bucket にアップロードする設計もシンプルで有効ですが、私たちのプロダクトでは以下の理由から Upload Bucket を分離しました。

1. 検証前のファイルが本番環境に入らない
私たちのプロダクトでは、ユーザーがアップロードしたファイルに対してウイルススキャンやファイル形式を検証しています。Upload Bucket を経由させることで、検証前のファイルが配信用 Bucket へ直接入ることを防いでいます(具体的な検証内容は「セキュリティ考慮事項」で後述)。

2. CloudFront 署名付き URL の特性による書き込み防止
前述の通り、CloudFront の署名付き URL は HTTP メソッドを区別しません。Upload Bucket を分離し、配信用 Bucket は Download 用 Distribution からのみアクセス可能にすることで、ダウンロード用 URL による書き込みリスクを排除しています。

CORS 対策: S3 の CORS 設定と Origin Request Policy

ブラウザから CloudFront 経由で S3 にファイルをアップロードする場合、CORS の問題に直面します。ここでは、その問題と解決策を解説します。

問題: Preflight リクエスト(OPTIONS)

ブラウザは、異なるオリジンへの PUT リクエストを送信する前に、Preflight リクエスト(OPTIONS メソッド)を自動的に送信します。

これは CORS(Cross-Origin Resource Sharing) の仕組みによるものです。GETHEAD などの単純リクエストでは Preflight は発生しませんが、PUTDELETE などサーバーのデータに影響を与える可能性があるメソッドでは、ブラウザが事前にサーバーへ許可を確認します。

解決策: S3 CORS 設定 + Origin Request Policy

S3 の CORS 設定と、CloudFront の Origin Request Policy を適切に構成することで、Preflight リクエストを処理できます。

S3 バケットの CORS 設定

resource "aws_s3_bucket_cors_configuration" "upload_bucket" {
  bucket = aws_s3_bucket.upload_bucket.id
  cors_rule {
    allowed_methods = ["GET", "PUT", "HEAD"]
    allowed_origins = ["https://your-app.example.com"]
    allowed_headers = ["*"]
    max_age_seconds = 3600
  }
}

CloudFront Origin Request Policy

S3 が CORS リクエストを認識するために、Origin ヘッダーなどを S3 に転送します。

resource "aws_cloudfront_origin_request_policy" "upload_policy" {
  name = "upload-origin-request-policy"

  cookies_config {
    cookie_behavior = "none"
  }

  headers_config {
    header_behavior = "whitelist"
    headers {
      # S3 が CORS リクエストを認識するために必要なヘッダー
      items = [
        "Content-Type",
        "Content-Length",
        "Origin",
        "Access-Control-Request-Method",
        "Access-Control-Request-Headers"
      ]
    }
  }

  query_strings_config {
    query_string_behavior = "all"
  }
}

動作の流れ

CloudFront CORS フロー
Preflight (OPTIONS) と PUT リクエストの流れ

  1. ブラウザが Preflight リクエスト(OPTIONS)を送信
  2. CloudFront が OPTIONS を S3 に転送
  3. S3 が CORS 設定に基づいて Access-Control-Allow-* ヘッダーを返却
  4. CloudFront 経由でブラウザに CORS レスポンスを返却
  5. ブラウザが署名付き URL で PUT リクエストを送信
  6. CloudFront が S3 に PutObject
  7. S3 が CloudFront に成功レスポンス(200 OK)を返却
  8. CloudFront がブラウザにレスポンスを返却

Go SDK での署名付き URL 生成

前回の記事で紹介した署名付き URL 生成コードは、アップロードでもそのまま使用できます。

func (s *CloudFrontSigner) CreateSignedURL(key string, expire time.Duration) (string, error) {
    if !strings.HasPrefix(key, "/") {
        key = "/" + key
    }

    rawURL := "https://" + s.domain + key
    expiresAt := time.Now().Add(expire)

    return s.signer.Sign(rawURL, expiresAt)
}

CloudFront 署名付き URL は HTTP メソッドを区別しないため、同じ URL を GET(ダウンロード)にも PUT(アップロード)にも使用できます。

使用例

// アップロード用の署名付き URL を生成
uploadURL, err := signer.CreateSignedURL("/upload/user-123/image.png", 5*time.Minute)
if err != nil {
    return err
}

// クライアントに返却
// クライアントはこの URL に対して PUT リクエストでファイルをアップロード

クライアント側の実装例(JavaScript)

async function uploadFile(file, presignedUrl) {
    const response = await fetch(presignedUrl, {
        method: 'PUT',
        body: file,
        headers: {
            'Content-Type': file.type,
        },
    });

    if (!response.ok) {
        throw new Error(`Upload failed: ${response.status}`);
    }

    return response;
}

セキュリティ考慮事項

CloudFront 経由でのアップロードを実装する際に考慮すべきセキュリティポイントを紹介します。

ファイルサイズ制限

CloudFront 経由でアップロード可能な最大ファイルサイズは 30 GB です。AWS WAF を併用している場合、WAF ルールによっては大容量ファイルをブロックする可能性があるため、設定を確認してください。

Lambda のトリガー方法

Upload Bucket へのファイルアップロードをトリガーにして Lambda を起動する方法はいくつかあります。

トリガー構成 特徴
S3 イベント通知 → Lambda シンプルな構成。ファイルアップロード時に直接 Lambda を起動
S3 イベント通知 → EventBridge → Lambda イベントのフィルタリングや複数の処理先への振り分けが可能
Frontend(S3 アップロード完了後)→ Backend API → Lambda Web App や API での柔軟な制御が可能

Lambda での検証内容

Upload Bucket から配信用 Bucket への移動時に、Lambda で以下のような検証を推奨します。

  • 拡張子だけでなく、マジックバイトでファイル形式を確認
  • 許可されたサイズ範囲内かを確認
  • ウイルススキャン
  • 必要なメタデータが含まれているかを確認
// 実装イメージ
func processUploadedFile(ctx context.Context, uploadKey string) error {
    // 1. ファイルパスから移動先 Bucket を判断
    destBucket := determineDestBucket(uploadKey) // 例: "images/..." → ImagesBucket

    // 2. 検証処理(ウイルススキャン、ファイル形式チェックなど)
    if err := validate(ctx, uploadKey); err != nil {
        return err
    }

    // 3. Upload Bucket からファイル取得
    file, err := uploadBucket.GetObject(ctx, uploadKey)
    if err != nil {
        return err
    }
    defer file.Close()

    // 4. 配信用 Bucket へコピー
    if err := destBucket.PutObject(ctx, uploadKey, file); err != nil {
        return err
    }

    // 5. Upload Bucket から削除
    return uploadBucket.DeleteObject(ctx, uploadKey)
}

S3 Bucket Policy

Upload Bucket へのアクセスは CloudFront からのみに制限することで、直接アクセスを防ぎます。Origin Access Control(OAC)を使用して、CloudFront 経由のアクセスのみを許可する設定を推奨します。

Content-Type の検証

クライアント側で設定される Content-Type ヘッダーは偽装される可能性があります。Lambda での検証時に、実際のファイル内容と Content-Type が一致しているかを確認することで、悪意のあるファイルのアップロードを防ぐことができます。

まとめ

CloudFront 署名付き URL を使ったアップロードフローを紹介しました。

  1. CloudFront + PUT でカスタムドメインからのアップロードを実現

    • allowed_methods に PUT を追加するだけで、CloudFront 経由のアップロードが可能
  2. Upload Bucket 分離で 配信用 Bucket を保護

    • 検証前のファイルが本番環境に直接入らない
  3. S3 CORS 設定 + Origin Request Policy で CORS 問題を解決

    • Origin ヘッダーを S3 に転送することで、S3 の CORS 設定が機能する
    • Preflight リクエスト(OPTIONS)も正しく処理される

前回のダウンロード編と合わせて、CloudFront を使ったファイル配信・アップロードの単一ドメイン化が実現できます。


本記事の設計は、CloudFront でのアップロードに関する情報が少ない中で試行錯誤した結果です。もし、より良いアプローチやベストプラクティスをご存知の方がいらっしゃれば、ぜひコメントで教えていただけると嬉しいです。

参考文献

https://docs.aws.amazon.com/AmazonS3/latest/userguide/cors.html

https://developer.mozilla.org/ja/docs/Web/HTTP/Guides/CORS

https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/controlling-origin-requests.html

https://docs.aws.amazon.com/ja_jp/cloudfront/latest/APIReference/API_AllowedMethods.html

PKSHAテックブログ

Discussion