AWS CDK でマルチリージョンリソースを1スタック定義にまとめる「cdk-multi-region-stack」を試してみた
はじめに
AWS CDK で CloudFront + ACM の構成を作るとき、CloudFront のビューワー証明書として ACM 証明書を使う場合は us-east-1 に配置する必要があります。従来はワイルドカード証明書を事前に別スタックで作成し、その ARN を参照する方法が一般的でした。しかしワイルドカード証明書が使えない要件では、ACM 用のスタックを us-east-1 に個別作成し、クロスリージョン参照や deploy/destroy の順序制御を都度工夫する必要がありました。
cdk-multi-region-stack は、この課題を解消するコンストラクトライブラリです。
MultiRegionStack を継承し、us-east-1 に配置したいリソースのスコープとして this.regionScope('us-east-1') を渡します。これにより、別リージョンのリソースを同じスタック定義内に記述できます。
以下は従来の方法と cdk-multi-region-stack を使った方法の比較です。
| 比較項目 | 従来(マルチスタック構成) | cdk-multi-region-stack |
|---|---|---|
| ユーザーが記述する Stack 定義 | 2つ(メイン + us-east-1 用) | 1つ |
| deploy 操作 | スタックごとに個別 deploy(順序指定が必要) | cdk deploy <メインスタック名> のみ(ツイン自動追従) |
| destroy 操作 | 依存順序を意識して個別 destroy | cdk destroy '<スタック名>*' 等でツインスタックも含めて削除 |
| クロスリージョン参照 | 明示的に SSM パラメータ等で受け渡し(CfnOutput 単体ではリージョンをまたいだ ImportValue は不可) |
ライブラリが SSM パラメータ経由で自動解決 |
実際にデプロイして動作を確認してみました。
検証内容
検証構成
以下の構成を ap-northeast-1(メイン)と us-east-1(ACM 証明書のみ)で構築します。
- ACM 証明書(us-east-1、DNS 検証)
- CloudFront ディストリビューション(ap-northeast-1)
- S3 バケット(CloudFront オリジン、OAC 利用)
- Route 53 エイリアスレコード
検証環境:
- aws-cdk-lib: 2.261.0
- cdk-multi-region-stack: 0.0.4
- 実行環境: podman コンテナ(node:20-slim + aws-cdk)
CDK コード
bin/app.ts:
#!/usr/bin/env node
import { App } from 'aws-cdk-lib';
import { MyAppStack } from '../lib/my-app-stack';
const app = new App();
new MyAppStack(app, 'MrsDemo', {
env: { account: '123456789012', region: 'ap-northeast-1' },
domainName: 'example.example.net',
hostedZoneId: 'ZXXXXXXXXXXXXX',
zoneName: 'example.net',
});
app.synth();
lib/my-app-stack.ts:
import { RemovalPolicy } from 'aws-cdk-lib';
import * as acm from 'aws-cdk-lib/aws-certificatemanager';
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import * as origins from 'aws-cdk-lib/aws-cloudfront-origins';
import * as route53 from 'aws-cdk-lib/aws-route53';
import * as targets from 'aws-cdk-lib/aws-route53-targets';
import * as s3 from 'aws-cdk-lib/aws-s3';
import { MultiRegionStack, MultiRegionStackProps } from 'cdk-multi-region-stack';
import { Construct } from 'constructs';
interface MyAppStackProps extends MultiRegionStackProps {
readonly domainName: string;
readonly hostedZoneId: string;
readonly zoneName: string;
}
export class MyAppStack extends MultiRegionStack {
constructor(scope: Construct, id: string, props: MyAppStackProps) {
super(scope, id, props);
// HostedZone の参照
const hostedZone = route53.HostedZone.fromHostedZoneAttributes(this, 'HostedZone', {
hostedZoneId: props.hostedZoneId,
zoneName: props.zoneName,
});
// === us-east-1: ACM 証明書(DNS 認証) ===
const cert = new acm.Certificate(this.regionScope('us-east-1'), 'Cert', {
domainName: props.domainName,
validation: acm.CertificateValidation.fromDns(hostedZone),
});
// === ap-northeast-1: S3 バケット(OAC 用) ===
const bucket = new s3.Bucket(this, 'WebBucket', {
removalPolicy: RemovalPolicy.DESTROY,
autoDeleteObjects: true,
});
// === ap-northeast-1: CloudFront ディストリビューション ===
const distribution = new cloudfront.Distribution(this, 'Distribution', {
defaultBehavior: {
origin: origins.S3BucketOrigin.withOriginAccessControl(bucket),
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
},
domainNames: [props.domainName],
certificate: cert,
defaultRootObject: 'index.html',
});
// === Route 53 エイリアスレコード ===
new route53.ARecord(this, 'AliasRecord', {
zone: hostedZone,
recordName: props.domainName,
target: route53.RecordTarget.fromAlias(
new targets.CloudFrontTarget(distribution),
),
});
}
}
ポイントは MultiRegionStack を継承し、us-east-1 に配置したいリソースのスコープに this.regionScope('us-east-1') を渡している点です。これにより、ライブラリが内部的に us-east-1 用のツインスタックを生成し、リージョン間の参照も自動的に処理されます(仕組みは後述)。
cdk synth の結果
cdk synth を実行すると、2つのスタックが生成されました。
Supply a stack id (MrsDemo, MrsDemo-us-east-1) to display its template.
各スタックのリソース構成:
MrsDemo(ap-northeast-1)
- S3 Bucket + BucketPolicy + AutoDeleteObjects(Lambda)
- CloudFront Distribution + OriginAccessControl
- Route 53 AliasRecord
- CrossRegionExportReader(Lambda)— SSM パラメータ経由のクロスリージョン参照に関わるカスタムリソース
MrsDemo-us-east-1(ツインスタック)
- ACM Certificate(DNS 検証)
- CrossRegionExportWriter(Lambda)— 証明書 ARN を SSM パラメータに書き込む
- WaitConditionHandle(Placeholder)
cdk deploy の実行
cdk deploy MrsDemo を実行すると、依存関係が自動検出され、ツインスタックが先にデプロイされました。
Including dependency stacks: MrsDemo-us-east-1
デプロイ順序:
| 順序 | スタック | 所要時間 | 主な処理 |
|---|---|---|---|
| 1 | MrsDemo-us-east-1 | 171.1 秒 | ACM 証明書の作成と DNS 検証、ExportWriter による証明書 ARN の書き込み |
| 2 | MrsDemo | 265.7 秒 | SSM パラメータの動的参照で証明書 ARN を解決し、CloudFront Distribution を作成 |
各スタックの所要時間の合計は約 437 秒ですが、スタック間の切替処理を含む CDK CLI 全体の実測では 464 秒でした。
CloudFormation スタックの状態確認
デプロイ後、両リージョンの CloudFormation スタックを確認しました。
us-east-1:
StackName: MrsDemo
StackStatus: CREATE_COMPLETE
CreationTime: 2026-07-09T13:58:19.367Z
ap-northeast-1:
StackName: MrsDemo
StackStatus: CREATE_COMPLETE
CreationTime: 2026-07-09T14:01:18.416Z
CDK のアーティファクト名は MrsDemo-us-east-1 ですが、CloudFormation 上のスタック名はどちらのリージョンも "MrsDemo" です。
クロスリージョン参照の仕組み
ツインスタック(us-east-1)の証明書 ARN は、メインスタック(ap-northeast-1)の SSM パラメータを経由して参照されます。
ap-northeast-1 に作成された SSM パラメータ:
Name: /cdk/exports/MrsDemo/MrsDemouseast1RefCert5C9FAEC153A50979
Value: arn:aws:acm:us-east-1:123456789012:certificate/98dbec9f-xxxx-xxxx-xxxx-xxxxxxxxxxxx
SSM パラメータが us-east-1 ではなく ap-northeast-1 に存在するのは、aws-cdk-lib のクロスリージョン参照の仕組みによるものです。参照する側(ap-northeast-1)のリージョンにパラメータが置かれます。これにより、CloudFormation テンプレートは同一リージョンの {{resolve:ssm:...}} で値を解決できます。
内部の仕組み:
- us-east-1 の ExportWriter(Lambda)が証明書 ARN を ap-northeast-1 の SSM パラメータに書き込む
- ap-northeast-1 の ExportReader(Lambda)がデプロイ時に対象の SSM パラメータを確認する
- ap-northeast-1 の CloudFront Distribution テンプレートが
{{resolve:ssm:/cdk/exports/MrsDemo/...}}で SSM パラメータを動的参照する
これは aws-cdk-lib のクロスリージョン参照機構(CrossRegionExportWriter / CrossRegionExportReader)をそのまま利用しています。
なお、cdk.json の context に "@aws-cdk/core:defaultCrossStackReferences": "weak" を設定すると、上記のカスタムリソースは生成されません。代わりに、ツインスタック側で CloudFormation の Outputs に値を公開し、メインスタック側は Fn::GetStackOutput で直接参照する形になります。
実際に同じコードで weak モードを有効にして cdk synth を実行しました。ExportWriter / ExportReader の Lambda や IAM Role が消え、テンプレートがシンプルになることを確認しました。
cdk destroy の実行
削除は cdk destroy 'MrsDemo*' で実行します。
MrsDemo: destroying... [1/2]
✅ MrsDemo: destroyed
MrsDemo-us-east-1 (MrsDemo): destroying... [2/2]
✅ MrsDemo-us-east-1 (MrsDemo): destroyed
削除順序はメインスタック(ap-northeast-1)が先、ツインスタック(us-east-1)が後でした。メインスタックがツインスタックの証明書を参照する依存関係になっているため、deploy とは逆順で参照元のメインスタックから削除されます。
循環依存パターンの制約
regionScope() でリージョン間の相互参照を行うと、スタックレベルで「メイン → ツイン → メイン」の循環依存が発生します。
実際に試すと、cdk synth が以下のエラーで失敗しました。
'CyclicTest' depends on 'CyclicTest-us-east-1' (CyclicTest -> CyclicTest-us-east-1/ParamA/Resource.Ref).
Adding this dependency (CyclicTest-us-east-1 -> CyclicTest/ParamB/Resource.Ref) would create a cyclic reference.
現実的にこのパターンが発生するのは、Lambda@Edge(us-east-1 必須)が東京リージョンの DynamoDB テーブル等を参照するような限定的なケースです。ACM 証明書や WAF WebACL を us-east-1 に置き、メインリージョンから一方向に参照する構成では循環は発生しません。
万が一このパターンに遭遇した場合は、リソースの配置を見直して一方向の参照に限定するか、別スタックに分離することで回避できます。
v0.0.5 の { group } オプションによる対応
v0.0.5 で追加された { group } オプションを使うと、循環参照を回避しつつ、同じスタック定義内に記述を維持できます。this.regionScope('us-east-1', { group: 'GroupName' }) のようにオプションを渡すと、通常のツインとは別にもう1つスタックが生成されます。
実際に以下のコードで cdk synth を実行しました。
// twin (us-east-1) — main より先にデプロイ
const remoteParam = new ssm.StringParameter(this.regionScope('us-east-1'), 'ParamA', {
parameterName: '/test/param-a',
stringValue: 'value-from-twin',
});
// main (ap-northeast-1) — twin の値を参照
const localParam = new ssm.StringParameter(this, 'ParamB', {
parameterName: '/test/param-b',
stringValue: remoteParam.parameterName,
});
// group (us-east-1) — main の値を参照
new ssm.StringParameter(this.regionScope('us-east-1', { group: 'Downstream' }), 'ParamC', {
parameterName: '/test/param-c',
stringValue: localParam.parameterName,
});
synth の結果、3つのスタックが生成されました。
Supply a stack id (GroupTest, GroupTest-us-east-1, GroupTest-us-east-1-Downstream) to display its template.
デプロイ順序は GroupTest-us-east-1(twin)→ GroupTest(main)→ GroupTest-us-east-1-Downstream(group)のリニアチェーンとなり、循環参照なく synth が成功しました。通常のツインは main より先にデプロイされますが、group は main より後にデプロイされます。そのため、group から main のリソースを参照しても循環が発生しません。
なお、cdk deploy GroupTest では group スタックはデプロイされません。synth 時に以下の Warning が出力されます。
WARNING 'GroupTest-us-east-1-Downstream' is not a dependency of 'GroupTest', so 'cdk deploy GroupTest' will NOT deploy it (the CLI extends the selection to dependencies only). Deploy with a wildcard: cdk deploy 'GroupTest*'
group を含む全スタックをデプロイするには cdk deploy 'GroupTest*' のようにワイルドカードを使用します。
注意事項
- deploy と destroy の非対称性:
cdk deploy <メインスタック名>でツインスタックも自動的にデプロイされます。一方cdk destroyではツインスタックも対象に含める必要があります。cdk destroy 'MrsDemo*'のようにワイルドカードを使うか、cdk destroy MrsDemo MrsDemo-us-east-1のように明示指定します。cdk destroy MrsDemoだけではツインスタックが削除されずに残ります。 - regionScope() の削除時の注意: コードから
regionScope()の呼び出しを削除すると、既にデプロイ済みのツインスタックが CDK の管理対象外になります。先にcdk destroyでツインスタックを削除してからコードを変更してください。
まとめ
cdk-multi-region-stack を使うと、CloudFront のビューワー証明書として ACM 証明書を使う構成のように、us-east-1 側のリソースが必要になる構成を1つのスタック定義にまとめられます。
内部的には2つの CloudFormation スタックに分かれますが、MultiRegionStack を継承したうえで、us-east-1 に配置したいリソースのスコープとして this.regionScope('us-east-1') を渡すことで、ツインスタックの生成とリージョン間参照が処理されます。今回の構成では、cdk deploy <メインスタック名> だけでツインスタックも依存スタックとしてデプロイされ、従来必要だった順序制御やパラメータ受け渡しのコードを省略できました。
一方で、destroy ではツインスタックも明示的に対象へ含める必要があります。cdk deploy と cdk destroy で指定方法が非対称になる点には注意が必要です。
なお、2026年5月に CloudFormation の Fn::GetStackOutput が正式リリースされました。CDK でも @aws-cdk/core:defaultCrossStackReferences: "weak" を設定すると、本記事で確認したとおり ExportWriter / ExportReader のカスタムリソースが不要になります(適用条件や制約については参考リンクを参照)。事前に作成したワイルドカード証明書の ARN を参照するだけの構成であれば、この仕組みで対応できる場合もあります。
一方、CloudFront ごとに非ワイルドカード証明書を個別管理したい構成では、証明書と CloudFront を1つのスタック定義にまとめられる cdk-multi-region-stack が管理の手間を減らす選択肢になります。また、本記事で確認した SSM パラメータ(カスタムリソース)による参照と Fn::GetStackOutput による参照の使い分け、v0.0.5 の { group } による依存方向の管理は、他のクロススタック構成を設計する際にも参考になるかと思います。
参考リンク








