この記事でやること
CloudFrontのマルチテナント・ディストリビューション(SaaS Manager for Amazon CloudFront)をCDK / TypeScriptで構築する手順をまとめます。
- 各テナントごとにACM証明書を個別発行してテナントへアタッチ
- S3(OAC)をオリジンにして、直接アクセスを禁止。CloudFront経由のみ許可
- L2未対応の箇所はL1コンストラクトへ降りて設定(
ConnectionMode=tenant-only
、ConnectionGroup
、DistributionTenant
)
バージョン
- aws-cdk-lib: 2.202.0
- constructs: 10.4.2
- aws-cdk (CLI): 2.1020.2
- TypeScript: 5.6系
- Node.js: 22系
リージョン
us-east-1(バージニア北部)
※ CloudFrontのビューワ証明書はus-east-1のACMに作成する必要があります。今回は管理をシンプルにするため、他のリソースも同一スタック・同一リージョン(us-east-1)にまとめて作成します。
事前準備
- 2つのドメインと、それぞれのPublic Hosted Zoneを用意
- 記事中では
<ドメイン1>
/<ドメイン2>
と表記
- 記事中では
- 各Hosted ZoneのIDを控えておく(この記事ではコード内に
<ホストゾーンID1>
/<ホストゾーンID2>
と記述)
プロジェクト作成
以下のコマンドで、CDKプロジェクトを作成します。
mkdir multi-tenant-distribution
cd multi-tenant-distribution
cdk init app --language typescript
フルコード(bin)
注意: 下記の<ドメイン1>
/<ドメイン2>
と<ホストゾーンID1>
/<ホストゾーンID2>
は、ご自身の環境の実際の値に置き換えてください。
#!/usr/bin/env node
import * as cdk from 'aws-cdk-lib';
import { MultiTenantDistributionStack } from '../lib/multi-tenant-distribution-stack';
const app = new cdk.App();
new MultiTenantDistributionStack(app, 'MultiTenantDistributionStack', {
env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: 'us-east-1' },
tenants: [
{ domainName: '<ドメイン1>', hostedZoneId: '<ホストゾーンID1>' },
{ domainName: '<ドメイン2>', hostedZoneId: '<ホストゾーンID2>' },
],
});
なぜL1が必要か(L2エスケープハッチの利用)
- CloudFormationでは、マルチテナント構成時にDistributionConfigの
ConnectionMode
へtenant-only
を指定する必要があります - 2025年8月28日現在、CDK L2の
Distribution
ではこのプロパティが未サポートのため、CfnDistribution
に対してプロパティを上書きします - 要望Issue/関連リリース(参考)
- L2コンストラクトからL1へ“エスケープハッチ”して操作します(
node.defaultChild
を介してCfn*
を取得し、addPropertyOverride
などで上書き)
フルコード(lib)
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import {
aws_cloudfront as cloudfront,
aws_route53 as route53,
aws_s3 as s3,
aws_certificatemanager as acm,
Fn,
} from 'aws-cdk-lib';
import * as origins from 'aws-cdk-lib/aws-cloudfront-origins';
import * as s3deploy from 'aws-cdk-lib/aws-s3-deployment';
export interface MultiTenantDistributionStackProps extends cdk.StackProps {
readonly tenants: Array<{
domainName: string; // 例: "<ドメイン1>"
hostedZoneId: string; // 事前に作成済みのパブリックHosted ZoneのID
}>;
}
export class MultiTenantDistributionStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: MultiTenantDistributionStackProps) {
super(scope, id, props);
if (!props || !props.tenants || props.tenants.length === 0) {
throw new Error('tenants is required');
}
// オリジン用S3バケット(パブリックブロック + OAC運用)
const originBucket = new s3.Bucket(this, 'OriginBucket', {
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
encryption: s3.BucketEncryption.S3_MANAGED,
enforceSSL: true,
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
});
// Hosted Zone(事前作成)をインポート
const hostedZones: Record<string, route53.IHostedZone> = {};
for (const tenant of props.tenants) {
hostedZones[tenant.domainName] = route53.HostedZone.fromHostedZoneAttributes(
this,
`HostedZone-${tenant.domainName.replace(/\./g, '-')}`,
{ hostedZoneId: tenant.hostedZoneId, zoneName: tenant.domainName },
);
}
// 各テナント用にACM(us-east-1)を個別発行
const tenantCertificates: Record<string, acm.Certificate> = {};
for (const tenant of props.tenants) {
const domain = tenant.domainName;
tenantCertificates[domain] = new acm.Certificate(this,
`CloudFrontCertificate-${domain.replace(/\./g, '-')}`, {
certificateName: `${this.stackName}-cloudfront-certificate-${domain}`,
domainName: domain,
validation: acm.CertificateValidation.fromDns(hostedZones[domain]),
},
);
}
// CloudFront Distribution(まずはL2で作成)
// 以降、エスケープハッチ(node.defaultChild)でL1に降りてマルチテナント固有設定を適用する
const distribution = new cloudfront.Distribution(this, 'Distribution', {
defaultBehavior: {
origin: origins.S3BucketOrigin.withOriginAccessControl(originBucket),
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
},
defaultRootObject: 'index.html',
httpVersion: cloudfront.HttpVersion.HTTP2_AND_3,
});
// L2 → L1 のエスケープハッチ
// - node.defaultChild から CfnDistribution を取得し、生の CloudFormation プロパティを上書きする
const cfnDistribution = distribution.node.defaultChild as cloudfront.CfnDistribution;
// マルチテナント構成では IPv6Enabled はサポート外のため削除
cfnDistribution.addPropertyDeletionOverride('DistributionConfig.IPV6Enabled');
// L1で ConnectionMode を tenant-only に変更
cfnDistribution.addPropertyOverride('DistributionConfig.ConnectionMode', 'tenant-only');
// Connection Group(テナントの受け皿)
const connectionGroup = new cloudfront.CfnConnectionGroup(this, 'ConnectionGroup', {
name: `${this.stackName}-ConnectionGroup`,
enabled: true,
ipv6Enabled: false,
});
// 各テナントのDistributionTenantを作成
for (const tenant of props.tenants) {
const domain = tenant.domainName;
const tenantId = tenant.domainName.replace(/\./g, '-');
new cloudfront.CfnDistributionTenant(this, `DistributionTenant-${tenantId}`, {
distributionId: distribution.distributionId,
connectionGroupId: connectionGroup.attrId,
name: `${this.stackName}-tenant-${tenantId}`,
domains: [domain],
enabled: true,
customizations: {
certificate: { arn: tenantCertificates[domain].certificateArn },
},
});
// Route53: ConnectionGroupのRoutingEndpointへALIAS
new route53.ARecord(this, `AliasRecord-${tenantId}`, {
zone: hostedZones[tenant.domainName],
recordName: tenant.domainName,
target: route53.RecordTarget.fromAlias({
bind: () => ({
dnsName: Fn.getAtt(connectionGroup.logicalId, 'RoutingEndpoint').toString(),
hostedZoneId: 'Z2FDTNDATAQYW2', // CloudFront固定のHostedZoneId
}),
}),
});
}
// デモ用に index.html をS3へ自動アップロード
new s3deploy.BucketDeployment(this, 'DeployIndexHtml', {
destinationBucket: originBucket,
sources: [
s3deploy.Source.data(
'index.html',
`<!doctype html><html><head><meta charset="utf-8"><title>Hello</title></head><body><h1>Hello World!</h1></body></html>`
),
],
});
}
}
デプロイと動作確認
- デプロイ
npx cdk deploy
- 検証
- Route53に
<ドメイン1>
/<ドメイン2>
のAレコードが作成されている - ACM(us-east-1)に各ドメインの証明書が発行されている
- S3バケットに
index.html
が配置されている - ブラウザで
https://<ドメイン1>/
とhttps://<ドメイン2>/
を開き、"Hello World!" が表示される
おまけ: 全テナントのキャッシュをまとめて削除する
CloudFrontのマルチテナント配信では「全テナント一括のキャッシュ削除API」は提供されていません。各テナントIDごとにキャッシュ削除(Invalidation)を発行します。
- 参考: create-invalidation-for-distribution-tenant
- このCLIコマンドは新しめです。未対応エラーが出る場合はAWS CLIを最新版へ更新してください。
以下は、親ディストリビューションに関連付いた全テナントのキャッシュを "/*" で削除するスクリプト例です。
# 親ディストリビューションIDを設定
PARENT_DISTRIBUTION_ID="<ディストリビューションID>"
# 親ディストリビューションに紐づく全テナントIDを取得
TENANT_IDS=$(aws cloudfront list-distribution-tenants \
--association-filter DistributionId="$PARENT_DISTRIBUTION_ID" \
--query "DistributionTenantList[*].Id" \
--output text)
# 各テナントに対してキャッシュ削除(Invalidation)を作成(全パス対象)
for TENANT_ID in $TENANT_IDS; do
aws cloudfront create-invalidation-for-distribution-tenant \
--id "$TENANT_ID" \
--invalidation-batch '{"Paths":{"Quantity":1,"Items":["/*"]},"CallerReference":"invalidation-'"$(date +%s)"'"}'
echo "テナント $TENANT_ID: キャッシュ削除リクエストを作成しました"
done
補足:
- パスは必要に応じて
/index.html
などに絞ってください -
CallerReference
は毎回ユニークである必要があります(上の例はUNIXタイムスタンプ)
終わりに
この記事では、CloudFrontのマルチテナント・ディストリビューションをCDK/TypeScriptで構築するうえでの肝となるポイントを押さえつつ、実際にHello World!
で動作確認できるところまでをまとめました。要点は次のとおりです。
-
ConnectionMode=tenant-only
のL1設定 -
ConnectionGroup
/DistributionTenant
の作成 - S3 OACの利用(S3直アクセス禁止、CloudFront経由のみ許可)
- テナント単位のACM証明書付与
- Route53のALIAS設定
この記事が参考になりますと幸いです。