★★ 中級

08. CloudFront / Route 53 / ACM

独自ドメインで HTTPS の静的サイトを公開する標準構成。このサイト自身(hcl-guide.com)がまさにこの章のコード でデプロイされています。

構成図と登場リソース

hcl-guide.com の配信アーキテクチャ:Route 53 → CloudFront(ACM 証明書)→ S3
独自ドメインの A レコード(alias)が CloudFront を指す。CloudFront が ACM 証明書を使って HTTPS 応答し、S3 はバケットポリシーで「この CloudFront からの署名つきリクエストだけ許可」。

リクエストの流れ

  1. ブラウザが hcl-guide.com を Route 53 で名前解決
  2. Route 53 の A レコード(alias) が CloudFront を指す
  3. CloudFront のエッジが応答。キャッシュにあればそのまま返す
  4. 無ければ OAC で署名 して S3 にアクセス、ファイルを取得して返す(同時にキャッシュ)
  5. S3 はバケットポリシーで 「この CloudFront からの署名つきリクエストだけ許可」

S3 をプライベートに保ったまま、世界中から HTTPS で配信できる、というのがこの構成のキモです。

Route 53 のホストゾーン

ドメインを Route 53 で取った場合、ホストゾーンは自動で作られています。data ソースで参照するのが楽。

# 既存のホストゾーンを参照(Route 53 でドメイン取得すると自動で作られる)
# zone_id はあとで A レコード作成時に使う
data "aws_route53_zone" "this" {
  name = "hcl-guide.com"
}

外部レジストラ(お名前.com 等)で取ったドメインを使う場合は、aws_route53_zone リソースで作って、NS レコード 4 つをレジストラ側に登録 する必要があります。

ACM 証明書(us-east-1 必須)

CloudFront に貼る ACM 証明書は 必ず us-east-1 リージョン で発行する必要があります(CloudFront がグローバルサービスで、内部的に us-east-1 を見ているため)。Terraform 側はプロバイダ alias で対応:

# デフォルトの provider(東京リージョン)
provider "aws" {
  region = "ap-northeast-1"
}

# CloudFront 用の証明書専用の alias 付き provider(us-east-1 固定)
# CloudFront の証明書だけは必ず us-east-1 で発行する必要がある
provider "aws" {
  alias  = "us_east_1"
  region = "us-east-1"
}

# ACM 証明書本体
resource "aws_acm_certificate" "site" {
  provider = aws.us_east_1     # 必ず us-east-1 の provider を使う

  domain_name       = "hcl-guide.com"
  validation_method = "DNS"    # DNS 検証(メール検証より自動化しやすい)

  # SAN(追加で証明書に含めるドメイン名)
  subject_alternative_names = ["www.hcl-guide.com"]

  lifecycle {
    # 更新時は「先に新証明書 → CloudFront 切り替え → 古いの破棄」の順に
    create_before_destroy = true
  }
}

# 検証用 DNS レコードを Route 53 に作る
# domain_validation_options(ACM が「このレコードを置いて」と教えてくれる値)を for_each で展開
resource "aws_route53_record" "cert_validation" {
  for_each = {
    for dvo in aws_acm_certificate.site.domain_validation_options :
    dvo.domain_name => {
      name   = dvo.resource_record_name
      type   = dvo.resource_record_type
      record = dvo.resource_record_value
    }
  }

  zone_id = data.aws_route53_zone.this.zone_id
  name    = each.value.name
  type    = each.value.type
  records = [each.value.record]
  ttl     = 60     # 検証完了後は使われないので短く
}

# 検証完了を待つ(ACM が DNS レコードを見て検証する間 Terraform をブロック)
# このリソースができてはじめて証明書が「ISSUED」状態
resource "aws_acm_certificate_validation" "site" {
  provider                = aws.us_east_1
  certificate_arn         = aws_acm_certificate.site.arn
  validation_record_fqdns = [for r in aws_route53_record.cert_validation : r.fqdn]
}

S3 バケットを「プライベートのまま」

# コンテンツ置き場の S3 バケット(CloudFront からのみアクセス可)
resource "aws_s3_bucket" "site" {
  bucket = "hcl-guide-site"
}

# パブリックアクセスを完全遮断(4 つの設定を全部 true に)
# CloudFront OAC 経由なら問題なくアクセスできる
resource "aws_s3_bucket_public_access_block" "site" {
  bucket                  = aws_s3_bucket.site.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

# 保管時の暗号化(S3 のデフォルト暗号化を有効化)
resource "aws_s3_bucket_server_side_encryption_configuration" "site" {
  bucket = aws_s3_bucket.site.id
  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256"   # S3 管理キー(無料)。KMS にする場合は "aws:kms"
    }
  }
}

CloudFront OAC(Origin Access Control)

OAC は CloudFront が S3 にアクセスする際の 署名つきリクエスト の仕組み。旧来の OAI(Origin Access Identity)の後継で、2024 年以降の新規構築では OAC が必須

# OAC(Origin Access Control):CloudFront が S3 にアクセスする際の署名設定
resource "aws_cloudfront_origin_access_control" "site" {
  name                              = "hcl-guide-site"
  description                       = "OAC for hcl-guide.com"
  origin_access_control_origin_type = "s3"     # 接続先タイプ(s3 / mediastore など)
  signing_behavior                  = "always" # 常に署名する(never/no-override も可)
  signing_protocol                  = "sigv4"  # 署名 v4(現行標準)
}

S3 バケットポリシー(CloudFront だけ許可)

# S3 バケットポリシー:「特定の CloudFront ディストリビューションだけ」許可する
data "aws_iam_policy_document" "site" {
  statement {
    sid     = "AllowCloudFrontOAC"
    effect  = "Allow"
    actions = ["s3:GetObject"]      # オブジェクト取得のみ(list は許可しない)

    # CloudFront サービスがプリンシパル
    principals {
      type        = "Service"
      identifiers = ["cloudfront.amazonaws.com"]
    }

    # バケット配下の全オブジェクト
    resources = ["${aws_s3_bucket.site.arn}/*"]

    # 条件:「この CloudFront ディストリビューションからのリクエストに限る」
    # これがないと別アカウントの CloudFront からもアクセスされうるので必須
    condition {
      test     = "StringEquals"
      variable = "AWS:SourceArn"
      values   = [aws_cloudfront_distribution.site.arn]
    }
  }
}

# 上で組み立てた policy_document を S3 バケットに貼り付け
resource "aws_s3_bucket_policy" "site" {
  bucket = aws_s3_bucket.site.id
  policy = data.aws_iam_policy_document.site.json
}

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

# CloudFront ディストリビューション本体
resource "aws_cloudfront_distribution" "site" {
  enabled             = true             # 作成と同時に配信開始
  is_ipv6_enabled     = true             # IPv6 対応(追加コストなし)
  default_root_object = "index.html"     # / にアクセス時に返すファイル

  # この distribution が応答する独自ドメイン名(証明書の domain と一致必須)
  aliases = ["hcl-guide.com", "www.hcl-guide.com"]
  # PriceClass: 100=北米/欧州のみ / 200=+アジア(日本含む)/ All=全エッジ
  price_class = "PriceClass_200"   # 北米+欧州+アジア(日本は含まれる)

  # オリジン(コンテンツの取得元)
  origin {
    # bucket_regional_domain_name は OAC 必須(bucket_domain_name ではダメ)
    domain_name              = aws_s3_bucket.site.bucket_regional_domain_name
    origin_id                = "s3-site"  # 後で behavior から参照する識別子
    origin_access_control_id = aws_cloudfront_origin_access_control.site.id
  }

  # デフォルトのキャッシュ動作(path pattern で別ルール追加も可)
  default_cache_behavior {
    target_origin_id       = "s3-site"           # 上の origin を指す
    viewer_protocol_policy = "redirect-to-https" # HTTP は HTTPS にリダイレクト
    allowed_methods        = ["GET", "HEAD"]     # 静的サイトは GET/HEAD だけ
    cached_methods         = ["GET", "HEAD"]
    compress               = true                # gzip/brotli 自動圧縮

    # AWS マネージドのキャッシュポリシー(CachingOptimized = 1 日キャッシュ)
    cache_policy_id = "658327ea-f89d-4fab-a63d-7e88639e58f6"  # AWS Managed CachingOptimized
  }

  # 404 エラー時のカスタムレスポンス
  custom_error_response {
    error_code            = 404
    response_code         = 404
    response_page_path    = "/404.html"   # S3 に置いた 404 ページを返す
    error_caching_min_ttl = 60            # 404 のキャッシュ時間(秒)
  }

  # TLS 証明書設定
  viewer_certificate {
    # 検証完了を待った後の ARN を使う(_validation の方を参照)
    acm_certificate_arn      = aws_acm_certificate_validation.site.certificate_arn
    ssl_support_method       = "sni-only"       # 専用 IP より安い(実質これ一択)
    minimum_protocol_version = "TLSv1.2_2021"   # 古い TLS は拒否
  }

  # 地域制限(none=制限なし、whitelist/blacklist で国指定可)
  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

  # 関連 SaaS と同じく、変更で再作成されると配信が止まるので保護
  lifecycle {
    create_before_destroy = false
  }
}

Route 53 alias レコード

独自ドメインを CloudFront に向けます。alias レコードは AWS 内部リソースを指す特別な A レコードで、料金がかからない

# apex(hcl-guide.com 自体)の A レコード
# alias は CNAME と違って zone apex でも使える AWS 独自の仕組み
resource "aws_route53_record" "apex" {
  zone_id = data.aws_route53_zone.this.zone_id
  name    = "hcl-guide.com"
  type    = "A"

  alias {
    name                   = aws_cloudfront_distribution.site.domain_name
    zone_id                = aws_cloudfront_distribution.site.hosted_zone_id
    evaluate_target_health = false   # CloudFront の場合は false 固定
  }
}

# www サブドメインも同じ CloudFront に向ける
resource "aws_route53_record" "www" {
  zone_id = data.aws_route53_zone.this.zone_id
  name    = "www.hcl-guide.com"
  type    = "A"

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

この章の全体構成

ここまでの 6 つのリソース(Route 53 + ACM + S3 + 公開遮断 + OAC + CloudFront + バケットポリシー)を 1 つのモジュールにまとめると、独自ドメインの静的サイト がワンコマンドで立ち上がります。

# 上記すべてを 1 モジュールにまとめて呼び出す例
module "static_site" {
  source = "./modules/static-site"

  domain_name        = "hcl-guide.com"            # apex ドメイン
  alternative_names  = ["www.hcl-guide.com"]      # SAN に追加するドメイン
  source_dir         = "${path.module}/../public" # アップロード元

  # モジュール内で 2 つの provider を使うので明示的に渡す
  # us-east-1 alias が必要なのは ACM 証明書のため
  providers = {
    aws       = aws
    aws.us_east_1 = aws.us_east_1
  }
}
このサイトの実物 hcl-guide.com の Terraform コード一式は本リポジトリの terraform/site/ にあります。terraform apply 1 発で再現可能。