CloudFront + S3 + OAC でセキュアな静的サイト配信

静的サイトの配信構成として最も枯れていてセキュアなのが S3 (非公開) + CloudFront + OAC の組み合わせです。本記事ではこの 3 点セットで「公開しないバケットを世界に配信する」仕組みと、Terraform での実装を解説します。

なぜ S3 を直接公開しないのか

S3 は静的ホスティング機能を持っていて、バケットを Public にすればそれだけでサイト配信できます。便利ですが、運用するなら避けたほうが良い理由が複数あります:

結論、S3 は「ファイル置き場」として完全非公開、世界に見せるのは「CloudFront だけ」。これがベストプラクティス。

OAC (Origin Access Control) とは

「CloudFront だけが S3 を読める」を実現する仕組みが OAC。CloudFront 経由のリクエストに AWS SigV4 署名を付与し、S3 はその署名をチェックして「これは認可された CloudFront からのアクセスだな」と判断します。

OAC(推奨)OAI(旧式)
方式SigV4 署名IAM Principal
SSE-KMS S3 対応×
S3 以外の Origin○(Lambda 関数 URL 等)×(S3 専用)
新規構築こちらを使う非推奨

面接で「OAC と OAI の違いは?」は定番質問。新規構築は OAC 一択です。

本シリーズの frontend_cdn モジュール

このモジュールが作るリソースは 7 個:

  1. aws_s3_bucket — バケット本体
  2. aws_s3_bucket_public_access_block — 公開ブロック 4 種
  3. aws_s3_bucket_versioning — ロールバック用
  4. aws_s3_bucket_server_side_encryption_configuration — AES256 暗号化
  5. aws_cloudfront_origin_access_control — OAC
  6. aws_cloudfront_distribution — CDN 本体
  7. aws_s3_bucket_policy — CloudFront だけ許可

S3 バケット + 安全装置 3 種

5.x 系の AWS Provider では、バケット作成と「公開ブロック・暗号化・バージョニング」は別リソースに分かれています:

resource "aws_s3_bucket" "this" {
  bucket = var.bucket_name
  tags   = var.tags
}

# 公開ブロック 4 種すべて true
resource "aws_s3_bucket_public_access_block" "this" {
  bucket = aws_s3_bucket.this.id

  block_public_acls       = true   # ACL の public 設定をブロック
  ignore_public_acls      = true   # 既存の public ACL を無視
  block_public_policy     = true   # public な Bucket Policy をブロック
  restrict_public_buckets = true   # public Bucket Policy を持つバケットを完全制限
}

# バージョニング ON
resource "aws_s3_bucket_versioning" "this" {
  bucket = aws_s3_bucket.this.id
  versioning_configuration { status = "Enabled" }
}

# AES256 暗号化
resource "aws_s3_bucket_server_side_encryption_configuration" "this" {
  bucket = aws_s3_bucket.this.id
  rule {
    apply_server_side_encryption_by_default { sse_algorithm = "AES256" }
  }
}

OAC リソース

resource "aws_cloudfront_origin_access_control" "this" {
  name                              = "${var.bucket_name}-oac"
  origin_access_control_origin_type = "s3"
  signing_behavior                  = "always"   # 常に SigV4 署名
  signing_protocol                  = "sigv4"
}

CloudFront Distribution の origin に OAC を紐付け

resource "aws_cloudfront_distribution" "this" {
  enabled         = true
  is_ipv6_enabled = true
  http_version    = "http2and3"
  aliases         = [var.domain_name]
  price_class     = "PriceClass_200"
  default_root_object = "index.html"

  origin {
    domain_name              = aws_s3_bucket.this.bucket_regional_domain_name
    origin_id                = "s3-${var.bucket_name}"
    origin_access_control_id = aws_cloudfront_origin_access_control.this.id
  }

  # ... default_cache_behavior, viewer_certificate, etc.
}

Bucket Policy の Source ARN 条件

OAC を作っただけでは S3 側で「お前を許可してない」と弾かれます(403)。Bucket Policy で「この Distribution からの GetObject だけ許可」を明示します:

data "aws_iam_policy_document" "bucket" {
  statement {
    sid    = "AllowCloudFrontReadViaOAC"
    effect = "Allow"

    principals {
      type        = "Service"
      identifiers = ["cloudfront.amazonaws.com"]
    }

    actions   = ["s3:GetObject"]
    resources = ["${aws_s3_bucket.this.arn}/*"]

    # この Distribution からのリクエストだけに絞る
    condition {
      test     = "StringEquals"
      variable = "aws:SourceArn"
      values   = [aws_cloudfront_distribution.this.arn]
    }
  }
}

resource "aws_s3_bucket_policy" "this" {
  bucket = aws_s3_bucket.this.id
  policy = data.aws_iam_policy_document.bucket.json
}

SourceArn 条件があるから、他人の CloudFront から自分の S3 を読まれる事故 が物理的に起きません。これが OAC + SourceArn の合わせ技の強さ。

OAC のハマりポイント: 両側設定

OAC は「Distribution 側」と「Bucket Policy 側」の両方を設定して初めて動きます。片方だけだと 黙って 403 を返すので「設定したつもり」事故が起きやすい。

症状: https://lab.iigtn.com/AccessDenied XML を返す 原因: 9 割は OAC ↔ Bucket Policy の片方だけ設定 or SourceArn が古い ARN 対処: Bucket Policy の aws:SourceArn condition と CloudFront Distribution ARN が一致しているか確認

Distribution の作成時間

CloudFront Distribution の terraform apply10〜15 分 かかります。これは世界中の Edge ロケーションへの設定配布が必要だから。「Still creating... [10m elapsed]」と出ても異常ではありません。

本シリーズの初回構築では 3 分 10 秒で完了しました。AWS 側の状況により変動します。

動作確認

apply 後、CloudFront ドメインに直接アクセスして 200 を確認:

curl -sI https://dXXXXXXXXX.cloudfront.net/
# HTTP/1.1 200 OK
# Content-Type: text/html
# x-amz-server-side-encryption: AES256   ← S3 暗号化が効いている証拠
# Server: AmazonS3
# Via: 1.1 ...cloudfront.net (CloudFront)
# X-Cache: Miss from cloudfront            ← 初回はキャッシュ未ヒット
# X-Amz-Cf-Pop: NRT12-P4                    ← 東京 Edge から配信

S3 を直接(バケット URL)叩くと 403。CloudFront 経由なら 200。これが OAC が効いている証拠です。

Bucket Policy 適用順序の罠

Public Access Block と Bucket Policy は 適用順序 に注意:

resource "aws_s3_bucket_policy" "this" {
  bucket = aws_s3_bucket.this.id
  policy = data.aws_iam_policy_document.bucket.json

  depends_on = [aws_s3_bucket_public_access_block.this]   # ← 明示的に依存を指定
}

depends_on を入れる理由: 先に Bucket Policy を作ると Public Access Block の BlockPublicPolicy=true に弾かれることがある。先に Public Access Block を確定させてから Bucket Policy を書くべき。

次の記事

CloudFront の基本構成ができたところで、次は キャッシュ動作・セキュリティヘッダ・SPA 用エラーフォールバック など、Distribution の設定深堀りに入ります。AWS マネージドの Cache Policy / Response Headers Policy の使いどころを解説します。

📚 用語集

S3 静的ホスティング
S3 バケット単体で「サイトとして公開」する機能。HTTPS が無料で付かない・キャッシュなし等の制約があるため、本格運用では CloudFront と組み合わせる。
CloudFront Distribution
CloudFront の設定単位。1 ドメイン分の CDN 構成。世界中の Edge ロケーションに展開される。
Origin (CloudFront)
CloudFront が大元のファイルを取りに行く先。S3・API Gateway・カスタムオリジン等。
OAC (Origin Access Control)
CloudFront から S3 へのアクセスを認証する最新の仕組み。SigV4 署名を使う。OAI の後継で、KMS 暗号化バケットや S3 以外のオリジンにも対応。
OAI (Origin Access Identity)
OAC の旧式。IAM Principal を使う方式。新規構築では OAC 推奨。
SigV4 署名
AWS API 呼び出しの認証署名方式。リクエストに HMAC-SHA256 ベースの署名を付ける。OAC はこれを使う。
Bucket Policy
S3 バケットへのアクセス権を JSON で定義するポリシー。Principal(誰)・Action(何を)・Resource(どれに)・Condition(どんな条件で)を記述。
aws:SourceArn condition
Bucket Policy の条件で「特定のサービス・リソースからのリクエストだけ許可」と絞る記法。これで「自分の Distribution からのみ」を実現。
Public Access Block
S3 バケットの「うっかり public 化」を構造的に防ぐ 4 つのスイッチ。BlockPublicAcls / IgnorePublicAcls / BlockPublicPolicy / RestrictPublicBuckets。全部 true が定石。
Versioning (S3)
S3 オブジェクトの旧バージョン保持機能。誤デプロイのロールバックに有効。
SSE-S3 (AES256)
AWS 管理キーでの自動暗号化。コスト 0、設定 1 行。
http2and3 (HTTP/3)
CloudFront でサポートする HTTP プロトコル。HTTP/3(QUIC)対応で新しいクライアントの体感速度が向上。
Price Class
CloudFront の Edge 範囲設定。PriceClass_100(北米欧州のみ・最安)/ PriceClass_200(+ アジア)/ PriceClass_All(全世界)。
default_root_object
CloudFront でルート URL(/)にアクセスされた時に返すデフォルトファイル。通常 index.html
X-Cache: Miss/Hit from cloudfront
CloudFront のレスポンスヘッダ。Hit ならキャッシュから返した、Miss なら Origin に取りに行ったことを示す。
X-Amz-Cf-Pop
CloudFront のレスポンスヘッダで、配信した Edge ロケーション識別子。NRT12-P4 なら東京(成田)の Edge。
depends_on (Terraform)
「このリソースは別のリソースが先に作られていることを必要とする」を明示する記法。通常は参照関係で自動推論されるが、暗黙の依存があるときに使う。