08. CloudFront / Route 53 / ACM
独自ドメインで HTTPS の静的サイトを公開する標準構成。このサイト自身(hcl-guide.com)がまさにこの章のコード でデプロイされています。
この章の目次
構成図と登場リソース
リクエストの流れ
- ブラウザが
hcl-guide.comを Route 53 で名前解決 - Route 53 の A レコード(alias) が CloudFront を指す
- CloudFront のエッジが応答。キャッシュにあればそのまま返す
- 無ければ OAC で署名 して S3 にアクセス、ファイルを取得して返す(同時にキャッシュ)
- 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 発で再現可能。