☁️

CloudFront 署名付き URL で複数の S3 Bucket から単一カスタムドメインで配信する

に公開

はじめに

こんにちは。PKSHA Technology で SWE をしている須藤です。

今回は、S3 からのファイル配信で遭遇した課題と、その解決策について紹介します。

S3 署名付き URL は、一時的なアクセス権限を付与してファイルをダウンロード・アップロードできる便利な機能です。

しかし、S3 署名付き URL はバケットごとに異なるドメインとなるため、利用者のセキュリティ要件によっては、バケット追加のたびにファイアウォールやプロキシの設定変更が必要です。

S3 署名付き URL の問題
S3 署名付き URL ではバケットごとに異なるドメインへアクセスが必要

用途ごとにバケットを分けることで、一時ファイルは数日で自動削除、重要データは長期保持など、それぞれに適したライフサイクルポリシーを設定できます。しかし、バケットが増えるたびにドメインも増え、前述の問題が発生します。

ドメイン追加のたびに設定変更が必要になると、お客様の運用負荷増加や、設定漏れによる不具合に繋がる可能性があります。

本記事では、CloudFront の署名付き URL とカスタムドメインを活用し、複数の S3 バケットから単一ドメインで統合配信するアーキテクチャを解説します。

本記事で扱わないこと。
  • RSA 鍵ペアの生成および CloudFront Key Group の作成
  • AWS Certificate Manager (ACM) での SSL 証明書発行(us-east-1 リージョン)
  • Route 53 でのホストゾーン作成と DNS 設定
  • S3 バケットの作成とバケットポリシーの基本設定

これらの詳細手順については、AWS 公式ドキュメントや参考文献をご参照ください。

S3 署名付き URL と CloudFront 署名付き URL の違い

両者の主な違いを整理します。

観点 S3 署名付き URL CloudFront 署名付き URL
ドメイン バケットごとに異なる 単一カスタムドメイン
署名方式 AWS Signature V4(IAM 認証情報) RSA 鍵ペア
有効期限 最大 7 日 ※ 任意に設定可能
キャッシュ なし あり(Edge Location)
設定場所 バケットポリシー CloudFront + Key Group

ドメイン形式の違い

S3 署名付き URL

https://<bucket-name>.s3.<region>.amazonaws.com/<key>?X-Amz-Algorithm=...

CloudFront 署名付き URL

https://<your-domain>/<key>?Expires=...&Signature=...&Key-Pair-Id=...

CloudFront を使えば、任意のカスタムドメイン(例: files.example.com)を設定し、単一ドメインで複数バケットのファイルを配信できます。

署名方式の違い

S3 署名付き URL は AWS の IAM 認証情報(アクセスキー)を使って署名します。一方、CloudFront 署名付き URL は RSA 鍵ペアを使用します。

CloudFront では事前に公開鍵を Key Group として登録し、アプリケーション側で秘密鍵を使って署名を生成します。鍵ペアの生成や Key Group の設定手順については、以下の記事が参考になります。

https://qiita.com/may-solty/items/a1ac7d64f9a24efad4ab

アーキテクチャ概要

以下の構成で、複数の S3 バケットを単一ドメインで配信します。

CloudFront 単一ドメインアーキテクチャ
CloudFront を使った単一ドメイン配信アーキテクチャ

ポイント

  1. 単一ドメイン: すべてのバケットへのアクセスがカスタムドメインに統一
  2. パスベースルーティング: URL のパスパターンで振り分け先バケットを決定
  3. Origin Access Control (OAC): S3 バケットへの直接アクセスをブロックし、CloudFront 経由でのみアクセスを許可

パスベースルーティング

CloudFront の Behavior(ビヘイビア)設定で、パスパターンごとに異なる Origin(S3 バケット)へルーティングします。

設定例

パスパターン Origin(S3 バケット) 用途
/* (Default) bucket-a その他(デフォルト)
/path-b/* bucket-b ファイル種別 B
/path-c/* bucket-c ファイル種別 C

CloudFront の設定イメージ

以下は、Terraform での設定例です(一部省略)。

resource "aws_cloudfront_distribution" "files" {
  # Origin 定義
  origin {
    domain_name              = aws_s3_bucket.bucket_a.bucket_regional_domain_name
    origin_id                = "bucket-a"
    origin_access_control_id = aws_cloudfront_origin_access_control.oac.id
  }

  origin {
    domain_name              = aws_s3_bucket.bucket_b.bucket_regional_domain_name
    origin_id                = "bucket-b"
    origin_access_control_id = aws_cloudfront_origin_access_control.oac.id
  }

  origin {
    domain_name              = aws_s3_bucket.bucket_c.bucket_regional_domain_name
    origin_id                = "bucket-c"
    origin_access_control_id = aws_cloudfront_origin_access_control.oac.id
  }

  # デフォルト Behavior
  default_cache_behavior {
    target_origin_id   = "bucket-a"
    allowed_methods    = ["GET", "HEAD"]
    cached_methods     = ["GET", "HEAD"]
    trusted_key_groups = [aws_cloudfront_key_group.main.id]  # 省略すると署名なしでアクセス可能
    # ...
  }

  # パスベースの Behavior
  ordered_cache_behavior {
    path_pattern       = "/path-b/*"
    target_origin_id   = "bucket-b"
    allowed_methods    = ["GET", "HEAD"]
    cached_methods     = ["GET", "HEAD"]
    trusted_key_groups = [aws_cloudfront_key_group.main.id]
    # ...
  }

  ordered_cache_behavior {
    path_pattern       = "/path-c/*"
    target_origin_id   = "bucket-c"
    allowed_methods    = ["GET", "HEAD"]
    cached_methods     = ["GET", "HEAD"]
    trusted_key_groups = [aws_cloudfront_key_group.main.id]
    # ...
  }

  # カスタムドメイン
  aliases = ["<your-domain>"]

  # SSL 証明書(ACM で発行、us-east-1 リージョン必須)
  viewer_certificate {
    acm_certificate_arn      = var.acm_certificate_arn
    ssl_support_method       = "sni-only"
    minimum_protocol_version = "TLSv1.2_2021"
  }
}

# Route 53 で CloudFront へのエイリアスレコードを作成
resource "aws_route53_record" "files" {
  zone_id = var.route53_zone_id
  name    = "<your-domain>"
  type    = "A"

  alias {
    name                   = aws_cloudfront_distribution.files.domain_name
    zone_id                = aws_cloudfront_distribution.files.hosted_zone_id
    evaluate_target_health = false
  }
}

カスタムドメインを CloudFront に紐づけるには、以下が必要です。

  • ACM 証明書 — us-east-1 リージョンで発行(CloudFront はグローバルサービスのため)
  • Route 53 レコード — CloudFront ディストリビューションへの A レコード(Alias)

Go SDK での署名付き URL 生成

実際のアプリケーションで署名付き URL を生成する実装例を紹介します。

必要なパッケージ

import (
    "crypto"
    "strings"
    "time"

    "github.com/aws/aws-sdk-go-v2/feature/cloudfront/sign"
)

署名付き URL 生成

type CloudFrontSigner struct {
    domain string           // カスタムドメイン
    signer *sign.URLSigner
}

func NewCloudFrontSigner(domain, keyPairId string, privateKey crypto.Signer) *CloudFrontSigner {
    return &CloudFrontSigner{
        domain: domain,
        signer: sign.NewURLSigner(keyPairId, privateKey),
    }
}

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)
}

使用例

// 秘密鍵は Secrets Manager 等から安全に取得
privateKey := getPrivateKeyFromSecretsManager()

signer := NewCloudFrontSigner(
    "<your-domain>",  // カスタムドメイン
    "KXXXXXXXXXX",    // Key Pair ID
    privateKey,
)

// パス A のファイルの署名付き URL を生成
urlA, err := signer.CreateSignedURL("/path-a/file-123.dat", 5*time.Minute)
// => https://<your-domain>/path-a/file-123.dat?Expires=...&Signature=...&Key-Pair-Id=...

// パス B のファイルの署名付き URL を生成
urlB, err := signer.CreateSignedURL("/path-b/file-456.dat", 5*time.Minute)
// => https://<your-domain>/path-b/file-456.dat?Expires=...&Signature=...&Key-Pair-Id=...

パスが /path-a/* の場合は Bucket A、/path-b/* の場合は Bucket B に自動的にルーティングされますが、クライアントから見えるドメインは単一のカスタムドメインで統一されています。

まとめ

CloudFront 署名付き URL + パスベースルーティングを導入すると、以下のメリットがあります。

  1. 単一ドメインに統一: ファイアウォール許可は初回のみ
  2. パスで振り分け: バケット追加も URL のパス変更だけ
  3. OAC でセキュリティ強化: S3 への直接アクセスをブロック

バケット数が増えるほど運用負荷の差が顕著になります。ドメイン管理の運用負荷を改善したい場合に、ぜひ検討してみてください!

参考文献

https://dev.classmethod.jp/articles/amazon-cloudfront-cname-and-host-header-test/
https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/feature/cloudfront/sign#URLSigner
https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/private-content-signed-urls.html
https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/private-content-trusted-signers.html
https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/cnames-and-https-requirements.html

PKSHAテックブログ

Discussion