@kensio/yulin 1.20.15 → 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.
- package/README.md +13 -0
- package/dist/sdk/module/sim-sdk-module-client-interceptor.d.ts +5 -0
- package/dist/sdk/module/sim-sdk-module-client-interceptor.js +14 -0
- package/dist/sdk/module/sim-sdk-static-client-factory.d.ts +26 -0
- package/dist/sdk/module/sim-sdk-static-client-factory.js +59 -0
- package/dist/sdk/send-patch.d.ts +8 -0
- package/dist/sdk/send-patch.js +11 -0
- package/dist/service/aws/caller/sim-aws-caller-resolver.d.ts +28 -6
- package/dist/service/aws/caller/sim-aws-caller-resolver.js +18 -11
- package/dist/service/aws/caller/sim-aws-caller.d.ts +9 -0
- package/dist/service/aws/caller/sim-aws-resolved-caller.d.ts +19 -0
- package/dist/service/aws/caller/sim-aws-resolved-caller.js +26 -0
- package/dist/service/aws/factory/sim-aws-account-region-service-builder.d.ts +6 -0
- package/dist/service/aws/factory/sim-aws-account-region-service-builder.js +3 -0
- package/dist/service/aws/factory/sim-aws-account-service-cache.d.ts +6 -0
- package/dist/service/aws/factory/sim-aws-account-service-cache.js +5 -0
- package/dist/service/aws/factory/sim-aws-service-factory.d.ts +5 -0
- package/dist/service/aws/factory/sim-aws-service-factory.js +6 -0
- package/dist/service/aws/sim-aws-properties.d.ts +15 -0
- package/dist/service/aws/sim-aws.js +2 -1
- package/dist/service/iam/authorize/context/sim-iam-auth-z-caller-context-builder.d.ts +30 -7
- package/dist/service/iam/authorize/context/sim-iam-auth-z-caller-context-builder.js +20 -11
- package/dist/service/iam/authorize/context/sim-iam-auth-z-context-builder.d.ts +2 -1
- package/dist/service/iam/authorize/context/sim-iam-auth-z-context-builder.js +5 -1
- package/dist/service/iam/authorize/match/sim-iam-policy-principal-matcher.d.ts +10 -0
- package/dist/service/iam/authorize/match/sim-iam-policy-principal-matcher.js +15 -3
- package/dist/service/iam/authorize/sim-iam-account-auth-z.d.ts +11 -4
- package/dist/service/iam/authorize/sim-iam-account-auth-z.js +5 -3
- package/dist/service/iam/authorize/sim-iam-authorizer.d.ts +2 -1
- package/dist/service/iam/authorize/sim-iam-authorizer.js +1 -0
- package/dist/service/iam/role/sim-iam-role-with-policy.factory.d.ts +9 -0
- package/dist/service/iam/role/sim-iam-role-with-policy.factory.js +4 -2
- package/dist/service/iam/sim-iam-account-parts.d.ts +8 -0
- package/dist/service/iam/sim-iam-account-parts.js +1 -0
- package/dist/service/sts/auth-z/assume-role-auth-z-coordinator.d.ts +9 -2
- package/dist/service/sts/auth-z/assume-role-auth-z-coordinator.js +2 -2
- package/dist/service/sts/auth-z/assume-role-source-account-auth-z.d.ts +10 -4
- package/dist/service/sts/auth-z/assume-role-source-account-auth-z.js +8 -7
- package/dist/service/sts/auth-z/assume-role-target-auth-z.d.ts +2 -2
- package/dist/service/sts/auth-z/assume-role-trust-policy-authorizer.d.ts +6 -2
- package/dist/service/sts/auth-z/assume-role-trust-policy-authorizer.js +14 -11
- package/dist/service/sts/command/assume-role/assume-role.handler.d.ts +5 -1
- package/dist/service/sts/command/assume-role/assume-role.handler.js +5 -2
- package/dist/service/sts/command/get-caller-identity/get-caller-identity.handler.d.ts +5 -1
- package/dist/service/sts/command/get-caller-identity/get-caller-identity.handler.js +4 -1
- package/dist/service/sts/service-role/sim-service-role.js +5 -1
- package/dist/service/sts/sim-sts.d.ts +9 -1
- package/dist/service/sts/sim-sts.js +4 -0
- package/docs/README.md +54 -0
- package/docs/ai-skill/README.md +69 -0
- package/docs/cli/README.md +284 -0
- package/docs/factories/README.md +140 -0
- package/docs/lint/README.md +176 -0
- package/docs/non-aws-dependencies/README.md +261 -0
- package/docs/sdk/README.md +272 -0
- package/docs/serve/README.md +1126 -0
- package/docs/services/acm/README.md +823 -0
- package/docs/services/apigateway/README.md +1919 -0
- package/docs/services/apigatewayv2/README.md +2678 -0
- package/docs/services/athena/README.md +1044 -0
- package/docs/services/bedrock/README.md +350 -0
- package/docs/services/cloudformation/README.md +3440 -0
- package/docs/services/cloudfront/README.md +2874 -0
- package/docs/services/cloudwatch/README.md +420 -0
- package/docs/services/cognito/README.md +4923 -0
- package/docs/services/dynamodb/README.md +3784 -0
- package/docs/services/ecr/README.md +302 -0
- package/docs/services/ecs/README.md +2010 -0
- package/docs/services/elbv2/README.md +1897 -0
- package/docs/services/eventbridge/README.md +1110 -0
- package/docs/services/firehose/README.md +805 -0
- package/docs/services/glue/README.md +501 -0
- package/docs/services/iam/README.md +1283 -0
- package/docs/services/kinesis/README.md +454 -0
- package/docs/services/kms/README.md +659 -0
- package/docs/services/lambda/README.md +3645 -0
- package/docs/services/logs/README.md +868 -0
- package/docs/services/organizations/README.md +574 -0
- package/docs/services/personalize/README.md +835 -0
- package/docs/services/rekognition/README.md +1070 -0
- package/docs/services/route53/README.md +1783 -0
- package/docs/services/s3/README.md +3024 -0
- package/docs/services/scheduler/README.md +806 -0
- package/docs/services/secretsmanager/README.md +667 -0
- package/docs/services/ses/README.md +1011 -0
- package/docs/services/sns/README.md +1904 -0
- package/docs/services/sqs/README.md +1185 -0
- package/docs/services/ssm/README.md +1072 -0
- package/docs/services/stepfunctions/README.md +1332 -0
- package/docs/services/sts/README.md +245 -0
- package/docs/services/wafv2/README.md +1378 -0
- package/docs/terraform/README.md +248 -0
- package/docs/time/README.md +284 -0
- package/llms.txt +59 -0
- package/package.json +7 -4
|
@@ -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.
|