CloudFront + S3 + OAC でセキュアな静的サイト配信
静的サイトの配信構成として最も枯れていてセキュアなのが S3 (非公開) + CloudFront + OAC の組み合わせです。本記事ではこの 3 点セットで「公開しないバケットを世界に配信する」仕組みと、Terraform での実装を解説します。
なぜ S3 を直接公開しないのか
S3 は静的ホスティング機能を持っていて、バケットを Public にすればそれだけでサイト配信できます。便利ですが、運用するなら避けたほうが良い理由が複数あります:
- HTTPS が無料で付かない(独自ドメインで HTTPS 配信したいなら CloudFront は事実上必須)
- キャッシュが効かない(毎リクエスト S3 を叩く → 遅い・S3 リクエスト課金が増える)
- 誤公開リスク(Bucket Policy 1 行で全世界に意図しないファイルが見える事故)
- セキュリティヘッダ付与不可(X-Frame-Options 等を返せない)
結論、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 個:
aws_s3_bucket— バケット本体aws_s3_bucket_public_access_block— 公開ブロック 4 種aws_s3_bucket_versioning— ロールバック用aws_s3_bucket_server_side_encryption_configuration— AES256 暗号化aws_cloudfront_origin_access_control— OACaws_cloudfront_distribution— CDN 本体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 apply は 10〜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)
- 「このリソースは別のリソースが先に作られていることを必要とする」を明示する記法。通常は参照関係で自動推論されるが、暗黙の依存があるときに使う。