@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.
- package/README.md +13 -0
- package/{dist/config/oxlint/cffjs2.oxlintrc.json → cffjs2.oxlintrc.json} +1 -1
- package/dist/config/oxlint/cffjs2.oxlint.config.js +4 -3
- package/dist/sdk/module/sim-sdk-module-client-interceptor.d.ts +5 -0
- package/dist/sdk/module/sim-sdk-module-client-interceptor.js +14 -0
- package/dist/sdk/module/sim-sdk-static-client-factory.d.ts +26 -0
- package/dist/sdk/module/sim-sdk-static-client-factory.js +59 -0
- package/dist/sdk/send-patch.d.ts +8 -0
- package/dist/sdk/send-patch.js +11 -0
- package/dist/service/aws/caller/sim-aws-caller-resolver.d.ts +28 -6
- package/dist/service/aws/caller/sim-aws-caller-resolver.js +18 -11
- package/dist/service/aws/caller/sim-aws-caller.d.ts +9 -0
- package/dist/service/aws/caller/sim-aws-resolved-caller.d.ts +19 -0
- package/dist/service/aws/caller/sim-aws-resolved-caller.js +26 -0
- package/dist/service/aws/factory/sim-aws-account-region-service-builder.d.ts +6 -0
- package/dist/service/aws/factory/sim-aws-account-region-service-builder.js +3 -0
- package/dist/service/aws/factory/sim-aws-account-service-cache.d.ts +6 -0
- package/dist/service/aws/factory/sim-aws-account-service-cache.js +5 -0
- package/dist/service/aws/factory/sim-aws-service-factory.d.ts +5 -0
- package/dist/service/aws/factory/sim-aws-service-factory.js +6 -0
- package/dist/service/aws/sim-aws-properties.d.ts +15 -0
- package/dist/service/aws/sim-aws.js +2 -1
- package/dist/service/cloudformation/resource/resolve/property/sim-cfn-resource-property-resolver.d.ts +11 -4
- package/dist/service/cloudformation/resource/resolve/property/sim-cfn-resource-property-resolver.js +21 -8
- package/dist/service/cloudformation/resource/sim-cfn-resource.type.d.ts +9 -0
- package/dist/service/cloudformation/template/dynamic/make-sim-cfn-dynamic-references.d.ts +2 -0
- package/dist/service/cloudformation/template/dynamic/make-sim-cfn-dynamic-references.js +2 -2
- package/dist/service/cloudformation/template/dynamic/sim-cfn-dynamic-reference-resolvers.d.ts +10 -0
- package/dist/service/cloudformation/template/dynamic/sim-cfn-dynamic-reference-resolvers.js +4 -3
- package/dist/service/cloudformation/template/dynamic/sim-cfn-dynamic-reference-scan.d.ts +11 -0
- package/dist/service/cloudformation/template/dynamic/sim-cfn-dynamic-reference-scan.js +21 -0
- package/dist/service/cloudformation/template/dynamic/sim-cfn-dynamic-references.d.ts +24 -2
- package/dist/service/cloudformation/template/dynamic/sim-cfn-dynamic-references.js +50 -11
- package/dist/service/cloudformation/template/dynamic/sim-cfn-prefetched-dynamic-references.d.ts +42 -0
- package/dist/service/cloudformation/template/dynamic/sim-cfn-prefetched-dynamic-references.js +80 -0
- package/dist/service/eventbridge/delivery/sim-event-bridge-delivery-failures.d.ts +16 -0
- package/dist/service/eventbridge/delivery/sim-event-bridge-delivery-failures.js +19 -0
- package/dist/service/iam/authorize/context/sim-iam-auth-z-caller-context-builder.d.ts +30 -7
- package/dist/service/iam/authorize/context/sim-iam-auth-z-caller-context-builder.js +20 -11
- package/dist/service/iam/authorize/context/sim-iam-auth-z-context-builder.d.ts +2 -1
- package/dist/service/iam/authorize/context/sim-iam-auth-z-context-builder.js +5 -1
- package/dist/service/iam/authorize/match/sim-iam-policy-principal-matcher.d.ts +10 -0
- package/dist/service/iam/authorize/match/sim-iam-policy-principal-matcher.js +15 -3
- package/dist/service/iam/authorize/sim-iam-account-auth-z.d.ts +11 -4
- package/dist/service/iam/authorize/sim-iam-account-auth-z.js +5 -3
- package/dist/service/iam/authorize/sim-iam-authorizer.d.ts +2 -1
- package/dist/service/iam/authorize/sim-iam-authorizer.js +1 -0
- package/dist/service/iam/command/policy/create-policy/create-policy-input-resolver.js +4 -1
- package/dist/service/iam/command/policy/put-role-policy/put-role-policy.handler.js +5 -1
- package/dist/service/iam/command/policy/put-user-policy/put-user-policy.handler.js +5 -1
- package/dist/service/iam/policy/parse/sim-iam-document-parser.d.ts +4 -1
- package/dist/service/iam/policy/parse/sim-iam-document-parser.js +13 -15
- package/dist/service/iam/policy/sim-iam-statement-label.d.ts +28 -0
- package/dist/service/iam/policy/sim-iam-statement-label.js +17 -0
- package/dist/service/iam/policy/sim-iam-statement-strings.d.ts +11 -0
- package/dist/service/iam/policy/sim-iam-statement-strings.js +27 -0
- package/dist/service/iam/role/sim-iam-role-with-policy.factory.d.ts +9 -0
- package/dist/service/iam/role/sim-iam-role-with-policy.factory.js +4 -2
- package/dist/service/iam/sim-iam-account-parts.d.ts +8 -0
- package/dist/service/iam/sim-iam-account-parts.js +1 -0
- package/dist/service/iam/validate/sim-iam-policy-document-validator.d.ts +9 -2
- package/dist/service/iam/validate/sim-iam-policy-document-validator.js +37 -19
- package/dist/service/lambda/function/code/sim-lambda-vm-zip-function.factory.d.ts +7 -0
- package/dist/service/lambda/function/code/sim-lambda-vm-zip-function.factory.js +1 -0
- package/dist/service/lambda/function/code/vm/sdk/sim-lambda-provided-sdk-module.d.ts +19 -0
- package/dist/service/lambda/function/code/vm/sdk/sim-lambda-provided-sdk-module.js +78 -0
- package/dist/service/lambda/function/code/vm/sdk/sim-lambda-sdk-packages-not-installed.error.d.ts +15 -0
- package/dist/service/lambda/function/code/vm/sdk/sim-lambda-sdk-packages-not-installed.error.js +27 -0
- package/dist/service/lambda/function/code/vm/sdk/sim-lambda-vm-sdk-module-provider.d.ts +9 -0
- package/dist/service/lambda/function/code/vm/sdk/sim-sdk-lambda-vm-module-provider.d.ts +10 -0
- package/dist/service/lambda/function/code/vm/sdk/sim-sdk-lambda-vm-module-provider.js +28 -34
- package/dist/service/lambda/function/code/vm/sim-lambda-host-modules.d.ts +15 -0
- package/dist/service/lambda/function/code/vm/sim-lambda-host-modules.js +31 -0
- package/dist/service/lambda/function/code/vm/sim-lambda-vm-module-resolver.d.ts +7 -0
- package/dist/service/lambda/function/code/vm/sim-lambda-vm-module-resolver.js +15 -0
- package/dist/service/lambda/function/code/vm/sim-lambda-vm-modules.js +5 -4
- package/dist/service/s3/command/put-bucket-policy/put-bucket-policy.handler.js +4 -1
- package/dist/service/scheduler/delivery/sim-scheduler-delivery-failures.d.ts +16 -0
- package/dist/service/scheduler/delivery/sim-scheduler-delivery-failures.js +19 -0
- package/dist/service/scheduler/index.d.ts +1 -1
- package/dist/service/scheduler/index.js +1 -1
- package/dist/service/secretsmanager/cfn/dynamic/sim-cfn-secrets-manager-dynamic-reference-resolver.d.ts +13 -15
- package/dist/service/secretsmanager/cfn/dynamic/sim-cfn-secrets-manager-dynamic-reference-resolver.js +22 -43
- package/dist/service/secretsmanager/cfn/dynamic/sim-cfn-secrets-manager-reference-stand-in.d.ts +9 -0
- package/dist/service/secretsmanager/cfn/dynamic/sim-cfn-secrets-manager-reference-stand-in.js +14 -0
- package/dist/service/secretsmanager/cfn/dynamic/sim-cfn-secrets-manager-reference-value.d.ts +12 -0
- package/dist/service/secretsmanager/cfn/dynamic/sim-cfn-secrets-manager-reference-value.js +29 -0
- package/dist/service/ssm/cfn/dynamic/sim-cfn-ssm-dynamic-reference-resolver.d.ts +22 -6
- package/dist/service/ssm/cfn/dynamic/sim-cfn-ssm-dynamic-reference-resolver.js +39 -24
- package/dist/service/ssm/cfn/dynamic/sim-cfn-ssm-secure-dynamic-reference-resolver.d.ts +10 -5
- package/dist/service/ssm/cfn/dynamic/sim-cfn-ssm-secure-dynamic-reference-resolver.js +10 -8
- package/dist/service/ssm/sim-ssm.d.ts +10 -2
- package/dist/service/ssm/sim-ssm.js +12 -4
- package/dist/service/sts/auth-z/assume-role-auth-z-coordinator.d.ts +9 -2
- package/dist/service/sts/auth-z/assume-role-auth-z-coordinator.js +2 -2
- package/dist/service/sts/auth-z/assume-role-source-account-auth-z.d.ts +10 -4
- package/dist/service/sts/auth-z/assume-role-source-account-auth-z.js +8 -7
- package/dist/service/sts/auth-z/assume-role-target-auth-z.d.ts +2 -2
- package/dist/service/sts/auth-z/assume-role-trust-policy-authorizer.d.ts +6 -2
- package/dist/service/sts/auth-z/assume-role-trust-policy-authorizer.js +14 -11
- package/dist/service/sts/command/assume-role/assume-role.handler.d.ts +5 -1
- package/dist/service/sts/command/assume-role/assume-role.handler.js +5 -2
- package/dist/service/sts/command/get-caller-identity/get-caller-identity.handler.d.ts +5 -1
- package/dist/service/sts/command/get-caller-identity/get-caller-identity.handler.js +4 -1
- package/dist/service/sts/service-role/sim-service-role.js +5 -1
- package/dist/service/sts/sim-sts.d.ts +9 -1
- package/dist/service/sts/sim-sts.js +4 -0
- package/docs/README.md +54 -0
- package/docs/ai-skill/README.md +69 -0
- package/docs/cli/README.md +284 -0
- package/docs/factories/README.md +140 -0
- package/docs/lint/README.md +176 -0
- package/docs/non-aws-dependencies/README.md +261 -0
- package/docs/sdk/README.md +272 -0
- package/docs/serve/README.md +1126 -0
- package/docs/services/acm/README.md +823 -0
- package/docs/services/apigateway/README.md +1919 -0
- package/docs/services/apigatewayv2/README.md +2678 -0
- package/docs/services/athena/README.md +1044 -0
- package/docs/services/bedrock/README.md +350 -0
- package/docs/services/cloudformation/README.md +3440 -0
- package/docs/services/cloudfront/README.md +2874 -0
- package/docs/services/cloudwatch/README.md +420 -0
- package/docs/services/cognito/README.md +4923 -0
- package/docs/services/dynamodb/README.md +3784 -0
- package/docs/services/ecr/README.md +302 -0
- package/docs/services/ecs/README.md +2010 -0
- package/docs/services/elbv2/README.md +1897 -0
- package/docs/services/eventbridge/README.md +1110 -0
- package/docs/services/firehose/README.md +805 -0
- package/docs/services/glue/README.md +501 -0
- package/docs/services/iam/README.md +1283 -0
- package/docs/services/kinesis/README.md +454 -0
- package/docs/services/kms/README.md +659 -0
- package/docs/services/lambda/README.md +3645 -0
- package/docs/services/logs/README.md +868 -0
- package/docs/services/organizations/README.md +574 -0
- package/docs/services/personalize/README.md +835 -0
- package/docs/services/rekognition/README.md +1070 -0
- package/docs/services/route53/README.md +1783 -0
- package/docs/services/s3/README.md +3024 -0
- package/docs/services/scheduler/README.md +806 -0
- package/docs/services/secretsmanager/README.md +667 -0
- package/docs/services/ses/README.md +1011 -0
- package/docs/services/sns/README.md +1904 -0
- package/docs/services/sqs/README.md +1185 -0
- package/docs/services/ssm/README.md +1072 -0
- package/docs/services/stepfunctions/README.md +1332 -0
- package/docs/services/sts/README.md +245 -0
- package/docs/services/wafv2/README.md +1378 -0
- package/docs/terraform/README.md +248 -0
- package/docs/time/README.md +284 -0
- package/llms.txt +59 -0
- package/package.json +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.
|