CloudFront マルチテナントディストリビューションを Terraform で構築してみた
はじめに
🐕大好きなクラウド事業本部、あきやまです。
CloudFront のマルチテナントディストリビューション(distribution tenants)を Terraform で扱う機会があり、今、勉強も兼ねて触っています。「multi distribution」と聞くと複数のディストリビューションを並べる構成を思い浮かべますが、今回試したのは 1 つのテンプレート(マルチテナントディストリビューション)を共有しつつ、ドメインごとに「テナント」を生やす新しい方式です。
Terraform の AWS Provider でも対応リソースが追加された(v6.28.0〜)ので、実際に apply して動かしてみました。
環境
| 項目 | 値 |
|---|---|
| OS | macOS |
| Terraform | v1.15.4 |
| AWS Provider | hashicorp/aws 6.28+ |
| リージョン | ap-northeast-1(コンテンツ S3)/ us-east-1(ACM・ログ) |
結論
Terraform でマルチテナントディストリビューションを構築し、複数ドメインを 1 つのテンプレートで配信できました。
ポイントを先にまとめます。
| 観点 | 結果 |
|---|---|
| 必要な Provider | hashicorp/aws >= 6.28.0 |
| 構成要素 | multitenant distribution(テンプレート)+ connection group(ルーティング)+ distribution tenant(ドメインごと) |
| オリジン | 共有 S3 バケット + prefix 分割 + OAC |
| 証明書 | テナントごとに BYO ACM(apex + *.apex) |
| DNS | テナントドメインを connection group の routing endpoint へ CNAME(alias 不可) |
| 設定できないもの | PriceClass / レガシー標準ログ / OAI / TTL 直指定 / WAF Classic |
standard distribution との一番の違いは、マルチテナントディストリビューション自体はルーティングエンドポイントを持たず、直接アクセスできない点です。connection group と distribution tenant をセットで使います。
マルチテナントディストリビューションとは
ざっくり言うと、設定のテンプレートを 1 つ作り、ドメインごとに「テナント」を生やす仕組みです。
- マルチテナントディストリビューション: オリジンやキャッシュ設定などを定義する共有テンプレート。
connection_modeは自動的にtenant-onlyになり、単体では配信できません。 - connection group: 実際にビューワーのリクエストを受けるルーティングエンドポイント(
dxxxx.cloudfront.net)を提供します。テナントの CNAME 先になります。 - distribution tenant: ドメインごとの「フロントドア」。テンプレートを継承しつつ、ドメイン・証明書・origin path などを個別に設定します。
SaaS のように「同じ構成で多数のドメインを配信したい」ケースに向いた構成です。
やってみた
今回は「共有 S3 バケットの中をディレクトリ(prefix)で分け、ドメインごとにテナントを割り当てる」構成にしました。Terraform は acm / s3 / cloudfront の 3 モジュールに分割しています。
Step 1: Provider の指定
マルチテナント対応リソースは AWS Provider v6.28.0 以降で追加されました。ACM 証明書とアクセスログの配信設定は CloudFront 用にグローバル扱いとなるため、us-east-1 の provider を別途用意します。
terraform {
required_version = ">= 1.6"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 6.28"
}
}
}
provider "aws" {
region = var.region # ap-northeast-1
}
# CloudFront 用 ACM・ログ配信は us-east-1 必須
provider "aws" {
alias = "us_east_1"
region = "us-east-1"
}
Step 2: 共有 S3 バケット(modules/s3)
オリジンは 1 つの S3 バケットを共有し、<テナントキー>/index.html というディレクトリ構成で各サイトを分けます。
resource "aws_s3_bucket" "content" {
bucket = "${var.project}-content-${data.aws_caller_identity.current.account_id}"
force_destroy = true
}
resource "aws_s3_object" "lp" {
for_each = var.distributions
bucket = aws_s3_bucket.content.id
key = "${each.key}/index.html" # テナントキーごとに prefix を分ける
content_type = "text/html"
content = templatefile("${path.module}/templates/index.html.tftpl", {
site_key = each.key
domain = each.value.domain
})
}
Step 3: ACM 証明書(modules/acm)
今回は証明書を apex + ワイルドカード(例: example.com と *.example.com)の SAN 付き 1 枚にしました。実際に配信するドメイン www.example.com はワイルドカード側でカバーされます。
ここで 1 つハマったのが検証レコードです。apex とワイルドカードは同じ検証用 CNAME を共有するため、domain_validation_options には 2 件出てくるものの実体は同じレコードになります。素朴に書くと「同名レコードが既に存在する」エラーになるので、merge() で平坦化しつつ allow_overwrite = true、validation_record_fqdns は distinct() で重複を除きます。
resource "aws_acm_certificate" "this" {
for_each = var.certificates
domain_name = each.value.domain_name # apex
subject_alternative_names = each.value.subject_alternative_names # ["*.apex"]
validation_method = "DNS"
lifecycle {
create_before_destroy = true
}
}
resource "aws_route53_record" "validation" {
for_each = merge([
for k, cert in aws_acm_certificate.this : {
for dvo in cert.domain_validation_options :
"${k}:${dvo.domain_name}" => {
zone_id = var.certificates[k].zone_id
name = dvo.resource_record_name
type = dvo.resource_record_type
value = dvo.resource_record_value
}
}
]...)
zone_id = each.value.zone_id
name = each.value.name
type = each.value.type
records = [each.value.value]
ttl = 60
allow_overwrite = true # apex/wildcard が同名レコードを指すため
}
resource "aws_acm_certificate_validation" "this" {
for_each = var.certificates
certificate_arn = aws_acm_certificate.this[each.key].arn
validation_record_fqdns = distinct([
for dvo in aws_acm_certificate.this[each.key].domain_validation_options :
aws_route53_record.validation["${each.key}:${dvo.domain_name}"].fqdn
])
}
Step 4: マルチテナント本体・connection group・テナント(modules/cloudfront)
マルチテナントディストリビューションのテンプレートを作ります。origin の引数が id(origin_id ではない)である点に注意です。origin path にはテナントのパラメータ({{prefix}})を埋め込み、テナントごとに /<key> へ解決させます。
resource "aws_cloudfront_multitenant_distribution" "this" {
comment = "${var.project} multi-tenant template"
enabled = true
default_root_object = "index.html"
origin {
domain_name = var.bucket_regional_domain_name
id = "s3-shared" # ← origin_id ではなく id
origin_path = "/{{prefix}}"
origin_access_control_id = aws_cloudfront_origin_access_control.this.id
}
default_cache_behavior {
target_origin_id = "s3-shared"
viewer_protocol_policy = "redirect-to-https"
cache_policy_id = var.cache_policy_id # TTL 直指定は不可 → ポリシー必須
allowed_methods {
items = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
}
}
restrictions {
geo_restriction {
restriction_type = "none"
}
}
viewer_certificate {
cloudfront_default_certificate = true # 実証明書はテナント側で指定
}
tenant_config {
parameter_definition {
name = "prefix"
definition {
string_schema {
required = true
}
}
}
}
}
resource "aws_cloudfront_connection_group" "this" {
name = "${var.project}-cg"
enabled = true
ipv6_enabled = true
}
テナントは for_each で量産します。parameter でテンプレートの {{prefix}} に値を注入し、customizations.certificate.arn に Step 3 の証明書を渡します。
resource "aws_cloudfront_distribution_tenant" "this" {
for_each = var.tenants
name = each.key
distribution_id = aws_cloudfront_multitenant_distribution.this.id
connection_group_id = aws_cloudfront_connection_group.this.id
enabled = true
domain {
domain = each.value.domain # 例: www.example.com
}
parameter {
name = "prefix"
value = each.key
}
customizations {
certificate {
arn = var.certificate_arns[each.key]
}
}
}
# テナントドメイン → connection group の routing endpoint へ CNAME
resource "aws_route53_record" "tenant" {
for_each = var.tenants
zone_id = each.value.zone_id
name = each.value.domain
type = "CNAME"
ttl = 300
records = [aws_cloudfront_connection_group.this.routing_endpoint]
allow_overwrite = true # 既存レコードを引き取れるように
}
※ standard distribution では Route53 の alias レコードを使いますが、マルチテナントでは alias が使えないため CNAME を使います。今回はサブドメイン(www)なので CNAME で問題ありません。
Step 5: apply して動作確認
terraform apply を実行します。CloudFront の作成・デプロイには数分かかります。
terraform apply
Apply complete! Resources: 1 added, 1 changed, 0 destroyed.
Outputs:
connection_group_endpoint = "d6xxxxxxxxxxx.cloudfront.net"
content_bucket = "cf-multi-content-xxxxxxxxxxxx"
multitenant_distribution_arn = "arn:aws:cloudfront::xxxxxxxxxxxx:distribution/Exxxxxxxxxxxxx"
tenants = {
"example" = {
"status" = "Deployed"
"url" = "https://www.example.com"
}
}
テナントの status が Deployed になれば配信可能です。あとはブラウザや curl でアクセスして確認します。
curl -sS -o /dev/null -w 'HTTP %{http_code}\n' https://www.example.com/
# => HTTP 200
リクエストは次のように流れます。
www.example.com
→ (Route53 CNAME) dxxxx.cloudfront.net ← connection group
→ (Host ヘッダでテナント振り分け) tenant
→ (parameter prefix) origin_path = /<key>
→ S3: bucket/<key>/index.html ← OAC
注意点・制約
実際に触ってみて気づいた点をまとめます。
- Provider は v6.28.0 以降が必要です。それ以前だとリソース自体が存在しません。
- テナントの CNAME には
allow_overwrite = trueを付けておくと安全です。apply が途中で失敗すると、状態に取り込まれないレコードが残り、再 apply 時にalready existsで落ちることがありました。 - apex + ワイルドカードの証明書は検証 CNAME を共有します。検証レコードを素朴に書くと同名衝突するので、
allow_overwriteとdistinct()で吸収しています。 - origin の引数は
idで、origin_idではありません(target_origin_idから参照)。 - マルチテナントでは設定できない項目があります。代表的なものは以下です。
- PriceClass(価格クラスの指定)
- レガシー標準ログ(標準ログ v2 を使う)
- OAI(OAC を使う)
- キャッシュの TTL 直指定(キャッシュポリシー必須)
- WAF Classic(V1)
- マルチテナントディストリビューション自体は直接アクセスできません。必ず connection group + tenant とセットで使います。
まとめ
今回試して実現できたことは以下です。
- Terraform でマルチテナントディストリビューション + connection group + テナントを構築
- 共有 S3 バケットを prefix で分割し、テナントごとに別ドメインで配信
- apex + ワイルドカード証明書を自前 ACM で用意し、
wwwサブドメインを配信 for_eachでテナントを量産する型を確認
standard distribution を並べる構成と比べて、共通設定をテンプレートに寄せられるのが大きな利点だと感じました。一方で、価格クラスやログまわりなど standard との差分(使えない設定)があるので、移行や新規採用の際は事前にチェックが必要です。
参考
- Understand how multi-tenant distributions work - Amazon CloudFront
- aws_cloudfront_multitenant_distribution - Terraform Registry
- aws_cloudfront_distribution_tenant - Terraform Registry
- aws_cloudfront_connection_group - Terraform Registry
- Request certificates for your CloudFront distribution tenant - Amazon CloudFront





