@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,3440 @@
1
+ # Simulated CloudFormation
2
+
3
+ Yulin includes a simulated CloudFormation service for tests and local development. It creates
4
+ simulated AWS resources from CloudFormation templates, and works with hand-written templates, AWS
5
+ SDK-style `CreateStackCommand` calls, or synthesized CDK template files.
6
+
7
+ ## Basic usage
8
+
9
+ Create a simulated AWS environment, get simulated CloudFormation, and deploy a template.
10
+
11
+ ```typescript sim-cloudformation-basic-template
12
+ /**
13
+ * Deploying a simple CloudFormation template into simulated AWS.
14
+ */
15
+
16
+ import { SimAws } from "@kensio/yulin";
17
+
18
+ const simAws = new SimAws();
19
+ const simCfn = simAws.cloudFormation();
20
+
21
+ const stack = await simCfn.deployTemplate({
22
+ stackName: "site-stack",
23
+ template: {
24
+ Resources: {
25
+ SiteBucket: {
26
+ Type: "AWS::S3::Bucket",
27
+ Properties: {
28
+ BucketName: "example-site-bucket",
29
+ WebsiteConfiguration: {
30
+ IndexDocument: "index.html",
31
+ },
32
+ },
33
+ },
34
+ },
35
+ },
36
+ });
37
+
38
+ await stack.waitForDeployComplete();
39
+
40
+ const bucket = simAws.s3().getSimBucketByName("example-site-bucket");
41
+ console.log(bucket?.bucketName);
42
+ ```
43
+
44
+ `deployTemplate(...)` returns the simulated stack object. If your test needs the created resources
45
+ to be available, wait for deployment to complete before asserting final state.
46
+
47
+ ### Naming the template type
48
+
49
+ A template written inline is typed by `deployTemplate(...)` itself. A test that builds a template up
50
+ somewhere else can name that type as `CfnTemplateBodyRecord`.
51
+
52
+ ```typescript sim-cloudformation-template-type
53
+ /**
54
+ * Naming the type of a template a test builds somewhere other than the call.
55
+ */
56
+
57
+ import { SimAws } from "@kensio/yulin";
58
+ import type { CfnTemplateBodyRecord } from "@kensio/yulin/cloudformation";
59
+
60
+ function siteTemplate(bucketName: string): CfnTemplateBodyRecord {
61
+ return {
62
+ Resources: {
63
+ SiteBucket: {
64
+ Type: "AWS::S3::Bucket",
65
+ Properties: {
66
+ BucketName: bucketName,
67
+ },
68
+ },
69
+ },
70
+ };
71
+ }
72
+
73
+ const simAws = new SimAws();
74
+
75
+ const stack = await simAws.cloudFormation().deployTemplate({
76
+ stackName: "typed-site-stack",
77
+ template: siteTemplate("typed-site-bucket"),
78
+ });
79
+
80
+ await stack.waitForDeployComplete();
81
+
82
+ console.log(simAws.s3().getSimBucketByName("typed-site-bucket")?.bucketName);
83
+ ```
84
+
85
+ ## Creating stacks with AWS SDK command shapes
86
+
87
+ You can also use AWS SDK-style CloudFormation commands.
88
+
89
+ ```typescript sim-cloudformation-create-stack-command
90
+ /**
91
+ * Creating a simulated CloudFormation Stack with CreateStackCommand.
92
+ */
93
+
94
+ import {
95
+ CreateStackCommand,
96
+ DescribeStacksCommand,
97
+ } from "@aws-sdk/client-cloudformation";
98
+
99
+ import { SimAws } from "@kensio/yulin";
100
+
101
+ const simAws = new SimAws();
102
+ const simCfn = simAws.cloudFormation();
103
+
104
+ await simCfn.createStack(
105
+ new CreateStackCommand({
106
+ StackName: "command-stack",
107
+ TemplateBody: JSON.stringify({
108
+ Resources: {
109
+ SiteBucket: {
110
+ Type: "AWS::S3::Bucket",
111
+ Properties: {
112
+ BucketName: "command-stack-bucket",
113
+ },
114
+ },
115
+ },
116
+ }),
117
+ }),
118
+ );
119
+
120
+ await simCfn.waitForStackDeployComplete("command-stack");
121
+
122
+ const describeOutput = await simCfn.describeStacks(
123
+ new DescribeStacksCommand({
124
+ StackName: "command-stack",
125
+ }),
126
+ );
127
+
128
+ console.log(describeOutput.Stacks?.[0]?.StackStatus);
129
+ ```
130
+
131
+ `createStack(...)` starts deployment and returns once the stack has been accepted. Resource creation
132
+ continues asynchronously, similar to real CloudFormation. Use `waitForStackDeployComplete(...)` when
133
+ you need final stack state.
134
+
135
+ ## Stack deployment is asynchronous
136
+
137
+ A stack may be visible before all resources have finished creating.
138
+
139
+ ```typescript sim-cloudformation-wait-for-deploy
140
+ /**
141
+ * Waiting for a simulated CloudFormation deployment to finish.
142
+ */
143
+
144
+ import { SimAws } from "@kensio/yulin";
145
+
146
+ const simAws = new SimAws();
147
+
148
+ await simAws.cloudFormation().deployTemplate({
149
+ stackName: "async-stack",
150
+ template: {
151
+ Resources: {
152
+ WaitHandle: {
153
+ Type: "AWS::CloudFormation::WaitConditionHandle",
154
+ },
155
+ },
156
+ },
157
+ });
158
+
159
+ await simAws.cloudFormation().waitForStackDeployComplete("async-stack");
160
+ ```
161
+
162
+ You can also wait through the returned stack object:
163
+
164
+ ```typescript sim-cloudformation-stack-wait
165
+ /**
166
+ * Waiting via the returned simulated CloudFormation Stack object.
167
+ */
168
+
169
+ import { SimAws } from "@kensio/yulin";
170
+
171
+ const simAws = new SimAws();
172
+
173
+ const stack = await simAws.cloudFormation().deployTemplate({
174
+ stackName: "stack-object-wait",
175
+ template: {
176
+ Resources: {
177
+ WaitHandle: {
178
+ Type: "AWS::CloudFormation::WaitConditionHandle",
179
+ },
180
+ },
181
+ },
182
+ });
183
+
184
+ await stack.waitForDeployComplete();
185
+ ```
186
+
187
+ If your scenario also depends on asynchronous work scheduled by the created services, you can drain
188
+ the broader simulator background tasks:
189
+
190
+ ```typescript sim-cloudformation-background-tasks
191
+ /**
192
+ * Waiting for simulated AWS background tasks to complete.
193
+ */
194
+
195
+ import { SimAws } from "@kensio/yulin";
196
+
197
+ const simAws = new SimAws();
198
+
199
+ // Deploy stacks and interact with simulated services...
200
+
201
+ await simAws.backgroundTasksComplete();
202
+ ```
203
+
204
+ ## Updating a stack
205
+
206
+ `UpdateStackCommand` applies a changed template to a stack that is already deployed. Resources the
207
+ new template adds are created, resources it drops are deleted, and resources it changed are
208
+ replaced. Everything else is left alone, holding whatever it holds in simulated S3, DynamoDB or
209
+ anywhere else. That is what lets a long-running local process pick up an infrastructure change
210
+ without restarting and losing its data.
211
+
212
+ ```typescript sim-cloudformation-update-stack
213
+ /**
214
+ * Applying a changed template with UpdateStackCommand.
215
+ */
216
+
217
+ import {
218
+ CreateStackCommand,
219
+ UpdateStackCommand,
220
+ } from "@aws-sdk/client-cloudformation";
221
+ import { GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
222
+
223
+ import { SimAws } from "@kensio/yulin";
224
+
225
+ const simAws = new SimAws();
226
+ const simCfn = simAws.cloudFormation();
227
+
228
+ const siteBucket = {
229
+ Type: "AWS::S3::Bucket",
230
+ Properties: { BucketName: "site-content" },
231
+ };
232
+
233
+ const deployedTemplate = JSON.stringify({
234
+ Resources: { SiteBucket: siteBucket },
235
+ });
236
+
237
+ const changedTemplate = JSON.stringify({
238
+ Resources: {
239
+ SiteBucket: siteBucket,
240
+ UploadsBucket: {
241
+ Type: "AWS::S3::Bucket",
242
+ Properties: { BucketName: "site-uploads" },
243
+ },
244
+ },
245
+ });
246
+
247
+ await simCfn.createStack(
248
+ new CreateStackCommand({
249
+ StackName: "site",
250
+ TemplateBody: deployedTemplate,
251
+ }),
252
+ );
253
+ await simCfn.waitForStackDeployComplete("site");
254
+
255
+ await simAws.s3().putObject(
256
+ new PutObjectCommand({
257
+ Bucket: "site-content",
258
+ Key: "index.html",
259
+ Body: "<h1>Hello</h1>",
260
+ }),
261
+ );
262
+
263
+ // Apply the changed template to the stack that is already there.
264
+ await simCfn.updateStack(
265
+ new UpdateStackCommand({
266
+ StackName: "site",
267
+ TemplateBody: changedTemplate,
268
+ }),
269
+ );
270
+ await simCfn.waitForStackUpdateComplete("site");
271
+
272
+ // The bucket the new template adds is in simulated S3.
273
+ console.log(simAws.s3().getSimBucketByName("site-uploads"));
274
+
275
+ // And the bucket the template did not change still holds its object.
276
+ const page = await simAws
277
+ .s3()
278
+ .getObject(
279
+ new GetObjectCommand({ Bucket: "site-content", Key: "index.html" }),
280
+ );
281
+ console.log(page.Body);
282
+ ```
283
+
284
+ The resource work runs in the background, as deployment does. `updateStack(...)` returns once the
285
+ stack has moved to `UPDATE_IN_PROGRESS`, and `waitForStackUpdateComplete(...)` waits for the
286
+ resources to change. `DescribeStacksCommand` reports `UPDATE_IN_PROGRESS` in between and
287
+ `UPDATE_COMPLETE` after, along with the outputs resolved again against the new template.
288
+
289
+ `UpdateStackCommand` refuses a stack name that was never deployed, with the same `ValidationError`
290
+ that `DescribeStacksCommand` answers such a name with. An update of a stack that was never there
291
+ could mean nothing else.
292
+
293
+ ### What counts as a change
294
+
295
+ Resources are compared as they resolve, not as they are written. A changed parameter value shows up
296
+ as a changed resource even when the template body is identical, and a template reordered without
297
+ being changed shows up as no change at all. Outputs are compared the same way. The rest of the
298
+ template body is compared as written. A change to a section the simulator ignores, such as
299
+ `Description`, is still an update.
300
+
301
+ A template that changes nothing at all is refused with a `ValidationError` reading
302
+ `No updates are to be performed.`, the same answer CloudFormation gives. So is an update asked for
303
+ while another is still running.
304
+
305
+ ### Changed resources are replaced
306
+
307
+ A resource whose template entry changed is deleted and created again from the new template. Real
308
+ CloudFormation updates most properties in place and keeps what the resource holds. That makes this a
309
+ divergence worth knowing about. A bucket that gains a property loses its objects here, where in AWS
310
+ it would keep them. In-place update is the obvious next step, still to be built.
311
+
312
+ Two things follow from replacement:
313
+
314
+ - A resource naming a replaced resource is replaced too, all the way up the dependency chain, and
315
+ nothing is left pointing at a resource that has gone. Real CloudFormation hands the dependent the
316
+ new physical name and leaves it standing.
317
+ - `UpdateReplacePolicy` is not read. Honouring `Retain` would leave the old resource holding the
318
+ name the replacement needs, and CDK marks buckets and tables with it as a matter of course, so
319
+ every such update would fail. The old resource is deleted whatever the policy says.
320
+
321
+ A failed update leaves the stack in `UPDATE_FAILED` with the reason on it, and leaves the resources
322
+ where the update got to. There is no rollback to the previous template.
323
+ `waitForStackUpdateComplete(...)` rethrows the error, and `DescribeStacksCommand` reports it as
324
+ `StackStatusReason`. Dealing with the cause and sending `UpdateStackCommand` again applies the rest
325
+ of the change.
326
+
327
+ ## Deleting a stack
328
+
329
+ `DeleteStackCommand` deletes the resources a stack created, in the reverse of the order they were
330
+ created in, and then releases the stack name.
331
+
332
+ ```typescript sim-cloudformation-delete-stack
333
+ /**
334
+ * Deleting a simulated CloudFormation Stack with DeleteStackCommand.
335
+ */
336
+
337
+ import {
338
+ CreateStackCommand,
339
+ DeleteStackCommand,
340
+ } from "@aws-sdk/client-cloudformation";
341
+
342
+ import { SimAws } from "@kensio/yulin";
343
+
344
+ const simAws = new SimAws();
345
+ const simCfn = simAws.cloudFormation();
346
+
347
+ const templateBody = JSON.stringify({
348
+ Resources: {
349
+ SiteBucket: {
350
+ Type: "AWS::S3::Bucket",
351
+ Properties: {
352
+ BucketName: "deletable-stack-bucket",
353
+ },
354
+ },
355
+ },
356
+ });
357
+
358
+ await simCfn.createStack(
359
+ new CreateStackCommand({
360
+ StackName: "deletable-stack",
361
+ TemplateBody: templateBody,
362
+ }),
363
+ );
364
+ await simCfn.waitForStackDeployComplete("deletable-stack");
365
+
366
+ await simCfn.deleteStack(
367
+ new DeleteStackCommand({ StackName: "deletable-stack" }),
368
+ );
369
+ await simCfn.waitForStackDeleteComplete("deletable-stack");
370
+
371
+ // The Bucket has gone from simulated S3.
372
+ console.log(simAws.s3().getSimBucketByName("deletable-stack-bucket"));
373
+
374
+ // And the Stack name is free, so the same Stack can be deployed again.
375
+ await simCfn.createStack(
376
+ new CreateStackCommand({
377
+ StackName: "deletable-stack",
378
+ TemplateBody: templateBody,
379
+ }),
380
+ );
381
+ await simCfn.waitForStackDeployComplete("deletable-stack");
382
+ ```
383
+
384
+ Deletion runs in the background, as deployment does. `deleteStack(...)` returns once the stack has
385
+ moved to `DELETE_IN_PROGRESS`, and `waitForStackDeleteComplete(...)` waits for the resources to go.
386
+ `DescribeStacksCommand` reports `DELETE_IN_PROGRESS` in between, and then refuses the stack name with
387
+ a `ValidationError` once the deletion has finished. That is how CloudFormation answers a name it no
388
+ longer holds.
389
+
390
+ Deleting a stack name that was never deployed succeeds, as it does in CloudFormation.
391
+
392
+ ### When a resource cannot be deleted
393
+
394
+ Some resources refuse to go, the same way they do in AWS. An S3 bucket that still holds objects is
395
+ the common one. CloudFormation fails there and never empties the bucket for you. That is why CDK
396
+ ships an `autoDeleteObjects` custom resource.
397
+
398
+ A refusal leaves the stack in `DELETE_FAILED` with the reason on it, and keeps the stack name in use.
399
+ `waitForStackDeleteComplete(...)` rethrows the error, and `DescribeStacksCommand` reports it as
400
+ `StackStatusReason`. Dealing with the cause and sending `DeleteStackCommand` again deletes the stack.
401
+
402
+ ### `DeletionPolicy`
403
+
404
+ A resource declared with `DeletionPolicy: Retain` is left in simulated AWS and reported as
405
+ `DELETE_SKIPPED`, the same as CloudFormation reports it. The rest of the stack still deletes around
406
+ it, and the stack name is still released. `RetainExceptOnCreate` is treated the same way, because the
407
+ two differ only in what a rolled back creation does, and sim CloudFormation never rolls a deployment
408
+ back.
409
+
410
+ Retained resources are readable from the stack:
411
+
412
+ ```typescript
413
+ console.log(stack.retainedResources.map((resource) => resource.logicalId));
414
+ ```
415
+
416
+ ## Parameters
417
+
418
+ Template parameters can be supplied when creating a stack.
419
+
420
+ ```typescript sim-cloudformation-parameters
421
+ /**
422
+ * Supplying simulated CloudFormation Parameters.
423
+ */
424
+
425
+ import { SimAws } from "@kensio/yulin";
426
+
427
+ const simAws = new SimAws();
428
+
429
+ const stack = await simAws.cloudFormation().deployTemplate({
430
+ stackName: "parameter-stack",
431
+ template: {
432
+ Parameters: {
433
+ BucketName: {
434
+ Type: "String",
435
+ Default: "default-parameter-bucket",
436
+ },
437
+ },
438
+ Resources: {
439
+ SiteBucket: {
440
+ Type: "AWS::S3::Bucket",
441
+ Properties: {
442
+ BucketName: {
443
+ Ref: "BucketName",
444
+ },
445
+ },
446
+ },
447
+ },
448
+ },
449
+ parameters: {
450
+ BucketName: "configured-parameter-bucket",
451
+ },
452
+ });
453
+
454
+ await stack.waitForDeployComplete();
455
+
456
+ console.log(
457
+ simAws.s3().getSimBucketByName("configured-parameter-bucket")?.bucketName,
458
+ );
459
+ ```
460
+
461
+ A parameter with no supplied value takes the template default, when the template has one.
462
+
463
+ A parameter's `Type` is read for the `AWS::SSM::Parameter::Value<...>` types, which hold a Parameter
464
+ Store name and resolve to the value stored under it. See
465
+ [reading a parameter through a template Parameter](https://yulinsim.dev/services/ssm/#reading-a-parameter-through-a-template-parameter).
466
+ Every other type is accepted and its value used as written, with no validation of the value against
467
+ the type.
468
+
469
+ ## Intrinsic functions
470
+
471
+ Sim CloudFormation supports common intrinsic functions used by supported resources.
472
+
473
+ ### `Ref`
474
+
475
+ ```typescript sim-cloudformation-ref
476
+ /**
477
+ * Using Ref between simulated CFN resources.
478
+ */
479
+
480
+ import { SimAws } from "@kensio/yulin";
481
+
482
+ const simAws = new SimAws();
483
+
484
+ const stack = await simAws.cloudFormation().deployTemplate({
485
+ stackName: "ref-stack",
486
+ template: {
487
+ Resources: {
488
+ SourceBucket: {
489
+ Type: "AWS::S3::Bucket",
490
+ Properties: {
491
+ BucketName: "source-ref-bucket",
492
+ },
493
+ },
494
+ WebsiteBucket: {
495
+ Type: "AWS::S3::Bucket",
496
+ Properties: {
497
+ BucketName: {
498
+ "Fn::Join": ["-", [{ Ref: "SourceBucket" }, "website"]],
499
+ },
500
+ },
501
+ },
502
+ },
503
+ },
504
+ });
505
+
506
+ await stack.waitForDeployComplete();
507
+
508
+ console.log(
509
+ simAws.s3().getSimBucketByName("source-ref-bucket-website")?.bucketName,
510
+ );
511
+ ```
512
+
513
+ For supported resource types, `Ref` returns the resource-specific CloudFormation value. For example,
514
+ an S3 Bucket `Ref` returns the Bucket name.
515
+
516
+ ### `Fn::GetAtt`
517
+
518
+ ```typescript sim-cloudformation-get-att
519
+ /**
520
+ * Using Fn::GetAtt with a simulated CloudFront Distribution.
521
+ */
522
+
523
+ import { SimAws } from "@kensio/yulin";
524
+
525
+ const simAws = new SimAws();
526
+
527
+ const stack = await simAws.cloudFormation().deployTemplate({
528
+ stackName: "get-att-stack",
529
+ template: {
530
+ Resources: {
531
+ SiteBucket: {
532
+ Type: "AWS::S3::Bucket",
533
+ Properties: {
534
+ BucketName: "get-att-site-bucket",
535
+ },
536
+ },
537
+ SiteDistribution: {
538
+ Type: "AWS::CloudFront::Distribution",
539
+ Properties: {
540
+ DistributionConfig: {
541
+ Enabled: true,
542
+ Origins: [
543
+ {
544
+ Id: "SiteOrigin",
545
+ DomainName: "get-att-site-bucket.s3.amazonaws.com",
546
+ S3OriginConfig: {},
547
+ },
548
+ ],
549
+ DefaultCacheBehavior: {
550
+ TargetOriginId: "SiteOrigin",
551
+ ViewerProtocolPolicy: "allow-all",
552
+ },
553
+ },
554
+ },
555
+ },
556
+ DistributionNameHandle: {
557
+ Type: "AWS::CloudFormation::WaitConditionHandle",
558
+ Properties: {
559
+ Value: {
560
+ "Fn::GetAtt": ["SiteDistribution", "DomainName"],
561
+ },
562
+ },
563
+ },
564
+ },
565
+ },
566
+ });
567
+
568
+ await stack.waitForDeployComplete();
569
+ ```
570
+
571
+ For `AWS::CloudFront::Distribution`, `Fn::GetAtt: ["Distribution", "DomainName"]` returns the
572
+ simulated CloudFront hostname, such as `e123example.cloudfront.net`.
573
+
574
+ #### Values from a skipped Resource
575
+
576
+ A Resource that was skipped, because its type is outside the simulation or because there is no
577
+ simulated Resource to create at all, still answers both intrinsics. `Ref` returns the logical ID, and
578
+ `Fn::GetAtt` returns the string `<logical ID>.<attribute name>`.
579
+
580
+ ```typescript sim-cloudformation-skipped-resource-values
581
+ /**
582
+ * The stand-in values a skipped CloudFormation Resource answers with.
583
+ */
584
+
585
+ import { SimAws } from "@kensio/yulin";
586
+
587
+ const simAws = new SimAws();
588
+
589
+ const stack = await simAws.cloudFormation().deployTemplate({
590
+ stackName: "stand-in-stack",
591
+ template: {
592
+ Resources: {
593
+ AlarmRule: {
594
+ Type: "AWS::CloudWatch::Alarm",
595
+ },
596
+ },
597
+ Outputs: {
598
+ AlarmRef: { Value: { Ref: "AlarmRule" } },
599
+ AlarmArn: { Value: { "Fn::GetAtt": ["AlarmRule", "Arn"] } },
600
+ },
601
+ },
602
+ });
603
+
604
+ await stack.waitForDeployComplete();
605
+
606
+ console.log(stack.output("AlarmRef"));
607
+ // "AlarmRule"
608
+
609
+ console.log(stack.output("AlarmArn"));
610
+ // "AlarmRule.Arn"
611
+
612
+ for (const skipped of stack.skippedResources) {
613
+ console.log(skipped.logicalId, skipped.skippedReason);
614
+ // "AlarmRule Unsupported sim CloudFormation Resource service CloudWatch"
615
+ }
616
+ ```
617
+
618
+ The stand-ins are what lets a template with unsimulated Resources in it deploy at all. Without them,
619
+ every Resource holding a `Ref` or `Fn::GetAtt` to a skipped Resource would fail too, and so would
620
+ every Resource depending on those, until one EventBridge rule took the whole stack down with it. The skip
621
+ stays where it happened.
622
+
623
+ A stand-in is deliberately shaped unlike an ARN. It fails closed wherever the simulator reads it.
624
+
625
+ - In an IAM policy `Resource` it matches no ARN, so a caller relying on that statement is denied.
626
+ - In a property that is parsed as an ARN it is refused as malformed, and that Resource fails.
627
+ Handing `Fn::GetAtt: ["Orders", "StreamArn"]` from a skipped DynamoDB table to an
628
+ `AWS::Lambda::EventSourceMapping` fails with
629
+ `EventSourceArn Orders.StreamArn names no simulated Lambda event source`.
630
+ - Handed to a Lambda function through its environment, it names something absent. The function's own
631
+ SDK call fails the way a call for a missing resource does. A `PutItem` naming the skipped table
632
+ gets `ResourceNotFoundException: No DynamoDB Table named Orders`.
633
+
634
+ A stand-in stands in for something absent, and is never a value to rely on. A test asserting against
635
+ one is asserting on a Resource that was never created. `stack.skippedResources` is where to find out
636
+ which Resources those are and why, under
637
+ [Inspecting stacks and resources](#inspecting-stacks-and-resources).
638
+
639
+ ### `Fn::Join`
640
+
641
+ ```typescript sim-cloudformation-fn-join
642
+ /**
643
+ * Joining literal values and Refs in a simulated CFN template.
644
+ */
645
+
646
+ import { SimAws } from "@kensio/yulin";
647
+
648
+ const simAws = new SimAws();
649
+
650
+ await simAws.cloudFormation().deployTemplate({
651
+ stackName: "join-stack",
652
+ template: {
653
+ Parameters: {
654
+ BucketPrefix: {
655
+ Type: "String",
656
+ Default: "joined",
657
+ },
658
+ },
659
+ Resources: {
660
+ SiteBucket: {
661
+ Type: "AWS::S3::Bucket",
662
+ Properties: {
663
+ BucketName: {
664
+ "Fn::Join": ["-", [{ Ref: "BucketPrefix" }, "site", "bucket"]],
665
+ },
666
+ },
667
+ },
668
+ },
669
+ },
670
+ });
671
+ ```
672
+
673
+ ### `Fn::Sub`
674
+
675
+ ```typescript sim-cloudformation-fn-sub
676
+ /**
677
+ * Substituting parameter and resource values in a simulated CFN template.
678
+ */
679
+
680
+ import { SimAws } from "@kensio/yulin";
681
+
682
+ const simAws = new SimAws();
683
+
684
+ const stack = await simAws.cloudFormation().deployTemplate({
685
+ stackName: "sub-stack",
686
+ template: {
687
+ Parameters: {
688
+ SiteName: {
689
+ Type: "String",
690
+ Default: "docs",
691
+ },
692
+ },
693
+ Resources: {
694
+ SiteBucket: {
695
+ Type: "AWS::S3::Bucket",
696
+ Properties: {
697
+ BucketName: {
698
+ // eslint-disable-next-line no-template-curly-in-string
699
+ "Fn::Sub": "${SiteName}-site-bucket",
700
+ },
701
+ },
702
+ },
703
+ },
704
+ },
705
+ });
706
+
707
+ await stack.waitForDeployComplete();
708
+
709
+ console.log(simAws.s3().getSimBucketByName("docs-site-bucket")?.bucketName);
710
+ ```
711
+
712
+ ### `Fn::FindInMap`
713
+
714
+ A template `Mappings` section holds two levels of keys against a value. `Fn::FindInMap` reads one of
715
+ those values, given the map name, the top-level key and the second-level key.
716
+
717
+ ```typescript sim-cloudformation-fn-find-in-map
718
+ /**
719
+ * Reading a value from template Mappings in a simulated CFN template.
720
+ */
721
+
722
+ import { SimAws } from "@kensio/yulin";
723
+
724
+ const simAws = new SimAws();
725
+
726
+ const stack = await simAws.cloudFormation().deployTemplate({
727
+ stackName: "find-in-map-stack",
728
+ template: {
729
+ Parameters: {
730
+ Environment: {
731
+ Type: "String",
732
+ Default: "staging",
733
+ },
734
+ },
735
+ Mappings: {
736
+ EnvironmentMap: {
737
+ staging: { BucketName: "staging-site-bucket" },
738
+ production: { BucketName: "production-site-bucket" },
739
+ },
740
+ },
741
+ Resources: {
742
+ SiteBucket: {
743
+ Type: "AWS::S3::Bucket",
744
+ Properties: {
745
+ BucketName: {
746
+ "Fn::FindInMap": [
747
+ "EnvironmentMap",
748
+ { Ref: "Environment" },
749
+ "BucketName",
750
+ ],
751
+ },
752
+ },
753
+ },
754
+ },
755
+ },
756
+ });
757
+
758
+ await stack.waitForDeployComplete();
759
+
760
+ console.log(simAws.s3().getSimBucketByName("staging-site-bucket")?.bucketName);
761
+ ```
762
+
763
+ Each of the three arguments can be a nested expression as well as a literal string, as long as it
764
+ resolves to a string. The example above uses a `Ref` to a parameter for the top-level key. A `Ref` to
765
+ the `AWS::Region` pseudo parameter works the same way, for the per-region maps that `Fn::FindInMap`
766
+ is most often used for, and a nested `Fn::FindInMap` can supply any of the three arguments.
767
+
768
+ The value a lookup returns can be any type. A list value is returned as a list.
769
+
770
+ `Fn::FindInMap` is resolved when the template is read, before any resource is created, and can be
771
+ used in resource properties and in `Outputs`. A map name or key missing from `Mappings` fails the
772
+ deployment with an error naming the path that could not be found.
773
+
774
+ ### `Fn::Split` and `Fn::Select`
775
+
776
+ `Fn::Split` cuts a string into a list on a delimiter. `Fn::Select` reads one value out of a list by
777
+ its zero-based index. They are usually written together, to pull one part out of a string another
778
+ resource gave.
779
+
780
+ ```typescript sim-cloudformation-fn-select-split
781
+ /**
782
+ * Naming a bucket after part of another bucket's domain name.
783
+ */
784
+
785
+ import { SimAws } from "@kensio/yulin";
786
+
787
+ const simAws = new SimAws();
788
+
789
+ const stack = await simAws.cloudFormation().deployTemplate({
790
+ stackName: "select-split-stack",
791
+ template: {
792
+ Resources: {
793
+ SiteBucket: {
794
+ Type: "AWS::S3::Bucket",
795
+ Properties: { BucketName: "site-bucket" },
796
+ },
797
+ LogsBucket: {
798
+ Type: "AWS::S3::Bucket",
799
+ Properties: {
800
+ BucketName: {
801
+ "Fn::Join": [
802
+ "-",
803
+ [
804
+ {
805
+ "Fn::Select": [
806
+ 0,
807
+ {
808
+ "Fn::Split": [
809
+ ".",
810
+ { "Fn::GetAtt": ["SiteBucket", "DomainName"] },
811
+ ],
812
+ },
813
+ ],
814
+ },
815
+ "logs",
816
+ ],
817
+ ],
818
+ },
819
+ },
820
+ },
821
+ },
822
+ },
823
+ });
824
+
825
+ await stack.waitForDeployComplete();
826
+
827
+ // site-bucket-logs, from the first part of site-bucket.s3.amazonaws.com
828
+ console.log(simAws.s3().getSimBucketByName("site-bucket-logs")?.bucketName);
829
+ ```
830
+
831
+ The delimiter is a literal string. The string being split can be any expression that resolves to a
832
+ string, including a `Ref`, an `Fn::GetAtt` or another function. A delimiter absent from the string
833
+ gives a one-element list, and a delimiter at the start or end of the string gives an empty element
834
+ there, as CloudFormation does.
835
+
836
+ `Fn::Select` takes its list from a literal list, from `Fn::Split`, or from anything else that
837
+ resolves to a list, such as an `Fn::FindInMap` of a list value. The index is a number or a string of
838
+ digits, so a `Ref` to a parameter can supply it.
839
+
840
+ That pair is how a host is read out of a URL. CDK writes this shape when a CloudFront origin points
841
+ at a Lambda function URL:
842
+
843
+ ```json
844
+ {
845
+ "DomainName": {
846
+ "Fn::Select": [
847
+ 2,
848
+ { "Fn::Split": ["/", { "Fn::GetAtt": ["Url", "FunctionUrl"] }] }
849
+ ]
850
+ }
851
+ }
852
+ ```
853
+
854
+ `https://abc123.lambda-url.eu-west-2.on.aws/` splits into
855
+ `["https:", "", "abc123.lambda-url.eu-west-2.on.aws", ""]`, so index 2 is the host.
856
+
857
+ An index past the end of the list, a negative or fractional index, and a second argument of any type
858
+ but a list all fail the deployment, as they are all templates AWS rejects. The error names the
859
+ resource and the property path the value sat at, for example
860
+ `Sim CloudFormation Resource LogsBucket value at Properties.BucketName`.
861
+
862
+ ### `Fn::ImportValue`
863
+
864
+ `Fn::ImportValue` reads a value another Stack exported. A Stack exports one by giving an Output an
865
+ `Export.Name`, and a Stack in the same Account and Region imports it by that name.
866
+
867
+ CDK writes both halves on its own. Referencing a resource in another Stack of the same app puts an
868
+ `Export` on the producer and an `Fn::ImportValue` on the consumer, with no opt-in.
869
+
870
+ ```typescript sim-cloudformation-fn-import-value
871
+ /**
872
+ * Sharing a value between two simulated CloudFormation Stacks.
873
+ */
874
+
875
+ import { SimAws } from "@kensio/yulin";
876
+
877
+ const simAws = new SimAws();
878
+ const cloudFormation = simAws.cloudFormation();
879
+
880
+ await cloudFormation.deployTemplate({
881
+ stackName: "producer-stack",
882
+ template: {
883
+ Resources: {
884
+ Uploads: {
885
+ Type: "AWS::S3::Bucket",
886
+ Properties: { BucketName: "shared-uploads" },
887
+ },
888
+ },
889
+ Outputs: {
890
+ UploadsBucket: {
891
+ Value: { Ref: "Uploads" },
892
+ Export: { Name: "producer-stack:UploadsBucket" },
893
+ },
894
+ },
895
+ },
896
+ });
897
+
898
+ const consumer = await cloudFormation.deployTemplate({
899
+ stackName: "consumer-stack",
900
+ template: {
901
+ Resources: {
902
+ UploadsTopic: {
903
+ Type: "AWS::SNS::Topic",
904
+ Properties: {
905
+ TopicName: "uploads-topic",
906
+ DisplayName: { "Fn::ImportValue": "producer-stack:UploadsBucket" },
907
+ },
908
+ },
909
+ },
910
+ },
911
+ });
912
+
913
+ await consumer.waitForDeployComplete();
914
+
915
+ // shared-uploads, read from the export the producer Stack published
916
+ console.log(consumer.getResource("UploadsTopic")?.properties["DisplayName"]);
917
+ ```
918
+
919
+ Deploy the producer first. An export is published once the producer's Outputs have resolved, which
920
+ happens after its Resources have been created. A consumer deployed ahead of its producer has
921
+ nothing to import.
922
+
923
+ An import naming an export no Stack has published fails with `No export named <name> found`, the
924
+ way CloudFormation refuses one. A Stack exporting a name another Stack already holds fails to
925
+ deploy. A deleted Stack releases its export names, leaving them free for the next Stack.
926
+
927
+ Exports are scoped per Account and Region, as they are on AWS. A Stack in one Region reads only the
928
+ exports published in that Region.
929
+
930
+ ## Dynamic references
931
+
932
+ A `{{resolve:...}}` dynamic reference reads a value from another service while a resource is being
933
+ created. It is written into the template as ordinary text, so it can sit inside a longer string.
934
+
935
+ `Fn::Sub` and `Fn::Join` resolve first, and the reference is read from the string they built. CDK
936
+ writes that shape whenever a secret sits in the same stack as the resource reading it (the secret's
937
+ ARN arrives as a `Ref`).
938
+
939
+ `{{resolve:ssm:name}}` and `{{resolve:ssm:name:3}}` read a simulated SSM parameter. See
940
+ [reading a parameter with a dynamic reference](https://yulinsim.dev/services/ssm/#reading-a-parameter-with-a-dynamic-reference)
941
+ for what they resolve to, and for what happens when Parameter Store cannot answer one.
942
+
943
+ `{{resolve:secretsmanager:secret-id:secret-string:json-key:version-stage:version-id}}` reads a
944
+ simulated secret. See
945
+ [reading a secret with a dynamic reference](https://yulinsim.dev/services/secretsmanager/#reading-a-secret-with-a-dynamic-reference)
946
+ for the segments and for what a reference Secrets Manager cannot answer resolves to.
947
+
948
+ `{{resolve:ssm-secure:...}}` is left in the template as written, and is not resolved yet.
949
+
950
+ ## Conditions
951
+
952
+ A template `Conditions` section names boolean expressions over the stack's parameter values. A
953
+ condition decides whether a resource is created, and which value `Fn::If` gives a property or an
954
+ output.
955
+
956
+ ```typescript sim-cloudformation-conditions
957
+ /**
958
+ * Choosing resources and property values by condition in a simulated CFN template.
959
+ */
960
+
961
+ import { SimAws } from "@kensio/yulin";
962
+
963
+ const simAws = new SimAws();
964
+
965
+ const stack = await simAws.cloudFormation().deployTemplate({
966
+ stackName: "conditions-stack",
967
+ template: {
968
+ Parameters: {
969
+ EnvName: { Type: "String" },
970
+ },
971
+ Conditions: {
972
+ IsProd: { "Fn::Equals": [{ Ref: "EnvName" }, "prod"] },
973
+ },
974
+ Resources: {
975
+ Backups: {
976
+ Type: "AWS::S3::Bucket",
977
+ Condition: "IsProd",
978
+ Properties: { BucketName: "site-backups" },
979
+ },
980
+ Site: {
981
+ Type: "AWS::S3::Bucket",
982
+ Properties: {
983
+ BucketName: {
984
+ // eslint-disable-next-line no-template-curly-in-string
985
+ "Fn::If": ["IsProd", "site", { "Fn::Sub": "site-${EnvName}" }],
986
+ },
987
+ },
988
+ },
989
+ },
990
+ },
991
+ parameters: { EnvName: "dev" },
992
+ });
993
+
994
+ await stack.waitForDeployComplete();
995
+
996
+ // site-dev
997
+ console.log(simAws.s3().getSimBucketByName("site-dev")?.bucketName);
998
+
999
+ // false, because IsProd is false
1000
+ console.log(stack.getResource("Backups") !== undefined);
1001
+ ```
1002
+
1003
+ ### Writing a condition
1004
+
1005
+ A condition is built from `Fn::Equals`, `Fn::And`, `Fn::Or` and `Fn::Not`. `Fn::And` and `Fn::Or`
1006
+ take a list of two to ten conditions, and `Fn::Not` takes a list of exactly one. A condition can
1007
+ name another condition with `{ "Condition": "OtherCondition" }`, in any order, so a condition may
1008
+ name one written below it in the section.
1009
+
1010
+ ```json
1011
+ {
1012
+ "IsProd": { "Fn::Equals": [{ "Ref": "EnvName" }, "prod"] },
1013
+ "IsStaging": { "Fn::Equals": [{ "Ref": "EnvName" }, "staging"] },
1014
+ "IsDeployed": {
1015
+ "Fn::Or": [{ "Condition": "IsProd" }, { "Condition": "IsStaging" }]
1016
+ }
1017
+ }
1018
+ ```
1019
+
1020
+ `Fn::Equals` compares its two values as strings, as CloudFormation does. A JSON number in the
1021
+ template matches the string a parameter carries.
1022
+
1023
+ The whole section is evaluated once per deployment, before any resource is created. A condition can
1024
+ read parameters and pseudo parameters and nothing else. A comparison that would need a created
1025
+ resource, such as an `Fn::GetAtt`, fails the deployment rather than reading as false.
1026
+
1027
+ ### `Fn::If`
1028
+
1029
+ `Fn::If` takes a condition name, a value to use when it is true, and a value to use when it is
1030
+ false. It works anywhere a resource property or an output value is read.
1031
+
1032
+ Only the branch the condition selects is resolved. The other branch is left alone, and may name a
1033
+ resource this deployment never creates.
1034
+
1035
+ ### The resource `Condition` attribute
1036
+
1037
+ A resource carrying a `Condition` attribute whose condition is false is never created. It is absent
1038
+ from `stack.getResource(...)`. A resource sim CloudFormation skips behaves differently. A skipped resource
1039
+ stays in the stack and answers `Ref` and `Fn::GetAtt` with
1040
+ [stand-in values](#values-from-a-skipped-resource).
1041
+
1042
+ With the resource absent, another resource naming it fails the deployment, with an error naming both
1043
+ resources and the condition. That covers a `Ref` or `Fn::GetAtt` that is actually reached, and a
1044
+ `DependsOn`. A name carried only by the unselected branch of an `Fn::If` is never reached, and never
1045
+ fails.
1046
+
1047
+ A `Condition` attribute naming a condition the template leaves undefined fails the same way.
1048
+
1049
+ ## Resource dependencies
1050
+
1051
+ Resources can depend on each other explicitly with `DependsOn`.
1052
+
1053
+ ```typescript sim-cloudformation-depends-on
1054
+ /**
1055
+ * Explicit resource dependencies in a simulated CFN template.
1056
+ */
1057
+
1058
+ import { SimAws } from "@kensio/yulin";
1059
+
1060
+ const simAws = new SimAws();
1061
+
1062
+ const stack = await simAws.cloudFormation().deployTemplate({
1063
+ stackName: "depends-on-stack",
1064
+ template: {
1065
+ Resources: {
1066
+ SourceBucket: {
1067
+ Type: "AWS::S3::Bucket",
1068
+ Properties: {
1069
+ BucketName: "dependency-source-bucket",
1070
+ },
1071
+ },
1072
+ DependentBucket: {
1073
+ Type: "AWS::S3::Bucket",
1074
+ DependsOn: "SourceBucket",
1075
+ Properties: {
1076
+ BucketName: "dependency-target-bucket",
1077
+ },
1078
+ },
1079
+ },
1080
+ },
1081
+ });
1082
+
1083
+ await stack.waitForDeployComplete();
1084
+ ```
1085
+
1086
+ Resources that reference another resource with `Ref` are also created after the referenced resource
1087
+ is ready.
1088
+
1089
+ ## Deploying synthesized CDK templates
1090
+
1091
+ Use `deployTemplateFile(...)` to deploy a template file, including the JSON templates CDK synthesis
1092
+ produces.
1093
+
1094
+ ```typescript sim-cloudformation-cdk-template-file
1095
+ /**
1096
+ * Deploying a synthesized CDK template file into simulated AWS.
1097
+ */
1098
+
1099
+ import path from "node:path";
1100
+
1101
+ import { SimAws } from "@kensio/yulin";
1102
+
1103
+ const simAws = new SimAws();
1104
+
1105
+ const stack = await simAws
1106
+ .cloudFormation()
1107
+ .deployTemplateFile(
1108
+ path.join(process.cwd(), "cdk.out", "TestStack.template.json"),
1109
+ );
1110
+
1111
+ await stack.waitForDeployComplete();
1112
+ ```
1113
+
1114
+ You can also pass an object when you need extra deployment options:
1115
+
1116
+ ```typescript sim-cloudformation-template-file-options
1117
+ import path from "node:path";
1118
+
1119
+ import { SimAws } from "@kensio/yulin";
1120
+
1121
+ const simAws = new SimAws();
1122
+
1123
+ const stack = await simAws.cloudFormation().deployTemplateFile({
1124
+ templatePath: path.join(process.cwd(), "cdk.out", "TestStack.template.json"),
1125
+ stackName: "local-cdk-stack",
1126
+ });
1127
+
1128
+ await stack.waitForDeployComplete();
1129
+ ```
1130
+
1131
+ This is useful for local integration tests where you want CDK to produce the template, then Yulin to
1132
+ create the simulated resources from that synthesized output template.
1133
+
1134
+ A template path with no file at it is refused with
1135
+ `No Sim CloudFormation template file at <path>`, naming the resolved path. A synthesized template
1136
+ is build output, and a checkout that has yet to synthesize one meets this on the first run.
1137
+
1138
+ ## Deploying a template written as YAML
1139
+
1140
+ CloudFormation takes a template in JSON or in YAML, and a template written by hand is usually YAML.
1141
+ `deployTemplateFile(...)` reads a `.yaml` or `.yml` file as YAML.
1142
+
1143
+ ```yaml
1144
+ Resources:
1145
+ WorkQueue:
1146
+ Type: AWS::SQS::Queue
1147
+ Properties:
1148
+ QueueName: !Sub "${AWS::StackName}-work"
1149
+ Outputs:
1150
+ QueueArn:
1151
+ Value: !GetAtt WorkQueue.Arn
1152
+ ```
1153
+
1154
+ ```typescript sim-cloudformation-yaml-template-file
1155
+ /**
1156
+ * Deploying a hand-written YAML template file into simulated AWS.
1157
+ */
1158
+
1159
+ import path from "node:path";
1160
+
1161
+ import { SimAws } from "@kensio/yulin";
1162
+
1163
+ const simAws = new SimAws();
1164
+
1165
+ const stack = await simAws
1166
+ .cloudFormation()
1167
+ .deployTemplateFile(
1168
+ path.join(process.cwd(), "infrastructure", "work-stack.yaml"),
1169
+ );
1170
+
1171
+ await stack.waitForDeployComplete();
1172
+
1173
+ console.log(stack.stackName); // "work-stack"
1174
+ ```
1175
+
1176
+ The Stack name comes from the file name with the extension dropped, the way a synthesized name drops
1177
+ `.template.json`.
1178
+
1179
+ Short-form tags resolve to what their long forms resolve to. `!GetAtt WorkQueue.Arn` and
1180
+ `Fn::GetAtt: [WorkQueue, Arn]` are the same Output. The tags Yulin reads are `!Ref`, `!GetAtt`,
1181
+ `!Join`, `!Sub`, `!FindInMap`, `!If`, `!Split`, `!Select`, `!ImportValue`, `!And`, `!Equals`, `!Not`,
1182
+ `!Or` and `!Condition`. A tag for an intrinsic Yulin has no behaviour for, such as `!Base64`, fails
1183
+ the deployment by name. Nothing deploys holding the bare value the tag was written against.
1184
+
1185
+ A file that does not parse is refused by naming the resolved path, along with the line and column the
1186
+ parser stopped at. `updateTemplateFile(...)` and watching read the file the same way, and a saved
1187
+ YAML template updates its Stack in place.
1188
+
1189
+ ## A YAML TemplateBody
1190
+
1191
+ `CreateStackCommand` and `UpdateStackCommand` take a YAML `TemplateBody`, as CloudFormation does.
1192
+ The field carries no file name to say which format it holds. Yulin reads the body as JSON, and as
1193
+ YAML when that fails.
1194
+
1195
+ ```typescript sim-cloudformation-yaml-template-body
1196
+ /**
1197
+ * Creating a simulated CloudFormation Stack from a YAML TemplateBody.
1198
+ */
1199
+
1200
+ import { CreateStackCommand } from "@aws-sdk/client-cloudformation";
1201
+
1202
+ import { SimAws } from "@kensio/yulin";
1203
+
1204
+ const simAws = new SimAws();
1205
+ const simCfn = simAws.cloudFormation();
1206
+
1207
+ await simCfn.createStack(
1208
+ new CreateStackCommand({
1209
+ StackName: "work-stack",
1210
+ TemplateBody: [
1211
+ "Resources:",
1212
+ " WorkQueue:",
1213
+ " Type: AWS::SQS::Queue",
1214
+ " Properties:",
1215
+ " QueueName: work-queue",
1216
+ "Outputs:",
1217
+ " QueueArn:",
1218
+ " Value: !GetAtt WorkQueue.Arn",
1219
+ ].join("\n"),
1220
+ }),
1221
+ );
1222
+
1223
+ await simCfn.waitForStackDeployComplete("work-stack");
1224
+
1225
+ const stack = simCfn.getStackByName("work-stack");
1226
+
1227
+ console.log(stack?.outputs.get("QueueArn")?.value);
1228
+ ```
1229
+
1230
+ Short-form tags resolve as they do in a template file, and a body naming the SAM transform is
1231
+ expanded the way a JSON one is. An `UpdateStackCommand` may hand a Stack a YAML body whichever
1232
+ format the Stack was deployed from.
1233
+
1234
+ A body that fails both attempts is refused by naming the Stack, along with what each format made of
1235
+ it.
1236
+
1237
+ ## Deploying a whole cloud assembly
1238
+
1239
+ `deployCdkOut(...)` deploys the Stacks a `cdk.out` directory holds, each into the region its own
1240
+ environment names. The assembly's `manifest.json` is where that comes from, so an app synthesizing
1241
+ several Stacks across several regions needs no loop of its own and no region constants beside it.
1242
+
1243
+ ```typescript sim-cloudformation-cdk-out-assembly
1244
+ /**
1245
+ * Deploying every Stack a synthesized CDK cloud assembly holds.
1246
+ */
1247
+
1248
+ import path from "node:path";
1249
+
1250
+ import { SimAws } from "@kensio/yulin";
1251
+
1252
+ const simAws = new SimAws({ defaultRegionName: "eu-west-2" });
1253
+
1254
+ const stacks = await simAws
1255
+ .cloudFormation()
1256
+ .deployCdkOut(path.join(process.cwd(), "cdk.out"));
1257
+
1258
+ const siteStack = stacks.get("SiteStack");
1259
+ const dnsStack = stacks.get("DnsStack");
1260
+
1261
+ console.log(siteStack?.getResource("SiteBucket")?.simResource);
1262
+ console.log(dnsStack?.getResource("SiteRecord")?.simResource);
1263
+ ```
1264
+
1265
+ A Stack synthesized with `env: { region: "us-east-1" }` deploys into simulated us-east-1, whatever
1266
+ region the call was made in. A Stack synthesized without `env` takes the region of the scope it was
1267
+ asked through, and every Stack takes that scope's Account.
1268
+
1269
+ Stacks deploy in an order their manifest dependencies allow, so a Stack that consumes another
1270
+ Stack's export goes second. The deployed Stacks come back keyed by name, each one the same
1271
+ `SimCfnStack` `deployTemplateFile(...)` answers with.
1272
+
1273
+ `cloudfront.experimental.EdgeFunction` in a Stack outside us-east-1 is one construct that needs the
1274
+ whole assembly. It puts the function in a us-east-1 support Stack, and the using Stack reads the
1275
+ function's ARN back from an SSM parameter that Stack wrote. Both have to deploy for the read to find
1276
+ anything. See [simulated Lambda@Edge](https://yulinsim.dev/services/cloudfront/#simulated-lambdaedge).
1277
+
1278
+ ### Deploying part of an assembly
1279
+
1280
+ Most apps synthesize Stacks a test has no use for, a deployment pipeline among them. `stackNames`
1281
+ picks the ones to deploy, naming each by Stack name or by CDK artifact ID.
1282
+
1283
+ ```typescript sim-cloudformation-cdk-out-stack-names
1284
+ import { SimAws } from "@kensio/yulin";
1285
+
1286
+ const simAws = new SimAws({ defaultRegionName: "eu-west-2" });
1287
+
1288
+ const stacks = await simAws.cloudFormation().deployCdkOut({
1289
+ directoryPath: "cdk.out",
1290
+ stackNames: ["SiteStack", "DnsStack"],
1291
+ });
1292
+
1293
+ console.log(stacks.keys().toArray());
1294
+ ```
1295
+
1296
+ Naming a Stack the assembly lacks fails the call, listing the Stacks it does hold.
1297
+
1298
+ The order the Stacks are named in is the order they deploy in. A Stack the manifest says another
1299
+ depends on still goes first, whatever order the two are named in, and an assembly deployed whole
1300
+ keeps the order its own manifest holds.
1301
+
1302
+ ### Bindings and transforms for one Stack
1303
+
1304
+ A call naming a directory has no single template to attach bindings to, so `stackOptions` keys them
1305
+ by Stack. Each entry takes the `bindings`, `parameters` and `transform` that
1306
+ `deployTemplateFile(...)` takes for one template.
1307
+
1308
+ ```typescript sim-cloudformation-cdk-out-stack-options
1309
+ import { SimAws } from "@kensio/yulin";
1310
+
1311
+ const simAws = new SimAws({ defaultRegionName: "eu-west-2" });
1312
+
1313
+ const stacks = await simAws.cloudFormation().deployCdkOut({
1314
+ directoryPath: "cdk.out",
1315
+ stackNames: ["ApiStack"],
1316
+ stackOptions: {
1317
+ ApiStack: {
1318
+ parameters: { Stage: "test" },
1319
+ bindings: [
1320
+ {
1321
+ logicalId: "UploadFunction",
1322
+ handler: (): { statusCode: number } => ({ statusCode: 200 }),
1323
+ },
1324
+ ],
1325
+ },
1326
+ },
1327
+ });
1328
+
1329
+ console.log(stacks.get("ApiStack")?.stackName);
1330
+ ```
1331
+
1332
+ An options key matching no Stack being deployed fails the call, so a renamed Stack takes its
1333
+ bindings with it rather than quietly losing them.
1334
+
1335
+ ### Transforming a Stack with an earlier Stack's values
1336
+
1337
+ A `stackOptions` transform is handed the Stacks the same call has already deployed, keyed by Stack
1338
+ name. A CDK app that creates a certificate in one Stack and uses it in another passes the ARN across
1339
+ as a plain string, and the ARN the synthesized template carries belongs to the real account.
1340
+ Simulated ACM issues its own. Reading it back off the Stack that created it keeps both Stacks in one
1341
+ `deployCdkOut` call.
1342
+
1343
+ ```typescript sim-cloudformation-cdk-out-stack-transform
1344
+ import { SimAws } from "@kensio/yulin";
1345
+ import type { CfnTemplateBodyRecord } from "@kensio/yulin/cloudformation";
1346
+
1347
+ const simAws = new SimAws({ defaultRegionName: "eu-west-2" });
1348
+
1349
+ /** The ARN the CDK app pins, because the Stack that issues it is another one. */
1350
+ const synthesizedCertificateArn =
1351
+ "arn:aws:acm:us-east-1:111122223333:certificate/11111111-2222-3333-4444-555555555555";
1352
+
1353
+ /** Put the ARN simulated ACM issued wherever the synthesized one is named. */
1354
+ function withSimulatedCertificate(
1355
+ template: CfnTemplateBodyRecord,
1356
+ certificateArn: string,
1357
+ ): CfnTemplateBodyRecord {
1358
+ return JSON.parse(
1359
+ JSON.stringify(template).replaceAll(
1360
+ synthesizedCertificateArn,
1361
+ () => certificateArn,
1362
+ ),
1363
+ ) as CfnTemplateBodyRecord;
1364
+ }
1365
+
1366
+ const stacks = await simAws.cloudFormation().deployCdkOut({
1367
+ directoryPath: "cdk.out",
1368
+ stackNames: ["DnsStack", "SiteStack"],
1369
+ stackOptions: {
1370
+ SiteStack: {
1371
+ transform: (template, deployed): CfnTemplateBodyRecord =>
1372
+ withSimulatedCertificate(
1373
+ template,
1374
+ deployed.get("DnsStack")?.output("SiteCertificateArn") ?? "",
1375
+ ),
1376
+ },
1377
+ },
1378
+ });
1379
+
1380
+ console.log(stacks.get("SiteStack")?.stackName);
1381
+ ```
1382
+
1383
+ The map holds the Stacks deployed ahead of this one and nothing else. The first Stack to deploy is
1384
+ handed an empty one. Naming the Stacks in the order they have to go in is what puts the certificate
1385
+ there in time, since two Stacks passing a plain string between them declare no dependency for the
1386
+ manifest to carry.
1387
+
1388
+ ## The principal a deployment runs as
1389
+
1390
+ Each resource is created through the command an SDK caller would reach, and that command
1391
+ authorizes. A deployment that names no principal is decided as the account root, which is what IAM
1392
+ falls back to everywhere else in the simulation. `caller` says otherwise.
1393
+
1394
+ ```typescript sim-cloudformation-deployment-caller
1395
+ /**
1396
+ * Deploying a template as the principal a real deployment would run as.
1397
+ */
1398
+
1399
+ import { SimAws } from "@kensio/yulin";
1400
+
1401
+ const simAws = new SimAws({ defaultAccountId: "123456789012" });
1402
+
1403
+ const stack = await simAws.cloudFormation().deployTemplate({
1404
+ stackName: "reports-stack",
1405
+ template: {
1406
+ Resources: {
1407
+ ReportsBucket: {
1408
+ Type: "AWS::S3::Bucket",
1409
+ Properties: { BucketName: "reports-bucket" },
1410
+ },
1411
+ },
1412
+ },
1413
+ caller: {
1414
+ kind: "arn",
1415
+ arn: "arn:aws:iam::123456789012:role/cdk-deploy-role",
1416
+ },
1417
+ });
1418
+
1419
+ console.log(stack.getResource("ReportsBucket")?.status); // "CREATE_COMPLETE"
1420
+ ```
1421
+
1422
+ `deployTemplateFile(...)` takes the same option. Every resource the stack creates is authorized as
1423
+ that principal, and so is every resource its teardown deletes. An update applied through
1424
+ `updateTemplateFile(...)` runs as it too, unless the update names a principal of its own.
1425
+
1426
+ The named principal has to be allowed what the template asks for. IAM allows the account root by
1427
+ default and allows a role only what its policies say. A role with no S3 permission leaves the bucket
1428
+ above `CREATE_FAILED`, with the role's ARN in the message.
1429
+
1430
+ ### One caller for an assembly, and one per Stack
1431
+
1432
+ `deployCdkOut(...)` takes a caller for every Stack in the assembly. A Stack deployed by a different
1433
+ role names its own in `stackOptions`, beside its parameters and its bindings.
1434
+
1435
+ ```typescript sim-cloudformation-cdk-out-caller
1436
+ /**
1437
+ * A caller for the whole cloud assembly, and one for a single Stack.
1438
+ */
1439
+
1440
+ import { SimAws } from "@kensio/yulin";
1441
+
1442
+ const simAws = new SimAws({
1443
+ defaultAccountId: "123456789012",
1444
+ defaultRegionName: "eu-west-2",
1445
+ });
1446
+
1447
+ const stacks = await simAws.cloudFormation().deployCdkOut({
1448
+ directoryPath: "cdk.out",
1449
+ caller: {
1450
+ kind: "arn",
1451
+ arn: "arn:aws:iam::123456789012:role/cdk-deploy-role",
1452
+ },
1453
+ stackOptions: {
1454
+ PipelineStack: {
1455
+ caller: {
1456
+ kind: "arn",
1457
+ arn: "arn:aws:iam::123456789012:role/pipeline-deploy-role",
1458
+ },
1459
+ },
1460
+ },
1461
+ });
1462
+
1463
+ console.log(stacks.get("PipelineStack")?.stackName);
1464
+ ```
1465
+
1466
+ A staged CDK asset is published under the same principal, before CloudFormation reads the template
1467
+ that points at it.
1468
+
1469
+ ## Editing a synthesized template before deploying it
1470
+
1471
+ Sometimes a synthesized template needs a change before Yulin will deploy it, such as dropping a
1472
+ resource or property that this simulator refuses. Read the file, edit the parsed object,
1473
+ then deploy it with `deployTemplate(...)`, naming the file it came from:
1474
+
1475
+ ```typescript sim-cloudformation-cdk-edited-template
1476
+ /**
1477
+ * Deploying a synthesized CDK template edited in memory.
1478
+ */
1479
+
1480
+ import { readFile } from "node:fs/promises";
1481
+ import path from "node:path";
1482
+
1483
+ import { SimAws } from "@kensio/yulin";
1484
+
1485
+ const templatePath = path.join(
1486
+ process.cwd(),
1487
+ "cdk.out",
1488
+ "TestStack.template.json",
1489
+ );
1490
+
1491
+ const synthesized = JSON.parse(await readFile(templatePath, "utf8")) as {
1492
+ Resources: Record<string, { Type: string }>;
1493
+ };
1494
+
1495
+ const resources = Object.fromEntries(
1496
+ Object.entries(synthesized.Resources).filter(
1497
+ ([logicalId]) => logicalId !== "AnalyticsQueue",
1498
+ ),
1499
+ );
1500
+
1501
+ const simAws = new SimAws();
1502
+
1503
+ const stack = await simAws.cloudFormation().deployTemplate({
1504
+ stackName: "local-cdk-stack",
1505
+ template: { ...synthesized, Resources: resources },
1506
+ templatePath,
1507
+ });
1508
+
1509
+ await stack.waitForDeployComplete();
1510
+ ```
1511
+
1512
+ The `template` object is what gets deployed, and the template file itself goes unread.
1513
+ `templatePath` only tells Yulin which cloud assembly the template came from, letting it find the
1514
+ sibling `TestStack.assets.json` manifest and the staged asset directories beside it. Without it,
1515
+ anything that needs a CDK asset, such as a `Custom::CDKBucketDeployment` or a Lambda function
1516
+ bundled with `Code.fromAsset`, fails with `No CDK assets manifest is available.`
1517
+
1518
+ ## Adapting a synthesized template on the way in
1519
+
1520
+ Editing the parsed object works for a template you deploy once. A template you keep reading, because
1521
+ it is [watched](#watching-a-template-file) or applied again as an update, needs the same change made
1522
+ every time it is read. `transform` is that hook. It is given the parsed template and answers with the
1523
+ one to deploy, on the deployment and again on every change:
1524
+
1525
+ ```typescript sim-cloudformation-transform-template-file
1526
+ /**
1527
+ * Adapting a synthesized template every time it is read.
1528
+ */
1529
+
1530
+ import { SimAws } from "@kensio/yulin";
1531
+ import type { CfnTemplateBodyRecord } from "@kensio/yulin/cloudformation";
1532
+
1533
+ const simAws = new SimAws();
1534
+
1535
+ /**
1536
+ * Drop the records pointing at a hosted zone that only exists in the real
1537
+ * account.
1538
+ */
1539
+ function withoutDnsRecords(
1540
+ template: CfnTemplateBodyRecord,
1541
+ ): CfnTemplateBodyRecord {
1542
+ const resources = Object.fromEntries(
1543
+ Object.entries(template.Resources).filter(
1544
+ ([, resource]) =>
1545
+ (resource as { Type?: string }).Type !== "AWS::Route53::RecordSet",
1546
+ ),
1547
+ );
1548
+
1549
+ return { ...template, Resources: resources };
1550
+ }
1551
+
1552
+ await simAws.cloudFormation().deployTemplateFile({
1553
+ templatePath: "cdk.out/TestStack.template.json",
1554
+ transform: withoutDnsRecords,
1555
+ watch: true,
1556
+ });
1557
+ ```
1558
+
1559
+ This is for what a simulation cannot resolve at all, such as an ARN carrying a real account or a
1560
+ hosted zone ID that came from `HostedZone.fromLookup`. A property Yulin leaves unmodelled rarely
1561
+ needs it. S3, DynamoDB, Cognito, API Gateway v2, SQS and KMS
1562
+ [record it and carry on](#properties-a-resource-was-created-without).
1563
+
1564
+ The template file is still the real one. There is no derived `.local.template.json` in `cdk.out` to
1565
+ keep in step with it. Staged assets resolve as they always did, from the assets manifest beside
1566
+ `templatePath`, since the cloud assembly is found by path and never read out of the template.
1567
+
1568
+ `updateTemplateFile(...)` takes it too, for a consumer driving updates itself. Give it the same
1569
+ deployment object, and the difference applied is the difference in the file.
1570
+
1571
+ A transform that throws fails the deployment, with what it threw as the cause. On a watched change it
1572
+ is reported the way a failed update is. The stack keeps the resources it had, and the watch carries on
1573
+ to the next save.
1574
+
1575
+ ## Applying a changed template file
1576
+
1577
+ `updateTemplateFile(...)` reads a deployed template file again and applies it to its stack, the same
1578
+ way [`UpdateStackCommand`](#updating-a-stack) applies a changed template body. Give it what the
1579
+ deployment was given, since parameters are part of what an update applies:
1580
+
1581
+ ```typescript sim-cloudformation-update-template-file
1582
+ /**
1583
+ * Applying a synthesized template file to the stack it was deployed as.
1584
+ */
1585
+
1586
+ import path from "node:path";
1587
+
1588
+ import { SimAws } from "@kensio/yulin";
1589
+
1590
+ const templatePath = path.join(
1591
+ process.cwd(),
1592
+ "cdk.out",
1593
+ "TestStack.template.json",
1594
+ );
1595
+
1596
+ const simAws = new SimAws();
1597
+
1598
+ await simAws.cloudFormation().deployTemplateFile({ templatePath });
1599
+
1600
+ // Something synthesizes the stack again here.
1601
+
1602
+ await simAws.cloudFormation().updateTemplateFile({ templatePath });
1603
+ ```
1604
+
1605
+ The sibling assets manifest is read again with the template. A resource the update replaces reads the
1606
+ assets that synthesis staged, not the ones the stack was deployed with.
1607
+
1608
+ A file written without being changed is refused with `No updates are to be performed.`, and a failed
1609
+ update leaves the stack in `UPDATE_FAILED` holding whatever the update reached. There is no rollback
1610
+ to the template it was deployed from. A failure part way through has already deleted, replaced or
1611
+ created some of the resources the change asked for.
1612
+
1613
+ ## Watching a template file
1614
+
1615
+ A local dev process holds simulated data that a restart would throw away, and a template file is
1616
+ data rather than code. `watch` keeps reading the file, so re-synthesizing the stack updates it in
1617
+ place while the process carries on:
1618
+
1619
+ ```typescript sim-cloudformation-watch-template-file
1620
+ /**
1621
+ * Updating a deployed stack whenever its template file is synthesized again.
1622
+ */
1623
+
1624
+ import path from "node:path";
1625
+
1626
+ import { SimAws } from "@kensio/yulin";
1627
+ import { serveSimAws } from "@kensio/yulin/serve";
1628
+
1629
+ const simAws = new SimAws();
1630
+ const srv = await serveSimAws({ simAws, port: 8787, liveReload: true });
1631
+
1632
+ await simAws.cloudFormation().deployTemplateFile({
1633
+ templatePath: path.join(process.cwd(), "cdk.out", "TestStack.template.json"),
1634
+ watch: {
1635
+ reload: srv,
1636
+ },
1637
+ });
1638
+ ```
1639
+
1640
+ `watch: true` watches with nothing to do afterwards.
1641
+
1642
+ `reload` is the local server, and reloads the browsers connected to it once the update is complete.
1643
+ It reloads when the resources have changed, not when the write lands. A browser therefore arrives on
1644
+ the resources the new template asked for. A write that changed nothing is a no-op, and reloads
1645
+ nothing. A failed update reloads nothing either, since a browser has no business on a stack the
1646
+ update never reached. Anything with a `reload()` method will do, and a test can watch a template
1647
+ without serving anything.
1648
+
1649
+ A server serving without live reload can never reload anything, and says so as the deployment asks it
1650
+ to. Saying it on the first change instead would be a long way from the mistake. Serve with
1651
+ [`{ liveReload: true }`](https://yulinsim.dev/serve/#live-reload).
1652
+
1653
+ `onUpdated` runs once the update is complete too, for whatever else a change is worth doing, with or
1654
+ without a `reload` alongside it. Given both, the callback runs first and the reload follows it, and a
1655
+ browser arriving on the new resources finds whatever the callback left ready for it.
1656
+
1657
+ `onFailed` is given the update the changed template failed to survive. It reports the failure and
1658
+ nothing else.
1659
+ The stack is left holding whatever the update reached, as it is after an update through the command.
1660
+ What a failure does keep is the process, and the resources it never got to. A template that no longer
1661
+ deploys leaves a working environment where a restart on it would leave none.
1662
+
1663
+ A burst of writes is one update. Saving a file is several filesystem events, so changes are held
1664
+ until they stop arriving. `settleMs` is how long that wait is, and it defaults to the 250ms
1665
+ [`yulin watch` settles at](https://yulinsim.dev/serve/#one-restart-for-a-burst-of-writes). A synth that
1666
+ keeps writing is updated from after five seconds of it, without waiting for it to stop.
1667
+
1668
+ [`transform`](#adapting-a-synthesized-template-on-the-way-in) runs again on every change. A template
1669
+ that needs adapting before Yulin will take it can still be watched as the file synthesis writes.
1670
+
1671
+ Watching holds a filesystem handle open, so the process stays alive on its own. That is what a dev
1672
+ process wants. Anything with an end, such as a test, calls `stopWatchingTemplateFiles()` when it is
1673
+ done. `watchedTemplateFiles()` names what is being watched. Both are per Account and Region, and a
1674
+ simulation deploying into more than one has one call each.
1675
+ [`simAws.close()`](https://yulinsim.dev/serve/#stopping-and-restarting) is the exception. It lets go of the
1676
+ template watches in every scope, along with everything else the environment is holding, and a served
1677
+ environment gets that from `srv.close()`.
1678
+
1679
+ Yulin never synthesizes anything. It reads the output template, so run your own `cdk synth` and let
1680
+ the watch pick up what it writes.
1681
+
1682
+ ### Under `yulin watch`
1683
+
1684
+ [`yulin watch`](https://yulinsim.dev/serve/#restarting-on-a-file-change) restarts the process when a
1685
+ deployed template changes. A watched template is left to the process that is watching it instead. The
1686
+ stack updates in place, and everything held in simulated S3, DynamoDB and SQS stays where it is. That
1687
+ needs no configuring, because the process names the file it is holding.
1688
+
1689
+ ## CDK S3 BucketDeployment
1690
+
1691
+ Yulin can simulate selected CDK custom resources. A common use case is CDK S3 BucketDeployment,
1692
+ where local files are deployed into a simulated S3 Bucket.
1693
+
1694
+ ```typescript sim-cloudformation-cdk-bucket-deployment
1695
+ /**
1696
+ * Serving CDK BucketDeployment files through simulated S3.
1697
+ */
1698
+
1699
+ import { SimAws } from "@kensio/yulin";
1700
+ import { serveSimAws } from "@kensio/yulin/serve";
1701
+
1702
+ const simAws = new SimAws();
1703
+ const srv = await serveSimAws({ simAws });
1704
+
1705
+ try {
1706
+ await simAws
1707
+ .cloudFormation()
1708
+ .deployTemplateFile("cdk.out/TestStack.template.json");
1709
+
1710
+ const response = await fetch(
1711
+ `http://foo-bucket.s3-website.us-east-1.sim-aws.localhost:${srv.port}/`,
1712
+ );
1713
+
1714
+ console.log(response.status);
1715
+ console.log(await response.text());
1716
+ } finally {
1717
+ await srv.close();
1718
+ }
1719
+ ```
1720
+
1721
+ The files in the staged asset directory become Objects in the destination Bucket, keyed by their path
1722
+ relative to the asset root. When the Bucket is configured for website hosting, or sits behind a
1723
+ CloudFront Distribution, those Objects are what gets served.
1724
+
1725
+ This is the `aws s3 sync` the real provider function shells out to. The properties CDK synthesizes
1726
+ around it are read the same way:
1727
+
1728
+ - `DestinationBucketKeyPrefix` puts the Objects under a key prefix.
1729
+ - `Exclude` and `Include` choose which files are copied. Every `Exclude` pattern is applied first and
1730
+ then every `Include` one, and the last pattern to match a path decides. That is what makes
1731
+ `exclude: ["*"], include: ["*.txt"]` mean "only the text files". A file no pattern matches is
1732
+ copied. `*` matches across `/`, so `data/*` covers everything under a `data` directory.
1733
+ - `SystemMetadata` sets content headers on every Object the deployment copies, such as
1734
+ `content-encoding` or `cache-control`. Without it, the content type is guessed from the file
1735
+ extension. See [Object system metadata](https://yulinsim.dev/services/s3/#object-system-metadata) for what comes back
1736
+ on a read. The deployment also tells the destination Bucket what it publishes. A directory
1737
+ [mounted over that Bucket](https://yulinsim.dev/services/s3/#inheriting-what-the-deployment-set) for local
1738
+ development is then served with the same headers without restating them.
1739
+ - `Prune` removes the Objects the deployment covers and its source no longer holds. It is on unless
1740
+ the deployment turns it off, as the construct is. Pruning only considers what the filters and the
1741
+ key prefix select, and leaves alone any Object the deployment would never have copied.
1742
+
1743
+ Several deployments can share one Bucket. That is the usual arrangement when the headers differ by
1744
+ file type, since a `BucketDeployment` sets them for all of its files at once. A second deployment is
1745
+ how the rest of the site gets different ones. Give the second one `prune: false`, or filters that miss
1746
+ what the first one covers, the same as you would in AWS.
1747
+
1748
+ A deployment with more than one entry in `SourceObjectKeys` copies each source in turn, and a path
1749
+ two of them share ends up with the later one's content.
1750
+
1751
+ Filter patterns take `*` and `?`. The CLI also takes character classes such as `[abc]`, and a pattern
1752
+ using one is refused by name. Matching it as written risks copying the wrong files, since the pattern
1753
+ quietly means something else.
1754
+
1755
+ ### The provider CDK synthesizes
1756
+
1757
+ One `BucketDeployment` construct is four resources in the synthesized template, and only one of them
1758
+ is the `Custom::CDKBucketDeployment` above. The other three are the provider that would have run it
1759
+ in AWS: an AWS CLI Lambda Layer, a Python Lambda function, and that function's log group. A second
1760
+ deployment adds another Layer and another custom resource, and shares the one function, because CDK
1761
+ builds it as a singleton.
1762
+
1763
+ None of those three do anything here. Yulin makes the copy itself. The function is never invoked, the
1764
+ Layer it would have loaded the CLI from is never read, and nothing is ever written to the log
1765
+ group. The function and the Layer are reported in
1766
+ [`stack.inertResources`](#resources-deliberately-left-out) rather than as skipped resources, and a
1767
+ stack whose deployments all worked reports no gaps at all. The log group is created like any other and
1768
+ stays empty, as an account's would.
1769
+
1770
+ That matters beyond tidiness. Sim Lambda declines the provider on its Python runtime with a message
1771
+ saying to [bind a real in-process handler](#lambda-function-bindings) to the function. That is sound
1772
+ advice for a Python function of your own, and exactly the wrong thing to do here. It would replace a
1773
+ working simulation with a hand-written one.
1774
+
1775
+ The provider is found through the `ServiceToken` its custom resource names it by, not by the logical
1776
+ ID CDK generated for it. That ID is a hash of the construct path, and no kind of thing to match on.
1777
+
1778
+ ## S3 Bucket notifications
1779
+
1780
+ The `NotificationConfiguration` property of `AWS::S3::Bucket` deploys through the ordinary
1781
+ `PutBucketNotificationConfiguration` path. An Object put into the deployed Bucket reaches the deployed
1782
+ function. CloudFormation spells the configuration differently from the SDK in four places, and Yulin
1783
+ reads the CloudFormation spelling and refuses the others. Accepting them would deploy a configuration
1784
+ that quietly lost its filter.
1785
+
1786
+ Real CloudFormation has a circular dependency here. The Bucket needs the function's ARN and the
1787
+ function's permission needs the Bucket's ARN. A template therefore hardcodes `BucketName`, names the
1788
+ Bucket by ARN literal on the permission, and adds a `DependsOn` so the permission is in place before
1789
+ S3 validates the destination. Simulated CloudFormation needs the same, and surfaces the alternative as
1790
+ a dependency resolution failure.
1791
+
1792
+ Note that every other `AWS::S3::Bucket` property the simulator has no behaviour for fails the stack
1793
+ by name. See [Buckets from CloudFormation](https://yulinsim.dev/services/s3/#buckets-from-cloudformation).
1794
+
1795
+ ### From a CDK app
1796
+
1797
+ `bucket.addEventNotification(...)` synthesizes a `Custom::S3BucketNotifications` resource rather than
1798
+ a Bucket property. Sim CloudFormation applies the configuration it carries through the ordinary
1799
+ `PutBucketNotificationConfiguration` path, and an Object put into the deployed Bucket reaches the
1800
+ deployed function.
1801
+
1802
+ Deploy into an Account and Region matching the ones the CDK app synthesized for. The `SourceAccount`
1803
+ on the `AWS::Lambda::Permission` CDK writes beside the notification is a synth-time literal. A stack
1804
+ deployed into another Account leaves S3 unable to validate the destination, and the stack fails.
1805
+
1806
+ See [Event notifications](https://yulinsim.dev/services/s3/#event-notifications) in the S3 docs for the configuration
1807
+ itself and what it refuses.
1808
+
1809
+ ## CloudFront resources from CDK
1810
+
1811
+ Sim CloudFormation can create CloudFront Distributions from CloudFormation or CDK templates.
1812
+
1813
+ ```typescript sim-cloudformation-cloudfront-distribution
1814
+ /**
1815
+ * Deploying a template with S3 and CloudFront resources.
1816
+ */
1817
+
1818
+ import { SimAws } from "@kensio/yulin";
1819
+
1820
+ const simAws = new SimAws();
1821
+
1822
+ const stack = await simAws.cloudFormation().deployTemplate({
1823
+ stackName: "cloudfront-stack",
1824
+ template: {
1825
+ Resources: {
1826
+ SiteBucket: {
1827
+ Type: "AWS::S3::Bucket",
1828
+ Properties: {
1829
+ BucketName: "cloudfront-site-bucket",
1830
+ },
1831
+ },
1832
+ SiteDistribution: {
1833
+ Type: "AWS::CloudFront::Distribution",
1834
+ Properties: {
1835
+ DistributionConfig: {
1836
+ Aliases: ["cdn.example.test"],
1837
+ Enabled: true,
1838
+ Origins: [
1839
+ {
1840
+ Id: "SiteOrigin",
1841
+ DomainName: "cloudfront-site-bucket.s3.amazonaws.com",
1842
+ S3OriginConfig: {},
1843
+ },
1844
+ ],
1845
+ DefaultCacheBehavior: {
1846
+ TargetOriginId: "SiteOrigin",
1847
+ ViewerProtocolPolicy: "allow-all",
1848
+ },
1849
+ },
1850
+ },
1851
+ },
1852
+ },
1853
+ },
1854
+ });
1855
+
1856
+ await stack.waitForDeployComplete();
1857
+
1858
+ const distributionResource = stack.getResource("SiteDistribution");
1859
+ console.log(distributionResource?.simResource);
1860
+ ```
1861
+
1862
+ When served on localhost, the resulting Distribution can be requested through its simulated
1863
+ CloudFront hostname adapted to the local server.
1864
+
1865
+ ## CloudFront Function bindings
1866
+
1867
+ When a CDK template contains a CloudFront Function, you can bind the template resource to a real
1868
+ local handler function. This lets local integration tests execute the same handler function that
1869
+ will run at the CloudFront edge.
1870
+
1871
+ ```typescript sim-cloudformation-cloudfront-function-binding
1872
+ /**
1873
+ * Binding a local CloudFront Function handler during template deployment.
1874
+ */
1875
+
1876
+ import { SimAws } from "@kensio/yulin";
1877
+ import type { CloudFrontFunction } from "@kensio/yulin/cloudfront";
1878
+ import { serveSimAws } from "@kensio/yulin/serve";
1879
+
1880
+ const simAws = new SimAws();
1881
+ const srv = await serveSimAws({ simAws });
1882
+
1883
+ function viewerRequestHandler(
1884
+ event: CloudFrontFunction.ViewerRequestEvent,
1885
+ ): CloudFrontFunction.Request | CloudFrontFunction.Response {
1886
+ if (event.request.uri === "/redirect-me.html") {
1887
+ return {
1888
+ statusCode: 302,
1889
+ statusDescription: "Found",
1890
+ headers: {
1891
+ location: {
1892
+ value: "https://example.test/from-bound-handler.html",
1893
+ },
1894
+ },
1895
+ };
1896
+ }
1897
+
1898
+ return event.request;
1899
+ }
1900
+
1901
+ try {
1902
+ const stack = await simAws.cloudFormation().deployTemplateFile({
1903
+ templatePath: "cdk.out/TestStack.template.json",
1904
+ bindings: [
1905
+ {
1906
+ logicalId: "RewriteFunction",
1907
+ handler: viewerRequestHandler,
1908
+ },
1909
+ ],
1910
+ });
1911
+
1912
+ await stack.waitForDeployComplete();
1913
+
1914
+ const distributionResource = stack.getResource("SiteDistribution");
1915
+ const distribution = distributionResource?.simResource;
1916
+
1917
+ if (
1918
+ distribution === undefined ||
1919
+ !("distributionId" in distribution) ||
1920
+ typeof distribution.distributionId !== "string"
1921
+ ) {
1922
+ throw new Error("Expected simulated CloudFront Distribution");
1923
+ }
1924
+
1925
+ const distributionHost = `${distribution.distributionId.toLowerCase()}.cloudfront.net`;
1926
+ const response = await fetch(
1927
+ srv.localUrl(`http://${distributionHost}/redirect-me.html`),
1928
+ { redirect: "manual" },
1929
+ );
1930
+
1931
+ console.log(response.status);
1932
+ console.log(response.headers.get("location"));
1933
+ } finally {
1934
+ await srv.close();
1935
+ }
1936
+ ```
1937
+
1938
+ The `bindings` array matches a template resource logical ID to a local handler function. Use it when
1939
+ CDK has embedded or transformed CloudFront Function source in synthesized output, but your test wants
1940
+ to provide an executable local function directly.
1941
+
1942
+ ## Lambda function bindings
1943
+
1944
+ `AWS::Lambda::Function` resources support the same bindings. The deployed function is backed by your
1945
+ real in-process handler. Tests can close over test state and step through the handler in a debugger,
1946
+ while the stack still wires roles, grants and references as the template declares. A bound function
1947
+ may omit template `Code` and `Handler` entirely.
1948
+
1949
+ ```typescript sim-cloudformation-lambda-binding
1950
+ /**
1951
+ * Binding a real in-process Lambda handler during template deployment.
1952
+ */
1953
+
1954
+ import { InvokeCommand } from "@aws-sdk/client-lambda";
1955
+ import { SimAws } from "@kensio/yulin";
1956
+
1957
+ const simAws = new SimAws();
1958
+
1959
+ await simAws.cloudFormation().deployTemplate({
1960
+ stackName: "greeter-stack",
1961
+ template: {
1962
+ Resources: {
1963
+ GreeterFunction: {
1964
+ Type: "AWS::Lambda::Function",
1965
+ Properties: {
1966
+ FunctionName: "greeter",
1967
+ Role: "arn:aws:iam::111111111111:role/GreeterRole",
1968
+ },
1969
+ },
1970
+ },
1971
+ },
1972
+ bindings: [
1973
+ {
1974
+ logicalId: "GreeterFunction",
1975
+ handler: (event: { name: string }): string => `Hello ${event.name}`,
1976
+ },
1977
+ ],
1978
+ });
1979
+
1980
+ const output = await simAws.lambda().invoke(
1981
+ new InvokeCommand({
1982
+ FunctionName: "greeter",
1983
+ Payload: JSON.stringify({ name: "Yulin" }),
1984
+ }),
1985
+ );
1986
+
1987
+ console.log(new TextDecoder().decode(output.Payload));
1988
+ ```
1989
+
1990
+ Bindings can target the CloudFormation logical ID (or the CDK construct ID recovered from
1991
+ synthesized metadata), the function name, or the function ARN. Bound handlers still run with the
1992
+ function's execution Role as the ambient simulated caller, so downstream calls made through
1993
+ `SimSdk`-intercepted clients are authorized by simulated IAM as on real Lambda. Functions without
1994
+ a matching binding keep their template code, running in the simulated vm runtime.
1995
+
1996
+ ### Naming the binding type
1997
+
1998
+ One entry of a `bindings` list is a `SimCfnBinding`, exported from `@kensio/yulin/cloudformation`. A
1999
+ test that builds its bindings in a fixture, or a factory that returns one, names the type from the
2000
+ import. The same list passes to `deployTemplate`, `deployTemplateFile` and the per-Stack `bindings`
2001
+ of `deployCdkOut`.
2002
+
2003
+ ```typescript sim-cloudformation-binding-type
2004
+ /**
2005
+ * Naming the bindings a deployment takes, for a list built somewhere else.
2006
+ */
2007
+
2008
+ import { InvokeCommand } from "@aws-sdk/client-lambda";
2009
+ import { SimAws } from "@kensio/yulin";
2010
+ import type { SimCfnBinding } from "@kensio/yulin/cloudformation";
2011
+
2012
+ const orders: string[] = [];
2013
+
2014
+ const bindings: readonly SimCfnBinding[] = [
2015
+ {
2016
+ logicalId: "PlaceOrderFunction",
2017
+ handler: (event: { item: string }): void => {
2018
+ orders.push(event.item);
2019
+ },
2020
+ },
2021
+ ];
2022
+
2023
+ const simAws = new SimAws();
2024
+
2025
+ await simAws.cloudFormation().deployTemplate({
2026
+ stackName: "orders-stack",
2027
+ template: {
2028
+ Resources: {
2029
+ PlaceOrderFunction: {
2030
+ Type: "AWS::Lambda::Function",
2031
+ Properties: {
2032
+ FunctionName: "place-order",
2033
+ Role: "arn:aws:iam::111111111111:role/PlaceOrderRole",
2034
+ },
2035
+ },
2036
+ },
2037
+ },
2038
+ bindings,
2039
+ });
2040
+
2041
+ await simAws.lambda().invoke(
2042
+ new InvokeCommand({
2043
+ FunctionName: "place-order",
2044
+ Payload: JSON.stringify({ item: "sourdough" }),
2045
+ }),
2046
+ );
2047
+
2048
+ console.log(orders);
2049
+ ```
2050
+
2051
+ A binding names one of five targets. `logicalId`, `functionName`, `arn`, `cdkPath` and
2052
+ `imageRepository` are the five, and a literal naming two of them fails to compile. The same type
2053
+ covers a binding to a container an `AWS::ECS::TaskDefinition` declares, which carries `run`, `http`
2054
+ or `consumes` in place of `handler`. See
2055
+ [Deploying ECS from CloudFormation](https://yulinsim.dev/services/ecs/#deploying-ecs-from-cloudformation) for those.
2056
+
2057
+ ## SAM templates
2058
+
2059
+ A template naming the `AWS::Serverless-2016-10-31` transform has its SAM resources expanded before
2060
+ the stack deploys, the way CloudFormation expands them. `AWS::Serverless::Function` becomes an
2061
+ `AWS::Lambda::Function` and the `AWS::IAM::Role` it runs as. `AWS::Serverless::SimpleTable` becomes
2062
+ an `AWS::DynamoDB::Table`. `AWS::Serverless::HttpApi` becomes an `AWS::ApiGatewayV2::Api` and its
2063
+ stage, and `AWS::Serverless::Api` becomes an `AWS::ApiGateway::RestApi` with the deployment and
2064
+ stage that publish it. `Globals.Function`, `Globals.HttpApi` and `Globals.Api` supply the defaults
2065
+ every function and every API takes, and a value on the resource itself wins.
2066
+
2067
+ The expanded function keeps the logical ID the SAM resource had. `Ref` and `Fn::GetAtt` against that
2068
+ name answer for the function, and a binding targeting that logical ID backs it with your real
2069
+ handler. `CodeUri` is never read from disk (a bound function can leave the code out of the template
2070
+ altogether).
2071
+
2072
+ ```typescript sim-cloudformation-sam-function
2073
+ /**
2074
+ * Deploying a SAM AWS::Serverless::Function into simulated AWS.
2075
+ */
2076
+
2077
+ import { InvokeCommand } from "@aws-sdk/client-lambda";
2078
+ import { SimAws } from "@kensio/yulin";
2079
+
2080
+ const simAws = new SimAws();
2081
+
2082
+ const stack = await simAws.cloudFormation().deployTemplate({
2083
+ stackName: "rates-stack",
2084
+ template: {
2085
+ Transform: "AWS::Serverless-2016-10-31",
2086
+ Globals: {
2087
+ Function: {
2088
+ Runtime: "nodejs22.x",
2089
+ Timeout: 10,
2090
+ },
2091
+ },
2092
+ Resources: {
2093
+ Rates: {
2094
+ Type: "AWS::Serverless::Function",
2095
+ Properties: {
2096
+ FunctionName: "rates",
2097
+ CodeUri: "src/rates/",
2098
+ Handler: "index.handler",
2099
+ Environment: {
2100
+ Variables: { TABLE_NAME: "rates-table" },
2101
+ },
2102
+ },
2103
+ },
2104
+ },
2105
+ },
2106
+ bindings: [
2107
+ {
2108
+ logicalId: "Rates",
2109
+ handler: (event: { currency: string }): string =>
2110
+ `rate for ${event.currency}`,
2111
+ },
2112
+ ],
2113
+ });
2114
+
2115
+ console.log(stack.getResource("Rates")?.type);
2116
+
2117
+ const output = await simAws.lambda().invoke(
2118
+ new InvokeCommand({
2119
+ FunctionName: "rates",
2120
+ Payload: JSON.stringify({ currency: "GBP" }),
2121
+ }),
2122
+ );
2123
+
2124
+ console.log(new TextDecoder().decode(output.Payload));
2125
+ ```
2126
+
2127
+ The execution role is named after the function, `RatesRole` for a function called `Rates`, and
2128
+ carries the basic execution policy SAM gives one. `Policies` on the function reach it. A policy
2129
+ document goes on as an inline policy, and a managed policy ARN is attached. SAM policy templates
2130
+ such as `DynamoDBCrudPolicy` are left ungenerated, because simulated IAM allows every call by
2131
+ default and a role missing those statements authorizes the same calls either way. A function naming
2132
+ its own `Role` runs as that role, and gets no expanded one.
2133
+
2134
+ ### Function events
2135
+
2136
+ `Events` on a SAM function expand into whatever puts the function behind them. `Api`, `HttpApi`,
2137
+ `SQS`, `DynamoDB`, `SNS`, `S3`, `Schedule`, `ScheduleV2` and `EventBridgeRule` are the types this
2138
+ covers. An event of any other type is left where it is, and the function deploys with nothing in
2139
+ front of it.
2140
+
2141
+ An `HttpApi` event becomes an `AWS::ApiGatewayV2::Integration`, an `AWS::ApiGatewayV2::Route` and the
2142
+ `AWS::Lambda::Permission` the API invokes the function under. `Path` and `Method` become the route
2143
+ key (`GET /rates/{currency}`). An event stating no method gets `ANY`, and a `Path` of `$default`
2144
+ becomes the catch-all route.
2145
+
2146
+ Events naming no `ApiId` share one API under the logical ID SAM gives it, `ServerlessHttpApi`, with a
2147
+ `$default` stage. Two functions with events of their own answer on the same endpoint. An event naming
2148
+ an `ApiId` routes to the API that logical ID belongs to, and the template brings the stage.
2149
+
2150
+ ```typescript sim-cloudformation-sam-http-api-event
2151
+ /**
2152
+ * A SAM function reached through the HTTP API its HttpApi event made.
2153
+ */
2154
+
2155
+ import { SimAws } from "@kensio/yulin";
2156
+ import { serveSimAws } from "@kensio/yulin/serve";
2157
+
2158
+ const simAws = new SimAws();
2159
+
2160
+ const stack = await simAws.cloudFormation().deployTemplate({
2161
+ stackName: "rates-api-stack",
2162
+ template: {
2163
+ Transform: "AWS::Serverless-2016-10-31",
2164
+ Resources: {
2165
+ Rates: {
2166
+ Type: "AWS::Serverless::Function",
2167
+ Properties: {
2168
+ Handler: "index.handler",
2169
+ Runtime: "nodejs22.x",
2170
+ Events: {
2171
+ Get: {
2172
+ Type: "HttpApi",
2173
+ Properties: { Path: "/rates/{currency}", Method: "GET" },
2174
+ },
2175
+ },
2176
+ },
2177
+ },
2178
+ },
2179
+ Outputs: {
2180
+ ApiEndpoint: {
2181
+ Value: { "Fn::GetAtt": ["ServerlessHttpApi", "ApiEndpoint"] },
2182
+ },
2183
+ },
2184
+ },
2185
+ bindings: [
2186
+ {
2187
+ logicalId: "Rates",
2188
+ handler: (request: {
2189
+ pathParameters?: Record<string, string>;
2190
+ }): { statusCode: number; body: string } => ({
2191
+ statusCode: 200,
2192
+ body: `rate for ${request.pathParameters?.["currency"]}`,
2193
+ }),
2194
+ },
2195
+ ],
2196
+ });
2197
+
2198
+ await stack.waitForDeployComplete();
2199
+
2200
+ const srv = await serveSimAws({ simAws });
2201
+
2202
+ const response = await fetch(
2203
+ srv.localUrl(`${stack.output("ApiEndpoint")}/rates/GBP`),
2204
+ );
2205
+
2206
+ console.log(await response.text());
2207
+
2208
+ await srv.close();
2209
+ ```
2210
+
2211
+ `Auth` on the event is left out. Every request matching the expanded route reaches the function.
2212
+
2213
+ An `Api` event is the REST half of the same idea. It becomes an `AWS::ApiGateway::Resource` for each
2214
+ segment of `Path`, an `AWS::ApiGateway::Method` on the last of them carrying a proxy `Integration`
2215
+ to the function, and the `AWS::Lambda::Permission` the API invokes it under. A `Method` of `any`
2216
+ becomes `ANY`, and an event stating none gets `ANY` as well. Two events sharing a path prefix share
2217
+ the resources that spell it, so `/rates` and `/rates/{currency}` sit on one branch of one tree.
2218
+
2219
+ Events naming no `RestApiId` share the API SAM calls `ServerlessRestApi`, published to a `Prod`
2220
+ stage. A REST API carries the stage as the first segment of every path it serves. The example below
2221
+ requests `/Prod/rates/GBP` for that reason.
2222
+
2223
+ ```typescript sim-cloudformation-sam-api-event
2224
+ /**
2225
+ * A SAM function reached through the REST API its Api event made.
2226
+ */
2227
+
2228
+ import { SimAws } from "@kensio/yulin";
2229
+ import { serveSimAws } from "@kensio/yulin/serve";
2230
+
2231
+ const simAws = new SimAws();
2232
+
2233
+ const stack = await simAws.cloudFormation().deployTemplate({
2234
+ stackName: "rates-api-stack",
2235
+ template: {
2236
+ Transform: "AWS::Serverless-2016-10-31",
2237
+ Resources: {
2238
+ Rates: {
2239
+ Type: "AWS::Serverless::Function",
2240
+ Properties: {
2241
+ Handler: "index.handler",
2242
+ Runtime: "nodejs22.x",
2243
+ Events: {
2244
+ Get: {
2245
+ Type: "Api",
2246
+ Properties: { Path: "/rates/{currency}", Method: "GET" },
2247
+ },
2248
+ },
2249
+ },
2250
+ },
2251
+ },
2252
+ Outputs: {
2253
+ ApiUrl: {
2254
+ Value: {
2255
+ "Fn::Join": [
2256
+ "",
2257
+ [
2258
+ "https://",
2259
+ { Ref: "ServerlessRestApi" },
2260
+ ".execute-api.",
2261
+ { Ref: "AWS::Region" },
2262
+ ".",
2263
+ { Ref: "AWS::URLSuffix" },
2264
+ "/",
2265
+ { Ref: "ServerlessRestApiProdStage" },
2266
+ "/",
2267
+ ],
2268
+ ],
2269
+ },
2270
+ },
2271
+ },
2272
+ },
2273
+ bindings: [
2274
+ {
2275
+ logicalId: "Rates",
2276
+ handler: (request: {
2277
+ pathParameters?: Record<string, string> | null;
2278
+ }): { statusCode: number; body: string } => ({
2279
+ statusCode: 200,
2280
+ body: `rate for ${request.pathParameters?.["currency"]}`,
2281
+ }),
2282
+ },
2283
+ ],
2284
+ });
2285
+
2286
+ await stack.waitForDeployComplete();
2287
+
2288
+ const srv = await serveSimAws({ simAws });
2289
+
2290
+ const response = await fetch(
2291
+ srv.localUrl(`${stack.output("ApiUrl")}rates/GBP`),
2292
+ );
2293
+
2294
+ console.log(await response.text());
2295
+ // "rate for GBP"
2296
+
2297
+ await srv.close();
2298
+ ```
2299
+
2300
+ An event naming a `RestApiId` puts its method on the API that logical ID belongs to, whether an
2301
+ `AWS::Serverless::Api` or an `AWS::ApiGateway::RestApi` the template declared. The API is named as
2302
+ the logical ID or as a `Ref` to it. An event naming it any other way, such as through
2303
+ `Fn::ImportValue`, expands into no resources at all, because a REST API path tree is built downwards
2304
+ from a root resource this has no way to reach. `Auth` on the event is left out, the same as on an `HttpApi` one.
2305
+
2306
+ A `Schedule` event becomes an `AWS::Events::Rule` on a timer, with the `AWS::Lambda::Permission` the
2307
+ rule invokes the function under. The event's `Schedule` is the rule's `ScheduleExpression`, and the
2308
+ function runs as a test advances simulated time past a due instant. An `EventBridgeRule` event
2309
+ becomes the same pair, with the event's `Pattern` as the rule's `EventPattern`. A matching event put
2310
+ on the bus invokes the function. Both events take `Name` (an `EventBridgeRule` event calls it
2311
+ `RuleName`), `Description`, `Input` and `Enabled`, and an event naming an `EventBusName` watches
2312
+ that bus.
2313
+
2314
+ ```typescript sim-cloudformation-sam-schedule-event
2315
+ /**
2316
+ * A SAM function put on a timer by its Schedule event.
2317
+ */
2318
+
2319
+ import { SimAws } from "@kensio/yulin";
2320
+
2321
+ const simAws = new SimAws();
2322
+
2323
+ const runs: string[] = [];
2324
+
2325
+ const stack = await simAws.cloudFormation().deployTemplate({
2326
+ stackName: "reconciliation-stack",
2327
+ template: {
2328
+ Transform: "AWS::Serverless-2016-10-31",
2329
+ Resources: {
2330
+ Reconcile: {
2331
+ Type: "AWS::Serverless::Function",
2332
+ Properties: {
2333
+ Handler: "index.handler",
2334
+ Runtime: "nodejs22.x",
2335
+ Events: {
2336
+ Hourly: {
2337
+ Type: "Schedule",
2338
+ Properties: {
2339
+ Schedule: "rate(1 hour)",
2340
+ Input: JSON.stringify({ ledger: "rates" }),
2341
+ },
2342
+ },
2343
+ },
2344
+ },
2345
+ },
2346
+ },
2347
+ },
2348
+ bindings: [
2349
+ {
2350
+ logicalId: "Reconcile",
2351
+ handler: (event: { ledger: string }): string => {
2352
+ runs.push(event.ledger);
2353
+
2354
+ return "reconciled";
2355
+ },
2356
+ },
2357
+ ],
2358
+ });
2359
+
2360
+ await stack.waitForDeployComplete();
2361
+
2362
+ await simAws.clock().advanceBy({ hours: 3 });
2363
+
2364
+ console.log(runs);
2365
+ ```
2366
+
2367
+ A `ScheduleV2` event becomes an `AWS::Scheduler::Schedule` and the `AWS::IAM::Role` Scheduler assumes
2368
+ to invoke the function. The role trusts `scheduler.amazonaws.com` and may invoke the one function the
2369
+ event was declared on. An event naming a `RoleArn` runs as that role, and the expansion makes no role
2370
+ of its own. `Name`, `Description`, `Input`, `Enabled`, `GroupName`, `StartDate`, `EndDate`,
2371
+ `ScheduleExpressionTimezone`, `KmsKeyArn` and `FlexibleTimeWindow` carry over. An event stating no
2372
+ time window gets `OFF`.
2373
+
2374
+ `DeadLetterConfig` and `RetryPolicy` on these three events are left out. A delivery is attempted
2375
+ once, and a failed one is recorded by the rule or the schedule that made it.
2376
+
2377
+ A `FunctionUrlConfig` on the function expands into an `AWS::Lambda::Url` named after it, `RatesUrl`
2378
+ for a function called `Rates`. `AuthType` and `InvokeMode` carry over. `Cors` is left out (the
2379
+ simulated Function URL answers no preflight request).
2380
+
2381
+ ### Queue, stream, topic and bucket events
2382
+
2383
+ These four event types point the function at something the template already has. Expanding one never
2384
+ creates the queue, table, topic or bucket it names.
2385
+
2386
+ An `SQS` event becomes an `AWS::Lambda::EventSourceMapping` polling the queue its `Queue` ARN names,
2387
+ and a `DynamoDB` event becomes one reading the stream its `Stream` ARN names. Both are named after
2388
+ the function and the event (`OrdersWorkEventSourceMapping` for an event called `Work` on a function
2389
+ called `Orders`). Whatever else the event states goes onto the mapping under the same name, so
2390
+ `BatchSize`, `StartingPosition`, `Enabled` and `FilterCriteria` all carry across. A property the
2391
+ mapping has no meaning for is refused by name.
2392
+
2393
+ The expanded execution role gains the policy it polls the source under, the way SAM attaches one of
2394
+ its own. Lambda refuses a mapping whose role cannot poll. A function naming its own `Role` runs as
2395
+ that role and keeps whatever the template granted it.
2396
+
2397
+ ```typescript sim-cloudformation-sam-queue-event
2398
+ /**
2399
+ * A SAM function fed by the queue its SQS event names.
2400
+ */
2401
+
2402
+ import { GetQueueUrlCommand, SendMessageCommand } from "@aws-sdk/client-sqs";
2403
+ import { SimAws } from "@kensio/yulin";
2404
+
2405
+ const simAws = new SimAws();
2406
+ const received: string[][] = [];
2407
+
2408
+ const stack = await simAws.cloudFormation().deployTemplate({
2409
+ stackName: "orders-stack",
2410
+ template: {
2411
+ Transform: "AWS::Serverless-2016-10-31",
2412
+ Resources: {
2413
+ OrdersQueue: {
2414
+ Type: "AWS::SQS::Queue",
2415
+ Properties: { QueueName: "orders" },
2416
+ },
2417
+ Orders: {
2418
+ Type: "AWS::Serverless::Function",
2419
+ Properties: {
2420
+ Handler: "index.handler",
2421
+ Runtime: "nodejs22.x",
2422
+ Events: {
2423
+ Work: {
2424
+ Type: "SQS",
2425
+ Properties: {
2426
+ Queue: { "Fn::GetAtt": ["OrdersQueue", "Arn"] },
2427
+ BatchSize: 5,
2428
+ },
2429
+ },
2430
+ },
2431
+ },
2432
+ },
2433
+ },
2434
+ },
2435
+ bindings: [
2436
+ {
2437
+ logicalId: "Orders",
2438
+ handler: (event: { Records: readonly { body: string }[] }): string[] => {
2439
+ const bodies = event.Records.map((record) => record.body);
2440
+ received.push(bodies);
2441
+
2442
+ return bodies;
2443
+ },
2444
+ },
2445
+ ],
2446
+ });
2447
+
2448
+ await stack.waitForDeployComplete();
2449
+
2450
+ const { QueueUrl } = await simAws
2451
+ .sqs()
2452
+ .getQueueUrl(new GetQueueUrlCommand({ QueueName: "orders" }));
2453
+
2454
+ await simAws
2455
+ .sqs()
2456
+ .sendMessage(new SendMessageCommand({ QueueUrl, MessageBody: "order-1" }));
2457
+
2458
+ await simAws.backgroundTasksComplete();
2459
+
2460
+ console.log(received);
2461
+ ```
2462
+
2463
+ An `SNS` event becomes an `AWS::SNS::Subscription` on the `lambda` protocol, together with the
2464
+ `AWS::Lambda::Permission` the topic invokes the function under. `Topic` is the topic's ARN.
2465
+ `FilterPolicy` and `FilterPolicyScope` carry across as the subscription attributes of those names.
2466
+
2467
+ An `S3` event is the one with no resource of its own. In CloudFormation a bucket carries its own
2468
+ notifications, so the event adds a `LambdaConfigurations` entry to the `NotificationConfiguration` of
2469
+ the bucket its `Bucket` names, and brings the permission S3 invokes the function under. `Bucket` is a
2470
+ logical ID here rather than an ARN, and the bucket has to be one the template declares. `Events`
2471
+ states one event name or a list of them, and each one becomes a configuration of its own with the
2472
+ event's `Filter` on it. Notifications the bucket already declared are kept, and so are the ones
2473
+ another function's event put there.
2474
+
2475
+ Two shapes are refused rather than expanded. A bucket writing its `NotificationConfiguration` or its
2476
+ `LambdaConfigurations` as an intrinsic such as `Fn::If` is one, because there is no appending to a
2477
+ list CloudFormation has not resolved yet, and adding the event's own entries would drop whatever the
2478
+ intrinsic resolved to. The other is a function the template conditions out, which real CloudFormation
2479
+ refuses for the same reason SAM cannot fix it: the notification belongs to the bucket, the bucket is
2480
+ not conditioned, and nothing can condition one entry of somebody else's property. Condition the
2481
+ bucket along with the function, or declare the notification on the bucket yourself.
2482
+
2483
+ ### Simple tables
2484
+
2485
+ `AWS::Serverless::SimpleTable` deploys a table with one partition key and on-demand billing.
2486
+ `PrimaryKey` names that key, and its `Type` is the SAM name for the attribute type (`String`,
2487
+ `Number` or `Binary`). A table naming no key is keyed on a string `id`, the key SAM gives it.
2488
+ `TableName`, `SSESpecification`, `PointInTimeRecoverySpecification` and `ProvisionedThroughput`
2489
+ carry across as the template wrote them, and a table asking for capacity is billed for the capacity
2490
+ it asked for. `Tags` are stated as a map
2491
+ of one value per tag name, and reach the table as the list of `Key` and `Value` pairs DynamoDB
2492
+ takes.
2493
+
2494
+ ### HTTP APIs
2495
+
2496
+ `AWS::Serverless::HttpApi` deploys an HTTP API and the stage that serves it. The stage is `$default`
2497
+ until `StageName` names another one, and it carries the logical ID SAM builds for it
2498
+ (`OrdersApiGatewayDefaultStage` for an API called `Orders`, `OrdersprodStage` for the same API with
2499
+ `StageName: prod`, and `OrdersStage` followed by ten characters of a hash where the name cannot be
2500
+ part of an identifier). `AccessLogSettings`, `DefaultRouteSettings`, `RouteSettings`,
2501
+ `StageVariables` and `Tags` go on the stage, and the rest of the API's properties on the API.
2502
+
2503
+ A `DefinitionBody` reaches the API as its OpenAPI `Body`, and the routes, integrations and
2504
+ authorizers the document declares are created from it. Routes declared outside the document name the
2505
+ API by `ApiId`, with `Ref` on the SAM logical ID. That is how an `HttpApi` event on a function
2506
+ reaches an API the template declared, and how an `AWS::ApiGatewayV2::Route` resource of its own
2507
+ does. An API naming a `DefinitionUri` is recorded as unsupported, because nothing here reads a
2508
+ document off disk or out of S3.
2509
+
2510
+ The API is deployed without `Auth` and `Domain`. SAM writes an `Auth` block into the document it
2511
+ generates, and deploys a `Domain` as a custom domain name resource, and neither is expanded here.
2512
+
2513
+ ```typescript sim-cloudformation-sam-table-api
2514
+ /**
2515
+ * Deploying a SAM AWS::Serverless::SimpleTable and AWS::Serverless::HttpApi.
2516
+ */
2517
+
2518
+ import { DescribeTableCommand } from "@aws-sdk/client-dynamodb";
2519
+ import { SimAws } from "@kensio/yulin";
2520
+
2521
+ const simAws = new SimAws();
2522
+
2523
+ const stack = await simAws.cloudFormation().deployTemplate({
2524
+ stackName: "orders-stack",
2525
+ template: {
2526
+ Transform: "AWS::Serverless-2016-10-31",
2527
+ Globals: {
2528
+ HttpApi: {
2529
+ StageVariables: { TABLE_NAME: "orders" },
2530
+ },
2531
+ },
2532
+ Resources: {
2533
+ OrdersTable: {
2534
+ Type: "AWS::Serverless::SimpleTable",
2535
+ Properties: {
2536
+ TableName: "orders",
2537
+ PrimaryKey: { Name: "orderId", Type: "String" },
2538
+ },
2539
+ },
2540
+ Orders: {
2541
+ Type: "AWS::Serverless::HttpApi",
2542
+ Properties: { Name: "orders" },
2543
+ },
2544
+ },
2545
+ },
2546
+ });
2547
+ await stack.waitForDeployComplete();
2548
+
2549
+ console.log(stack.getResource("Orders")?.type);
2550
+ console.log(stack.getResource("OrdersApiGatewayDefaultStage")?.type);
2551
+
2552
+ const described = await simAws
2553
+ .dynamoDb()
2554
+ .describeTable(new DescribeTableCommand({ TableName: "orders" }));
2555
+
2556
+ console.log(described.Table?.KeySchema);
2557
+ ```
2558
+
2559
+ ### REST APIs
2560
+
2561
+ `AWS::Serverless::Api` deploys a REST API, the deployment that publishes it, and the stage `StageName`
2562
+ names. The stage carries the logical ID SAM builds for it (`OrdersprodStage` for an API called
2563
+ `Orders` with `StageName: prod`, and `OrdersStage` followed by ten characters of a hash where the
2564
+ name cannot be part of an identifier). The deployment is `OrdersDeployment`, where SAM appends a
2565
+ hash of the document it generates. `Variables` become the stage's stage variables, and `Name`,
2566
+ `Description` and `DisableExecuteApiEndpoint` go on the API.
2567
+
2568
+ `StageName` is required on an `AWS::Serverless::Api`, and real SAM refuses a template leaving it
2569
+ out. Deployment here is best effort, so an API without one publishes to `Prod`, the stage SAM gives
2570
+ the implicit API.
2571
+
2572
+ A REST API takes a name whatever else it declares, and an API stating no `Name` is named after its
2573
+ logical ID. Methods reach it as resources naming it by `RestApiId`. That is how an `Api` event on a
2574
+ function reaches an API the template declared, and how an `AWS::ApiGateway::Method` resource of its
2575
+ own does.
2576
+
2577
+ A `DefinitionBody` reaches the API as its Swagger `Body` and is recorded as a property the API was
2578
+ created without, because reading the document is outside this. An API declaring one deploys with a
2579
+ root resource and an empty tree under it. An API naming a `DefinitionUri` is recorded as
2580
+ unsupported, because this reads no document off disk or out of S3.
2581
+
2582
+ The API is deployed without `Auth`, `Cors`, `Domain`, `GatewayResponses`, `MethodSettings` and
2583
+ `BinaryMediaTypes`. SAM writes each of them into the document it generates, and none is expanded
2584
+ here.
2585
+
2586
+ ```typescript sim-cloudformation-sam-rest-api
2587
+ /**
2588
+ * Deploying a SAM AWS::Serverless::Api, with the Globals.Api defaults.
2589
+ */
2590
+
2591
+ import { SimAws } from "@kensio/yulin";
2592
+
2593
+ const simAws = new SimAws();
2594
+
2595
+ const stack = await simAws.cloudFormation().deployTemplate({
2596
+ stackName: "orders-stack",
2597
+ template: {
2598
+ Transform: "AWS::Serverless-2016-10-31",
2599
+ Globals: {
2600
+ Api: { Variables: { TABLE_NAME: "orders" } },
2601
+ },
2602
+ Resources: {
2603
+ Orders: {
2604
+ Type: "AWS::Serverless::Api",
2605
+ Properties: { Name: "orders-api", StageName: "prod" },
2606
+ },
2607
+ },
2608
+ },
2609
+ });
2610
+ await stack.waitForDeployComplete();
2611
+
2612
+ console.log(stack.getResource("Orders")?.type);
2613
+ // "AWS::ApiGateway::RestApi"
2614
+
2615
+ console.log(stack.getResource("OrdersDeployment")?.type);
2616
+ // "AWS::ApiGateway::Deployment"
2617
+
2618
+ console.log(stack.getResource("OrdersprodStage")?.type);
2619
+ // "AWS::ApiGateway::Stage"
2620
+ ```
2621
+
2622
+ SAM expands a REST API through the Swagger document it generates, with the paths, methods and
2623
+ integrations inside the `Body` of one resource. The expansion here writes the
2624
+ `AWS::ApiGateway::Resource` and `AWS::ApiGateway::Method` resources directly. A method is then
2625
+ something the stack holds, answers `Ref` for and tears down. The APIs, stages and methods come out
2626
+ the same either way, and the logical IDs of the path resources have no counterpart in what SAM
2627
+ produces.
2628
+
2629
+ ## Serving deployed resources on localhost
2630
+
2631
+ CloudFormation itself is not served as an HTTP API. Instead, you deploy infrastructure through Sim
2632
+ CloudFormation, then serve the simulated AWS environment with `serveSimAws`.
2633
+
2634
+ ```typescript sim-cloudformation-serve-localhost
2635
+ /**
2636
+ * Deploy with sim CloudFormation, then serve the simulated resources on localhost.
2637
+ */
2638
+
2639
+ import { SimAws } from "@kensio/yulin";
2640
+ import { serveSimAws } from "@kensio/yulin/serve";
2641
+
2642
+ const simAws = new SimAws();
2643
+ const srv = await serveSimAws({ simAws });
2644
+
2645
+ try {
2646
+ const stack = await simAws.cloudFormation().deployTemplate({
2647
+ stackName: "local-site-stack",
2648
+ template: {
2649
+ Resources: {
2650
+ SiteBucket: {
2651
+ Type: "AWS::S3::Bucket",
2652
+ Properties: {
2653
+ BucketName: "local-site-bucket",
2654
+ WebsiteConfiguration: {
2655
+ IndexDocument: "index.html",
2656
+ },
2657
+ // A website Bucket needs a public Bucket policy, which Block
2658
+ // Public Access refuses until the Bucket opts out.
2659
+ PublicAccessBlockConfiguration: {
2660
+ BlockPublicAcls: true,
2661
+ IgnorePublicAcls: true,
2662
+ },
2663
+ },
2664
+ },
2665
+ SiteBucketPolicy: {
2666
+ Type: "AWS::S3::BucketPolicy",
2667
+ Properties: {
2668
+ Bucket: { Ref: "SiteBucket" },
2669
+ PolicyDocument: {
2670
+ Version: "2012-10-17",
2671
+ Statement: [
2672
+ {
2673
+ Effect: "Allow",
2674
+ Principal: "*",
2675
+ Action: "s3:GetObject",
2676
+ Resource: "arn:aws:s3:::local-site-bucket/*",
2677
+ },
2678
+ ],
2679
+ },
2680
+ },
2681
+ },
2682
+ },
2683
+ },
2684
+ });
2685
+
2686
+ await stack.waitForDeployComplete();
2687
+
2688
+ await simAws.s3().putObject({
2689
+ input: {
2690
+ Bucket: "local-site-bucket",
2691
+ Key: "index.html",
2692
+ Body: "<h1>Hello from Sim CloudFormation</h1>",
2693
+ ContentType: "text/html; charset=utf-8",
2694
+ },
2695
+ });
2696
+
2697
+ const websiteUrl = simAws.s3().getBucketWebsiteUrl("local-site-bucket");
2698
+ const response = await fetch(srv.localUrl(websiteUrl));
2699
+
2700
+ console.log(response.status);
2701
+ console.log(await response.text());
2702
+ } finally {
2703
+ await srv.close();
2704
+ }
2705
+ ```
2706
+
2707
+ Use `srv.localUrl(...)` to adapt simulated service URLs to the local server while preserving the
2708
+ simulated hostname and service routing information.
2709
+
2710
+ ## Accounts and Regions
2711
+
2712
+ Use `SimAws` scopes to create stacks in different simulated Accounts and Regions.
2713
+
2714
+ ```typescript sim-cloudformation-account-region-scoping
2715
+ /**
2716
+ * Deploying stacks in different simulated Accounts and Regions.
2717
+ */
2718
+
2719
+ import { SimAws } from "@kensio/yulin";
2720
+
2721
+ const simAws = new SimAws();
2722
+
2723
+ const defaultCfn = simAws.cloudFormation();
2724
+ const euWest2Cfn = simAws.region("eu-west-2").cloudFormation();
2725
+ const accountCfn = simAws.account("111111111111").cloudFormation();
2726
+ const scopedCfn = simAws
2727
+ .account("222222222222")
2728
+ .region("ap-east-1")
2729
+ .cloudFormation();
2730
+
2731
+ await defaultCfn.deployTemplate({
2732
+ stackName: "default-stack",
2733
+ template: {
2734
+ Resources: {
2735
+ DefaultHandle: {
2736
+ Type: "AWS::CloudFormation::WaitConditionHandle",
2737
+ },
2738
+ },
2739
+ },
2740
+ });
2741
+
2742
+ await euWest2Cfn.deployTemplate({
2743
+ stackName: "regional-stack",
2744
+ template: {
2745
+ Resources: {
2746
+ RegionalHandle: {
2747
+ Type: "AWS::CloudFormation::WaitConditionHandle",
2748
+ },
2749
+ },
2750
+ },
2751
+ });
2752
+
2753
+ await accountCfn.deployTemplate({
2754
+ stackName: "account-stack",
2755
+ template: {
2756
+ Resources: {
2757
+ AccountHandle: {
2758
+ Type: "AWS::CloudFormation::WaitConditionHandle",
2759
+ },
2760
+ },
2761
+ },
2762
+ });
2763
+
2764
+ await scopedCfn.deployTemplate({
2765
+ stackName: "scoped-stack",
2766
+ template: {
2767
+ Resources: {
2768
+ ScopedHandle: {
2769
+ Type: "AWS::CloudFormation::WaitConditionHandle",
2770
+ },
2771
+ },
2772
+ },
2773
+ });
2774
+ ```
2775
+
2776
+ Stacks are scoped to the selected simulated account and region. Resources created by a stack are
2777
+ created through that same simulated account/region scope unless the underlying simulated service has
2778
+ different AWS-like scoping behaviour.
2779
+
2780
+ An Account ID can always be written as a plain string, as above. Code that wants to name the type
2781
+ can get a `SimAwsAccountId` from `simAwsAccountId("111111111111")`, which refuses anything other than
2782
+ a 12-digit AWS Account ID.
2783
+
2784
+ ## Reading stack Outputs
2785
+
2786
+ A deployed stack resolves its template `Outputs` once its resources exist. `stack.output(key)`
2787
+ answers one of them as a string.
2788
+
2789
+ ```typescript sim-cloudformation-stack-output
2790
+ /**
2791
+ * Reading a resolved Stack Output as a string.
2792
+ */
2793
+
2794
+ import { SimAws } from "@kensio/yulin";
2795
+
2796
+ const simAws = new SimAws();
2797
+
2798
+ const stack = await simAws.cloudFormation().deployTemplate({
2799
+ stackName: "output-stack",
2800
+ template: {
2801
+ Resources: {
2802
+ SiteBucket: {
2803
+ Type: "AWS::S3::Bucket",
2804
+ Properties: {
2805
+ BucketName: "output-site-bucket",
2806
+ },
2807
+ },
2808
+ },
2809
+ Outputs: {
2810
+ SiteBucketName: {
2811
+ Description: "The bucket the site is served from",
2812
+ Value: {
2813
+ Ref: "SiteBucket",
2814
+ },
2815
+ },
2816
+ },
2817
+ },
2818
+ });
2819
+
2820
+ await stack.waitForDeployComplete();
2821
+
2822
+ const bucketName = stack.output("SiteBucketName");
2823
+
2824
+ console.log(bucketName); // "output-site-bucket"
2825
+ ```
2826
+
2827
+ `output` throws where the template declares no such Output, naming the stack, the key asked for and
2828
+ the keys it does declare. It throws again where the Output resolved to something other than a
2829
+ string, since `DescribeStacks` types an `OutputValue` as a string and a template's Output `Value` is
2830
+ a string field.
2831
+
2832
+ `stack.outputs` holds every resolved Output whole, keyed by name, and is where to go for the
2833
+ description, the export name, or a value that is not a string. Yulin resolves a few attributes as
2834
+ the lists and booleans they are, and `Fn::GetAtt` on `AWS::Route53::HostedZone` `NameServers` is the
2835
+ one that reaches an Output in these docs.
2836
+
2837
+ ```typescript
2838
+ const nameServers = stack.outputs.get("HostedZoneNameServers")?.value;
2839
+ ```
2840
+
2841
+ ## Inspecting stacks and resources
2842
+
2843
+ After deployment, you can inspect the returned stack and its resources.
2844
+
2845
+ ```typescript sim-cloudformation-inspect-stack
2846
+ /**
2847
+ * Inspecting resources created by a simulated CloudFormation Stack.
2848
+ */
2849
+
2850
+ import { SimAws } from "@kensio/yulin";
2851
+
2852
+ const simAws = new SimAws();
2853
+
2854
+ const stack = await simAws.cloudFormation().deployTemplate({
2855
+ stackName: "inspect-stack",
2856
+ template: {
2857
+ Resources: {
2858
+ SiteBucket: {
2859
+ Type: "AWS::S3::Bucket",
2860
+ Properties: {
2861
+ BucketName: "inspect-site-bucket",
2862
+ },
2863
+ },
2864
+ },
2865
+ },
2866
+ });
2867
+
2868
+ await stack.waitForDeployComplete();
2869
+
2870
+ const bucketResource = stack.getResource("SiteBucket");
2871
+
2872
+ console.log(bucketResource?.simResource);
2873
+ ```
2874
+
2875
+ This is useful in tests when you want to assert that a specific template resource created the
2876
+ expected simulated service resource.
2877
+
2878
+ ### Listing a Stack's Resources
2879
+
2880
+ `stack.resources` holds every Resource the template declared, in the order it declared them. Each
2881
+ entry is the same `SimCfnDeployedResource` that `getResource` answers with, and a caller after a
2882
+ group of them filters the array itself.
2883
+
2884
+ ```typescript sim-cloudformation-list-resources
2885
+ /**
2886
+ * Counting the Resources of one type in a deployed Stack.
2887
+ */
2888
+
2889
+ import { SimAws } from "@kensio/yulin";
2890
+
2891
+ const simAws = new SimAws();
2892
+
2893
+ const stack = await simAws.cloudFormation().deployTemplate({
2894
+ stackName: "list-resources-stack",
2895
+ template: {
2896
+ Resources: {
2897
+ UploadsBucket: {
2898
+ Type: "AWS::S3::Bucket",
2899
+ Properties: { BucketName: "list-resources-uploads" },
2900
+ },
2901
+ ArchiveBucket: {
2902
+ Type: "AWS::S3::Bucket",
2903
+ Properties: { BucketName: "list-resources-archive" },
2904
+ },
2905
+ UploadsTopic: {
2906
+ Type: "AWS::SNS::Topic",
2907
+ Properties: { TopicName: "list-resources-uploads" },
2908
+ },
2909
+ },
2910
+ },
2911
+ });
2912
+
2913
+ await stack.waitForDeployComplete();
2914
+
2915
+ const buckets = stack.resources.filter(
2916
+ (resource) => resource.type === "AWS::S3::Bucket",
2917
+ );
2918
+
2919
+ console.log(buckets.map((bucket) => bucket.logicalId));
2920
+ // ["UploadsBucket", "ArchiveBucket"]
2921
+ ```
2922
+
2923
+ `getResource` covers the Resources a test can name. Counting them is the other question. CDK
2924
+ hashes the logical ID it synthesizes, and a test asserting how many Resources of a type a Stack
2925
+ declared has no name to ask for in advance. The array is where that assertion goes. Yulin ships no
2926
+ filtering helpers on top of it (the shapes a test filters on vary too much for an API to guess).
2927
+
2928
+ ### Naming the deployed Stack type
2929
+
2930
+ A deployment answers with a `SimCfnDeployedStack`, and `getResource(...)` with a
2931
+ `SimCfnDeployedResource`. A helper written away from the deploy call names both from
2932
+ `@kensio/yulin/cloudformation`.
2933
+
2934
+ ```typescript sim-cloudformation-deployed-stack-type
2935
+ /**
2936
+ * Naming what a deployment answers with, for a helper written somewhere else.
2937
+ */
2938
+
2939
+ import { SimAws } from "@kensio/yulin";
2940
+ import type {
2941
+ SimCfnDeployedResource,
2942
+ SimCfnDeployedStack,
2943
+ } from "@kensio/yulin/cloudformation";
2944
+
2945
+ function deployedBucket(
2946
+ stack: SimCfnDeployedStack,
2947
+ ): SimCfnDeployedResource | undefined {
2948
+ return stack.getResource("SiteBucket");
2949
+ }
2950
+
2951
+ const simAws = new SimAws();
2952
+
2953
+ const stack = await simAws.cloudFormation().deployTemplate({
2954
+ stackName: "named-stack",
2955
+ template: {
2956
+ Resources: {
2957
+ SiteBucket: {
2958
+ Type: "AWS::S3::Bucket",
2959
+ Properties: {
2960
+ BucketName: "named-site-bucket",
2961
+ },
2962
+ },
2963
+ },
2964
+ },
2965
+ });
2966
+
2967
+ await stack.waitForDeployComplete();
2968
+
2969
+ console.log(deployedBucket(stack)?.type);
2970
+ ```
2971
+
2972
+ A deployed Stack holds what the caller that deployed it reads back:
2973
+
2974
+ - `stackName`, `status` and `error`
2975
+ - `outputs` and `output(...)`, covered under [Reading stack Outputs](#reading-stack-outputs)
2976
+ - `resources` and `getResource(...)`
2977
+ - `ignoredProperties`, `skippedResources`, `inertResources`, `skippedResourceDeletions` and
2978
+ `retainedResources`
2979
+ - `waitForDeployComplete()`, `waitForUpdateComplete()` and `waitForDeleteComplete()`
2980
+ - `delete()` and `teardown()`
2981
+
2982
+ The same type is what a `deployCdkOut` transform is handed for the Stacks in front of the one it is
2983
+ adapting, as `ReadonlyMap<string, SimCfnDeployedStack>`.
2984
+
2985
+ ### Looking a Resource up by CDK construct ID
2986
+
2987
+ `getResource` takes the CDK construct ID as well as the synthesized logical ID. A construct named
2988
+ `UploadsBucket` synthesizes as `UploadsBucket9F8E7D6C`, and either name answers with that Resource.
2989
+ The construct ID is the identifier a [binding](#lambda-function-bindings) takes. A test that bound a
2990
+ handler by construct ID can ask the Stack what it bound, without reading the synthesized template
2991
+ for the hash.
2992
+
2993
+ ```typescript sim-cloudformation-construct-id-resource
2994
+ /**
2995
+ * Finding a synthesized Resource by the CDK construct ID it came from.
2996
+ */
2997
+
2998
+ import { SimAws } from "@kensio/yulin";
2999
+
3000
+ const simAws = new SimAws();
3001
+
3002
+ const stack = await simAws.cloudFormation().deployTemplate({
3003
+ stackName: "construct-id-stack",
3004
+ template: {
3005
+ Resources: {
3006
+ // As CDK synthesizes it, with a hash on the logical ID and the construct
3007
+ // path in Metadata.
3008
+ UploadsBucket9F8E7D6C: {
3009
+ Type: "AWS::S3::Bucket",
3010
+ Metadata: {
3011
+ "aws:cdk:path": "UploadsStack/UploadsBucket/Resource",
3012
+ },
3013
+ Properties: {
3014
+ BucketName: "construct-id-uploads",
3015
+ },
3016
+ },
3017
+ },
3018
+ },
3019
+ });
3020
+
3021
+ await stack.waitForDeployComplete();
3022
+
3023
+ console.log(stack.getResource("UploadsBucket")?.logicalId);
3024
+ // "UploadsBucket9F8E7D6C"
3025
+ ```
3026
+
3027
+ A logical ID that matches exactly is answered first. A template naming its own Resources resolves
3028
+ the way it always has. An identifier no Resource carries either way answers `undefined`.
3029
+
3030
+ `stack.skippedResources` lists the Resources the deployment did not create. Each one carries a
3031
+ `skippedReason` saying why that Resource was skipped. A test that expected a resource to exist can
3032
+ find out why it is missing.
3033
+
3034
+ ```typescript sim-cloudformation-inspect-skipped
3035
+ /**
3036
+ * Finding out which Resources a simulated CloudFormation Stack skipped.
3037
+ */
3038
+
3039
+ import { SimAws } from "@kensio/yulin";
3040
+
3041
+ const simAws = new SimAws();
3042
+
3043
+ const stack = await simAws.cloudFormation().deployTemplate({
3044
+ stackName: "skipped-stack",
3045
+ template: {
3046
+ Resources: {
3047
+ SiteBucket: {
3048
+ Type: "AWS::S3::Bucket",
3049
+ Properties: {
3050
+ BucketName: "skipped-site-bucket",
3051
+ },
3052
+ },
3053
+ AlarmRule: {
3054
+ Type: "AWS::CloudWatch::Alarm",
3055
+ },
3056
+ },
3057
+ },
3058
+ });
3059
+
3060
+ await stack.waitForDeployComplete();
3061
+
3062
+ console.log(stack.skippedResources.map((resource) => resource.logicalId));
3063
+ // ["AlarmRule"]
3064
+
3065
+ console.log(stack.getResource("AlarmRule")?.skippedReason);
3066
+ // "Unsupported sim CloudFormation Resource service CloudWatch"
3067
+ ```
3068
+
3069
+ A skipped Resource is still there for `stack.getResource(...)`, and still answers `Ref` and `Fn::GetAtt` with
3070
+ [stand-in values](#values-from-a-skipped-resource).
3071
+
3072
+ A skip is more than a whole Resource type nothing simulates. A service can decline one Resource of a
3073
+ type it does create, when that Resource asks for something the service cannot model, and the
3074
+ `skippedReason` says which part it was. An `AWS::Route53::RecordSet` declaring a record type sim
3075
+ Route53 has no room for is skipped with the record type named. A DNS stack carrying a record beside
3076
+ the point of the test still deploys. See [record types](https://yulinsim.dev/services/route53/#record-types).
3077
+
3078
+ A Resource that was skipped on create is stepped over by a teardown, never deleted, because nothing
3079
+ reached simulated AWS to delete. It reaches `DELETE_COMPLETE` and stays out of
3080
+ `stack.skippedResourceDeletions`. That list is for Resources that were created and could not be
3081
+ removed.
3082
+
3083
+ ### Resources deliberately left out
3084
+
3085
+ `stack.skippedResources` is for gaps. A Resource it names is one a test written against would find
3086
+ missing, so some Resources are deliberately kept out of it. Those are the ones the simulator left
3087
+ uncreated on purpose, because nothing it models could tell them apart from Resources it had created.
3088
+ They are in `stack.inertResources` instead, each with an `inertReason` for what it would take for the
3089
+ difference to start mattering.
3090
+
3091
+ ```typescript sim-cloudformation-inert-resources
3092
+ /**
3093
+ * Telling a Resource a Stack is missing from one it left out on purpose.
3094
+ */
3095
+
3096
+ import { SimAws } from "@kensio/yulin";
3097
+
3098
+ const simAws = new SimAws();
3099
+
3100
+ const stack = await simAws.cloudFormation().deployTemplate({
3101
+ stackName: "inert-stack",
3102
+ template: {
3103
+ Resources: {
3104
+ AwsCliLayer: {
3105
+ Type: "AWS::Lambda::LayerVersion",
3106
+ Properties: {
3107
+ Description: "/opt/awscli/aws",
3108
+ },
3109
+ },
3110
+ AlarmRule: {
3111
+ Type: "AWS::CloudWatch::Alarm",
3112
+ },
3113
+ },
3114
+ },
3115
+ });
3116
+
3117
+ await stack.waitForDeployComplete();
3118
+
3119
+ console.log(stack.skippedResources.map((resource) => resource.logicalId));
3120
+ // ["AlarmRule"]
3121
+
3122
+ console.log(stack.inertResources.map((resource) => resource.logicalId));
3123
+ // ["AwsCliLayer"]
3124
+
3125
+ console.log(stack.getResource("AwsCliLayer")?.inertReason);
3126
+ // "sim Lambda runs a function's own code archive, or a real in-process handler
3127
+ // bound to it, so nothing a Layer carries is ever on a simulated function's
3128
+ // module path"
3129
+ ```
3130
+
3131
+ An inert Resource behaves in every other way like a skipped one. It is still there for `stack.getResource(...)`,
3132
+ it answers `Ref` and `Fn::GetAtt` with the same [stand-in values](#values-from-a-skipped-resource),
3133
+ and a teardown steps over it.
3134
+
3135
+ Two things make a Resource inert. Its type can be one no simulated service reads:
3136
+
3137
+ - `AWS::Lambda::LayerVersion`, because sim Lambda runs a function's own code archive, or a real
3138
+ in-process handler [bound to it](#lambda-function-bindings), and never assembles a Layer onto a
3139
+ function's module path.
3140
+ - `AWS::CDK::Metadata`, the construct-library analytics CDK adds to every synthesized stack.
3141
+
3142
+ Or the stack around it can. The provider Lambda function for a CDK custom resource the simulator
3143
+ carries out itself is inert. Its log group stays ordinary, because log groups are created, and an
3144
+ empty one is what an account is left with when nothing invokes the provider either. See
3145
+ [the provider CDK synthesizes](#the-provider-cdk-synthesizes).
3146
+
3147
+ ## Properties a Resource was created without
3148
+
3149
+ Deployment is best effort. A Resource type outside the simulation is skipped and the rest of the
3150
+ stack still deploys. The same goes one level down. A property the Resource's own service cannot act
3151
+ on still leaves the Resource created. It is left out, and the omission is recorded in
3152
+ `stack.ignoredProperties`.
3153
+
3154
+ ```typescript sim-cloudformation-ignored-properties
3155
+ /**
3156
+ * Finding out which properties a Stack created its Resources without.
3157
+ */
3158
+
3159
+ import { SimAws } from "@kensio/yulin";
3160
+
3161
+ const simAws = new SimAws();
3162
+
3163
+ const stack = await simAws.cloudFormation().deployTemplate({
3164
+ stackName: "uploads-stack",
3165
+ template: {
3166
+ Resources: {
3167
+ UploadsBucket: {
3168
+ Type: "AWS::S3::Bucket",
3169
+ Properties: {
3170
+ BucketName: "uploads",
3171
+ VersioningConfiguration: { Status: "Enabled" },
3172
+ },
3173
+ },
3174
+ },
3175
+ },
3176
+ });
3177
+
3178
+ await stack.waitForDeployComplete();
3179
+
3180
+ // The Bucket exists and is usable, unversioned.
3181
+ console.log(stack.getResource("UploadsBucket")?.deployed);
3182
+ // true
3183
+
3184
+ for (const ignored of stack.ignoredProperties) {
3185
+ console.log(ignored.logicalId, ignored.path, ignored.reason);
3186
+ // "UploadsBucket VersioningConfiguration VersioningConfiguration is a real
3187
+ // AWS::S3::Bucket property simulated S3 does not act on: Object versions
3188
+ // are not simulated, ..."
3189
+ }
3190
+ ```
3191
+
3192
+ A template parameter that resolved to a stand-in value is recorded in the same list, with the
3193
+ parameter name as the `logicalId`, its declared `Type` as the `resourceType`, and a `path` of
3194
+ `Parameters.<parameter name>`. Only the `AWS::SSM::Parameter::Value<...>` types can produce one.
3195
+
3196
+ Each entry names the `logicalId` and `resourceType` of the Resource, the `path` to the property, and
3197
+ a `reason`. The path is the whole way down, and a setting on one entry of a list says which entry it
3198
+ was on, such as `GlobalSecondaryIndexes.1.WarmThroughput`. The same list is on each Resource as
3199
+ `resource.ignoredProperties`.
3200
+
3201
+ **An ignored property means the simulated Resource behaves differently to the one the template
3202
+ describes.** That is the trade this makes. A template deploys as far as it can, and the record is
3203
+ where to check whether what it could not do matters to the test you are writing. A test asserting on
3204
+ object versions, on a dead-letter queue, or on a rotated key needs to look here before trusting the
3205
+ result.
3206
+
3207
+ A property name AWS has never had is recorded the same way, and never fails the stack. A typo and a
3208
+ property AWS added after this simulator read the docs look identical from here, and a Resource that
3209
+ deploys with the unread name reported is more useful than a stack that fails over either.
3210
+
3211
+ Two things are still refused outright, and fail the Resource:
3212
+
3213
+ - A property that leaves nothing coherent to create, such as an `AWS::S3::Bucket` whose `BucketName`
3214
+ is some type other than a string, or an `AWS::DynamoDB::GlobalTable` whose replica list omits the
3215
+ region the stack is deploying into. Real CloudFormation refuses these templates too.
3216
+ - A value the simulated service itself refuses, in the same words an SDK caller gets. An
3217
+ `AWS::SQS::Queue` with `FifoQueue: true` is one. A FIFO queue is named `<name>.fifo`, which
3218
+ simulated SQS refuses, leaving no queue to create under the name the template gave it.
3219
+
3220
+ Properties nothing simulated could tell apart are left off the list. There is no simulated KMS and
3221
+ Object bytes are stored as they arrive. An `AWS::S3::Bucket` carrying `BucketEncryption` and `Tags`,
3222
+ as almost every Bucket CDK synthesizes does, therefore records nothing. A report of differences that
3223
+ make no difference is one nobody can read.
3224
+
3225
+ ## Handling deployment failures
3226
+
3227
+ Some deployment failures happen asynchronously after stack creation has started. To observe those
3228
+ failures in tests, wait for deployment completion.
3229
+
3230
+ ```typescript sim-cloudformation-deployment-failure
3231
+ /**
3232
+ * Observing simulated CloudFormation deployment failures.
3233
+ */
3234
+
3235
+ import { SimAws } from "@kensio/yulin";
3236
+
3237
+ const simAws = new SimAws();
3238
+
3239
+ const stack = await simAws.cloudFormation().deployTemplate({
3240
+ stackName: "failure-stack",
3241
+ template: {
3242
+ Resources: {
3243
+ InvalidBucket: {
3244
+ Type: "AWS::S3::Bucket",
3245
+ Properties: {
3246
+ BucketName: "Invalid_Bucket_Name",
3247
+ },
3248
+ },
3249
+ },
3250
+ },
3251
+ });
3252
+
3253
+ try {
3254
+ await stack.waitForDeployComplete();
3255
+ } catch (error) {
3256
+ console.error("Stack deployment failed", error);
3257
+ }
3258
+ ```
3259
+
3260
+ If you use `waitForStackDeployComplete(...)`, deployment errors are also rethrown there.
3261
+
3262
+ ```typescript
3263
+ import { SimAws } from "@kensio/yulin";
3264
+
3265
+ const simAws = new SimAws();
3266
+
3267
+ // Create a stack...
3268
+
3269
+ try {
3270
+ await simAws.cloudFormation().waitForStackDeployComplete("failure-stack");
3271
+ } catch (error) {
3272
+ console.error("Stack deployment failed", error);
3273
+ }
3274
+ ```
3275
+
3276
+ ## Standalone SimCloudFormation
3277
+
3278
+ Most users should access CloudFormation through `SimAws` so that CloudFormation can create resources
3279
+ in the same simulated AWS environment as S3, CloudFront, and other services.
3280
+
3281
+ ```typescript
3282
+ import { SimAws } from "@kensio/yulin";
3283
+
3284
+ const simAws = new SimAws();
3285
+ const simCfn = simAws.cloudFormation();
3286
+ ```
3287
+
3288
+ `SimCloudFormation` is also exported from `@kensio/yulin/cloudformation` for advanced cases. In
3289
+ normal application tests, prefer the `SimAws` entry point.
3290
+
3291
+ ## Available functionality
3292
+
3293
+ Sim CloudFormation currently supports:
3294
+
3295
+ - `CreateStackCommand`, `DescribeStacksCommand`, `UpdateStackCommand` and `DeleteStackCommand`,
3296
+ taking a `TemplateBody` written as JSON or as YAML with short-form intrinsic tags
3297
+ - Waiting for simulated stack deployment, update and deletion completion
3298
+ - The resource `DeletionPolicy` attribute, for `Retain` and `RetainExceptOnCreate`
3299
+ - `deployTemplate(...)` for parsed template objects, optionally naming the synthesized template file
3300
+ a template edited in memory came from
3301
+ - `deployTemplateFile(...)` for template files, written as JSON or as YAML with short-form
3302
+ intrinsic tags
3303
+ - `updateTemplateFile(...)` for applying a synthesized template file to the stack it was deployed as
3304
+ - Watching a deployed template file, updating its stack in place whenever the file changes
3305
+ - Template `Parameters` with supplied values and defaults
3306
+ - Template `Outputs`, resolved after resource creation and read from `stack.outputs`
3307
+ - Template `Mappings`, read with `Fn::FindInMap`
3308
+ - Template `Conditions`, built from `Fn::Equals`, `Fn::And`, `Fn::Or` and `Fn::Not`
3309
+ - The resource `Condition` attribute, which decides whether a resource is created
3310
+ - The `Ref`, `Fn::GetAtt`, `Fn::Join`, `Fn::Sub`, `Fn::FindInMap`, `Fn::If`, `Fn::Split` and
3311
+ `Fn::Select` intrinsic functions
3312
+ - Explicit resource dependencies with `DependsOn`
3313
+ - Implicit dependencies from resource `Ref` expressions
3314
+ - SAM templates naming the `AWS::Serverless-2016-10-31` transform, with `AWS::Serverless::Function`
3315
+ expanded into a Lambda function and its execution Role, `AWS::Serverless::SimpleTable` into a
3316
+ DynamoDB table, `AWS::Serverless::HttpApi` into an HTTP API and its stage,
3317
+ `AWS::Serverless::Api` into a REST API with its deployment and stage, and the `Globals.Function`,
3318
+ `Globals.HttpApi` and `Globals.Api` defaults applied
3319
+ - The `HttpApi` event of a SAM function, expanded into the API, integration, route, stage and invoke
3320
+ permission that serve it, and the `Api` event, expanded into the API, path resources, method,
3321
+ deployment, stage and invoke permission that serve it
3322
+ - `FunctionUrlConfig` on a SAM function, expanded into a Function URL
3323
+ - The `Schedule`, `ScheduleV2` and `EventBridgeRule` events of a SAM function, expanded into the
3324
+ EventBridge rule or Scheduler schedule that fires the function, and the permission or execution
3325
+ Role the invocation is authorized by
3326
+
3327
+ The resource types it creates are:
3328
+
3329
+ - `AWS::ApiGateway::RestApi`, `AWS::ApiGateway::Resource`, `AWS::ApiGateway::Method`,
3330
+ `AWS::ApiGateway::Deployment` and `AWS::ApiGateway::Stage`
3331
+ - `AWS::ApiGatewayV2::Api`, `AWS::ApiGatewayV2::Integration`, `AWS::ApiGatewayV2::Route` and
3332
+ `AWS::ApiGatewayV2::Stage`
3333
+ - `AWS::CertificateManager::Certificate`
3334
+ - `AWS::CloudFormation::WaitConditionHandle`
3335
+ - `AWS::CloudFront::Distribution`, `AWS::CloudFront::Function` and
3336
+ `AWS::CloudFront::ResponseHeadersPolicy`
3337
+ - `AWS::CloudWatch::Alarm`
3338
+ - `AWS::Cognito::UserPool`, `AWS::Cognito::UserPoolClient` and
3339
+ `AWS::Cognito::UserPoolGroup`
3340
+ - `AWS::DynamoDB::Table` and `AWS::DynamoDB::GlobalTable`
3341
+ - `AWS::ECR::Repository`
3342
+ - `AWS::Events::EventBus` and `AWS::Events::Rule`, with the rule's inline `Targets`
3343
+ - `AWS::IAM::Role`, `AWS::IAM::User`, `AWS::IAM::ManagedPolicy` and `AWS::IAM::Policy`
3344
+ - `AWS::Kinesis::Stream`
3345
+ - `AWS::KinesisFirehose::DeliveryStream`
3346
+ - `AWS::KMS::Key` and `AWS::KMS::Alias`
3347
+ - `AWS::Lambda::Function`, `AWS::Lambda::Url` and `AWS::Lambda::Permission`
3348
+ - `AWS::Logs::LogGroup`
3349
+ - `AWS::Route53::HostedZone`, `AWS::Route53::RecordSet`, `AWS::Route53::KeySigningKey` and
3350
+ `AWS::Route53::DNSSEC`
3351
+ - `AWS::S3::Bucket` and `AWS::S3::BucketPolicy`
3352
+ - `AWS::Scheduler::Schedule`
3353
+ - `AWS::SecretsManager::Secret`
3354
+ - `AWS::SQS::Queue`
3355
+ - `AWS::SSM::Parameter`
3356
+ - `AWS::StepFunctions::StateMachine`
3357
+ - selected CDK custom resources: `Custom::CDKBucketDeployment`, `Custom::S3BucketNotifications` and
3358
+ `Custom::CrossRegionStringParameterReader`
3359
+
3360
+ Each service's own docs describe what its resource types support.
3361
+
3362
+ ## Limitations
3363
+
3364
+ - Only supported resource types create simulated service resources. An unsupported resource may be
3365
+ skipped or may fail the stack, depending on how safely the simulator can model it. A skipped
3366
+ resource answers `Ref` and `Fn::GetAtt` with
3367
+ [stand-in values](#values-from-a-skipped-resource) rather than the value a created resource would
3368
+ have given.
3369
+ - `stack.skippedResources` deliberately leaves out the resources the simulator did not create on
3370
+ purpose, because nothing it models could tell them apart from ones it had. Those are in
3371
+ `stack.inertResources` instead, and are listed under
3372
+ [resources deliberately left out](#resources-deliberately-left-out). Read both when accounting for
3373
+ every resource in a template.
3374
+ - `AWS::IAM::ManagedPolicy` is created from `ManagedPolicyName`, `Path`, `Description`,
3375
+ `PolicyDocument` and `Roles`. Every `Roles` entry names a Role in the Stack's Account, and the
3376
+ created policy is attached to each of them. An entry naming no simulated Role fails the resource,
3377
+ as an `AWS::IAM::Policy` naming one does. `Users` and `Groups` fail the resource, because neither
3378
+ can hold a managed policy attachment in the simulation. Deleting the stack takes the policy off
3379
+ the Roles still carrying it before deleting the policy itself.
3380
+ - `AWS::Logs::LogGroup` is created, including the one CDK writes for a custom resource provider. That
3381
+ one is left empty, because the provider is never invoked. A log group a stack declares for a
3382
+ Lambda function is the same group that function writes to, and a group already there is taken over
3383
+ rather than failing the deploy the way real CloudFormation does. See the
3384
+ [simulated CloudWatch Logs docs](https://yulinsim.dev/services/logs/ "Simulated CloudWatch Logs usage docs").
3385
+ - A resource property outside the simulation is left out and recorded in `stack.ignoredProperties`
3386
+ rather than failing the stack. The resource is created behaving differently to the one the
3387
+ template describes. See
3388
+ [properties a Resource was created without](#properties-a-resource-was-created-without) for what
3389
+ is still refused outright.
3390
+ - A stack update replaces a changed resource rather than updating it in place, so what the resource
3391
+ held is lost. See [changed resources are replaced](#changed-resources-are-replaced).
3392
+ - A watched template file updates its stack in place. That makes the update itself no gentler. A
3393
+ changed resource is still replaced and loses what it holds, the same as any other update.
3394
+ - Yulin never synthesizes a CDK app. It watches the synthesized output template. A change to the app
3395
+ itself reaches the stack once something has run `cdk synth` over it.
3396
+ - A stack update applies a whole template directly. Change sets are outside the simulation, so
3397
+ `CreateChangeSetCommand` and `ExecuteChangeSetCommand` have nothing behind them, and neither does
3398
+ drift detection.
3399
+ - A failed stack update is not rolled back to the template the stack was deployed from. The stack is
3400
+ left in `UPDATE_FAILED` holding whatever the update managed.
3401
+ - `UpdateStackCommand` reads `StackName`, `TemplateBody` and `Parameters`. `UsePreviousTemplate` and
3402
+ `UsePreviousValue` are not read, so an update has to be given the whole new template.
3403
+ - An update asked for while another is still running is refused, as CloudFormation refuses it. There
3404
+ is no queue behind it.
3405
+ - A stack deletion deletes only the resource types the simulator can delete. A resource type it
3406
+ creates but cannot delete is recorded in `stack.skippedResourceDeletions` and stepped over, the
3407
+ same way an unsupported resource type is on create, and the stack still deletes with that resource
3408
+ left behind.
3409
+ - `DeletionPolicy` is read for `Retain` and `RetainExceptOnCreate` only. `Snapshot` is treated as
3410
+ `Delete`, because no simulated service takes snapshots.
3411
+ - `UpdateReplacePolicy` is not read. A replaced resource is deleted whatever it says, for the reason
3412
+ given under [changed resources are replaced](#changed-resources-are-replaced).
3413
+ - `DeleteStackCommand` reads only `StackName`. `RetainResources`, `DeletionMode`, `RoleARN` and
3414
+ `ClientRequestToken` are not read, so a stack left in `DELETE_FAILED` cannot be forced through the
3415
+ way `FORCE_DELETE_STACK` forces it in AWS.
3416
+ - A deleted stack cannot be described. Real CloudFormation keeps a deleted stack readable by its
3417
+ unique stack ID, and the simulator identifies a stack by its name alone.
3418
+ - `Fn::FindInMap` accepts only the three-argument form. The four-argument form, where the fourth
3419
+ argument is `{ "DefaultValue": ... }`, is rejected.
3420
+ - `Fn::FindInMap` arguments are resolved from literals, `Parameters` and pseudo parameters. An
3421
+ argument that depends on a created resource, such as a `Ref` to a resource logical ID, fails the
3422
+ resource with a "could not find map" error. Real CloudFormation allows only `Ref` and a nested
3423
+ `Fn::FindInMap` inside `Fn::FindInMap`. The templates affected here are ones real CloudFormation
3424
+ would reject as well. The simulator just rejects them later rather than up front.
3425
+ - `Fn::If` is unsupported inside the `Conditions` section itself. It is rejected there rather than
3426
+ read against a half-evaluated section.
3427
+ - `Fn::Split` and `Fn::Select` accept any argument that resolves to the type they need. Real
3428
+ CloudFormation allows only a named set of functions inside each of them. A template the simulator
3429
+ resolves may still be one CloudFormation rejects.
3430
+ - The `Condition` attribute is read on resources but not on outputs. An output carrying one is
3431
+ resolved and present in `stack.outputs` whichever way its condition falls, where real
3432
+ CloudFormation would leave it out.
3433
+ - The SAM transform is expanded for `AWS::Serverless::Function`, `AWS::Serverless::SimpleTable`,
3434
+ `AWS::Serverless::HttpApi` and `AWS::Serverless::Api`. Every other `AWS::Serverless::*` resource
3435
+ type is recorded as unsupported. `Api`, `HttpApi`, `SQS`, `DynamoDB`, `SNS`, `S3`, `Schedule`,
3436
+ `ScheduleV2` and `EventBridgeRule` are the event types expanded, and an event of another type,
3437
+ such as `Cognito`, is left where it is. `Auth` on an `Api` or `HttpApi` event, and on the implicit API
3438
+ either of them shares, is left out. `AutoPublishAlias` and `DeploymentPreference` are left out
3439
+ too, because the simulator has one version of a function and nothing to shift traffic between.
3440
+ - Many advanced CloudFormation features are outside the simulation.