@kensio/yulin 1.20.14 → 1.20.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/README.md +13 -0
  2. package/{dist/config/oxlint/cffjs2.oxlintrc.json → cffjs2.oxlintrc.json} +1 -1
  3. package/dist/config/oxlint/cffjs2.oxlint.config.js +4 -3
  4. package/dist/sdk/module/sim-sdk-module-client-interceptor.d.ts +5 -0
  5. package/dist/sdk/module/sim-sdk-module-client-interceptor.js +14 -0
  6. package/dist/sdk/module/sim-sdk-static-client-factory.d.ts +26 -0
  7. package/dist/sdk/module/sim-sdk-static-client-factory.js +59 -0
  8. package/dist/sdk/send-patch.d.ts +8 -0
  9. package/dist/sdk/send-patch.js +11 -0
  10. package/dist/service/aws/caller/sim-aws-caller-resolver.d.ts +28 -6
  11. package/dist/service/aws/caller/sim-aws-caller-resolver.js +18 -11
  12. package/dist/service/aws/caller/sim-aws-caller.d.ts +9 -0
  13. package/dist/service/aws/caller/sim-aws-resolved-caller.d.ts +19 -0
  14. package/dist/service/aws/caller/sim-aws-resolved-caller.js +26 -0
  15. package/dist/service/aws/factory/sim-aws-account-region-service-builder.d.ts +6 -0
  16. package/dist/service/aws/factory/sim-aws-account-region-service-builder.js +3 -0
  17. package/dist/service/aws/factory/sim-aws-account-service-cache.d.ts +6 -0
  18. package/dist/service/aws/factory/sim-aws-account-service-cache.js +5 -0
  19. package/dist/service/aws/factory/sim-aws-service-factory.d.ts +5 -0
  20. package/dist/service/aws/factory/sim-aws-service-factory.js +6 -0
  21. package/dist/service/aws/sim-aws-properties.d.ts +15 -0
  22. package/dist/service/aws/sim-aws.js +2 -1
  23. package/dist/service/cloudformation/resource/resolve/property/sim-cfn-resource-property-resolver.d.ts +11 -4
  24. package/dist/service/cloudformation/resource/resolve/property/sim-cfn-resource-property-resolver.js +21 -8
  25. package/dist/service/cloudformation/resource/sim-cfn-resource.type.d.ts +9 -0
  26. package/dist/service/cloudformation/template/dynamic/make-sim-cfn-dynamic-references.d.ts +2 -0
  27. package/dist/service/cloudformation/template/dynamic/make-sim-cfn-dynamic-references.js +2 -2
  28. package/dist/service/cloudformation/template/dynamic/sim-cfn-dynamic-reference-resolvers.d.ts +10 -0
  29. package/dist/service/cloudformation/template/dynamic/sim-cfn-dynamic-reference-resolvers.js +4 -3
  30. package/dist/service/cloudformation/template/dynamic/sim-cfn-dynamic-reference-scan.d.ts +11 -0
  31. package/dist/service/cloudformation/template/dynamic/sim-cfn-dynamic-reference-scan.js +21 -0
  32. package/dist/service/cloudformation/template/dynamic/sim-cfn-dynamic-references.d.ts +24 -2
  33. package/dist/service/cloudformation/template/dynamic/sim-cfn-dynamic-references.js +50 -11
  34. package/dist/service/cloudformation/template/dynamic/sim-cfn-prefetched-dynamic-references.d.ts +42 -0
  35. package/dist/service/cloudformation/template/dynamic/sim-cfn-prefetched-dynamic-references.js +80 -0
  36. package/dist/service/eventbridge/delivery/sim-event-bridge-delivery-failures.d.ts +16 -0
  37. package/dist/service/eventbridge/delivery/sim-event-bridge-delivery-failures.js +19 -0
  38. package/dist/service/iam/authorize/context/sim-iam-auth-z-caller-context-builder.d.ts +30 -7
  39. package/dist/service/iam/authorize/context/sim-iam-auth-z-caller-context-builder.js +20 -11
  40. package/dist/service/iam/authorize/context/sim-iam-auth-z-context-builder.d.ts +2 -1
  41. package/dist/service/iam/authorize/context/sim-iam-auth-z-context-builder.js +5 -1
  42. package/dist/service/iam/authorize/match/sim-iam-policy-principal-matcher.d.ts +10 -0
  43. package/dist/service/iam/authorize/match/sim-iam-policy-principal-matcher.js +15 -3
  44. package/dist/service/iam/authorize/sim-iam-account-auth-z.d.ts +11 -4
  45. package/dist/service/iam/authorize/sim-iam-account-auth-z.js +5 -3
  46. package/dist/service/iam/authorize/sim-iam-authorizer.d.ts +2 -1
  47. package/dist/service/iam/authorize/sim-iam-authorizer.js +1 -0
  48. package/dist/service/iam/command/policy/create-policy/create-policy-input-resolver.js +4 -1
  49. package/dist/service/iam/command/policy/put-role-policy/put-role-policy.handler.js +5 -1
  50. package/dist/service/iam/command/policy/put-user-policy/put-user-policy.handler.js +5 -1
  51. package/dist/service/iam/policy/parse/sim-iam-document-parser.d.ts +4 -1
  52. package/dist/service/iam/policy/parse/sim-iam-document-parser.js +13 -15
  53. package/dist/service/iam/policy/sim-iam-statement-label.d.ts +28 -0
  54. package/dist/service/iam/policy/sim-iam-statement-label.js +17 -0
  55. package/dist/service/iam/policy/sim-iam-statement-strings.d.ts +11 -0
  56. package/dist/service/iam/policy/sim-iam-statement-strings.js +27 -0
  57. package/dist/service/iam/role/sim-iam-role-with-policy.factory.d.ts +9 -0
  58. package/dist/service/iam/role/sim-iam-role-with-policy.factory.js +4 -2
  59. package/dist/service/iam/sim-iam-account-parts.d.ts +8 -0
  60. package/dist/service/iam/sim-iam-account-parts.js +1 -0
  61. package/dist/service/iam/validate/sim-iam-policy-document-validator.d.ts +9 -2
  62. package/dist/service/iam/validate/sim-iam-policy-document-validator.js +37 -19
  63. package/dist/service/lambda/function/code/sim-lambda-vm-zip-function.factory.d.ts +7 -0
  64. package/dist/service/lambda/function/code/sim-lambda-vm-zip-function.factory.js +1 -0
  65. package/dist/service/lambda/function/code/vm/sdk/sim-lambda-provided-sdk-module.d.ts +19 -0
  66. package/dist/service/lambda/function/code/vm/sdk/sim-lambda-provided-sdk-module.js +78 -0
  67. package/dist/service/lambda/function/code/vm/sdk/sim-lambda-sdk-packages-not-installed.error.d.ts +15 -0
  68. package/dist/service/lambda/function/code/vm/sdk/sim-lambda-sdk-packages-not-installed.error.js +27 -0
  69. package/dist/service/lambda/function/code/vm/sdk/sim-lambda-vm-sdk-module-provider.d.ts +9 -0
  70. package/dist/service/lambda/function/code/vm/sdk/sim-sdk-lambda-vm-module-provider.d.ts +10 -0
  71. package/dist/service/lambda/function/code/vm/sdk/sim-sdk-lambda-vm-module-provider.js +28 -34
  72. package/dist/service/lambda/function/code/vm/sim-lambda-host-modules.d.ts +15 -0
  73. package/dist/service/lambda/function/code/vm/sim-lambda-host-modules.js +31 -0
  74. package/dist/service/lambda/function/code/vm/sim-lambda-vm-module-resolver.d.ts +7 -0
  75. package/dist/service/lambda/function/code/vm/sim-lambda-vm-module-resolver.js +15 -0
  76. package/dist/service/lambda/function/code/vm/sim-lambda-vm-modules.js +5 -4
  77. package/dist/service/s3/command/put-bucket-policy/put-bucket-policy.handler.js +4 -1
  78. package/dist/service/scheduler/delivery/sim-scheduler-delivery-failures.d.ts +16 -0
  79. package/dist/service/scheduler/delivery/sim-scheduler-delivery-failures.js +19 -0
  80. package/dist/service/scheduler/index.d.ts +1 -1
  81. package/dist/service/scheduler/index.js +1 -1
  82. package/dist/service/secretsmanager/cfn/dynamic/sim-cfn-secrets-manager-dynamic-reference-resolver.d.ts +13 -15
  83. package/dist/service/secretsmanager/cfn/dynamic/sim-cfn-secrets-manager-dynamic-reference-resolver.js +22 -43
  84. package/dist/service/secretsmanager/cfn/dynamic/sim-cfn-secrets-manager-reference-stand-in.d.ts +9 -0
  85. package/dist/service/secretsmanager/cfn/dynamic/sim-cfn-secrets-manager-reference-stand-in.js +14 -0
  86. package/dist/service/secretsmanager/cfn/dynamic/sim-cfn-secrets-manager-reference-value.d.ts +12 -0
  87. package/dist/service/secretsmanager/cfn/dynamic/sim-cfn-secrets-manager-reference-value.js +29 -0
  88. package/dist/service/ssm/cfn/dynamic/sim-cfn-ssm-dynamic-reference-resolver.d.ts +22 -6
  89. package/dist/service/ssm/cfn/dynamic/sim-cfn-ssm-dynamic-reference-resolver.js +39 -24
  90. package/dist/service/ssm/cfn/dynamic/sim-cfn-ssm-secure-dynamic-reference-resolver.d.ts +10 -5
  91. package/dist/service/ssm/cfn/dynamic/sim-cfn-ssm-secure-dynamic-reference-resolver.js +10 -8
  92. package/dist/service/ssm/sim-ssm.d.ts +10 -2
  93. package/dist/service/ssm/sim-ssm.js +12 -4
  94. package/dist/service/sts/auth-z/assume-role-auth-z-coordinator.d.ts +9 -2
  95. package/dist/service/sts/auth-z/assume-role-auth-z-coordinator.js +2 -2
  96. package/dist/service/sts/auth-z/assume-role-source-account-auth-z.d.ts +10 -4
  97. package/dist/service/sts/auth-z/assume-role-source-account-auth-z.js +8 -7
  98. package/dist/service/sts/auth-z/assume-role-target-auth-z.d.ts +2 -2
  99. package/dist/service/sts/auth-z/assume-role-trust-policy-authorizer.d.ts +6 -2
  100. package/dist/service/sts/auth-z/assume-role-trust-policy-authorizer.js +14 -11
  101. package/dist/service/sts/command/assume-role/assume-role.handler.d.ts +5 -1
  102. package/dist/service/sts/command/assume-role/assume-role.handler.js +5 -2
  103. package/dist/service/sts/command/get-caller-identity/get-caller-identity.handler.d.ts +5 -1
  104. package/dist/service/sts/command/get-caller-identity/get-caller-identity.handler.js +4 -1
  105. package/dist/service/sts/service-role/sim-service-role.js +5 -1
  106. package/dist/service/sts/sim-sts.d.ts +9 -1
  107. package/dist/service/sts/sim-sts.js +4 -0
  108. package/docs/README.md +54 -0
  109. package/docs/ai-skill/README.md +69 -0
  110. package/docs/cli/README.md +284 -0
  111. package/docs/factories/README.md +140 -0
  112. package/docs/lint/README.md +176 -0
  113. package/docs/non-aws-dependencies/README.md +261 -0
  114. package/docs/sdk/README.md +272 -0
  115. package/docs/serve/README.md +1126 -0
  116. package/docs/services/acm/README.md +823 -0
  117. package/docs/services/apigateway/README.md +1919 -0
  118. package/docs/services/apigatewayv2/README.md +2678 -0
  119. package/docs/services/athena/README.md +1044 -0
  120. package/docs/services/bedrock/README.md +350 -0
  121. package/docs/services/cloudformation/README.md +3440 -0
  122. package/docs/services/cloudfront/README.md +2874 -0
  123. package/docs/services/cloudwatch/README.md +420 -0
  124. package/docs/services/cognito/README.md +4923 -0
  125. package/docs/services/dynamodb/README.md +3784 -0
  126. package/docs/services/ecr/README.md +302 -0
  127. package/docs/services/ecs/README.md +2010 -0
  128. package/docs/services/elbv2/README.md +1897 -0
  129. package/docs/services/eventbridge/README.md +1110 -0
  130. package/docs/services/firehose/README.md +805 -0
  131. package/docs/services/glue/README.md +501 -0
  132. package/docs/services/iam/README.md +1283 -0
  133. package/docs/services/kinesis/README.md +454 -0
  134. package/docs/services/kms/README.md +659 -0
  135. package/docs/services/lambda/README.md +3645 -0
  136. package/docs/services/logs/README.md +868 -0
  137. package/docs/services/organizations/README.md +574 -0
  138. package/docs/services/personalize/README.md +835 -0
  139. package/docs/services/rekognition/README.md +1070 -0
  140. package/docs/services/route53/README.md +1783 -0
  141. package/docs/services/s3/README.md +3024 -0
  142. package/docs/services/scheduler/README.md +806 -0
  143. package/docs/services/secretsmanager/README.md +667 -0
  144. package/docs/services/ses/README.md +1011 -0
  145. package/docs/services/sns/README.md +1904 -0
  146. package/docs/services/sqs/README.md +1185 -0
  147. package/docs/services/ssm/README.md +1072 -0
  148. package/docs/services/stepfunctions/README.md +1332 -0
  149. package/docs/services/sts/README.md +245 -0
  150. package/docs/services/wafv2/README.md +1378 -0
  151. package/docs/terraform/README.md +248 -0
  152. package/docs/time/README.md +284 -0
  153. package/llms.txt +59 -0
  154. package/package.json +9 -5
@@ -0,0 +1,1783 @@
1
+ # Simulated Route53
2
+
3
+ Yulin includes a simulated Route53 service for tests and local development.
4
+
5
+ Sim Route53 can be used directly through `SimAws`, instantiated on its own as `SimRoute53`, and used
6
+ by sim CloudFormation when deploying Route53 resources from CloudFormation or CDK templates. When
7
+ served on localhost, Route53 records can route custom local hostnames to other simulated AWS
8
+ services, such as simulated CloudFront distributions or simulated S3 bucket websites.
9
+
10
+ ## Basic Hosted Zone usage
11
+
12
+ Create a simulated AWS environment, get simulated Route53, and create a Hosted Zone.
13
+
14
+ ```typescript sim-route53-hosted-zone
15
+ /**
16
+ * Creating a simulated Route53 Hosted Zone.
17
+ */
18
+
19
+ import {
20
+ CreateHostedZoneCommand,
21
+ GetHostedZoneCommand,
22
+ } from "@aws-sdk/client-route-53";
23
+
24
+ import { SimAws } from "@kensio/yulin";
25
+
26
+ const simAws = new SimAws();
27
+ const route53 = simAws.route53();
28
+
29
+ const hostedZoneCreation = await route53.createHostedZone(
30
+ new CreateHostedZoneCommand({
31
+ Name: "example.test",
32
+ CallerReference: "example-test-zone",
33
+ HostedZoneConfig: {
34
+ Comment: "Example local test zone",
35
+ PrivateZone: false,
36
+ },
37
+ }),
38
+ );
39
+
40
+ const hostedZoneId = hostedZoneCreation.HostedZone!.Id!;
41
+
42
+ await simAws.backgroundTasksComplete();
43
+
44
+ const hostedZoneOut = await route53.getHostedZone(
45
+ new GetHostedZoneCommand({
46
+ Id: hostedZoneId,
47
+ }),
48
+ );
49
+
50
+ console.log(hostedZoneOut.HostedZone?.Name);
51
+ console.log(hostedZoneOut.HostedZone?.ResourceRecordSetCount);
52
+ ```
53
+
54
+ Hosted Zone names are normalised with a trailing dot in Route53-style outputs, so `example.test`
55
+ becomes `example.test.`.
56
+
57
+ Hosted Zone creation uses background tasks to move the zone to `INSYNC`. If your test needs final
58
+ state, call `await simAws.backgroundTasksComplete()` before continuing.
59
+
60
+ Hosted Zone IDs are accepted in any real Route53 shape, being a `Z` prefix followed by uppercase
61
+ alphanumerics, up to 32 characters. A real Hosted Zone ID copied out of an AWS account, such as
62
+ `Z2FDTNDATAQYW2`, can therefore be used in your test setup. An ID with no matching Hosted Zone gives
63
+ `NoSuchHostedZone`, while a malformed one gives `InvalidInput`. Commands also accept the
64
+ `/hostedzone/Z...` form as well as the bare ID.
65
+
66
+ ## Registering a Hosted Zone with a chosen ID
67
+
68
+ `CreateHostedZoneCommand` allocates its own Hosted Zone ID, as real Route53 does, and takes none
69
+ from you. When something else already decided the ID, register the Hosted Zone as part of your test
70
+ setup instead.
71
+
72
+ The usual reason is a CDK app that looks its zone up with `HostedZone.fromLookup` instead of
73
+ creating it. That bakes the real Hosted Zone ID into the synthesized template, and every
74
+ `AWS::Route53::RecordSet` in the template names that ID. Registering the zone first lets the
75
+ template deploy as it is, with no rewriting.
76
+
77
+ ```typescript sim-route53-register-hosted-zone
78
+ /**
79
+ * Registering a simulated Route53 Hosted Zone with a chosen Hosted Zone ID.
80
+ */
81
+
82
+ import { SimAws } from "@kensio/yulin";
83
+
84
+ const simAws = new SimAws();
85
+ const route53 = simAws.route53();
86
+
87
+ // The Hosted Zone ID a CDK HostedZone.fromLookup baked into the template.
88
+ route53.registerHostedZone({
89
+ id: "Z0123456789ABCDEFGHIJ",
90
+ name: "example.test",
91
+ });
92
+
93
+ const stack = await simAws.cloudFormation().deployTemplate({
94
+ stackName: "site-stack",
95
+ template: {
96
+ Resources: {
97
+ SiteRecord: {
98
+ Type: "AWS::Route53::RecordSet",
99
+ Properties: {
100
+ HostedZoneId: "Z0123456789ABCDEFGHIJ",
101
+ Name: "www.example.test",
102
+ Type: "A",
103
+ TTL: "300",
104
+ ResourceRecords: ["192.0.2.10"],
105
+ },
106
+ },
107
+ },
108
+ },
109
+ });
110
+
111
+ await stack.waitForDeployComplete();
112
+ await simAws.backgroundTasksComplete();
113
+
114
+ console.log(stack.getResource("SiteRecord")?.status);
115
+ ```
116
+
117
+ A registered Hosted Zone behaves like any other. It answers `GetHostedZoneCommand`,
118
+ `ListHostedZonesByNameCommand` and `ChangeResourceRecordSetsCommand`, its records resolve through
119
+ local hostname routing and simulated DNS, and it is `INSYNC` straight away, having been described as
120
+ already existing rather than created.
121
+
122
+ `registerHostedZone` takes the same optional `config` as `CreateHostedZoneCommand`, and accepts the
123
+ `/hostedzone/Z...` form of the ID. An ID that another Hosted Zone already holds is refused with
124
+ `HostedZoneAlreadyExists`, and one that is no Route53 Hosted Zone ID at all with `InvalidInput`.
125
+
126
+ ### A zone a template only names
127
+
128
+ Registering the zone first is optional. An `AWS::Route53::RecordSet` naming a `HostedZoneId` that no
129
+ Hosted Zone holds gets one registered under that ID as the record is created. A template built with
130
+ `HostedZone.fromLookup` therefore deploys without being told separately about a zone it already
131
+ describes.
132
+
133
+ The template carries the zone's ID and no name, so the name is inferred from the records. The first
134
+ record to name the zone names it, and a record above that name widens it. A stack holding
135
+ `example.test` and `www.example.test` ends up with a zone called `example.test`. A stack holding only
136
+ `www.example.test` ends up with a zone called `www.example.test`.
137
+
138
+ Register the zone yourself when a test depends on its name, such as one listing zones by name, or
139
+ when its name is a suffix of no record the stack holds. A registered zone keeps the name it was
140
+ given, and its records are stored under the name you chose.
141
+
142
+ ## Creating records
143
+
144
+ Use `ChangeResourceRecordSetsCommand` to add records to a Hosted Zone.
145
+
146
+ ```typescript sim-route53-create-record
147
+ /**
148
+ * Creating a simulated Route53 record.
149
+ */
150
+
151
+ import {
152
+ ChangeResourceRecordSetsCommand,
153
+ CreateHostedZoneCommand,
154
+ } from "@aws-sdk/client-route-53";
155
+
156
+ import { SimAws } from "@kensio/yulin";
157
+
158
+ const simAws = new SimAws();
159
+ const route53 = simAws.route53();
160
+
161
+ const hostedZoneCreation = await route53.createHostedZone(
162
+ new CreateHostedZoneCommand({
163
+ Name: "example.test",
164
+ CallerReference: "record-zone",
165
+ }),
166
+ );
167
+
168
+ const hostedZoneId = hostedZoneCreation.HostedZone!.Id!;
169
+
170
+ await simAws.backgroundTasksComplete();
171
+
172
+ await route53.changeResourceRecordSets(
173
+ new ChangeResourceRecordSetsCommand({
174
+ HostedZoneId: hostedZoneId,
175
+ ChangeBatch: {
176
+ Comment: "Create web record",
177
+ Changes: [
178
+ {
179
+ Action: "CREATE",
180
+ ResourceRecordSet: {
181
+ Name: "www.example.test",
182
+ Type: "A",
183
+ TTL: 300,
184
+ ResourceRecords: [{ Value: "192.0.2.1" }],
185
+ },
186
+ },
187
+ ],
188
+ },
189
+ }),
190
+ );
191
+
192
+ await simAws.backgroundTasksComplete();
193
+ ```
194
+
195
+ Record changes are applied through background tasks. The command returns a `ChangeInfo` with a
196
+ pending or synced status, and the record is available after the scheduled work has completed.
197
+
198
+ ## Upserting and deleting records
199
+
200
+ `UPSERT` replaces an existing record with the same name and type. `DELETE` removes the matching
201
+ record. Deleting a missing record is a no-op.
202
+
203
+ ```typescript sim-route53-upsert-delete-record
204
+ /**
205
+ * Upserting and deleting simulated Route53 records.
206
+ */
207
+
208
+ import {
209
+ ChangeResourceRecordSetsCommand,
210
+ CreateHostedZoneCommand,
211
+ } from "@aws-sdk/client-route-53";
212
+
213
+ import { SimAws } from "@kensio/yulin";
214
+
215
+ const simAws = new SimAws();
216
+ const route53 = simAws.route53();
217
+
218
+ const hostedZoneCreation = await route53.createHostedZone(
219
+ new CreateHostedZoneCommand({
220
+ Name: "example.test",
221
+ CallerReference: "upsert-delete-zone",
222
+ }),
223
+ );
224
+
225
+ const hostedZoneId = hostedZoneCreation.HostedZone!.Id!;
226
+
227
+ await simAws.backgroundTasksComplete();
228
+
229
+ await route53.changeResourceRecordSets(
230
+ new ChangeResourceRecordSetsCommand({
231
+ HostedZoneId: hostedZoneId,
232
+ ChangeBatch: {
233
+ Changes: [
234
+ {
235
+ Action: "UPSERT",
236
+ ResourceRecordSet: {
237
+ Name: "www.example.test",
238
+ Type: "A",
239
+ TTL: 60,
240
+ ResourceRecords: [{ Value: "192.0.2.2" }],
241
+ },
242
+ },
243
+ ],
244
+ },
245
+ }),
246
+ );
247
+
248
+ await simAws.backgroundTasksComplete();
249
+
250
+ await route53.changeResourceRecordSets(
251
+ new ChangeResourceRecordSetsCommand({
252
+ HostedZoneId: hostedZoneId,
253
+ ChangeBatch: {
254
+ Changes: [
255
+ {
256
+ Action: "DELETE",
257
+ ResourceRecordSet: {
258
+ Name: "www.example.test",
259
+ Type: "A",
260
+ ResourceRecords: [{ Value: "192.0.2.2" }],
261
+ },
262
+ },
263
+ ],
264
+ },
265
+ }),
266
+ );
267
+
268
+ await simAws.backgroundTasksComplete();
269
+ ```
270
+
271
+ ## Alias records
272
+
273
+ Alias records store the alias target DNS name as the simulated record value. This is useful when a
274
+ Route53 record should point to another simulated service hostname, such as a CloudFront distribution
275
+ or a load balancer. See [What a name can resolve to](#what-a-name-can-resolve-to) for the hostnames a
276
+ record can point at.
277
+
278
+ ```typescript sim-route53-alias-record
279
+ /**
280
+ * Creating a simulated Route53 alias record.
281
+ */
282
+
283
+ import {
284
+ ChangeResourceRecordSetsCommand,
285
+ CreateHostedZoneCommand,
286
+ } from "@aws-sdk/client-route-53";
287
+
288
+ import { SimAws } from "@kensio/yulin";
289
+
290
+ const simAws = new SimAws();
291
+ const route53 = simAws.route53();
292
+
293
+ const hostedZoneCreation = await route53.createHostedZone(
294
+ new CreateHostedZoneCommand({
295
+ Name: "example.test",
296
+ CallerReference: "alias-zone",
297
+ }),
298
+ );
299
+
300
+ const hostedZoneId = hostedZoneCreation.HostedZone!.Id!;
301
+
302
+ await simAws.backgroundTasksComplete();
303
+
304
+ await route53.changeResourceRecordSets(
305
+ new ChangeResourceRecordSetsCommand({
306
+ HostedZoneId: hostedZoneId,
307
+ ChangeBatch: {
308
+ Changes: [
309
+ {
310
+ Action: "CREATE",
311
+ ResourceRecordSet: {
312
+ Name: "app.example.test",
313
+ Type: "A",
314
+ AliasTarget: {
315
+ HostedZoneId: "Z2FDTNDATAQYW2",
316
+ DNSName: "d111111abcdef8.cloudfront.net.",
317
+ EvaluateTargetHealth: false,
318
+ },
319
+ },
320
+ },
321
+ ],
322
+ },
323
+ }),
324
+ );
325
+
326
+ await simAws.backgroundTasksComplete();
327
+ ```
328
+
329
+ The stored alias value is normalized without the trailing dot.
330
+
331
+ ## Record types
332
+
333
+ Sim Route53 stores ten record types: `A`, `AAAA`, `CAA`, `CNAME`, `MX`, `NS`, `PTR`, `SOA`, `SRV` and
334
+ `TXT`. All ten can be created through `ChangeResourceRecordSetsCommand`, read back through
335
+ `ListResourceRecordSetsCommand`, and declared as an `AWS::Route53::RecordSet`. A zone that models a
336
+ real one (mail records, certificate pinning, a service record) deploys as it stands.
337
+
338
+ A type outside that list is refused by `ChangeResourceRecordSetsCommand`, because the call asked for
339
+ a record the simulator cannot keep. A template declaring one is treated more gently. The
340
+ `AWS::Route53::RecordSet` is skipped and the rest of the stack deploys. See
341
+ [unsupported record types in a template](#unsupported-record-types-in-a-template).
342
+
343
+ Simulated DNS answers queries for six of them, being `A`, `AAAA`, `CNAME`, `TXT`, `NS` and `SOA`.
344
+ `MX`, `SRV`, `CAA` and `PTR` are stored for a test to assert the presence and value of, and a DNS
345
+ query for one is answered as no data. See [What is answered](#what-is-answered).
346
+
347
+ Values of those four are stored exactly as written, along with `TXT`. An `MX` preference number and
348
+ an `SRV` priority are kept whole, so what you assert on is the string your stack declared. Values of
349
+ the other types are hostnames or addresses, and are normalized like DNS names so they compare
350
+ consistently.
351
+
352
+ ```typescript sim-route53-mail-records
353
+ /**
354
+ * Creating simulated Route53 records a resolver never answers for.
355
+ */
356
+
357
+ import {
358
+ ChangeResourceRecordSetsCommand,
359
+ CreateHostedZoneCommand,
360
+ ListResourceRecordSetsCommand,
361
+ } from "@aws-sdk/client-route-53";
362
+
363
+ import { SimAws } from "@kensio/yulin";
364
+
365
+ const simAws = new SimAws();
366
+ const route53 = simAws.route53();
367
+
368
+ const hostedZoneCreation = await route53.createHostedZone(
369
+ new CreateHostedZoneCommand({
370
+ Name: "example.test",
371
+ CallerReference: "mail-zone",
372
+ }),
373
+ );
374
+
375
+ const hostedZoneId = hostedZoneCreation.HostedZone!.Id!;
376
+
377
+ await simAws.backgroundTasksComplete();
378
+
379
+ await route53.changeResourceRecordSets(
380
+ new ChangeResourceRecordSetsCommand({
381
+ HostedZoneId: hostedZoneId,
382
+ ChangeBatch: {
383
+ Changes: [
384
+ {
385
+ Action: "CREATE",
386
+ ResourceRecordSet: {
387
+ Name: "example.test",
388
+ Type: "MX",
389
+ TTL: 3600,
390
+ ResourceRecords: [
391
+ { Value: "10 mx1.example.test." },
392
+ { Value: "20 mx2.example.test." },
393
+ ],
394
+ },
395
+ },
396
+ {
397
+ Action: "CREATE",
398
+ ResourceRecordSet: {
399
+ Name: "example.test",
400
+ Type: "CAA",
401
+ TTL: 300,
402
+ ResourceRecords: [{ Value: '0 issue "letsencrypt.org"' }],
403
+ },
404
+ },
405
+ ],
406
+ },
407
+ }),
408
+ );
409
+
410
+ await simAws.backgroundTasksComplete();
411
+
412
+ const listOutput = await route53.listResourceRecordSets(
413
+ new ListResourceRecordSetsCommand({
414
+ HostedZoneId: hostedZoneId,
415
+ }),
416
+ );
417
+
418
+ const mailRecord = listOutput.ResourceRecordSets?.find(
419
+ (recordSet) => recordSet.Type === "MX",
420
+ );
421
+
422
+ // [ '10 mx1.example.test.', '20 mx2.example.test.' ]
423
+ console.log(mailRecord?.ResourceRecords?.map((record) => record.Value));
424
+ ```
425
+
426
+ ## Listing Hosted Zones by name
427
+
428
+ Use `ListHostedZonesByNameCommand` to inspect zones in sorted Route53 order.
429
+
430
+ ```typescript sim-route53-list-hosted-zones-by-name
431
+ /**
432
+ * Listing simulated Route53 Hosted Zones by name.
433
+ */
434
+
435
+ import {
436
+ CreateHostedZoneCommand,
437
+ ListHostedZonesByNameCommand,
438
+ } from "@aws-sdk/client-route-53";
439
+
440
+ import { SimAws } from "@kensio/yulin";
441
+
442
+ const simAws = new SimAws();
443
+ const route53 = simAws.route53();
444
+
445
+ await route53.createHostedZone(
446
+ new CreateHostedZoneCommand({
447
+ Name: "z.example.test",
448
+ CallerReference: "z-zone",
449
+ }),
450
+ );
451
+
452
+ await route53.createHostedZone(
453
+ new CreateHostedZoneCommand({
454
+ Name: "a.example.test",
455
+ CallerReference: "a-zone",
456
+ }),
457
+ );
458
+
459
+ await simAws.backgroundTasksComplete();
460
+
461
+ const listOutput = await route53.listHostedZonesByName(
462
+ new ListHostedZonesByNameCommand({
463
+ DNSName: "example.test",
464
+ }),
465
+ );
466
+
467
+ const hostedZones = listOutput.HostedZones ?? [];
468
+ for (const hostedZone of hostedZones) {
469
+ console.log(hostedZone.Name, hostedZone.Id);
470
+ }
471
+ ```
472
+
473
+ The simulator supports duplicate Hosted Zone names when they have different caller references or
474
+ CloudFormation logical IDs.
475
+
476
+ ## Listing records in a Hosted Zone
477
+
478
+ Use `ListResourceRecordSetsCommand` to read back the records a Hosted Zone holds, whether they were
479
+ created through the SDK or by sim CloudFormation.
480
+
481
+ ```typescript sim-route53-list-resource-record-sets
482
+ /**
483
+ * Listing the records in a simulated Route53 Hosted Zone.
484
+ */
485
+
486
+ import {
487
+ ChangeResourceRecordSetsCommand,
488
+ CreateHostedZoneCommand,
489
+ ListResourceRecordSetsCommand,
490
+ } from "@aws-sdk/client-route-53";
491
+
492
+ import { SimAws } from "@kensio/yulin";
493
+
494
+ const simAws = new SimAws();
495
+ const route53 = simAws.route53();
496
+
497
+ const hostedZoneCreation = await route53.createHostedZone(
498
+ new CreateHostedZoneCommand({
499
+ Name: "example.test",
500
+ CallerReference: "records-listing-zone",
501
+ }),
502
+ );
503
+
504
+ const hostedZoneId = hostedZoneCreation.HostedZone?.Id;
505
+
506
+ await route53.changeResourceRecordSets(
507
+ new ChangeResourceRecordSetsCommand({
508
+ HostedZoneId: hostedZoneId,
509
+ ChangeBatch: {
510
+ Changes: [
511
+ {
512
+ Action: "CREATE",
513
+ ResourceRecordSet: {
514
+ Name: "www.example.test",
515
+ Type: "CNAME",
516
+ TTL: 300,
517
+ ResourceRecords: [{ Value: "my-site.s3-website.eu-west-2" }],
518
+ },
519
+ },
520
+ ],
521
+ },
522
+ }),
523
+ );
524
+
525
+ await simAws.backgroundTasksComplete();
526
+
527
+ const listOutput = await route53.listResourceRecordSets(
528
+ new ListResourceRecordSetsCommand({
529
+ HostedZoneId: hostedZoneId,
530
+ }),
531
+ );
532
+
533
+ const recordSets = listOutput.ResourceRecordSets ?? [];
534
+ for (const recordSet of recordSets) {
535
+ console.log(recordSet.Name, recordSet.Type, recordSet.ResourceRecords);
536
+ }
537
+ ```
538
+
539
+ Records are returned in Route53 DNS name order, which compares names from the rightmost label
540
+ inwards. The zone apex comes first, then names are grouped by shared parent, so `example.test.`
541
+ sorts before `api.example.test.`, which sorts before `b.api.example.test.` and then
542
+ `www.example.test.`. Where one name holds several record types, the record type breaks the tie.
543
+
544
+ Record names are returned with a trailing dot, as Route53 returns them.
545
+
546
+ Paginate with `MaxItems`. When the listing is truncated, `IsTruncated` is `true` and
547
+ `NextRecordName` and `NextRecordType` identify the first record of the next page, which you pass
548
+ back as `StartRecordName` and `StartRecordType`. Marker names are normalised, so `www.example.test`
549
+ and `WWW.EXAMPLE.TEST.` select the same starting point.
550
+
551
+ Alias records are returned with an `AliasTarget` in place of `ResourceRecords`. The simulator stores
552
+ an alias target as a record value plus an alias flag, and `AliasTarget.DNSName` is returned.
553
+ `AliasTarget.HostedZoneId` is outside the stored record, and is left out.
554
+
555
+ ## Inspecting hosted zones in a browser
556
+
557
+ A served simulated AWS environment reports its Route53 state at `dns.sim-aws.localhost`. You can see
558
+ which hosted zones and records exist without writing code to read them back.
559
+
560
+ ```typescript sim-route53-zone-summary
561
+ /**
562
+ * Inspecting simulated Route53 hosted zones in a browser.
563
+ */
564
+
565
+ import { CreateHostedZoneCommand } from "@aws-sdk/client-route-53";
566
+
567
+ import { SimAws } from "@kensio/yulin";
568
+ import { serveSimAws } from "@kensio/yulin/serve";
569
+
570
+ const simAws = new SimAws();
571
+
572
+ await simAws.route53().createHostedZone(
573
+ new CreateHostedZoneCommand({
574
+ Name: "example.test",
575
+ CallerReference: "browsable-zone",
576
+ }),
577
+ );
578
+
579
+ await simAws.backgroundTasksComplete();
580
+
581
+ const srv = await serveSimAws({ simAws });
582
+
583
+ // Open this in a browser to see every hosted zone and record.
584
+ console.log(`http://dns.sim-aws.localhost:${srv.port}/`);
585
+ ```
586
+
587
+ The page lists each hosted zone with its ID, synchronization status and record count, then a table
588
+ of that zone's records in Route53 DNS name order. Alias records show their target marked `(alias)`
589
+ with an em dash in place of a TTL, because Route53 answers an alias using the TTL of whatever it
590
+ points at.
591
+
592
+ Hosted zones from every simulated Account appear, not just the default one, because Route53 name
593
+ resolution is environment-wide even though the Route53 service object is Account-scoped.
594
+
595
+ `dns.sim-aws.localhost` is where the summary is served, not a name it answers for. It is a built-in
596
+ Yulin hostname. It stays reachable whatever records your test creates, and it stands apart from any
597
+ hosted zone you might name `dns`.
598
+
599
+ ## Querying simulated records with dig
600
+
601
+ A served simulated environment answers real DNS queries over UDP, on the same port number the HTTP
602
+ server took on TCP. UDP and TCP port namespaces are separate, and one number covers both.
603
+
604
+ ```typescript sim-route53-dns
605
+ /**
606
+ * Querying simulated Route53 records with a DNS client.
607
+ */
608
+
609
+ import {
610
+ ChangeResourceRecordSetsCommand,
611
+ CreateHostedZoneCommand,
612
+ } from "@aws-sdk/client-route-53";
613
+
614
+ import { SimAws } from "@kensio/yulin";
615
+ import { serveSimAws } from "@kensio/yulin/serve";
616
+
617
+ const simAws = new SimAws();
618
+ const route53 = simAws.route53();
619
+
620
+ const hostedZoneCreation = await route53.createHostedZone(
621
+ new CreateHostedZoneCommand({
622
+ Name: "example.test",
623
+ CallerReference: "dns-zone",
624
+ }),
625
+ );
626
+
627
+ await route53.changeResourceRecordSets(
628
+ new ChangeResourceRecordSetsCommand({
629
+ HostedZoneId: hostedZoneCreation.HostedZone?.Id,
630
+ ChangeBatch: {
631
+ Changes: [
632
+ {
633
+ Action: "CREATE",
634
+ ResourceRecordSet: {
635
+ Name: "www.example.test",
636
+ Type: "CNAME",
637
+ TTL: 300,
638
+ ResourceRecords: [{ Value: "my-site.s3-website.eu-west-2" }],
639
+ },
640
+ },
641
+ ],
642
+ },
643
+ }),
644
+ );
645
+
646
+ await simAws.backgroundTasksComplete();
647
+
648
+ const srv = await serveSimAws({ simAws });
649
+
650
+ console.log(`dig @127.0.0.1 -p ${srv.dnsPort} www.example.test`);
651
+ ```
652
+
653
+ Running that `dig` command against the served simulator:
654
+
655
+ ```text
656
+ ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 17691
657
+ ;; flags: qr aa rd; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 0
658
+
659
+ www.example.test. 300 IN CNAME my-site.s3-website.eu-west-2.
660
+ my-site.s3-website.eu-west-2. 60 IN A 127.0.0.1
661
+ ```
662
+
663
+ The CNAME is followed and an address record is synthesised for the simulated S3 website, pointing at
664
+ the address the local HTTP server listens on. A name that resolves to a simulated service therefore
665
+ answers with somewhere you can actually make a request.
666
+
667
+ Any DNS client works. Node's own resolver needs no extra dependency, which suits a test:
668
+
669
+ ```typescript sim-route53-dns-resolver
670
+ /**
671
+ * Resolving a simulated Route53 record with Node's DNS resolver.
672
+ */
673
+
674
+ import { Resolver } from "node:dns/promises";
675
+
676
+ import {
677
+ ChangeResourceRecordSetsCommand,
678
+ CreateHostedZoneCommand,
679
+ } from "@aws-sdk/client-route-53";
680
+
681
+ import { SimAws } from "@kensio/yulin";
682
+ import { serveSimAws } from "@kensio/yulin/serve";
683
+
684
+ const simAws = new SimAws();
685
+ const route53 = simAws.route53();
686
+
687
+ const zone = await route53.createHostedZone(
688
+ new CreateHostedZoneCommand({
689
+ Name: "example.test",
690
+ CallerReference: "resolver-zone",
691
+ }),
692
+ );
693
+
694
+ await route53.changeResourceRecordSets(
695
+ new ChangeResourceRecordSetsCommand({
696
+ HostedZoneId: zone.HostedZone?.Id,
697
+ ChangeBatch: {
698
+ Changes: [
699
+ {
700
+ Action: "CREATE",
701
+ ResourceRecordSet: {
702
+ Name: "api.example.test",
703
+ Type: "A",
704
+ TTL: 60,
705
+ ResourceRecords: [{ Value: "192.0.2.10" }],
706
+ },
707
+ },
708
+ ],
709
+ },
710
+ }),
711
+ );
712
+
713
+ await simAws.backgroundTasksComplete();
714
+
715
+ const srv = await serveSimAws({ simAws });
716
+
717
+ try {
718
+ const resolver = new Resolver({ timeout: 1000, tries: 1 });
719
+ resolver.setServers([`127.0.0.1:${srv.dnsPort}`]);
720
+
721
+ const addresses = await resolver.resolve4("api.example.test");
722
+
723
+ console.log(addresses); // [ '192.0.2.10' ]
724
+ } finally {
725
+ await srv.close();
726
+ }
727
+ ```
728
+
729
+ A short timeout with a single try keeps a test failing quickly, where a
730
+ missing record would otherwise leave it hanging.
731
+
732
+ ### Ports
733
+
734
+ DNS binds the same port number as HTTP, as a convenience and not a guarantee. Where the number is
735
+ already held on UDP by something else, DNS binds an ephemeral port instead. Read `srv.dnsPort`, which
736
+ may differ from `srv.port`.
737
+
738
+ Port 53 is left alone, since binding it would need root. To resolve simulated names system-wide
739
+ without naming a port, point your resolver at the simulator yourself. On macOS, a file such as `/etc/resolver/test`
740
+ containing `nameserver 127.0.0.1` and `port <dnsPort>` makes the whole `.test` TLD resolve through
741
+ it. That is a change to your machine, so Yulin does not make it for you. With that in place, the HTTP
742
+ server answers for those names too. See [Local hostname resolution](#local-hostname-resolution).
743
+
744
+ ### What is answered
745
+
746
+ - Six of the ten record types sim Route53 stores, being `A`, `AAAA`, `CNAME`, `TXT`, `NS` and
747
+ `SOA`. A query for a stored type outside that list (`MX`, `SRV`, `CAA` or `PTR`) is answered as no
748
+ data, the same as a query type the simulator has never heard of. Those records exist to be
749
+ asserted on. What a browser reaching a simulated site needs is an address or a CNAME. See
750
+ [Record types](#record-types).
751
+ - CNAME chains are followed, so an `A` query on a name holding a CNAME returns the CNAME and the
752
+ address it leads to together. Chains are bounded, and a cycle stops immediately.
753
+ - Alias records are resolved to the address of whatever they point at, answered under the name that
754
+ holds the alias, as Route53 answers an alias. The alias record itself never appears.
755
+ - A name in a zone holding no record of the queried type gives `NOERROR` with no answers, and a name
756
+ the zone lacks gives `NXDOMAIN`. Both carry the zone `SOA` in the authority section, so a resolver
757
+ knows how long it may cache the negative answer. Where the zone holds no `SOA` of its own, one is
758
+ synthesised.
759
+ - A name held by no hosted zone is `REFUSED`, and never `NXDOMAIN`. The simulator answers only for
760
+ the zones it holds, and cannot claim a name exists nowhere.
761
+ - Zones from every simulated Account are answered, because DNS resolution is environment-wide.
762
+
763
+ Left out are EDNS0, the `ANY` query type, DNS over TCP, more than one question per query, recursion,
764
+ and DNSSEC. Answers are always authoritative.
765
+
766
+ ## Local hostname resolution
767
+
768
+ When Yulin is served on localhost, Route53 can map your own test hostnames to simulated service
769
+ targets. Request the local server using the hostname plus the `sim-aws.localhost` suffix. A test in
770
+ the same process needs no server at all, and is covered under
771
+ [Fetching a hostname in the same process](#fetching-a-hostname-in-the-same-process).
772
+
773
+ For example, if Route53 contains a record for `www.example.test`, request:
774
+
775
+ ```text
776
+ http://www.example.test.sim-aws.localhost:<port>/
777
+ ```
778
+
779
+ The local server resolves the logical hostname `www.example.test` through sim Route53 and routes the
780
+ request to the simulated target named by the record.
781
+
782
+ The suffix is optional. It exists so a client can reach the local server without the hostname
783
+ resolving on the public internet, and it is only needed while the name resolves to the simulator
784
+ nowhere else. Once a resolver is pointed at the served DNS server, as under [Ports](#ports) above,
785
+ the name resolves on its own and the request can be made under the hostname your application really
786
+ uses:
787
+
788
+ ```text
789
+ http://www.example.test:<port>/
790
+ ```
791
+
792
+ Both forms reach the same simulated target. The suffix-free form is what makes an exact apex `Host`
793
+ possible. A CloudFront Function redirecting `example.test` to `www.example.test` can then be
794
+ exercised in a browser.
795
+
796
+ This is most useful with CloudFront aliases. You can create a CloudFront distribution, create a
797
+ Route53 record pointing at the distribution hostname, then fetch through your application hostname.
798
+
799
+ The hostname you fetch has to be one of the Distribution's alternate domain names, the same as it
800
+ would be in real CloudFront, which refuses a `Host` outside the ones it serves. So the record name
801
+ goes in the Distribution's `Aliases` as well as in the Route53 record. Real CloudFront also wants an
802
+ ACM certificate covering those names, described under
803
+ [Viewer certificates](https://yulinsim.dev/services/cloudfront/#viewer-certificates). It is left out here because this
804
+ request is served over plain HTTP on localhost.
805
+
806
+ ```typescript sim-route53-cloudfront-localhost
807
+ /**
808
+ * Serving a CloudFront distribution through a simulated Route53 hostname.
809
+ */
810
+
811
+ import { CreateDistributionCommand } from "@aws-sdk/client-cloudfront";
812
+ import {
813
+ ChangeResourceRecordSetsCommand,
814
+ CreateHostedZoneCommand,
815
+ } from "@aws-sdk/client-route-53";
816
+ import {
817
+ CreateBucketCommand,
818
+ PutBucketPolicyCommand,
819
+ PutObjectCommand,
820
+ PutPublicAccessBlockCommand,
821
+ } from "@aws-sdk/client-s3";
822
+
823
+ import { SimAws } from "@kensio/yulin";
824
+ import { serveSimAws } from "@kensio/yulin/serve";
825
+
826
+ const simAws = new SimAws();
827
+ const srv = await serveSimAws({ simAws });
828
+
829
+ try {
830
+ const s3 = simAws.s3();
831
+ const cloudFront = simAws.cloudFront();
832
+ const route53 = simAws.route53();
833
+
834
+ await s3.createBucket(
835
+ new CreateBucketCommand({
836
+ Bucket: "site-bucket",
837
+ }),
838
+ );
839
+
840
+ await s3.putObject(
841
+ new PutObjectCommand({
842
+ Bucket: "site-bucket",
843
+ Key: "index.html",
844
+ Body: "<h1>Hello from a Route53 hostname</h1>",
845
+ ContentType: "text/html; charset=utf-8",
846
+ }),
847
+ );
848
+
849
+ // The Distribution's S3 Origin reads the Bucket anonymously, so the site has
850
+ // to be publicly readable.
851
+ await s3.putPublicAccessBlock(
852
+ new PutPublicAccessBlockCommand({
853
+ Bucket: "site-bucket",
854
+ PublicAccessBlockConfiguration: {
855
+ BlockPublicAcls: true,
856
+ IgnorePublicAcls: true,
857
+ },
858
+ }),
859
+ );
860
+ await s3.putBucketPolicy(
861
+ new PutBucketPolicyCommand({
862
+ Bucket: "site-bucket",
863
+ Policy: JSON.stringify({
864
+ Version: "2012-10-17",
865
+ Statement: {
866
+ Effect: "Allow",
867
+ Principal: "*",
868
+ Action: "s3:GetObject",
869
+ Resource: "arn:aws:s3:::site-bucket/*",
870
+ },
871
+ }),
872
+ }),
873
+ );
874
+
875
+ const distributionCreation = await cloudFront.createDistribution(
876
+ new CreateDistributionCommand({
877
+ DistributionConfig: {
878
+ CallerReference: "route53-site-distribution",
879
+ Comment: "Route53 local site distribution",
880
+ Enabled: true,
881
+ Aliases: { Quantity: 1, Items: ["www.example.test"] },
882
+ DefaultRootObject: "index.html",
883
+ Origins: {
884
+ Quantity: 1,
885
+ Items: [
886
+ {
887
+ Id: "site-origin",
888
+ DomainName: "site-bucket.s3.amazonaws.com",
889
+ S3OriginConfig: {
890
+ OriginAccessIdentity: "",
891
+ },
892
+ },
893
+ ],
894
+ },
895
+ DefaultCacheBehavior: {
896
+ TargetOriginId: "site-origin",
897
+ ViewerProtocolPolicy: "allow-all",
898
+ },
899
+ },
900
+ }),
901
+ );
902
+
903
+ const distributionHostname = distributionCreation.Distribution!.DomainName!;
904
+
905
+ const hostedZoneCreation = await route53.createHostedZone(
906
+ new CreateHostedZoneCommand({
907
+ Name: "example.test",
908
+ CallerReference: "route53-localhost-zone",
909
+ }),
910
+ );
911
+
912
+ const hostedZoneId = hostedZoneCreation.HostedZone!.Id!;
913
+
914
+ await route53.changeResourceRecordSets(
915
+ new ChangeResourceRecordSetsCommand({
916
+ HostedZoneId: hostedZoneId,
917
+ ChangeBatch: {
918
+ Changes: [
919
+ {
920
+ Action: "CREATE",
921
+ ResourceRecordSet: {
922
+ Name: "www.example.test",
923
+ Type: "CNAME",
924
+ TTL: 300,
925
+ ResourceRecords: [{ Value: distributionHostname }],
926
+ },
927
+ },
928
+ ],
929
+ },
930
+ }),
931
+ );
932
+
933
+ await simAws.backgroundTasksComplete();
934
+
935
+ const response = await fetch(
936
+ `http://www.example.test.sim-aws.localhost:${srv.port}/`,
937
+ );
938
+
939
+ console.log(response.status);
940
+ console.log(await response.text());
941
+ } finally {
942
+ await srv.close();
943
+ }
944
+ ```
945
+
946
+ You can also call `srv.localUrl(...)` with a URL that contains the simulated hostname when you want
947
+ the server to adapt it to the selected local port.
948
+
949
+ ### Fetching a hostname in the same process
950
+
951
+ `SimAwsHttp` sends a request into the simulation with nothing listening. It resolves the hostname
952
+ through sim Route53 the same way the local server does. The name is requested as your application
953
+ writes it, with no suffix and no port.
954
+
955
+ Reach for `SimAwsHttp` in a test, and for `serveSimAws` when the request comes from outside the
956
+ process, such as a browser, `curl` or an SDK client pointed at a local endpoint. Both go through the
957
+ same routing and service code, and a request answered one way is answered the same way the other.
958
+ See [requests without a port](https://yulinsim.dev/serve/#requests-without-a-port "Requests without a port docs").
959
+
960
+ Below, `www.example.test` is redirected to the apex. One request checks four pieces of the stack at
961
+ once. The ACM certificate has to cover the alternate domain name, the Hosted Zone record has to
962
+ point at the Distribution, the Distribution has to accept that `Host`, and the viewer-request
963
+ Function has to write the `Location`. An assertion against the synthesized template passes with the
964
+ Route53 record missing.
965
+
966
+ ```typescript sim-route53-in-process-redirect
967
+ /**
968
+ * Redirecting a simulated Route53 hostname with no server listening.
969
+ */
970
+
971
+ import { RequestCertificateCommand } from "@aws-sdk/client-acm";
972
+ import {
973
+ CreateDistributionCommand,
974
+ CreateFunctionCommand,
975
+ } from "@aws-sdk/client-cloudfront";
976
+ import {
977
+ ChangeResourceRecordSetsCommand,
978
+ CreateHostedZoneCommand,
979
+ } from "@aws-sdk/client-route-53";
980
+
981
+ import { SimAws } from "@kensio/yulin";
982
+ import {
983
+ makeCffFunctionCodeInput,
984
+ type CloudFrontFunction,
985
+ } from "@kensio/yulin/cloudfront";
986
+ import { SimAwsHttp } from "@kensio/yulin/serve";
987
+
988
+ const simAws = new SimAws();
989
+ const route53 = simAws.route53();
990
+ const cloudFront = simAws.cloudFront();
991
+
992
+ const hostedZoneCreation = await route53.createHostedZone(
993
+ new CreateHostedZoneCommand({
994
+ Name: "example.test",
995
+ CallerReference: "redirect-zone",
996
+ }),
997
+ );
998
+
999
+ // CloudFront reads its certificate from us-east-1, wherever the rest of the
1000
+ // stack lives.
1001
+ const certificateRequest = await simAws
1002
+ .region("us-east-1")
1003
+ .acm()
1004
+ .requestCertificate(
1005
+ new RequestCertificateCommand({ DomainName: "www.example.test" }),
1006
+ );
1007
+
1008
+ await simAws
1009
+ .region("us-east-1")
1010
+ .acm()
1011
+ .completeDnsValidation(certificateRequest.CertificateArn);
1012
+
1013
+ function redirectToApex(
1014
+ event: CloudFrontFunction.ViewerRequestEvent,
1015
+ ): CloudFrontFunction.Response {
1016
+ const query = Object.entries(event.request.querystring)
1017
+ .map(([name, parameter]) => `${name}=${parameter.value}`)
1018
+ .join("&");
1019
+
1020
+ return {
1021
+ statusCode: 301,
1022
+ statusDescription: "Moved Permanently",
1023
+ headers: {
1024
+ location: {
1025
+ value: `https://example.test${event.request.uri}${query.length > 0 ? `?${query}` : ""}`,
1026
+ },
1027
+ },
1028
+ };
1029
+ }
1030
+
1031
+ const functionCreation = await cloudFront.createFunction(
1032
+ new CreateFunctionCommand({
1033
+ Name: "redirect-to-apex",
1034
+ FunctionConfig: {
1035
+ Comment: "Redirect www to the apex",
1036
+ Runtime: "cloudfront-js-2.0",
1037
+ },
1038
+ FunctionCode: makeCffFunctionCodeInput(redirectToApex),
1039
+ }),
1040
+ );
1041
+
1042
+ const distributionCreation = await cloudFront.createDistribution(
1043
+ new CreateDistributionCommand({
1044
+ DistributionConfig: {
1045
+ CallerReference: "redirect-distribution",
1046
+ Comment: "Apex redirect",
1047
+ Enabled: true,
1048
+ Aliases: { Quantity: 1, Items: ["www.example.test"] },
1049
+ // The Function answers every request, and the Origin goes unread.
1050
+ Origins: {
1051
+ Quantity: 1,
1052
+ Items: [
1053
+ {
1054
+ Id: "apex-origin",
1055
+ DomainName: "origin.example.test",
1056
+ CustomOriginConfig: {
1057
+ HTTPPort: 80,
1058
+ HTTPSPort: 443,
1059
+ OriginProtocolPolicy: "http-only",
1060
+ },
1061
+ },
1062
+ ],
1063
+ },
1064
+ DefaultCacheBehavior: {
1065
+ TargetOriginId: "apex-origin",
1066
+ ViewerProtocolPolicy: "redirect-to-https",
1067
+ FunctionAssociations: {
1068
+ Quantity: 1,
1069
+ Items: [
1070
+ {
1071
+ EventType: "viewer-request",
1072
+ FunctionARN: functionCreation.FunctionMetadata.FunctionARN,
1073
+ },
1074
+ ],
1075
+ },
1076
+ },
1077
+ ViewerCertificate: {
1078
+ ACMCertificateArn: certificateRequest.CertificateArn,
1079
+ SSLSupportMethod: "sni-only",
1080
+ },
1081
+ },
1082
+ }),
1083
+ );
1084
+
1085
+ await route53.changeResourceRecordSets(
1086
+ new ChangeResourceRecordSetsCommand({
1087
+ HostedZoneId: hostedZoneCreation.HostedZone!.Id!,
1088
+ ChangeBatch: {
1089
+ Changes: [
1090
+ {
1091
+ Action: "CREATE",
1092
+ ResourceRecordSet: {
1093
+ Name: "www.example.test",
1094
+ Type: "A",
1095
+ AliasTarget: {
1096
+ HostedZoneId: "Z2FDTNDATAQYW2",
1097
+ DNSName: distributionCreation.Distribution!.DomainName!,
1098
+ EvaluateTargetHealth: false,
1099
+ },
1100
+ },
1101
+ },
1102
+ ],
1103
+ },
1104
+ }),
1105
+ );
1106
+
1107
+ await simAws.backgroundTasksComplete();
1108
+
1109
+ const http = new SimAwsHttp({ simAws });
1110
+ const response = await http.fetch("https://www.example.test/docs/x?a=1");
1111
+
1112
+ console.log(response.status); // 301
1113
+ console.log(response.headers.get("location")); // https://example.test/docs/x?a=1
1114
+ ```
1115
+
1116
+ `SimAwsHttp` returns the response the service answered with, and leaves the `Location` for the
1117
+ caller to follow.
1118
+
1119
+ Take the record out and the request answers 501. Nothing else in the simulation answers for
1120
+ `www.example.test`. Take the name out of the Distribution's `Aliases` and it answers 404, since the
1121
+ `Host` has to be one of the alternate domain names, as it does on AWS.
1122
+
1123
+ ### What a name can resolve to
1124
+
1125
+ A record chain ends when it reaches a hostname a simulated service owns. Those are recognised by
1126
+ their shape, and the value to point a record at is whatever the service reported:
1127
+
1128
+ | Hostname | What it reaches |
1129
+ | ---------------------------------------- | ------------------------------------------------------------------------ |
1130
+ | `<distribution-id>.cloudfront.net` | a [CloudFront](https://yulinsim.dev/services/cloudfront/) distribution |
1131
+ | `<name>-<id>.<region>.elb.amazonaws.com` | an [ELBv2](https://yulinsim.dev/services/elbv2/) load balancer |
1132
+ | `<bucket>.s3-website.<region>` | an [S3](https://yulinsim.dev/services/s3/) bucket website |
1133
+ | `<bucket>.s3.<region>` | the S3 REST endpoint |
1134
+ | `<url-id>.lambda-url.<region>` | a [Lambda](https://yulinsim.dev/services/lambda/) Function URL |
1135
+ | `<api-id>.execute-api.<region>` | an [API Gateway](https://yulinsim.dev/services/apigatewayv2/) HTTP API |
1136
+ | `d-<id>.execute-api.<region>` | an API Gateway custom domain |
1137
+ | `cognito-idp.<region>` | the [Cognito](https://yulinsim.dev/services/cognito/) user pool endpoint |
1138
+
1139
+ The hostnames the AWS SDK talks to are written without their `.amazonaws.com` or `.on.aws` tail,
1140
+ the same rewriting Yulin applies to an SDK endpoint. A load balancer's name keeps its whole domain,
1141
+ since it goes unrewritten. `DNSName` is what a record points at and what a client asks for. A custom
1142
+ domain's regional endpoint is read either way, so a record can point at `RegionalDomainName` as API
1143
+ Gateway answered it.
1144
+
1145
+ A name pointing at a load balancer resolves to it. A request to that name reaches the load balancer's
1146
+ listeners and rules, and a `host-header` condition on a rule sees the name the request was made to,
1147
+ in place of the load balancer's own:
1148
+
1149
+ ```typescript sim-route53-elbv2-alias
1150
+ /**
1151
+ * Resolving a name to a simulated load balancer, over HTTP and over DNS.
1152
+ */
1153
+
1154
+ import { Resolver } from "node:dns/promises";
1155
+
1156
+ import {
1157
+ CreateListenerCommand,
1158
+ CreateLoadBalancerCommand,
1159
+ } from "@aws-sdk/client-elastic-load-balancing-v2";
1160
+ import {
1161
+ ChangeResourceRecordSetsCommand,
1162
+ CreateHostedZoneCommand,
1163
+ } from "@aws-sdk/client-route-53";
1164
+
1165
+ import { SimAws } from "@kensio/yulin";
1166
+ import { serveSimAws } from "@kensio/yulin/serve";
1167
+
1168
+ const simAws = new SimAws();
1169
+ const elbV2 = simAws.elbV2();
1170
+
1171
+ const created = await elbV2.createLoadBalancer(
1172
+ new CreateLoadBalancerCommand({ Name: "shop-alb" }),
1173
+ );
1174
+
1175
+ await elbV2.createListener(
1176
+ new CreateListenerCommand({
1177
+ LoadBalancerArn: created.LoadBalancers?.[0]?.LoadBalancerArn,
1178
+ Protocol: "HTTP",
1179
+ Port: 80,
1180
+ DefaultActions: [
1181
+ {
1182
+ Type: "fixed-response",
1183
+ FixedResponseConfig: {
1184
+ StatusCode: "200",
1185
+ ContentType: "text/plain",
1186
+ MessageBody: "orders",
1187
+ },
1188
+ },
1189
+ ],
1190
+ }),
1191
+ );
1192
+
1193
+ const zone = await simAws.route53().createHostedZone(
1194
+ new CreateHostedZoneCommand({
1195
+ Name: "example.test",
1196
+ CallerReference: "shop-zone",
1197
+ }),
1198
+ );
1199
+
1200
+ // A CNAME below the apex reaches a load balancer as an alias record does.
1201
+ await simAws.route53().changeResourceRecordSets(
1202
+ new ChangeResourceRecordSetsCommand({
1203
+ HostedZoneId: zone.HostedZone?.Id,
1204
+ ChangeBatch: {
1205
+ Changes: [
1206
+ {
1207
+ Action: "CREATE",
1208
+ ResourceRecordSet: {
1209
+ Name: "api.example.test",
1210
+ Type: "CNAME",
1211
+ TTL: 300,
1212
+ ResourceRecords: [{ Value: created.LoadBalancers?.[0]?.DNSName }],
1213
+ },
1214
+ },
1215
+ ],
1216
+ },
1217
+ }),
1218
+ );
1219
+
1220
+ await simAws.backgroundTasksComplete();
1221
+
1222
+ const srv = await serveSimAws({ simAws });
1223
+
1224
+ try {
1225
+ const response = await fetch(srv.localUrl("http://api.example.test/orders"));
1226
+
1227
+ console.log(await response.text()); // "orders"
1228
+
1229
+ const resolver = new Resolver({ timeout: 1000, tries: 1 });
1230
+ resolver.setServers([`127.0.0.1:${srv.dnsPort}`]);
1231
+
1232
+ console.log(await resolver.resolve4("api.example.test")); // [ '127.0.0.1' ]
1233
+ } finally {
1234
+ await srv.close();
1235
+ }
1236
+ ```
1237
+
1238
+ A request served under the suffix reaches the listener on port 80, since the port such a request
1239
+ carries is the local server's and never one a client chose. See
1240
+ [Simulated Elastic Load Balancing](https://yulinsim.dev/services/elbv2/) for what happens once the request is there.
1241
+
1242
+ ### A hostname a resource claimed for itself
1243
+
1244
+ An API Gateway custom domain answers on the hostname it was created with, and a record for that
1245
+ hostname takes it back. The record decides where the name goes, as it does on AWS, where the custom
1246
+ domain name is reached only through one. A custom domain no record names resolves to the domain.
1247
+
1248
+ A Cognito hosted domain answers on its hostname whatever records exist. Real Cognito puts a
1249
+ CloudFront distribution in front of a custom domain and expects a record pointing at that
1250
+ distribution, and the distribution name it reports resolves to no simulated service here.
1251
+
1252
+ ## DNSSEC
1253
+
1254
+ A Hosted Zone can be signed. Signing needs a key-signing key, and a key-signing key needs a KMS
1255
+ customer managed key. That is an enabled `ECC_NIST_P256` `SIGN_VERIFY` key, the only kind real
1256
+ Route53 accepts.
1257
+
1258
+ ```typescript sim-route53-dnssec
1259
+ /**
1260
+ * Signing a simulated Route53 Hosted Zone with DNSSEC.
1261
+ */
1262
+
1263
+ import { CreateKeyCommand } from "@aws-sdk/client-kms";
1264
+ import {
1265
+ CreateHostedZoneCommand,
1266
+ CreateKeySigningKeyCommand,
1267
+ EnableHostedZoneDNSSECCommand,
1268
+ GetDNSSECCommand,
1269
+ } from "@aws-sdk/client-route-53";
1270
+
1271
+ import { SimAws } from "@kensio/yulin";
1272
+
1273
+ const simAws = new SimAws();
1274
+
1275
+ const zone = await simAws.route53().createHostedZone(
1276
+ new CreateHostedZoneCommand({
1277
+ Name: "example.test",
1278
+ CallerReference: "dnssec-zone",
1279
+ }),
1280
+ );
1281
+ const HostedZoneId = zone.HostedZone?.Id;
1282
+
1283
+ const key = await simAws.kms().createKey(
1284
+ new CreateKeyCommand({
1285
+ KeySpec: "ECC_NIST_P256",
1286
+ KeyUsage: "SIGN_VERIFY",
1287
+ }),
1288
+ );
1289
+
1290
+ await simAws.route53().createKeySigningKey(
1291
+ new CreateKeySigningKeyCommand({
1292
+ CallerReference: "ksk",
1293
+ HostedZoneId,
1294
+ KeyManagementServiceArn: key.KeyMetadata?.Arn,
1295
+ Name: "zone_signing_key",
1296
+ Status: "ACTIVE",
1297
+ }),
1298
+ );
1299
+
1300
+ await simAws
1301
+ .route53()
1302
+ .enableHostedZoneDnssec(new EnableHostedZoneDNSSECCommand({ HostedZoneId }));
1303
+
1304
+ const dnssec = await simAws
1305
+ .route53()
1306
+ .getDnssec(new GetDNSSECCommand({ HostedZoneId }));
1307
+
1308
+ console.log(dnssec.Status?.ServeSignature); // "SIGNING"
1309
+
1310
+ // The DS record the zone's registrar would be given, computed from the KMS
1311
+ // key's own public key: "<KeyTag> 13 2 <DigestValue>".
1312
+ console.log(dnssec.KeySigningKeys?.[0]?.DSRecord);
1313
+ ```
1314
+
1315
+ Every part of the key-signing key is computed from the KMS key. Its `PublicKey` is that key's own
1316
+ public key in the base64 form RFC 4034 defines, its `KeyTag` comes from the RFC 4034 Appendix B
1317
+ algorithm, and its `DigestValue` is the SHA-256 delegation signer digest over the zone name and the
1318
+ DNSKEY. A test can assert on the DS record it would hand to a registrar, and two zones on two keys
1319
+ get two different ones.
1320
+
1321
+ Adding a key-signing key leaves signing off, and stopping signing leaves the keys in place, the same
1322
+ as on AWS. `ActivateKeySigningKey` and `DeactivateKeySigningKey` move a key between `ACTIVE` and
1323
+ `INACTIVE`. `DeleteKeySigningKey` refuses a key that is still active. `EnableHostedZoneDNSSEC`
1324
+ refuses a zone with no active key to sign with, and `DisableHostedZoneDNSSEC` refuses an unsigned
1325
+ zone.
1326
+
1327
+ A KMS key that is symmetric, disabled, or absent altogether is refused when the key-signing key is
1328
+ created. A stack naming the wrong key fails here, ahead of the deployment. A signed zone cannot
1329
+ be deleted either, for the reason real Route53 gives. The DS record at the parent would be left
1330
+ pointing at a zone that had gone.
1331
+
1332
+ The key's policy goes unchecked. Real Route53 needs the key to allow `kms:DescribeKey`,
1333
+ `kms:GetPublicKey` and `kms:Sign` to the `dnssec-route53.amazonaws.com` service principal, and
1334
+ `kms:CreateGrant` conditioned on `kms:GrantIsForAWSResource`, which is what CDK's `KeySigningKey`
1335
+ construct adds for you. A key created here without those statements takes a key-signing key anyway,
1336
+ and a template that would fail on AWS for that reason deploys here.
1337
+
1338
+ ### DNSSEC from CloudFormation
1339
+
1340
+ `AWS::Route53::KeySigningKey` and `AWS::Route53::DNSSEC` deploy, which is the shape CDK's
1341
+ `KeySigningKey` construct and `CfnDNSSEC` synthesize.
1342
+
1343
+ ```typescript sim-route53-cloudformation-dnssec
1344
+ /**
1345
+ * Deploying a signed Route53 Hosted Zone from CloudFormation.
1346
+ */
1347
+
1348
+ import { GetDNSSECCommand } from "@aws-sdk/client-route-53";
1349
+
1350
+ import { SimAws } from "@kensio/yulin";
1351
+
1352
+ const simAws = new SimAws();
1353
+
1354
+ const stack = await simAws.cloudFormation().deployTemplate({
1355
+ stackName: "dns-stack",
1356
+ template: {
1357
+ Resources: {
1358
+ SiteZone: {
1359
+ Type: "AWS::Route53::HostedZone",
1360
+ Properties: { Name: "example.test" },
1361
+ },
1362
+ ZoneSigningKey: {
1363
+ Type: "AWS::KMS::Key",
1364
+ Properties: {
1365
+ KeySpec: "ECC_NIST_P256",
1366
+ KeyUsage: "SIGN_VERIFY",
1367
+ },
1368
+ },
1369
+ ZoneKeySigningKey: {
1370
+ Type: "AWS::Route53::KeySigningKey",
1371
+ Properties: {
1372
+ HostedZoneId: { Ref: "SiteZone" },
1373
+ KeyManagementServiceArn: { "Fn::GetAtt": ["ZoneSigningKey", "Arn"] },
1374
+ Name: "zone_signing_key",
1375
+ Status: "ACTIVE",
1376
+ },
1377
+ },
1378
+ ZoneDnssec: {
1379
+ Type: "AWS::Route53::DNSSEC",
1380
+ Properties: { HostedZoneId: { Ref: "SiteZone" } },
1381
+ DependsOn: "ZoneKeySigningKey",
1382
+ },
1383
+ },
1384
+ Outputs: {
1385
+ ZoneId: { Value: { Ref: "SiteZone" } },
1386
+ },
1387
+ },
1388
+ });
1389
+ await stack.waitForDeployComplete();
1390
+
1391
+ const hostedZoneId = stack.output("ZoneId");
1392
+
1393
+ if (typeof hostedZoneId !== "string") {
1394
+ throw new TypeError("The stack did not output a hosted zone ID");
1395
+ }
1396
+
1397
+ const dnssec = await simAws
1398
+ .route53()
1399
+ .getDnssec(new GetDNSSECCommand({ HostedZoneId: hostedZoneId }));
1400
+
1401
+ console.log(dnssec.Status?.ServeSignature); // "SIGNING"
1402
+ console.log(dnssec.KeySigningKeys?.[0]?.Status); // "ACTIVE"
1403
+ ```
1404
+
1405
+ `Ref` on an `AWS::Route53::KeySigningKey` returns `<HostedZoneId>|<Name>`, which CDK reads as
1406
+ `keySigningKeyId`. `Ref` on an `AWS::Route53::DNSSEC` returns the hosted zone ID. Neither type has
1407
+ any `Fn::GetAtt` attributes, and a `Fn::GetAtt` on one is refused.
1408
+
1409
+ Tearing the stack down stops signing and takes the key-signing key with it, deactivating it first,
1410
+ because an active key cannot be deleted.
1411
+
1412
+ ## CloudFormation Hosted Zones
1413
+
1414
+ Sim CloudFormation can create Route53 Hosted Zones from `AWS::Route53::HostedZone`.
1415
+
1416
+ ```typescript sim-route53-cloudformation-hosted-zone
1417
+ /**
1418
+ * Creating a Route53 Hosted Zone through simulated CloudFormation.
1419
+ */
1420
+
1421
+ import { SimAws } from "@kensio/yulin";
1422
+
1423
+ const simAws = new SimAws();
1424
+
1425
+ const stack = await simAws.cloudFormation().deployTemplate({
1426
+ stackName: "route53-zone-stack",
1427
+ template: {
1428
+ Resources: {
1429
+ SiteZone: {
1430
+ Type: "AWS::Route53::HostedZone",
1431
+ Properties: {
1432
+ Name: "example.test",
1433
+ HostedZoneConfig: {
1434
+ Comment: "Example hosted zone",
1435
+ },
1436
+ },
1437
+ },
1438
+ },
1439
+ Outputs: {
1440
+ HostedZoneId: {
1441
+ Value: {
1442
+ Ref: "SiteZone",
1443
+ },
1444
+ },
1445
+ HostedZoneNameServers: {
1446
+ Value: {
1447
+ "Fn::GetAtt": ["SiteZone", "NameServers"],
1448
+ },
1449
+ },
1450
+ },
1451
+ },
1452
+ });
1453
+
1454
+ await stack.waitForDeployComplete();
1455
+
1456
+ console.log(stack.output("HostedZoneId"));
1457
+ console.log(stack.outputs.get("HostedZoneNameServers")?.value);
1458
+ ```
1459
+
1460
+ For `AWS::Route53::HostedZone`, `Ref` returns the simulated Hosted Zone ID. `Fn::GetAtt` supports
1461
+ `Id` and `NameServers`.
1462
+
1463
+ ## CloudFormation RecordSets
1464
+
1465
+ Sim CloudFormation can create sim Route53 records from `AWS::Route53::RecordSet`.
1466
+
1467
+ ```typescript sim-route53-cloudformation-record-set
1468
+ /**
1469
+ * Creating Route53 records through simulated CloudFormation.
1470
+ */
1471
+
1472
+ import { SimAws } from "@kensio/yulin";
1473
+
1474
+ const simAws = new SimAws();
1475
+
1476
+ const stack = await simAws.cloudFormation().deployTemplate({
1477
+ stackName: "route53-record-stack",
1478
+ template: {
1479
+ Resources: {
1480
+ SiteZone: {
1481
+ Type: "AWS::Route53::HostedZone",
1482
+ Properties: {
1483
+ Name: "example.test",
1484
+ },
1485
+ },
1486
+ SiteRecord: {
1487
+ Type: "AWS::Route53::RecordSet",
1488
+ Properties: {
1489
+ HostedZoneId: {
1490
+ Ref: "SiteZone",
1491
+ },
1492
+ Name: "www.example.test",
1493
+ Type: "A",
1494
+ TTL: "300",
1495
+ ResourceRecords: ["192.0.2.1"],
1496
+ },
1497
+ },
1498
+ },
1499
+ },
1500
+ });
1501
+
1502
+ await stack.waitForDeployComplete();
1503
+ await simAws.backgroundTasksComplete();
1504
+ ```
1505
+
1506
+ Record sets can use either `HostedZoneId` or `HostedZoneName`. `HostedZoneId` is usually the
1507
+ clearest option in templates, since it can reference the zone resource directly.
1508
+
1509
+ ### Unsupported record types in a template
1510
+
1511
+ A real DNS stack usually holds a few records beside the point of what is being tested. When one of
1512
+ them declares a [record type](#record-types) sim Route53 leaves unstored, the RecordSet is skipped
1513
+ and the rest of the stack deploys, the same way an unsupported resource type is. The skipped
1514
+ RecordSet is in `stack.skippedResources` with a `skippedReason` naming the record type.
1515
+
1516
+ ```typescript sim-route53-skipped-record-type
1517
+ /**
1518
+ * Deploying a template that carries a record type sim Route53 does not store.
1519
+ */
1520
+
1521
+ import { SimAws } from "@kensio/yulin";
1522
+
1523
+ const simAws = new SimAws();
1524
+
1525
+ const stack = await simAws.cloudFormation().deployTemplate({
1526
+ stackName: "signed-dns-stack",
1527
+ template: {
1528
+ Resources: {
1529
+ SiteZone: {
1530
+ Type: "AWS::Route53::HostedZone",
1531
+ Properties: {
1532
+ Name: "example.test",
1533
+ },
1534
+ },
1535
+ SiteRecord: {
1536
+ Type: "AWS::Route53::RecordSet",
1537
+ Properties: {
1538
+ HostedZoneId: { Ref: "SiteZone" },
1539
+ Name: "www.example.test",
1540
+ Type: "A",
1541
+ TTL: "300",
1542
+ ResourceRecords: ["192.0.2.1"],
1543
+ },
1544
+ },
1545
+ DelegationSigner: {
1546
+ Type: "AWS::Route53::RecordSet",
1547
+ Properties: {
1548
+ HostedZoneId: { Ref: "SiteZone" },
1549
+ Name: "example.test",
1550
+ Type: "DS",
1551
+ TTL: "3600",
1552
+ ResourceRecords: ["12345 13 2 49FD46E6C4B45C55D4AC"],
1553
+ },
1554
+ },
1555
+ },
1556
+ },
1557
+ });
1558
+
1559
+ await stack.waitForDeployComplete();
1560
+ await simAws.backgroundTasksComplete();
1561
+
1562
+ // The A record the test is about was created.
1563
+ console.log(stack.getResource("SiteRecord")?.status);
1564
+ // "CREATE_COMPLETE"
1565
+
1566
+ console.log(stack.skippedResources.map((resource) => resource.logicalId));
1567
+ // ["DelegationSigner"]
1568
+
1569
+ console.log(stack.getResource("DelegationSigner")?.skippedReason);
1570
+ // "Unsupported sim Route53 CloudFormation Resource DelegationSigner: sim Route53
1571
+ // does not model the DS record type, and stores A, AAAA, CAA, CNAME, MX, NS,
1572
+ // PTR, SOA, SRV, TXT."
1573
+ ```
1574
+
1575
+ Only the record type is treated this way. A RecordSet that makes no sense as a RecordSet still fails
1576
+ the stack. A non-string `Name`, a non-string `Type`, or a negative `TTL` is refused. An unmodelled
1577
+ record type is a gap in the simulation, where a malformed RecordSet is a broken template.
1578
+
1579
+ Tearing the stack down works with the skipped RecordSet in it. Nothing was created for it, and the
1580
+ teardown steps over it without asking Route53 to remove a record it never stored.
1581
+
1582
+ ## CDK integration
1583
+
1584
+ You can synthesize a CDK app and deploy the generated template with sim CloudFormation. CDK Route53
1585
+ Hosted Zones and records can then participate in the same local simulated AWS environment as S3 and
1586
+ CloudFront.
1587
+
1588
+ A common pattern for local website tests is:
1589
+
1590
+ 1. Create a CDK stack with an S3 Bucket, CloudFront Distribution, Route53 Hosted Zone, and Route53
1591
+ record.
1592
+ 2. Synthesize the CDK app.
1593
+ 3. Deploy the synthesized template through `simAws.cloudFormation().deployTemplateFile(...)`.
1594
+ 4. Serve the simulated AWS environment with `serveSimAws(...)`.
1595
+ 5. Fetch the site through the Route53 hostname using the `sim-aws.localhost` suffix.
1596
+
1597
+ ```typescript sim-route53-cdk-template-file
1598
+ /**
1599
+ * Deploying a CDK template with Route53 resources into simulated AWS.
1600
+ */
1601
+
1602
+ import path from "node:path";
1603
+
1604
+ import { SimAws } from "@kensio/yulin";
1605
+ import { serveSimAws } from "@kensio/yulin/serve";
1606
+
1607
+ const simAws = new SimAws();
1608
+ const srv = await serveSimAws({ simAws });
1609
+
1610
+ try {
1611
+ const stack = await simAws
1612
+ .cloudFormation()
1613
+ .deployTemplateFile(
1614
+ path.join(process.cwd(), "cdk.out", "TestStack.template.json"),
1615
+ );
1616
+
1617
+ await stack.waitForDeployComplete();
1618
+ await simAws.backgroundTasksComplete();
1619
+
1620
+ const response = await fetch(
1621
+ `http://www.example.test.sim-aws.localhost:${srv.port}/`,
1622
+ );
1623
+
1624
+ console.log(response.status);
1625
+ console.log(await response.text());
1626
+ } finally {
1627
+ await srv.close();
1628
+ }
1629
+ ```
1630
+
1631
+ This lets local integration tests use the same CDK infrastructure shape as production while keeping
1632
+ the test process local.
1633
+
1634
+ A CDK app whose Hosted Zone comes from `HostedZone.fromLookup` names a real Hosted Zone ID
1635
+ throughout its template, in place of creating the zone. The template deploys as it is. The zone is
1636
+ registered under that ID as the first RecordSet naming it is created, with its name inferred from the
1637
+ record names. Register it yourself with
1638
+ [Registering a Hosted Zone with a chosen ID](#registering-a-hosted-zone-with-a-chosen-id) when a test
1639
+ depends on the zone's name, or when no record in the stack sits inside it.
1640
+
1641
+ ## Accounts and Regions
1642
+
1643
+ Use `SimAws` scopes to create Route53 state in different simulated Accounts and Regions.
1644
+
1645
+ ```typescript sim-route53-account-region-scoping
1646
+ /**
1647
+ * Simulated Route53 Account and Region scoping.
1648
+ */
1649
+
1650
+ import { CreateHostedZoneCommand } from "@aws-sdk/client-route-53";
1651
+
1652
+ import { SimAws } from "@kensio/yulin";
1653
+
1654
+ const simAws = new SimAws();
1655
+
1656
+ const defaultRoute53 = simAws.route53();
1657
+ const euWest2Route53 = simAws.region("eu-west-2").route53();
1658
+ const accountRoute53 = simAws.account("111111111111").route53();
1659
+ const scopedRoute53 = simAws
1660
+ .account("222222222222")
1661
+ .region("ap-east-1")
1662
+ .route53();
1663
+
1664
+ await defaultRoute53.createHostedZone(
1665
+ new CreateHostedZoneCommand({
1666
+ Name: "default.example.test",
1667
+ CallerReference: "default-zone",
1668
+ }),
1669
+ );
1670
+
1671
+ await euWest2Route53.createHostedZone(
1672
+ new CreateHostedZoneCommand({
1673
+ Name: "eu-west-2.example.test",
1674
+ CallerReference: "eu-west-2-zone",
1675
+ }),
1676
+ );
1677
+
1678
+ await accountRoute53.createHostedZone(
1679
+ new CreateHostedZoneCommand({
1680
+ Name: "account.example.test",
1681
+ CallerReference: "account-zone",
1682
+ }),
1683
+ );
1684
+
1685
+ await scopedRoute53.createHostedZone(
1686
+ new CreateHostedZoneCommand({
1687
+ Name: "scoped.example.test",
1688
+ CallerReference: "scoped-zone",
1689
+ }),
1690
+ );
1691
+ ```
1692
+
1693
+ Each `SimAws` instance has its own isolated state. Create a fresh instance per test, or share one
1694
+ across related local setup.
1695
+
1696
+ ## Standalone SimRoute53
1697
+
1698
+ If you only need Route53 alone, instantiate `SimRoute53` directly.
1699
+
1700
+ ```typescript sim-route53-standalone
1701
+ /**
1702
+ * Standalone simulated Route53 instance.
1703
+ */
1704
+
1705
+ import { CreateHostedZoneCommand } from "@aws-sdk/client-route-53";
1706
+
1707
+ import { SimRoute53 } from "@kensio/yulin/route53";
1708
+
1709
+ const route53 = new SimRoute53();
1710
+
1711
+ const hostedZoneCreation = await route53.createHostedZone(
1712
+ new CreateHostedZoneCommand({
1713
+ Name: "standalone.example.test",
1714
+ CallerReference: "standalone-zone",
1715
+ }),
1716
+ );
1717
+
1718
+ console.log(hostedZoneCreation.HostedZone?.Id);
1719
+ ```
1720
+
1721
+ A standalone `SimRoute53` instance has its own isolated state, standing apart from any wider
1722
+ `SimAws` environment. Use `SimAws` when Route53 needs to resolve names to other simulated services.
1723
+
1724
+ ## Available functionality
1725
+
1726
+ Sim Route53 currently supports:
1727
+
1728
+ - `CreateHostedZoneCommand`, `GetHostedZoneCommand` and `ListHostedZonesByNameCommand`
1729
+ - Registering a Hosted Zone with a chosen Hosted Zone ID, for a zone a template looked up rather
1730
+ than created
1731
+ - `ChangeResourceRecordSetsCommand` and `ListResourceRecordSetsCommand`
1732
+ - `CREATE`, `UPSERT` and `DELETE` record changes
1733
+ - Stored record types: `A`, `AAAA`, `CAA`, `CNAME`, `MX`, `NS`, `PTR`, `SOA`, `SRV` and `TXT`
1734
+ - Local HTTP hostname routing through `CNAME` records that point to simulated service hostnames
1735
+ - Alias records, with `AliasTarget.DNSName` stored as the record value
1736
+ - Local hostname resolution, with or without the `sim-aws.localhost` suffix on the requested hostname
1737
+ - Hostname resolution with no server listening, through `SimAwsHttp`
1738
+ - Names resolving to simulated S3 websites and buckets, CloudFront distributions, ELBv2 load
1739
+ balancers, Lambda Function URLs, HTTP APIs and the Cognito user pool endpoint, listed under
1740
+ [What a name can resolve to](#what-a-name-can-resolve-to)
1741
+ - A browser-viewable hosted zone and record summary at `dns.sim-aws.localhost`
1742
+ - DNS answers over UDP for `A`, `AAAA`, `CNAME`, `TXT`, `NS` and `SOA`, so those records can be
1743
+ queried with `dig` or any DNS client
1744
+ - `CreateKeySigningKeyCommand`, `ActivateKeySigningKeyCommand`,
1745
+ `DeactivateKeySigningKeyCommand`, `DeleteKeySigningKeyCommand`,
1746
+ `EnableHostedZoneDNSSECCommand`, `DisableHostedZoneDNSSECCommand` and `GetDNSSECCommand`
1747
+ - The `AWS::Route53::HostedZone` and `AWS::Route53::RecordSet` CloudFormation resources, with a
1748
+ RecordSet declaring an unstored record type skipped rather than failing the stack
1749
+ - The `AWS::Route53::KeySigningKey` and `AWS::Route53::DNSSEC` CloudFormation resources
1750
+ - CDK-created Route53 Hosted Zones and records in synthesized templates
1751
+
1752
+ The simulator focuses on useful behaviour for tests and local development, ahead of full Route53
1753
+ feature parity. Unsupported Route53 options may be ignored or may throw errors depending on whether
1754
+ the simulator needs them to model the requested behaviour.
1755
+
1756
+ ## Limitations
1757
+
1758
+ Where sim Route53 knowingly behaves differently from AWS:
1759
+
1760
+ - **A RecordSet naming an absent Hosted Zone creates one.** CloudFormation would refuse with
1761
+ `NoSuchHostedZone`, because the ID names a zone in an account the simulation is not. Every
1762
+ template built with `HostedZone.fromLookup` would then be undeployable here. The zone is
1763
+ registered on demand instead. See
1764
+ [A zone a template only names](#a-zone-a-template-only-names).
1765
+ - **The name of such a zone is a guess.** A synthesized template says nothing about what a
1766
+ looked-up zone is called. The name is inferred from the records that reference it, and is only as
1767
+ specific as they are. Register the zone yourself when a test depends on its name.
1768
+ - **A signed zone is signed on paper only.** No RRSIG records are produced, no DNSKEY records are
1769
+ added to the zone, and a query answered over UDP is unsigned whatever `GetDNSSEC` says. Signing is
1770
+ observable through `GetDNSSEC`, which holds the key-signing keys and the DS record fields. Sim
1771
+ Route53 is no general DNS server, and an RRSIG needs canonical RRset ordering and wire-format
1772
+ signing that nothing here would read back.
1773
+ - **A key-signing key never rotates, and a zone never reports trouble.** `ACTIVE` and `INACTIVE` are
1774
+ the only key statuses, and `SIGNING` and `NOT_SIGNING` the only zone statuses. Real Route53 also
1775
+ has `DELETING`, `ACTION_NEEDED` and `INTERNAL_FAILURE`, which describe a key mid-operation or a
1776
+ zone that needs attention, and neither is produced here.
1777
+ - **A key-signing key's KMS key policy goes unchecked.** Real Route53 refuses a key that leaves the
1778
+ `dnssec-route53.amazonaws.com` service principal out, and this accepts one. A template missing
1779
+ those statements deploys here and fails on AWS. Checking it would mean authorizing the service
1780
+ principal against the key policy, which is its own piece of work.
1781
+ - **DNSSEC needs KMS wired to Route53.** A standalone `new SimRoute53()` has no simulated KMS to
1782
+ resolve a key ARN against, so `CreateKeySigningKey` refuses. Reach Route53 through `SimAws` when a
1783
+ test signs a zone.