★★ 中級

08. CloudFront / Route 53 / ACM

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

構成図と登場リソース

              www                                          AWS Account
              │
        [Browser https://hcl-guide.com]
              │ ① DNS 解決
              ↓
        ┌──────────────────┐
        │ Route 53 (Zone)  │ → A レコード (alias) → CloudFront
        └──────────────────┘
              │ ② 名前解決後、CloudFront に到達
              ↓
        ┌──────────────────────────────────┐
        │ CloudFront (Edge, グローバル)    │
        │   - Viewer Cert (ACM, us-east-1) │
        │   - OAC (origin への署名)        │
        └──────────────────────────────────┘
              │ ③ キャッシュなし or 期限切れ → Origin へ
              ↓
        ┌──────────────────┐
        │ S3 Bucket (private) │ ← OAC 経由でのみアクセス許可
        │   - index.html     │
        │   - assets/...     │
        └──────────────────┘

リクエストの流れ

  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 ソースで参照するのが楽。

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 "aws" {
  region = "ap-northeast-1"
}

provider "aws" {
  alias  = "us_east_1"
  region = "us-east-1"
}

resource "aws_acm_certificate" "site" {
  provider = aws.us_east_1

  domain_name       = "hcl-guide.com"
  validation_method = "DNS"

  subject_alternative_names = ["www.hcl-guide.com"]

  lifecycle {
    create_before_destroy = true
  }
}

# 検証用 DNS レコードを Route 53 に作る
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
}

# 検証完了を待つ
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 バケットを「プライベートのまま」

resource "aws_s3_bucket" "site" {
  bucket = "hcl-guide-site"
}

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
}

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

CloudFront OAC(Origin Access Control)

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

resource "aws_cloudfront_origin_access_control" "site" {
  name                              = "hcl-guide-site"
  description                       = "OAC for hcl-guide.com"
  origin_access_control_origin_type = "s3"
  signing_behavior                  = "always"
  signing_protocol                  = "sigv4"
}

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

data "aws_iam_policy_document" "site" {
  statement {
    sid     = "AllowCloudFrontOAC"
    effect  = "Allow"
    actions = ["s3:GetObject"]

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

    resources = ["${aws_s3_bucket.site.arn}/*"]

    condition {
      test     = "StringEquals"
      variable = "AWS:SourceArn"
      values   = [aws_cloudfront_distribution.site.arn]
    }
  }
}

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

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

resource "aws_cloudfront_distribution" "site" {
  enabled             = true
  is_ipv6_enabled     = true
  default_root_object = "index.html"

  aliases = ["hcl-guide.com", "www.hcl-guide.com"]
  price_class = "PriceClass_200"   # 北米+欧州+アジア(日本は含まれる)

  origin {
    domain_name              = aws_s3_bucket.site.bucket_regional_domain_name
    origin_id                = "s3-site"
    origin_access_control_id = aws_cloudfront_origin_access_control.site.id
  }

  default_cache_behavior {
    target_origin_id       = "s3-site"
    viewer_protocol_policy = "redirect-to-https"
    allowed_methods        = ["GET", "HEAD"]
    cached_methods         = ["GET", "HEAD"]
    compress               = true

    cache_policy_id = "658327ea-f89d-4fab-a63d-7e88639e58f6"  # AWS Managed CachingOptimized
  }

  custom_error_response {
    error_code            = 404
    response_code         = 404
    response_page_path    = "/404.html"
    error_caching_min_ttl = 60
  }

  viewer_certificate {
    acm_certificate_arn      = aws_acm_certificate_validation.site.certificate_arn
    ssl_support_method       = "sni-only"
    minimum_protocol_version = "TLSv1.2_2021"
  }

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

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

Route 53 alias レコード

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

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

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 つのモジュールにまとめると、独自ドメインの静的サイト がワンコマンドで立ち上がります。

module "static_site" {
  source = "./modules/static-site"

  domain_name        = "hcl-guide.com"
  alternative_names  = ["www.hcl-guide.com"]
  source_dir         = "${path.module}/../public"

  providers = {
    aws       = aws
    aws.us_east_1 = aws.us_east_1
  }
}
このサイトの実物 hcl-guide.com の Terraform コード一式は本リポジトリの terraform/site/ にあります。terraform apply 1 発で再現可能。