Route 53 を CloudFormation のスタック間で移動してみる
はじめに
平素は大変お世話になっております。
クイックガードのパー子です。
AWS CloudFormation で管理している Amazon Route 53 のホストゾーンを別のスタックに移動してみました。
基本的に 公式ドキュメントどおりの手順 にしたがえばリソースを破壊することなくスタック間移動できるはずなのですが、DNS は Webサービスの提供において最も重要なリソースです。
リソースの移動中にゾーンやレコードの再作成が発生してサービスが中断することは万が一にも避けたいところです。
本記事では、公式ドキュメントに記載された手順を実際に実行してみて、無停止でスタック間の移動ができることを確かめました。
手順の概要
手順はざっくり以下の流れです。
移動元のスタックでやること:
- 対象のリソースに
DeletionPolicy: Retain
をセットしてデプロイする。 - テンプレートから対象リソースの記述を削除してデプロイする。
移動先のスタックでやること:
- テンプレートに対象リソースを追記する。
このとき、DeletionPolicy: Retain
とする。
続いてインポート操作を実行し、対象リソースをテンプレートに紐付ける。 - 必要に応じて DeletionPolicy を変更する。
無停止であることの確認
Route 53 のレコードには、CloudFormation においてリソースを一意に識別する ID が存在しないようです。
どういうことかというと、例えば CloudFormation の実行前後で abc.
というレコードが存在した場合、それが実行の前後で同一のリソースをそのまま維持したのか? それとも旧abc.
を廃棄して新abc.
を作り直したのか?
そのどちらなのか、何らかの ID を見て判断できないのです。
目視に頼らずこの変化を観測するには、CloudTrail で ChangeResourceRecordSets
というイベントを探す必要があります。
レコードを作成/削除/変更する際にこのイベントが発行されるため、CloudFormation の実行前後で CloudTrail を観察すればレコードが変更されたかどうかを知ることができるのです。
なお、このイベントは us-east-1リージョンの CloudTrail に記録されます。
今回の検証では、gondawara.yumeko
という夢のマイドメインを建てて say.gondawara.yumeko
(Type: TXT) が YUMEKO!
と返すようにレコードを登録します。
$ aws route53 list-resource-record-sets --hosted-zone-id 'Z005575336OGNL4QJGGU6'
ResourceRecordSets:
- Name: gondawara.yumeko.
ResourceRecords:
- Value: ns-1690.awsdns-19.co.uk.
- Value: ns-1325.awsdns-37.org.
- Value: ns-654.awsdns-17.net.
- Value: ns-266.awsdns-33.com.
TTL: 172800
Type: NS
- Name: gondawara.yumeko.
ResourceRecords:
- Value: ns-1690.awsdns-19.co.uk. awsdns-hostmaster.amazon.com. 1 7200 900 1209600
86400
TTL: 900
Type: SOA
- Name: say.gondawara.yumeko.
ResourceRecords:
- Value: '"YUMEKO!"'
TTL: 300
Type: TXT
この TXTレコードに対するイベントが、一連の移動手順の最中に発生しなければヨシとします。
ツール
生の CloudFormationテンプレートを書くのはちょっと面倒くさいので、AWS CDK (Cloud Development Kit) を使用しました。
検証
それでは検証を始めます。
初期状態
まず初期状態としてスタックを 2つ用意します。
移動元スタック FromStack と移動先スタック ToStack です。
bin/app.ts
import { App } from "aws-cdk-lib";
import "source-map-support/register";
import { FromStack } from "../lib/from-stack";
import { ToStack } from "../lib/to-stack";
const app = new App();
new FromStack(app, "FromStack");
new ToStack(app, "ToStack");
まず FromStack にゾーン&レコードを配置します。
lib/from-stack.ts
import { Duration, Stack } from "aws-cdk-lib";
import { PublicHostedZone, TxtRecord } from "aws-cdk-lib/aws-route53";
import { Construct } from "constructs";
import type { StackProps } from "aws-cdk-lib";
export class FromStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
const zone = new PublicHostedZone(this, "HostedZone", {
zoneName: "gondawara.yumeko",
});
const record = new TxtRecord(this, "TxtRecord", {
zone: zone,
recordName: "say",
values: ["YUMEKO!"],
ttl: Duration.minutes(5),
});
}
}
ToStack は空のスタックです。
lib/to-stack.ts
import { Stack } from "aws-cdk-lib";
import { Construct } from "constructs";
import type { StackProps } from "aws-cdk-lib";
export class ToStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
}
}
これらのスタックを $ npx cdk deploy --all
でデプロイしておきます。
FromStack のリソースと gondawara.yumeko
ドメインはそれぞれ以下のようになります。
移動準備
ゾーンとレコードに DeletionPolicy: Retain
をセットします。
lib/from-stack.ts
--- a/lib/from-stack.ts
+++ b/lib/from-stack.ts
@@ -1,4 +1,4 @@
-import { Duration, Stack } from "aws-cdk-lib";
+import { Duration, RemovalPolicy, Stack } from "aws-cdk-lib";
import { PublicHostedZone, TxtRecord } from "aws-cdk-lib/aws-route53";
import { Construct } from "constructs";
@@ -18,5 +18,8 @@ export class FromStack extends Stack {
values: ["YUMEKO!"],
ttl: Duration.minutes(5),
});
+
+ zone.applyRemovalPolicy(RemovalPolicy.RETAIN);
+ record.applyRemovalPolicy(RemovalPolicy.RETAIN);
}
}
CDK には DeletionPolicy という名称は存在せず、代わりに RemovalPolicy というプロパティを使用します。
これは DeletionPolicy だけでなく UpdateReplacePolicy も同時に設定するものだからなのです。
$ npx cdk deploy FromStack
で FromStack だけデプロイすると、ゾーンとレコードに DeletionPolicy: Retain
(と、ついでに UpdateReplacePolicy: Retain
) がセットされます。
FromStack から切り離し
準備が整ったので対象リソースを FromStack から切り離します。
lib/from-stack.ts
--- a/lib/from-stack.ts
+++ b/lib/from-stack.ts
@@ -1,5 +1,4 @@
-import { Duration, RemovalPolicy, Stack } from "aws-cdk-lib";
-import { PublicHostedZone, TxtRecord } from "aws-cdk-lib/aws-route53";
+import { Stack } from "aws-cdk-lib";
import { Construct } from "constructs";
import type { StackProps } from "aws-cdk-lib";
@@ -7,19 +6,5 @@ import type { StackProps } from "aws-cdk-lib";
export class FromStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
-
- const zone = new PublicHostedZone(this, "HostedZone", {
- zoneName: "gondawara.yumeko",
- });
-
- const record = new TxtRecord(this, "TxtRecord", {
- zone: zone,
- recordName: "say",
- values: ["YUMEKO!"],
- ttl: Duration.minutes(5),
- });
-
- zone.applyRemovalPolicy(RemovalPolicy.RETAIN);
- record.applyRemovalPolicy(RemovalPolicy.RETAIN);
}
}
これをデプロイすると、リソースは削除されることなくスタックの管理下から外れます。
CloudFormation のコンソール上でイベントを確認すると、DELETE_SKIPPED
となっていることがわかります。
実際、CloudTrail を見ても、切り離し中 (= 15:49:01〜15:49:02 前後) にゾーンの変更イベントは発生していません。
ToStack へゾーンをインポート
続いて、切り離したゾーンとレコードを ToStack へインポートしますが、まずはゾーンだけインポートします。
ToStack に以下のようにゾーンを定義します。
この段階では DeletionPolicy: Retain
をセットしておく必要があります。
lib/to-stack.ts
--- a/lib/to-stack.ts
+++ b/lib/to-stack.ts
@@ -1,4 +1,5 @@
-import { Stack } from "aws-cdk-lib";
+import { RemovalPolicy, Stack } from "aws-cdk-lib";
+import { PublicHostedZone } from "aws-cdk-lib/aws-route53";
import { Construct } from "constructs";
import type { StackProps } from "aws-cdk-lib";
@@ -6,5 +7,11 @@ import type { StackProps } from "aws-cdk-lib";
export class ToStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
+
+ const zone = new PublicHostedZone(this, "HostedZone", {
+ zoneName: "gondawara.yumeko",
+ });
+
+ zone.applyRemovalPolicy(RemovalPolicy.RETAIN);
}
}
ゾーンを定義したら $ npx cdk diff ToStack
を実行し、差分がインポート対象のリソースだけであることを確認します。
$ npx cdk diff ToStack
start: Building 645d176a4ee7d49434b189a1a7850124f8ef0a7ad7dc9544941f7b377cef8f67:current_account-current_region
success: Built 645d176a4ee7d49434b189a1a7850124f8ef0a7ad7dc9544941f7b377cef8f67:current_account-current_region
start: Publishing 645d176a4ee7d49434b189a1a7850124f8ef0a7ad7dc9544941f7b377cef8f67:current_account-current_region
success: Published 645d176a4ee7d49434b189a1a7850124f8ef0a7ad7dc9544941f7b377cef8f67:current_account-current_region
Hold on while we create a read-only change set to get a diff with accurate replacement information (use --no-change-set to use a less accurate but faster template-only diff)
Stack ToStack
Resources
[+] AWS::Route53::HostedZone HostedZone HostedZoneDB99F866
✨ Number of stacks with differences: 1
問題なければ $ npx cdk import ToStack
でインポートを実行します。
このとき、ゾーンID の入力を求められるので入力します。
$ npx cdk import ToStack
ToStack
ToStack/HostedZone/Resource (AWS::Route53::HostedZone): enter Id (empty to skip): Z005575336OGNL4QJGGU6
ToStack: importing resources into stack...
ToStack: creating CloudFormation changeset...
✅ ToStack
Import operation complete. We recommend you run a drift detection operation to confirm your CDK app resource definitions are up-to-date. Read more here: <https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/detect-drift-stack.html>
無事にゾーンがインポートされました。
この段階ではまだレコードはインポートされていませんが、Route 53上のレコードとしては特に削除されることなどもなく、そのまま維持されています。
なお、インポートする前に $ npx cdk diff ...
を実行しておかないと S3 error: Access Denied
というエラーが発生してインポートに失敗するのでご注意ください。
($ npx cdk diff ...
を実行することで、インポートに必要なリソースが S3 にアップロードされます)
ToStack へレコードをインポート
この調子でレコードも ToStack へインポート… したいところですが、残念ながらレコード (AWS::Route53::RecordSet
リソース) は インポートに対応していません。
そのため、ゾーンと同じノリでインポートしようとしても空振りしてしまいます。
lib/to-stack.ts
--- a/lib/to-stack.ts
+++ b/lib/to-stack.ts
@@ -1,5 +1,5 @@
-import { RemovalPolicy, Stack } from "aws-cdk-lib";
-import { PublicHostedZone } from "aws-cdk-lib/aws-route53";
+import { Duration, RemovalPolicy, Stack } from "aws-cdk-lib";
+import { PublicHostedZone, TxtRecord } from "aws-cdk-lib/aws-route53";
import { Construct } from "constructs";
import type { StackProps } from "aws-cdk-lib";
@@ -12,6 +12,14 @@ export class ToStack extends Stack {
zoneName: "gondawara.yumeko",
});
+ const record = new TxtRecord(this, "TxtRecord", {
+ zone: zone,
+ recordName: "say",
+ values: ["YUMEKO!"],
+ ttl: Duration.minutes(5),
+ });
+
zone.applyRemovalPolicy(RemovalPolicy.RETAIN);
+ record.applyRemovalPolicy(RemovalPolicy.RETAIN);
}
}
$ npx cdk import ToStack
ToStack
ToStack/TxtRecord/Resource: unsupported resource type AWS::Route53::RecordSet, skipping import.
No resources selected for import.
ならばどうするかというと、インポートではなく、このまま普通にデプロイするだけで OK です。
$ npx cdk deploy ToStack
✨ Synthesis time: 2.99s
Stack undefined
ToStack: deploying... [1/1]
ToStack: creating CloudFormation changeset...
✅ ToStack
✨ Deployment time: 82.87s
Stack ARN:
arn:aws:cloudformation:ap-northeast-1:123456789012:stack/ToStack/758e0c70-8531-11ef-8d22-0e57741be0eb
✨ Total time: 85.86s
これでレコードが CloudFormation の管理下に入ります。
イベントを見ると、既存のレコードがあるにも関わらず、競合することなくレコードの作成に成功しています。
CloudTrail を見てみると、連続して 2つの変更イベントが発生していました。
最初のイベントは、
{
"eventVersion": "1.10",
"userIdentity": {
...
},
"eventTime": "2024-10-08T08:11:31Z",
"eventSource": "route53.amazonaws.com",
"eventName": "ChangeResourceRecordSets",
"awsRegion": "us-east-1",
"sourceIPAddress": "cloudformation.amazonaws.com",
"userAgent": "cloudformation.amazonaws.com",
"errorCode": "InvalidChangeBatch",
"errorMessage": "[Tried to create resource record set [name='say.gondawara.yumeko.', type='TXT'] but it already exists]",
"requestParameters": {
"hostedZoneId": "Z005575336OGNL4QJGGU6",
"changeBatch": {
"changes": [
{
"action": "CREATE",
"resourceRecordSet": {
"name": "say.gondawara.yumeko.",
"type": "TXT",
"tTL": 300,
"resourceRecords": [
{
"value": "\"YUMEKO!\""
}
]
}
}
]
}
},
"responseElements": null,
"additionalEventData": {
"Note": "Do not use to reconstruct hosted zone"
},
"requestID": "625e8474-2045-42d0-bcfe-c395c2995ab5",
"eventID": "0a7bec9b-c47a-42a5-b528-daa0e4dc98e1",
"readOnly": false,
"eventType": "AwsApiCall",
"apiVersion": "2013-04-01",
"managementEvent": true,
"recipientAccountId": ...
"eventCategory": "Management"
}
TXTレコードの作成を試みていますが、既に同名のレコードが存在するという理由で失敗しています。
“errorMessage”: “[Tried to create resource record set [name=‘say.gondawara.yumeko.’, type=‘TXT’] but it already exists]”,
続く 2つ目のイベントは、
{
"eventVersion": "1.10",
"userIdentity": {
...
},
"eventTime": "2024-10-08T08:11:32Z",
"eventSource": "route53.amazonaws.com",
"eventName": "ChangeResourceRecordSets",
"awsRegion": "us-east-1",
"sourceIPAddress": "cloudformation.amazonaws.com",
"userAgent": "cloudformation.amazonaws.com",
"requestParameters": {
"hostedZoneId": "Z005575336OGNL4QJGGU6",
"changeBatch": {
"comment": "Making no-op change to get a ChangeId",
"changes": [
{
"action": "DELETE",
"resourceRecordSet": {
"name": "say.gondawara.yumeko.",
"type": "TXT",
"tTL": 300,
"resourceRecords": [
{
"value": "\"YUMEKO!\""
}
]
}
},
{
"action": "CREATE",
"resourceRecordSet": {
"name": "say.gondawara.yumeko.",
"type": "TXT",
"tTL": 300,
"resourceRecords": [
{
"value": "\"YUMEKO!\""
}
]
}
}
]
}
},
"responseElements": {
"changeInfo": {
"id": "/change/C00749273VM8093SWUIEB",
"status": "PENDING",
"submittedAt": "Oct 8, 2024 8:11:32 AM",
"comment": "Making no-op change to get a ChangeId"
}
},
"additionalEventData": {
"Note": "Do not use to reconstruct hosted zone"
},
"requestID": "c2325280-5b80-4b65-861b-a8737809f39c",
"eventID": "395589af-18e3-44fd-8bc3-63d0b3d5324f",
"readOnly": false,
"eventType": "AwsApiCall",
"apiVersion": "2013-04-01",
"managementEvent": true,
"recipientAccountId": ...
"eventCategory": "Management"
}
処理の中身としては TXTレコードの削除 + 作成なので、結果としてゾーンの変更はありません。
このイベントの出自はよくわかりませんが、
“comment”: “Making no-op change to get a ChangeId”,
とのことなので、CloudFormation で状態を追跡するために必要な処理なのだと思います。
(このあたりの話は GitHub Issue で言及がありました)
後始末
インポートが完了したら DeletionPolicy は削除して構いません。
lib/to-stack.ts
--- a/lib/to-stack.ts
+++ b/lib/to-stack.ts
@@ -1,4 +1,4 @@
-import { Duration, RemovalPolicy, Stack } from "aws-cdk-lib";
+import { Duration, Stack } from "aws-cdk-lib";
import { PublicHostedZone, TxtRecord } from "aws-cdk-lib/aws-route53";
import { Construct } from "constructs";
@@ -18,8 +18,5 @@ export class ToStack extends Stack {
values: ["YUMEKO!"],
ttl: Duration.minutes(5),
});
-
- zone.applyRemovalPolicy(RemovalPolicy.RETAIN);
- record.applyRemovalPolicy(RemovalPolicy.RETAIN);
}
}
まとめ
以上、Route 53 のホストゾーンについて、CloudFormationスタック間で無停止のリソース移動ができることを確かめました。
- 基本的に公式ドキュメントの手順にしたがうことで、リソースを安全に移動できる。
- レコードについてはインポート非対応。
その代わり、新しいスタックで普通にレコードを作成することで移動できる。 - リソースへの操作は CloudTrail で確認できる。
この記事が、これからホストゾーンを移動しようとしている方のお役に立てば幸いです。
今後ともよろしくお願い申し上げます。
当記事の図表には Material Symbols を使用しています。