CloudFront マルチテナントディストリビューションを Terraform で構築してみた

CloudFront マルチテナントディストリビューションを Terraform で構築してみた

2026.06.13

はじめに

🐕大好きなクラウド事業本部、あきやまです。

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 を別途用意します。

provider.tf
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 というディレクトリ構成で各サイトを分けます。

modules/s3/main.tf
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 = truevalidation_record_fqdnsdistinct() で重複を除きます。

modules/acm/main.tf
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 の引数が idorigin_id ではない)である点に注意です。origin path にはテナントのパラメータ({{prefix}})を埋め込み、テナントごとに /<key> へ解決させます。

modules/cloudfront/main.tf
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 の証明書を渡します。

modules/cloudfront/main.tf
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"
  }
}

テナントの statusDeployed になれば配信可能です。あとはブラウザや 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_overwritedistinct() で吸収しています。
  • 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 との差分(使えない設定)があるので、移行や新規採用の際は事前にチェックが必要です。

参考

この記事をシェアする

関連記事