@kensio/yulin 1.20.14 → 1.20.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/README.md +13 -0
  2. package/{dist/config/oxlint/cffjs2.oxlintrc.json → cffjs2.oxlintrc.json} +1 -1
  3. package/dist/config/oxlint/cffjs2.oxlint.config.js +4 -3
  4. package/dist/sdk/module/sim-sdk-module-client-interceptor.d.ts +5 -0
  5. package/dist/sdk/module/sim-sdk-module-client-interceptor.js +14 -0
  6. package/dist/sdk/module/sim-sdk-static-client-factory.d.ts +26 -0
  7. package/dist/sdk/module/sim-sdk-static-client-factory.js +59 -0
  8. package/dist/sdk/send-patch.d.ts +8 -0
  9. package/dist/sdk/send-patch.js +11 -0
  10. package/dist/service/aws/caller/sim-aws-caller-resolver.d.ts +28 -6
  11. package/dist/service/aws/caller/sim-aws-caller-resolver.js +18 -11
  12. package/dist/service/aws/caller/sim-aws-caller.d.ts +9 -0
  13. package/dist/service/aws/caller/sim-aws-resolved-caller.d.ts +19 -0
  14. package/dist/service/aws/caller/sim-aws-resolved-caller.js +26 -0
  15. package/dist/service/aws/factory/sim-aws-account-region-service-builder.d.ts +6 -0
  16. package/dist/service/aws/factory/sim-aws-account-region-service-builder.js +3 -0
  17. package/dist/service/aws/factory/sim-aws-account-service-cache.d.ts +6 -0
  18. package/dist/service/aws/factory/sim-aws-account-service-cache.js +5 -0
  19. package/dist/service/aws/factory/sim-aws-service-factory.d.ts +5 -0
  20. package/dist/service/aws/factory/sim-aws-service-factory.js +6 -0
  21. package/dist/service/aws/sim-aws-properties.d.ts +15 -0
  22. package/dist/service/aws/sim-aws.js +2 -1
  23. package/dist/service/cloudformation/resource/resolve/property/sim-cfn-resource-property-resolver.d.ts +11 -4
  24. package/dist/service/cloudformation/resource/resolve/property/sim-cfn-resource-property-resolver.js +21 -8
  25. package/dist/service/cloudformation/resource/sim-cfn-resource.type.d.ts +9 -0
  26. package/dist/service/cloudformation/template/dynamic/make-sim-cfn-dynamic-references.d.ts +2 -0
  27. package/dist/service/cloudformation/template/dynamic/make-sim-cfn-dynamic-references.js +2 -2
  28. package/dist/service/cloudformation/template/dynamic/sim-cfn-dynamic-reference-resolvers.d.ts +10 -0
  29. package/dist/service/cloudformation/template/dynamic/sim-cfn-dynamic-reference-resolvers.js +4 -3
  30. package/dist/service/cloudformation/template/dynamic/sim-cfn-dynamic-reference-scan.d.ts +11 -0
  31. package/dist/service/cloudformation/template/dynamic/sim-cfn-dynamic-reference-scan.js +21 -0
  32. package/dist/service/cloudformation/template/dynamic/sim-cfn-dynamic-references.d.ts +24 -2
  33. package/dist/service/cloudformation/template/dynamic/sim-cfn-dynamic-references.js +50 -11
  34. package/dist/service/cloudformation/template/dynamic/sim-cfn-prefetched-dynamic-references.d.ts +42 -0
  35. package/dist/service/cloudformation/template/dynamic/sim-cfn-prefetched-dynamic-references.js +80 -0
  36. package/dist/service/eventbridge/delivery/sim-event-bridge-delivery-failures.d.ts +16 -0
  37. package/dist/service/eventbridge/delivery/sim-event-bridge-delivery-failures.js +19 -0
  38. package/dist/service/iam/authorize/context/sim-iam-auth-z-caller-context-builder.d.ts +30 -7
  39. package/dist/service/iam/authorize/context/sim-iam-auth-z-caller-context-builder.js +20 -11
  40. package/dist/service/iam/authorize/context/sim-iam-auth-z-context-builder.d.ts +2 -1
  41. package/dist/service/iam/authorize/context/sim-iam-auth-z-context-builder.js +5 -1
  42. package/dist/service/iam/authorize/match/sim-iam-policy-principal-matcher.d.ts +10 -0
  43. package/dist/service/iam/authorize/match/sim-iam-policy-principal-matcher.js +15 -3
  44. package/dist/service/iam/authorize/sim-iam-account-auth-z.d.ts +11 -4
  45. package/dist/service/iam/authorize/sim-iam-account-auth-z.js +5 -3
  46. package/dist/service/iam/authorize/sim-iam-authorizer.d.ts +2 -1
  47. package/dist/service/iam/authorize/sim-iam-authorizer.js +1 -0
  48. package/dist/service/iam/command/policy/create-policy/create-policy-input-resolver.js +4 -1
  49. package/dist/service/iam/command/policy/put-role-policy/put-role-policy.handler.js +5 -1
  50. package/dist/service/iam/command/policy/put-user-policy/put-user-policy.handler.js +5 -1
  51. package/dist/service/iam/policy/parse/sim-iam-document-parser.d.ts +4 -1
  52. package/dist/service/iam/policy/parse/sim-iam-document-parser.js +13 -15
  53. package/dist/service/iam/policy/sim-iam-statement-label.d.ts +28 -0
  54. package/dist/service/iam/policy/sim-iam-statement-label.js +17 -0
  55. package/dist/service/iam/policy/sim-iam-statement-strings.d.ts +11 -0
  56. package/dist/service/iam/policy/sim-iam-statement-strings.js +27 -0
  57. package/dist/service/iam/role/sim-iam-role-with-policy.factory.d.ts +9 -0
  58. package/dist/service/iam/role/sim-iam-role-with-policy.factory.js +4 -2
  59. package/dist/service/iam/sim-iam-account-parts.d.ts +8 -0
  60. package/dist/service/iam/sim-iam-account-parts.js +1 -0
  61. package/dist/service/iam/validate/sim-iam-policy-document-validator.d.ts +9 -2
  62. package/dist/service/iam/validate/sim-iam-policy-document-validator.js +37 -19
  63. package/dist/service/lambda/function/code/sim-lambda-vm-zip-function.factory.d.ts +7 -0
  64. package/dist/service/lambda/function/code/sim-lambda-vm-zip-function.factory.js +1 -0
  65. package/dist/service/lambda/function/code/vm/sdk/sim-lambda-provided-sdk-module.d.ts +19 -0
  66. package/dist/service/lambda/function/code/vm/sdk/sim-lambda-provided-sdk-module.js +78 -0
  67. package/dist/service/lambda/function/code/vm/sdk/sim-lambda-sdk-packages-not-installed.error.d.ts +15 -0
  68. package/dist/service/lambda/function/code/vm/sdk/sim-lambda-sdk-packages-not-installed.error.js +27 -0
  69. package/dist/service/lambda/function/code/vm/sdk/sim-lambda-vm-sdk-module-provider.d.ts +9 -0
  70. package/dist/service/lambda/function/code/vm/sdk/sim-sdk-lambda-vm-module-provider.d.ts +10 -0
  71. package/dist/service/lambda/function/code/vm/sdk/sim-sdk-lambda-vm-module-provider.js +28 -34
  72. package/dist/service/lambda/function/code/vm/sim-lambda-host-modules.d.ts +15 -0
  73. package/dist/service/lambda/function/code/vm/sim-lambda-host-modules.js +31 -0
  74. package/dist/service/lambda/function/code/vm/sim-lambda-vm-module-resolver.d.ts +7 -0
  75. package/dist/service/lambda/function/code/vm/sim-lambda-vm-module-resolver.js +15 -0
  76. package/dist/service/lambda/function/code/vm/sim-lambda-vm-modules.js +5 -4
  77. package/dist/service/s3/command/put-bucket-policy/put-bucket-policy.handler.js +4 -1
  78. package/dist/service/scheduler/delivery/sim-scheduler-delivery-failures.d.ts +16 -0
  79. package/dist/service/scheduler/delivery/sim-scheduler-delivery-failures.js +19 -0
  80. package/dist/service/scheduler/index.d.ts +1 -1
  81. package/dist/service/scheduler/index.js +1 -1
  82. package/dist/service/secretsmanager/cfn/dynamic/sim-cfn-secrets-manager-dynamic-reference-resolver.d.ts +13 -15
  83. package/dist/service/secretsmanager/cfn/dynamic/sim-cfn-secrets-manager-dynamic-reference-resolver.js +22 -43
  84. package/dist/service/secretsmanager/cfn/dynamic/sim-cfn-secrets-manager-reference-stand-in.d.ts +9 -0
  85. package/dist/service/secretsmanager/cfn/dynamic/sim-cfn-secrets-manager-reference-stand-in.js +14 -0
  86. package/dist/service/secretsmanager/cfn/dynamic/sim-cfn-secrets-manager-reference-value.d.ts +12 -0
  87. package/dist/service/secretsmanager/cfn/dynamic/sim-cfn-secrets-manager-reference-value.js +29 -0
  88. package/dist/service/ssm/cfn/dynamic/sim-cfn-ssm-dynamic-reference-resolver.d.ts +22 -6
  89. package/dist/service/ssm/cfn/dynamic/sim-cfn-ssm-dynamic-reference-resolver.js +39 -24
  90. package/dist/service/ssm/cfn/dynamic/sim-cfn-ssm-secure-dynamic-reference-resolver.d.ts +10 -5
  91. package/dist/service/ssm/cfn/dynamic/sim-cfn-ssm-secure-dynamic-reference-resolver.js +10 -8
  92. package/dist/service/ssm/sim-ssm.d.ts +10 -2
  93. package/dist/service/ssm/sim-ssm.js +12 -4
  94. package/dist/service/sts/auth-z/assume-role-auth-z-coordinator.d.ts +9 -2
  95. package/dist/service/sts/auth-z/assume-role-auth-z-coordinator.js +2 -2
  96. package/dist/service/sts/auth-z/assume-role-source-account-auth-z.d.ts +10 -4
  97. package/dist/service/sts/auth-z/assume-role-source-account-auth-z.js +8 -7
  98. package/dist/service/sts/auth-z/assume-role-target-auth-z.d.ts +2 -2
  99. package/dist/service/sts/auth-z/assume-role-trust-policy-authorizer.d.ts +6 -2
  100. package/dist/service/sts/auth-z/assume-role-trust-policy-authorizer.js +14 -11
  101. package/dist/service/sts/command/assume-role/assume-role.handler.d.ts +5 -1
  102. package/dist/service/sts/command/assume-role/assume-role.handler.js +5 -2
  103. package/dist/service/sts/command/get-caller-identity/get-caller-identity.handler.d.ts +5 -1
  104. package/dist/service/sts/command/get-caller-identity/get-caller-identity.handler.js +4 -1
  105. package/dist/service/sts/service-role/sim-service-role.js +5 -1
  106. package/dist/service/sts/sim-sts.d.ts +9 -1
  107. package/dist/service/sts/sim-sts.js +4 -0
  108. package/docs/README.md +54 -0
  109. package/docs/ai-skill/README.md +69 -0
  110. package/docs/cli/README.md +284 -0
  111. package/docs/factories/README.md +140 -0
  112. package/docs/lint/README.md +176 -0
  113. package/docs/non-aws-dependencies/README.md +261 -0
  114. package/docs/sdk/README.md +272 -0
  115. package/docs/serve/README.md +1126 -0
  116. package/docs/services/acm/README.md +823 -0
  117. package/docs/services/apigateway/README.md +1919 -0
  118. package/docs/services/apigatewayv2/README.md +2678 -0
  119. package/docs/services/athena/README.md +1044 -0
  120. package/docs/services/bedrock/README.md +350 -0
  121. package/docs/services/cloudformation/README.md +3440 -0
  122. package/docs/services/cloudfront/README.md +2874 -0
  123. package/docs/services/cloudwatch/README.md +420 -0
  124. package/docs/services/cognito/README.md +4923 -0
  125. package/docs/services/dynamodb/README.md +3784 -0
  126. package/docs/services/ecr/README.md +302 -0
  127. package/docs/services/ecs/README.md +2010 -0
  128. package/docs/services/elbv2/README.md +1897 -0
  129. package/docs/services/eventbridge/README.md +1110 -0
  130. package/docs/services/firehose/README.md +805 -0
  131. package/docs/services/glue/README.md +501 -0
  132. package/docs/services/iam/README.md +1283 -0
  133. package/docs/services/kinesis/README.md +454 -0
  134. package/docs/services/kms/README.md +659 -0
  135. package/docs/services/lambda/README.md +3645 -0
  136. package/docs/services/logs/README.md +868 -0
  137. package/docs/services/organizations/README.md +574 -0
  138. package/docs/services/personalize/README.md +835 -0
  139. package/docs/services/rekognition/README.md +1070 -0
  140. package/docs/services/route53/README.md +1783 -0
  141. package/docs/services/s3/README.md +3024 -0
  142. package/docs/services/scheduler/README.md +806 -0
  143. package/docs/services/secretsmanager/README.md +667 -0
  144. package/docs/services/ses/README.md +1011 -0
  145. package/docs/services/sns/README.md +1904 -0
  146. package/docs/services/sqs/README.md +1185 -0
  147. package/docs/services/ssm/README.md +1072 -0
  148. package/docs/services/stepfunctions/README.md +1332 -0
  149. package/docs/services/sts/README.md +245 -0
  150. package/docs/services/wafv2/README.md +1378 -0
  151. package/docs/terraform/README.md +248 -0
  152. package/docs/time/README.md +284 -0
  153. package/llms.txt +59 -0
  154. package/package.json +9 -5
@@ -0,0 +1,2010 @@
1
+ # Simulated ECS
2
+
3
+ Yulin includes a simulated Amazon ECS for tests and local development. It holds clusters, task
4
+ definitions and services in memory, runs tasks from handlers you bind to their containers, and
5
+ authorizes every operation with simulated IAM.
6
+
7
+ ECS-specific types are imported from the `@kensio/yulin/ecs` subpath.
8
+
9
+ ## What Yulin does with a container image
10
+
11
+ Yulin never looks inside a container image, and it could not if it tried. An image may hold a Go
12
+ binary, nginx, Redis or anything else, and the only thing Yulin can run is JavaScript or TypeScript
13
+ in its own process.
14
+
15
+ So an image URI is only ever an identifier. Nothing here reads it, pulls it, or runs anything from
16
+ it. It is stored as declared, and it is what a container is matched on when a task runs, in the same
17
+ way an image URI identifies a container image Lambda function. The rule that follows is that a
18
+ container matched to a handler runs that handler, and a container with no match is recorded as not
19
+ simulated while the rest of the task carries on.
20
+
21
+ A realistic task definition holds an application container, a log router and an observability agent.
22
+ Only the first of those is something Yulin could ever run, and all three are stored and reported
23
+ back exactly as declared.
24
+
25
+ The case this leaves out is a sidecar the application depends on, such as a Redis or a database in
26
+ the same task. Point its connection details, which are ordinary environment variables, at a real one
27
+ you run yourself. See [non-AWS dependencies](https://yulinsim.dev/non-aws-dependencies/) for how that fits
28
+ together.
29
+
30
+ ## Registering a task definition
31
+
32
+ `RegisterTaskDefinition` stores a revision under its family. Revisions number from one and go up by
33
+ one with each registration.
34
+
35
+ ```typescript sim-ecs-register-task-definition
36
+ /**
37
+ * Registering a simulated task definition and reading it back.
38
+ */
39
+
40
+ import {
41
+ DescribeTaskDefinitionCommand,
42
+ RegisterTaskDefinitionCommand,
43
+ } from "@aws-sdk/client-ecs";
44
+
45
+ import { SimAws } from "@kensio/yulin";
46
+
47
+ const simAws = new SimAws();
48
+ const ecs = simAws.ecs();
49
+
50
+ await ecs.registerTaskDefinition(
51
+ new RegisterTaskDefinitionCommand({
52
+ family: "checkout",
53
+ cpu: "512",
54
+ memory: "1024",
55
+ networkMode: "awsvpc",
56
+ requiresCompatibilities: ["FARGATE"],
57
+ containerDefinitions: [
58
+ {
59
+ name: "app",
60
+ image: "example.dkr.ecr.eu-west-2.amazonaws.com/checkout:1",
61
+ essential: true,
62
+ portMappings: [{ containerPort: 8080, protocol: "tcp" }],
63
+ environment: [{ name: "LOG_LEVEL", value: "debug" }],
64
+ },
65
+ ],
66
+ }),
67
+ );
68
+
69
+ const described = await ecs.describeTaskDefinition(
70
+ new DescribeTaskDefinitionCommand({ taskDefinition: "checkout" }),
71
+ );
72
+
73
+ console.log(described.taskDefinition?.revision); // 1
74
+ console.log(described.taskDefinition?.status); // "ACTIVE"
75
+ console.log(described.taskDefinition?.containerDefinitions?.[0]?.image);
76
+ // "example.dkr.ecr.eu-west-2.amazonaws.com/checkout:1"
77
+ ```
78
+
79
+ Container definitions are stored as declared, whatever the image is and whether or not Yulin could
80
+ ever run it. That covers port mappings, environment variables and secrets. A `valueFrom` naming a
81
+ Secrets Manager secret is held as the identifier it is, and never resolved.
82
+
83
+ A registration that declares something this simulation has no room for is refused outright, and
84
+ never trimmed. That way a declaration cannot go missing from the revision it made.
85
+
86
+ ## Revisions
87
+
88
+ Each registration of a family takes the next revision number. Naming the family alone means its
89
+ latest active revision, which follows registrations as they are made.
90
+
91
+ ```typescript sim-ecs-task-definition-revisions
92
+ /**
93
+ * Registering a second revision of a simulated task definition family.
94
+ */
95
+
96
+ import {
97
+ DescribeTaskDefinitionCommand,
98
+ RegisterTaskDefinitionCommand,
99
+ } from "@aws-sdk/client-ecs";
100
+
101
+ import { SimAws } from "@kensio/yulin";
102
+
103
+ const simAws = new SimAws();
104
+ const ecs = simAws.ecs();
105
+
106
+ for (const tag of ["checkout:1", "checkout:2"]) {
107
+ await ecs.registerTaskDefinition(
108
+ new RegisterTaskDefinitionCommand({
109
+ family: "checkout",
110
+ containerDefinitions: [{ name: "app", image: tag }],
111
+ }),
112
+ );
113
+ }
114
+
115
+ const latest = await ecs.describeTaskDefinition(
116
+ new DescribeTaskDefinitionCommand({ taskDefinition: "checkout" }),
117
+ );
118
+
119
+ console.log(latest.taskDefinition?.revision); // 2
120
+
121
+ const first = await ecs.describeTaskDefinition(
122
+ new DescribeTaskDefinitionCommand({ taskDefinition: "checkout:1" }),
123
+ );
124
+
125
+ console.log(first.taskDefinition?.containerDefinitions?.[0]?.image);
126
+ // "checkout:1"
127
+ ```
128
+
129
+ `DescribeTaskDefinition` takes all three forms ECS takes, namely a family, a `family:revision`, and
130
+ a full task definition ARN. The ARN of a revision is
131
+ `arn:aws:ecs:<region>:<account>:task-definition/<family>:<revision>`.
132
+
133
+ ## Deregistering a revision
134
+
135
+ `DeregisterTaskDefinition` marks one revision `INACTIVE` without removing it. It stays describable
136
+ by `family:revision` and by ARN, because something already holding either of those still needs to
137
+ find out what it declared. What it stops being is the revision the family resolves to.
138
+
139
+ ```typescript sim-ecs-deregister-task-definition
140
+ /**
141
+ * Deregistering a simulated task definition revision.
142
+ */
143
+
144
+ import {
145
+ DeregisterTaskDefinitionCommand,
146
+ DescribeTaskDefinitionCommand,
147
+ RegisterTaskDefinitionCommand,
148
+ } from "@aws-sdk/client-ecs";
149
+
150
+ import { SimAws } from "@kensio/yulin";
151
+
152
+ const simAws = new SimAws();
153
+ const ecs = simAws.ecs();
154
+
155
+ for (const tag of ["checkout:1", "checkout:2"]) {
156
+ await ecs.registerTaskDefinition(
157
+ new RegisterTaskDefinitionCommand({
158
+ family: "checkout",
159
+ containerDefinitions: [{ name: "app", image: tag }],
160
+ }),
161
+ );
162
+ }
163
+
164
+ await ecs.deregisterTaskDefinition(
165
+ new DeregisterTaskDefinitionCommand({ taskDefinition: "checkout:2" }),
166
+ );
167
+
168
+ const deregistered = await ecs.describeTaskDefinition(
169
+ new DescribeTaskDefinitionCommand({ taskDefinition: "checkout:2" }),
170
+ );
171
+
172
+ console.log(deregistered.taskDefinition?.status); // "INACTIVE"
173
+
174
+ const latest = await ecs.describeTaskDefinition(
175
+ new DescribeTaskDefinitionCommand({ taskDefinition: "checkout" }),
176
+ );
177
+
178
+ console.log(latest.taskDefinition?.revision); // 1
179
+ ```
180
+
181
+ Deregistering must name one revision, as it must on real ECS. A family on its own is refused.
182
+
183
+ Revision numbers are never reused. Registering the family again after deregistering revision 2 gives
184
+ revision 3.
185
+
186
+ ## Listing task definitions
187
+
188
+ `ListTaskDefinitions` reports revision ARNs and `ListTaskDefinitionFamilies` reports family names.
189
+ Both take a `familyPrefix`. A `ListTaskDefinitions` request silent on status gets the active
190
+ revisions, while a `ListTaskDefinitionFamilies` request silent on it gets both the active and the
191
+ inactive families. That is how real ECS defaults each of them.
192
+
193
+ ```typescript sim-ecs-list-task-definitions
194
+ /**
195
+ * Listing simulated task definition revisions and families.
196
+ */
197
+
198
+ import {
199
+ ListTaskDefinitionFamiliesCommand,
200
+ ListTaskDefinitionsCommand,
201
+ RegisterTaskDefinitionCommand,
202
+ } from "@aws-sdk/client-ecs";
203
+
204
+ import { SimAws } from "@kensio/yulin";
205
+
206
+ const simAws = new SimAws();
207
+ const ecs = simAws.ecs();
208
+
209
+ for (const family of ["checkout", "billing"]) {
210
+ await ecs.registerTaskDefinition(
211
+ new RegisterTaskDefinitionCommand({
212
+ family,
213
+ containerDefinitions: [{ name: "app", image: `${family}:1` }],
214
+ }),
215
+ );
216
+ }
217
+
218
+ const revisions = await ecs.listTaskDefinitions(
219
+ new ListTaskDefinitionsCommand({ familyPrefix: "check" }),
220
+ );
221
+
222
+ console.log(revisions.taskDefinitionArns?.length); // 1
223
+
224
+ const families = await ecs.listTaskDefinitionFamilies(
225
+ new ListTaskDefinitionFamiliesCommand({}),
226
+ );
227
+
228
+ console.log(families.families); // ["checkout", "billing"]
229
+ ```
230
+
231
+ A family counts as inactive once every one of its revisions has been deregistered.
232
+ `ListTaskDefinitionFamilies` takes `ACTIVE`, `INACTIVE` and `ALL`, and `ListTaskDefinitions` takes
233
+ `ACTIVE` and `INACTIVE` along with a `sort` of `ASC` or `DESC`.
234
+
235
+ ## Clusters
236
+
237
+ A cluster is a named scope for the tasks and services that will run in it. `CreateCluster`,
238
+ `DescribeClusters`, `ListClusters` and `DeleteCluster` hold them.
239
+
240
+ ```typescript sim-ecs-clusters
241
+ /**
242
+ * Creating and describing a simulated ECS cluster.
243
+ */
244
+
245
+ import {
246
+ CreateClusterCommand,
247
+ DescribeClustersCommand,
248
+ ListClustersCommand,
249
+ } from "@aws-sdk/client-ecs";
250
+
251
+ import { SimAws } from "@kensio/yulin";
252
+
253
+ const simAws = new SimAws();
254
+ const ecs = simAws.ecs();
255
+
256
+ await ecs.createCluster(
257
+ new CreateClusterCommand({
258
+ clusterName: "services",
259
+ settings: [{ name: "containerInsights", value: "enabled" }],
260
+ tags: [{ key: "team", value: "platform" }],
261
+ }),
262
+ );
263
+
264
+ const described = await ecs.describeClusters(
265
+ new DescribeClustersCommand({
266
+ clusters: ["services"],
267
+ include: ["SETTINGS", "TAGS"],
268
+ }),
269
+ );
270
+
271
+ console.log(described.clusters?.[0]?.status); // "ACTIVE"
272
+ console.log(described.clusters?.[0]?.runningTasksCount); // 0
273
+ console.log(described.clusters?.[0]?.tags?.[0]?.key); // "team"
274
+
275
+ const listed = await ecs.listClusters(new ListClustersCommand({}));
276
+
277
+ console.log(listed.clusterArns?.[0]);
278
+ // "arn:aws:ecs:us-east-1:888888888888:cluster/services"
279
+ ```
280
+
281
+ Settings, configuration and tags are reported only where the request asked for them by name, as they
282
+ are on real ECS. A request naming no cluster means the `default` cluster.
283
+
284
+ `DeleteCluster` marks a cluster `INACTIVE`. It stays describable and drops out of `ListClusters`,
285
+ and creating a cluster of the same name again makes a new active one, listed in the position it was
286
+ created in. Unlike the operations that read a cluster, `DeleteCluster` needs one to be named. A
287
+ request naming none is refused, and never read as meaning the `default` cluster.
288
+
289
+ A cluster is named either by its short name or by its full ARN, and the two are interchangeable. An
290
+ ARN belonging to another account or region names a different cluster. `DescribeClusters` reports it
291
+ as a `MISSING` failure, and `DeleteCluster` refuses it.
292
+
293
+ A cluster has to exist before a task can run in it, including the `default` one. Yulin creates no
294
+ cluster on its own, so create the one the tasks run in.
295
+
296
+ ## Running a task
297
+
298
+ `bindContainer` says what a container runs. `RunTask` then starts a task in a cluster, and the bound
299
+ handlers run in this process.
300
+
301
+ ```typescript sim-ecs-run-task
302
+ /**
303
+ * Running a simulated ECS task from a bound container handler.
304
+ */
305
+
306
+ import {
307
+ CreateClusterCommand,
308
+ DescribeTasksCommand,
309
+ RegisterTaskDefinitionCommand,
310
+ RunTaskCommand,
311
+ } from "@aws-sdk/client-ecs";
312
+
313
+ import { SimAws } from "@kensio/yulin";
314
+
315
+ const simAws = new SimAws();
316
+ const ecs = simAws.ecs();
317
+
318
+ await ecs.createCluster(new CreateClusterCommand({ clusterName: "orders" }));
319
+
320
+ const processed: string[] = [];
321
+
322
+ ecs.bindContainer({
323
+ family: "orders-worker",
324
+ containerName: "app",
325
+ run: async () => {
326
+ await Promise.resolve();
327
+ processed.push("outstanding orders");
328
+ },
329
+ });
330
+
331
+ await ecs.registerTaskDefinition(
332
+ new RegisterTaskDefinitionCommand({
333
+ family: "orders-worker",
334
+ containerDefinitions: [
335
+ { name: "app", image: "orders-worker:1" },
336
+ { name: "log-router", image: "aws-for-fluent-bit:latest" },
337
+ ],
338
+ }),
339
+ );
340
+
341
+ const run = await ecs.runTask(
342
+ new RunTaskCommand({ cluster: "orders", taskDefinition: "orders-worker" }),
343
+ );
344
+
345
+ console.log(run.tasks?.[0]?.lastStatus); // "PROVISIONING"
346
+
347
+ // The containers run in the background, as they do on real ECS.
348
+ await simAws.backgroundTasksComplete();
349
+
350
+ console.log(processed); // ["outstanding orders"]
351
+
352
+ const described = await ecs.describeTasks(
353
+ new DescribeTasksCommand({
354
+ cluster: "orders",
355
+ tasks: [run.tasks?.[0]?.taskArn ?? ""],
356
+ }),
357
+ );
358
+
359
+ console.log(described.tasks?.[0]?.lastStatus); // "STOPPED"
360
+ console.log(described.tasks?.[0]?.containers?.[0]?.exitCode); // 0
361
+ console.log(described.tasks?.[0]?.containers?.[1]?.reason);
362
+ // "Not simulated: no executable binding matches this container, ..."
363
+ ```
364
+
365
+ `RunTask` answers before the containers have run, with the task in `PROVISIONING`, as real ECS
366
+ answers it. Waiting for the simulator's background work is what runs them.
367
+
368
+ The log router in that task definition has no binding. It never starts, and says why. That is the
369
+ ordinary shape of a real task definition, with an application container Yulin can run next to
370
+ containers it never could.
371
+
372
+ A handler that throws stops its container with an exit code of 1 and the error message as its
373
+ reason. The `RunTask` call itself still succeeds, because a real container fails with nobody
374
+ watching the call either. A task definition where nothing at all is bound still creates the task.
375
+ The task stops with a `stopCode` of `TaskFailedToStart` saying that nothing ran.
376
+
377
+ ### Binding by image repository
378
+
379
+ A container built by CDK or by a pipeline has an image tag that changes with every build, so naming
380
+ the container by hand is the wrong way round. A binding can name the repository instead, and the tag
381
+ is ignored on both sides.
382
+
383
+ ```typescript sim-ecs-bind-image-repository
384
+ /**
385
+ * Binding a simulated ECS container by the repository its image comes from.
386
+ */
387
+
388
+ import {
389
+ CreateClusterCommand,
390
+ RegisterTaskDefinitionCommand,
391
+ RunTaskCommand,
392
+ } from "@aws-sdk/client-ecs";
393
+
394
+ import { SimAws } from "@kensio/yulin";
395
+
396
+ const simAws = new SimAws();
397
+ const ecs = simAws.ecs();
398
+
399
+ await ecs.createCluster(new CreateClusterCommand({}));
400
+
401
+ const started: string[] = [];
402
+
403
+ ecs.bindContainer({
404
+ imageRepository: "111111111111.dkr.ecr.eu-west-2.amazonaws.com/orders",
405
+ run: () => {
406
+ started.push("app");
407
+ },
408
+ });
409
+
410
+ await ecs.registerTaskDefinition(
411
+ new RegisterTaskDefinitionCommand({
412
+ family: "orders-worker",
413
+ containerDefinitions: [
414
+ {
415
+ name: "app",
416
+ image: "111111111111.dkr.ecr.eu-west-2.amazonaws.com/orders:8f2c1a9b",
417
+ },
418
+ ],
419
+ }),
420
+ );
421
+
422
+ await ecs.runTask(new RunTaskCommand({ taskDefinition: "orders-worker" }));
423
+ await simAws.backgroundTasksComplete();
424
+
425
+ console.log(started); // ["app"]
426
+ ```
427
+
428
+ The registry host is part of the repository, and a same-named repository in another account or
429
+ region misses. A binding naming the family and the container name beats one naming a repository
430
+ where both would match, and binding the same container again replaces what it runs.
431
+
432
+ The other shape a binding can take is `http`, a fetch-style `(request: Request) => Response |
433
+ Promise<Response>` for a service container behind a load balancer. It is called once for each
434
+ request routed to the container, and is covered under
435
+ [serving requests behind a load balancer](#serving-requests-behind-a-load-balancer).
436
+
437
+ ## What a container sees while it runs
438
+
439
+ The container definition's `environment` is visible through `process.env` for the length of the run,
440
+ along with any `RunTask` container override and the region variables a real task agent sets. This
441
+ works the same way it does for a sim Lambda function handler, through Node.js asynchronous context
442
+ tracking. The host process environment is underneath, and what the test process set stays readable.
443
+ Every container gets the region variables, whatever its definition declares, and an SDK client the
444
+ container builds reads `AWS_REGION` to find the region its task runs in.
445
+
446
+ ```typescript sim-ecs-run-task-environment
447
+ /**
448
+ * The environment variables a simulated ECS container runs with.
449
+ */
450
+
451
+ import {
452
+ CreateClusterCommand,
453
+ RegisterTaskDefinitionCommand,
454
+ RunTaskCommand,
455
+ } from "@aws-sdk/client-ecs";
456
+
457
+ import { SimAws } from "@kensio/yulin";
458
+
459
+ const simAws = new SimAws();
460
+ const ecs = simAws.ecs();
461
+
462
+ await ecs.createCluster(new CreateClusterCommand({}));
463
+
464
+ const batchSizes: (string | undefined)[] = [];
465
+
466
+ ecs.bindContainer({
467
+ family: "orders-worker",
468
+ containerName: "app",
469
+ run: () => {
470
+ batchSizes.push(process.env["BATCH_SIZE"]);
471
+ },
472
+ });
473
+
474
+ await ecs.registerTaskDefinition(
475
+ new RegisterTaskDefinitionCommand({
476
+ family: "orders-worker",
477
+ containerDefinitions: [
478
+ {
479
+ name: "app",
480
+ image: "orders-worker:1",
481
+ environment: [{ name: "BATCH_SIZE", value: "100" }],
482
+ },
483
+ ],
484
+ }),
485
+ );
486
+
487
+ await ecs.runTask(
488
+ new RunTaskCommand({
489
+ taskDefinition: "orders-worker",
490
+ overrides: {
491
+ containerOverrides: [
492
+ { name: "app", environment: [{ name: "BATCH_SIZE", value: "10" }] },
493
+ ],
494
+ },
495
+ }),
496
+ );
497
+ await simAws.backgroundTasksComplete();
498
+
499
+ console.log(batchSizes); // ["10"]
500
+ ```
501
+
502
+ A variable read at module scope, as in `const size = process.env.BATCH_SIZE` at the top of a file,
503
+ is read when the test imports that file and not when the container runs, and it sees the host value.
504
+ Read inside the handler to get the container's own.
505
+
506
+ ## The task role
507
+
508
+ While a container runs, its AWS calls are attributed to the task definition's `taskRoleArn`, in the
509
+ same way a sim Lambda function's are to its execution role. Calls made through an SDK client
510
+ intercepted by `SimSdk` pick this up without the code under test knowing.
511
+
512
+ ```typescript sim-ecs-task-role
513
+ /**
514
+ * Authorizing what a simulated ECS container does as the task Role.
515
+ */
516
+
517
+ import {
518
+ CreateClusterCommand,
519
+ RegisterTaskDefinitionCommand,
520
+ RunTaskCommand,
521
+ } from "@aws-sdk/client-ecs";
522
+ import { CreateRoleCommand, PutRolePolicyCommand } from "@aws-sdk/client-iam";
523
+ import { PutParameterCommand, SSMClient } from "@aws-sdk/client-ssm";
524
+
525
+ import { SimSdk } from "@kensio/yulin/sdk";
526
+
527
+ using simSdk = new SimSdk();
528
+ const { simAws } = simSdk;
529
+ const ecs = simAws.ecs();
530
+ const accountId = simAws.defaultAccountId;
531
+
532
+ const taskRole = await simAws.iam().createRole(
533
+ new CreateRoleCommand({
534
+ RoleName: "OrdersTaskRole",
535
+ AssumeRolePolicyDocument: JSON.stringify({
536
+ Version: "2012-10-17",
537
+ Statement: {
538
+ Effect: "Allow",
539
+ Principal: { Service: "ecs-tasks.amazonaws.com" },
540
+ Action: "sts:AssumeRole",
541
+ },
542
+ }),
543
+ }),
544
+ );
545
+
546
+ await simAws.iam().putRolePolicy(
547
+ new PutRolePolicyCommand({
548
+ RoleName: "OrdersTaskRole",
549
+ PolicyName: "WriteLastRun",
550
+ PolicyDocument: JSON.stringify({
551
+ Version: "2012-10-17",
552
+ Statement: {
553
+ Effect: "Allow",
554
+ Action: "ssm:PutParameter",
555
+ Resource: `arn:aws:ssm:${simAws.defaultRegionName}:${accountId}:parameter/orders/last-run`,
556
+ },
557
+ }),
558
+ }),
559
+ );
560
+
561
+ await ecs.createCluster(new CreateClusterCommand({}));
562
+
563
+ simSdk.intercept(SSMClient);
564
+
565
+ ecs.bindContainer({
566
+ family: "orders-worker",
567
+ containerName: "app",
568
+ run: async () => {
569
+ await new SSMClient({}).send(
570
+ new PutParameterCommand({
571
+ Name: "/orders/last-run",
572
+ Value: "done",
573
+ Type: "String",
574
+ }),
575
+ );
576
+ },
577
+ });
578
+
579
+ await ecs.registerTaskDefinition(
580
+ new RegisterTaskDefinitionCommand({
581
+ family: "orders-worker",
582
+ taskRoleArn: taskRole.Role.Arn,
583
+ containerDefinitions: [{ name: "app", image: "orders-worker:1" }],
584
+ }),
585
+ );
586
+
587
+ await ecs.runTask(new RunTaskCommand({ taskDefinition: "orders-worker" }));
588
+ await simAws.backgroundTasksComplete();
589
+ ```
590
+
591
+ Take the policy away and the same run stops the container with an exit code of 1, carrying the IAM
592
+ denial as its reason. That is the test worth writing. A task role missing a permission fails in the
593
+ test, ahead of the deployment.
594
+
595
+ A `RunTask` request can override the role with `overrides.taskRoleArn`. A task definition declaring
596
+ no task role runs its containers as nobody, and their AWS calls are denied. A real task without one
597
+ has no credentials of its own, and taking the identity of whoever called `RunTask` would let a test
598
+ pass on permissions the deployed task lacks. The execution role covers a container's `secrets`
599
+ alone, since there is no image to pull and no log driver to write to.
600
+
601
+ ## Container secrets
602
+
603
+ A container definition's `secrets` are resolved when the task starts, from simulated Secrets Manager
604
+ or simulated SSM Parameter Store according to what each `valueFrom` names, and the values appear in
605
+ the container's environment alongside its declared `environment`. A handler reads them through
606
+ `process.env` like anything else.
607
+
608
+ They are read as the task definition's `executionRoleArn` rather than its `taskRoleArn`. That is the
609
+ split real ECS makes. The execution role is what the task agent pulls secrets with before a
610
+ container starts, and the task role is what the running container's own AWS calls are attributed to.
611
+ A role allowed one is not thereby allowed the other.
612
+
613
+ ```typescript sim-ecs-container-secrets
614
+ /**
615
+ * Resolving a simulated ECS container's secrets as the execution Role.
616
+ */
617
+
618
+ import {
619
+ CreateClusterCommand,
620
+ RegisterTaskDefinitionCommand,
621
+ RunTaskCommand,
622
+ } from "@aws-sdk/client-ecs";
623
+ import { CreateRoleCommand, PutRolePolicyCommand } from "@aws-sdk/client-iam";
624
+ import { CreateSecretCommand } from "@aws-sdk/client-secrets-manager";
625
+
626
+ import { SimAws } from "@kensio/yulin";
627
+
628
+ const simAws = new SimAws();
629
+ const ecs = simAws.ecs();
630
+
631
+ const secret = await simAws.secretsManager().createSecret(
632
+ new CreateSecretCommand({
633
+ Name: "orders/db",
634
+ SecretString: JSON.stringify({ username: "orders", password: "s3cr3t" }),
635
+ }),
636
+ );
637
+
638
+ const executionRole = await simAws.iam().createRole(
639
+ new CreateRoleCommand({
640
+ RoleName: "OrdersExecutionRole",
641
+ AssumeRolePolicyDocument: JSON.stringify({
642
+ Version: "2012-10-17",
643
+ Statement: {
644
+ Effect: "Allow",
645
+ Principal: { Service: "ecs-tasks.amazonaws.com" },
646
+ Action: "sts:AssumeRole",
647
+ },
648
+ }),
649
+ }),
650
+ );
651
+
652
+ await simAws.iam().putRolePolicy(
653
+ new PutRolePolicyCommand({
654
+ RoleName: "OrdersExecutionRole",
655
+ PolicyName: "ReadOrdersDbSecret",
656
+ PolicyDocument: JSON.stringify({
657
+ Version: "2012-10-17",
658
+ Statement: {
659
+ Effect: "Allow",
660
+ Action: "secretsmanager:GetSecretValue",
661
+ Resource: secret.ARN,
662
+ },
663
+ }),
664
+ }),
665
+ );
666
+
667
+ await ecs.createCluster(new CreateClusterCommand({}));
668
+
669
+ const passwords: (string | undefined)[] = [];
670
+
671
+ ecs.bindContainer({
672
+ family: "orders-worker",
673
+ containerName: "app",
674
+ run: () => {
675
+ passwords.push(process.env["DB_PASSWORD"]);
676
+ },
677
+ });
678
+
679
+ await ecs.registerTaskDefinition(
680
+ new RegisterTaskDefinitionCommand({
681
+ family: "orders-worker",
682
+ executionRoleArn: executionRole.Role.Arn,
683
+ containerDefinitions: [
684
+ {
685
+ name: "app",
686
+ image: "orders-worker:1",
687
+ secrets: [
688
+ {
689
+ name: "DB_PASSWORD",
690
+ valueFrom: `${String(secret.ARN)}:password::`,
691
+ },
692
+ ],
693
+ },
694
+ ],
695
+ }),
696
+ );
697
+
698
+ await ecs.runTask(new RunTaskCommand({ taskDefinition: "orders-worker" }));
699
+ await simAws.backgroundTasksComplete();
700
+
701
+ console.log(passwords); // ["s3cr3t"]
702
+ ```
703
+
704
+ A `valueFrom` may be a Secrets Manager ARN, an SSM parameter ARN, or a bare parameter name, which
705
+ real ECS accepts for a parameter in the task's own region. A Secrets Manager ARN may carry a JSON
706
+ key, a version stage and a version id after the secret id, in the form
707
+ `...:secret:orders/db-AbCdEf:password::` that a CDK construct given a field writes. The key selects
708
+ one field of a secret holding a JSON object. A `SecureString` parameter is decrypted, as it is for a
709
+ real task.
710
+
711
+ A secret that cannot be resolved stops the task before any container runs, with a
712
+ `ResourceInitializationError` reason naming the variable:
713
+
714
+ ```text
715
+ ResourceInitializationError: unable to pull secrets: DB_PASSWORD: User:
716
+ arn:aws:iam::111111111111:role/OrdersExecutionRole is not authorized to perform:
717
+ secretsmanager:GetSecretValue on resource: arn:aws:secretsmanager:...
718
+ ```
719
+
720
+ The task stops with `TaskFailedToStart`, never reaches `RUNNING`, and the bound handler stays unrun.
721
+ A secret that was never created, a task definition declaring secrets with no `executionRoleArn`, and
722
+ a JSON key the secret lacks each stop it the same way with their own reason.
723
+
724
+ ## Running a task from a rule or a schedule
725
+
726
+ A `RunTask` call is one way to start a task, and there are two others. A
727
+ [simulated EventBridge](https://yulinsim.dev/services/eventbridge/) rule target and a [simulated Scheduler](https://yulinsim.dev/services/scheduler/)
728
+ schedule target can both name an ECS cluster, and both then run a task here when the rule matches an
729
+ event or the schedule falls due. That is the usual shape of a nightly batch job or an import kicked
730
+ off by something happening.
731
+
732
+ Both go through `RunTask`, and a task started that way is the same task as one started by a caller.
733
+ That means the same cluster and revision lookups, the same IAM decision against `ecs:RunTask`, and
734
+ the same task state afterwards. What a target may ask for differs, though. `EcsParameters` takes and
735
+ ignores the launch type, platform version, network configuration and capacity provider strategy that
736
+ `RunTask` refuses, since a target written for real AWS carries them and refusing one would make an
737
+ otherwise workable target unusable. The other difference is who runs it. A rule or a schedule runs
738
+ the task as the role on its target, and that role needs `ecs:RunTask` on the revision. The task role
739
+ inside the task definition is still what the containers' own AWS calls are attributed to.
740
+
741
+ The container model applies unchanged. Only a bound container runs, and a target naming a task
742
+ definition with nothing bound records a task that never started, leaving the rule or the schedule
743
+ itself successful. Writing one of these targets is documented where the target is written, in
744
+ [running an ECS task](https://yulinsim.dev/services/eventbridge/#running-an-ecs-task) for a rule and
745
+ [running an ECS task on a schedule](https://yulinsim.dev/services/scheduler/#running-an-ecs-task-on-a-schedule) for a schedule.
746
+
747
+ ## Describing, listing and stopping tasks
748
+
749
+ `DescribeTasks` reports a task by its id or its full ARN, with its containers, which of them ran and
750
+ their exit codes. A task it cannot find comes back as a `MISSING` failure entry, and never as an
751
+ error.
752
+
753
+ `ListTasks` filters on a desired status of `RUNNING` when a request says nothing, as real ECS does.
754
+ A task that has finished is only listed by asking for the stopped ones with `desiredStatus:
755
+ "STOPPED"`. It also filters by `family`, `startedBy` and `launchType`.
756
+
757
+ `StopTask` sets the desired status to `STOPPED` and records the reason. A task whose containers have
758
+ yet to start runs none of them, and one stopped part way through runs no more. A test can therefore
759
+ stop a task between `RunTask` and the background work that runs it. A task that has already stopped
760
+ is reported as it stands, keeping the reason it stopped for.
761
+
762
+ ## Services
763
+
764
+ A task runs and stops. A service keeps tasks running, and that is what a deployed application
765
+ usually is. It is a named service in a cluster, running some number of tasks from a task definition.
766
+
767
+ `CreateService` creates one. Its tasks exist as soon as the request is answered and reach `RUNNING`
768
+ on the simulation's background work, as real ECS brings a new service up. The service reports the
769
+ desired count it was given, and a running count that catches up.
770
+
771
+ ```typescript sim-ecs-create-service
772
+ /**
773
+ * Creating a simulated ECS service that keeps three tasks running.
774
+ */
775
+
776
+ import {
777
+ CreateClusterCommand,
778
+ CreateServiceCommand,
779
+ DescribeServicesCommand,
780
+ ListTasksCommand,
781
+ RegisterTaskDefinitionCommand,
782
+ } from "@aws-sdk/client-ecs";
783
+
784
+ import { SimAws } from "@kensio/yulin";
785
+
786
+ const simAws = new SimAws();
787
+ const ecs = simAws.ecs();
788
+
789
+ await ecs.createCluster(new CreateClusterCommand({ clusterName: "orders" }));
790
+
791
+ ecs.bindContainer({
792
+ family: "checkout",
793
+ containerName: "app",
794
+ run: async () => {
795
+ await handleOneRequest();
796
+ },
797
+ });
798
+
799
+ await ecs.registerTaskDefinition(
800
+ new RegisterTaskDefinitionCommand({
801
+ family: "checkout",
802
+ containerDefinitions: [{ name: "app", image: "checkout:1" }],
803
+ }),
804
+ );
805
+
806
+ const created = await ecs.createService(
807
+ new CreateServiceCommand({
808
+ cluster: "orders",
809
+ serviceName: "checkout",
810
+ taskDefinition: "checkout",
811
+ desiredCount: 3,
812
+ }),
813
+ );
814
+
815
+ console.log(created.service?.desiredCount); // 3
816
+ console.log(created.service?.runningCount); // 0, as real ECS answers one
817
+
818
+ // The tasks come up in the background, as they do on real ECS.
819
+ await simAws.backgroundTasksComplete();
820
+
821
+ const described = await ecs.describeServices(
822
+ new DescribeServicesCommand({ cluster: "orders", services: ["checkout"] }),
823
+ );
824
+
825
+ console.log(described.services?.[0]?.runningCount); // 3
826
+
827
+ const listed = await ecs.listTasks(
828
+ new ListTasksCommand({ cluster: "orders", serviceName: "checkout" }),
829
+ );
830
+
831
+ console.log(listed.taskArns?.length); // 3
832
+
833
+ async function handleOneRequest(): Promise<void> {
834
+ await Promise.resolve();
835
+ }
836
+ ```
837
+
838
+ Each of those tasks is a task like any other. `ListTasks` returns its ARN, `DescribeTasks` describes
839
+ it, and its containers report which of them Yulin is simulating.
840
+
841
+ ### What the desired count models
842
+
843
+ A desired count of three does not mean three copies of your handler. Yulin runs in one Node.js
844
+ process and there are no containers to copy, so the count is simulated as state. Three tasks exist
845
+ and are reported as running, while the handler bound to a container is called once per request or
846
+ per poll, whenever something reaches it.
847
+
848
+ That is a deliberate divergence. In practice a service is worth asserting on for its state (how many
849
+ tasks it keeps, which revision they run) and not for anything about running three things at once. A
850
+ test that needs concurrency is a test about your own code.
851
+
852
+ A container of a service that has come up is treated as running from that point, and what calls it
853
+ is whatever reaches it, either a queue it consumes or a load balancer sending it a request. Both are
854
+ covered below.
855
+
856
+ ### Consuming a queue
857
+
858
+ A worker container reads an SQS queue in a loop. It receives a batch, handles it, deletes it, and
859
+ goes round again. A binding cannot do that. An endless loop in a single Node.js process blocks
860
+ everything and never yields to the test running it, leaving the test no chance to assert on what the
861
+ loop did.
862
+
863
+ So Yulin runs the loop and the binding supplies its body. A container declares `consumes` in place
864
+ of `run`, naming the queue and a handler for a batch of messages, and Yulin receives, hands the
865
+ batch over, and deletes it when the handler returns. The bound thing is the body of the loop, and
866
+ not the loop itself. That is the one divergence worth keeping in mind here.
867
+
868
+ ```typescript sim-ecs-consume-queue
869
+ /**
870
+ * A simulated ECS service whose container consumes an SQS queue.
871
+ */
872
+
873
+ import {
874
+ CreateClusterCommand,
875
+ CreateServiceCommand,
876
+ RegisterTaskDefinitionCommand,
877
+ } from "@aws-sdk/client-ecs";
878
+ import { CreateRoleCommand, PutRolePolicyCommand } from "@aws-sdk/client-iam";
879
+ import { CreateQueueCommand, SendMessageCommand } from "@aws-sdk/client-sqs";
880
+
881
+ import { SimAws } from "@kensio/yulin";
882
+
883
+ const simAws = new SimAws();
884
+ const ecs = simAws.ecs();
885
+ const sqs = simAws.sqs();
886
+ const queueArn = `arn:aws:sqs:${simAws.defaultRegionName}:${simAws.defaultAccountId}:orders`;
887
+
888
+ const queue = await sqs.createQueue(
889
+ new CreateQueueCommand({ QueueName: "orders" }),
890
+ );
891
+ const queueUrl = queue.QueueUrl ?? "";
892
+
893
+ const taskRole = await simAws.iam().createRole(
894
+ new CreateRoleCommand({
895
+ RoleName: "OrdersWorkerTaskRole",
896
+ AssumeRolePolicyDocument: JSON.stringify({
897
+ Version: "2012-10-17",
898
+ Statement: {
899
+ Effect: "Allow",
900
+ Principal: { Service: "ecs-tasks.amazonaws.com" },
901
+ Action: "sts:AssumeRole",
902
+ },
903
+ }),
904
+ }),
905
+ );
906
+
907
+ await simAws.iam().putRolePolicy(
908
+ new PutRolePolicyCommand({
909
+ RoleName: "OrdersWorkerTaskRole",
910
+ PolicyName: "ConsumeOrders",
911
+ PolicyDocument: JSON.stringify({
912
+ Version: "2012-10-17",
913
+ Statement: {
914
+ Effect: "Allow",
915
+ Action: [
916
+ "sqs:ReceiveMessage",
917
+ "sqs:DeleteMessage",
918
+ "sqs:GetQueueAttributes",
919
+ ],
920
+ Resource: queueArn,
921
+ },
922
+ }),
923
+ }),
924
+ );
925
+
926
+ const handled: string[] = [];
927
+
928
+ ecs.bindContainer({
929
+ family: "orders-worker",
930
+ containerName: "app",
931
+ consumes: {
932
+ queueUrl,
933
+ batchSize: 10,
934
+ handler: (messages) => {
935
+ handled.push(...messages.map((message) => message.Body));
936
+ },
937
+ },
938
+ });
939
+
940
+ await ecs.createCluster(new CreateClusterCommand({ clusterName: "orders" }));
941
+ await ecs.registerTaskDefinition(
942
+ new RegisterTaskDefinitionCommand({
943
+ family: "orders-worker",
944
+ taskRoleArn: taskRole.Role.Arn,
945
+ containerDefinitions: [{ name: "app", image: "orders-worker:1" }],
946
+ }),
947
+ );
948
+ await ecs.createService(
949
+ new CreateServiceCommand({
950
+ cluster: "orders",
951
+ serviceName: "orders-worker",
952
+ taskDefinition: "orders-worker",
953
+ desiredCount: 1,
954
+ }),
955
+ );
956
+
957
+ await sqs.sendMessage(
958
+ new SendMessageCommand({ QueueUrl: queueUrl, MessageBody: "order-1" }),
959
+ );
960
+ await simAws.backgroundTasksComplete();
961
+
962
+ console.log(handled); // ["order-1"]
963
+ ```
964
+
965
+ The queue is named by the URL `CreateQueue` answered with, and it has to be in the same account and
966
+ region as the service, as a real task's own queue is. A `batchSize` is how many messages the handler
967
+ is given at once, up to the ten one SQS receive hands out, and it defaults to ten. The handler may
968
+ be async, and is awaited. A binding declares one of `run`, `consumes` and `http`. Where a binding
969
+ carries more than one, `consumes` wins, then `http`, then `run`.
970
+
971
+ The task role is required here. That is the part worth noticing before writing the first one.
972
+ Polling is done as the task role, and a task definition with no `taskRoleArn` polls as nobody and
973
+ has its very first poll denied. That is what a real worker container with no credentials would hit.
974
+
975
+ #### Polling runs on the simulated clock
976
+
977
+ Yulin never polls in the background of a test. A message that can be received now is delivered as
978
+ soon as the simulation settles, and `await simAws.backgroundTasksComplete()` is enough for an
979
+ ordinary send. Anything that has to wait (a message sent with `DelaySeconds`, or a batch coming back
980
+ after its visibility timeout) waits on the simulated clock. Freezing time holds it, and advancing
981
+ time delivers it.
982
+
983
+ ```typescript sim-ecs-consume-queue-clock
984
+ /**
985
+ * Driving a simulated ECS container's polling with the simulated clock.
986
+ */
987
+
988
+ import { SendMessageCommand } from "@aws-sdk/client-sqs";
989
+
990
+ import type { SimAws } from "@kensio/yulin";
991
+
992
+ declare const simAws: SimAws;
993
+ declare const queueUrl: string;
994
+ declare const handled: string[];
995
+
996
+ simAws.clock().freeze();
997
+
998
+ await simAws.sqs().sendMessage(
999
+ new SendMessageCommand({
1000
+ QueueUrl: queueUrl,
1001
+ MessageBody: "order-1",
1002
+ DelaySeconds: 60,
1003
+ }),
1004
+ );
1005
+ await simAws.backgroundTasksComplete();
1006
+
1007
+ console.log(handled.length); // 0, the message is not receivable yet
1008
+
1009
+ await simAws.clock().advanceBy({ seconds: 60 });
1010
+
1011
+ console.log(handled.length); // 1, the clock got there and the poll happened
1012
+ ```
1013
+
1014
+ #### What the handler returning and throwing mean
1015
+
1016
+ A handler that returns has handled the batch, and Yulin deletes it. A handler that throws leaves the
1017
+ whole batch on the queue. It stays hidden for the queue's visibility timeout and is handed over
1018
+ again when that runs out. That is what a real worker crashing part way through a batch does. A
1019
+ redrive policy therefore gives up on a message the handler keeps throwing on, exactly as it would
1020
+ for the deployed container.
1021
+
1022
+ The error goes no further than the container. What the sender sees is the message coming back.
1023
+
1024
+ #### Polling is authorized as the task role
1025
+
1026
+ Receiving, deleting and reading the queue's visibility timeout are all made as the task definition's
1027
+ `taskRoleArn`, and so is anything the handler itself does. A task role without `sqs:ReceiveMessage`,
1028
+ `sqs:DeleteMessage` or `sqs:GetQueueAttributes` on the queue is refused, and the refusal surfaces
1029
+ from `backgroundTasksComplete()`. That is the point of it. A policy that would break the deployed
1030
+ worker breaks the test.
1031
+
1032
+ A task definition with no `taskRoleArn` polls anonymously and is denied at its very first poll, as a
1033
+ real task with no credentials of its own would be.
1034
+
1035
+ #### Polling starts and stops with the service
1036
+
1037
+ Polling starts when the service's first task comes up and stops when the service is deleted, scaled
1038
+ to zero, or its `SimAws` is closed. A stopped poller releases both the queue watch and the clock
1039
+ wait. A test that finishes with a consuming service leaves nothing behind it.
1040
+
1041
+ There is one poller per service and container, and never one per task. A desired count of three is
1042
+ three simulated tasks reported as running, and the handler is still called once per poll, which is
1043
+ the same divergence the desired count already rests on. Three real containers would each run their
1044
+ own loop and share the queue between them, coming to the same messages being handled once.
1045
+
1046
+ A consuming container is the one kind a `RunTask` task cannot run. It has no handler that ends, and
1047
+ a task has to end. A task started from the same definition records the container as not simulated,
1048
+ with a reason saying to create a service instead.
1049
+
1050
+ ### Serving requests behind a load balancer
1051
+
1052
+ A service container that answers HTTP requests declares `http` in place of `run` or `consumes`. The
1053
+ handler is fetch-style. It is given a `Request` and answers with a `Response`, the same shape a
1054
+ simulated load balancer already answers a served request with.
1055
+
1056
+ What reaches it is a request routed through simulated [Elastic Load Balancing](https://yulinsim.dev/services/elbv2/). A service
1057
+ declares `loadBalancers` naming a target group, a container and a container port. Creating the
1058
+ service registers each of its tasks into that target group, and a request the load balancer forwards
1059
+ there reaches the bound container's handler.
1060
+
1061
+ ```typescript sim-ecs-serve-load-balancer
1062
+ /**
1063
+ * A simulated ECS service answering requests behind a load balancer.
1064
+ */
1065
+
1066
+ import {
1067
+ CreateClusterCommand,
1068
+ CreateServiceCommand,
1069
+ RegisterTaskDefinitionCommand,
1070
+ } from "@aws-sdk/client-ecs";
1071
+ import {
1072
+ CreateListenerCommand,
1073
+ CreateLoadBalancerCommand,
1074
+ CreateTargetGroupCommand,
1075
+ } from "@aws-sdk/client-elastic-load-balancing-v2";
1076
+
1077
+ import { SimAws } from "@kensio/yulin";
1078
+ import { simElbV2Fetch } from "@kensio/yulin/elbv2";
1079
+
1080
+ const simAws = new SimAws();
1081
+ const ecs = simAws.ecs();
1082
+ const elbV2 = simAws.elbV2();
1083
+
1084
+ const targetGroup = await elbV2.createTargetGroup(
1085
+ new CreateTargetGroupCommand({
1086
+ Name: "orders-tg",
1087
+ TargetType: "ip",
1088
+ Protocol: "HTTP",
1089
+ Port: 8080,
1090
+ }),
1091
+ );
1092
+
1093
+ const targetGroupArn = targetGroup.TargetGroups?.[0]?.TargetGroupArn;
1094
+
1095
+ const loadBalancer = await elbV2.createLoadBalancer(
1096
+ new CreateLoadBalancerCommand({ Name: "orders-alb" }),
1097
+ );
1098
+
1099
+ await elbV2.createListener(
1100
+ new CreateListenerCommand({
1101
+ LoadBalancerArn: loadBalancer.LoadBalancers?.[0]?.LoadBalancerArn,
1102
+ Protocol: "HTTP",
1103
+ Port: 80,
1104
+ DefaultActions: [{ Type: "forward", TargetGroupArn: targetGroupArn }],
1105
+ }),
1106
+ );
1107
+
1108
+ await ecs.createCluster(new CreateClusterCommand({ clusterName: "orders" }));
1109
+
1110
+ ecs.bindContainer({
1111
+ family: "orders-api",
1112
+ containerName: "app",
1113
+ http: (request) => {
1114
+ const { pathname } = new URL(request.url);
1115
+
1116
+ return Response.json({ path: pathname }, { status: 200 });
1117
+ },
1118
+ });
1119
+
1120
+ await ecs.registerTaskDefinition(
1121
+ new RegisterTaskDefinitionCommand({
1122
+ family: "orders-api",
1123
+ containerDefinitions: [
1124
+ {
1125
+ name: "app",
1126
+ image: "orders-api:1",
1127
+ portMappings: [{ containerPort: 8080 }],
1128
+ },
1129
+ ],
1130
+ }),
1131
+ );
1132
+
1133
+ await ecs.createService(
1134
+ new CreateServiceCommand({
1135
+ cluster: "orders",
1136
+ serviceName: "orders-api",
1137
+ taskDefinition: "orders-api",
1138
+ desiredCount: 2,
1139
+ loadBalancers: [
1140
+ { targetGroupArn, containerName: "app", containerPort: 8080 },
1141
+ ],
1142
+ }),
1143
+ );
1144
+
1145
+ // The tasks come up in the background, as they do on real ECS, and each of
1146
+ // them is registered in the target group as it starts.
1147
+ await simAws.backgroundTasksComplete();
1148
+
1149
+ const dnsName = loadBalancer.LoadBalancers?.[0]?.DNSName;
1150
+ const response = await simElbV2Fetch(simAws, `http://${dnsName}/orders/42`);
1151
+
1152
+ console.log(response.status); // 200
1153
+ console.log(await response.json()); // { path: "/orders/42" }
1154
+ ```
1155
+
1156
+ The handler runs as the task role, with the container's environment applied, exactly as a container
1157
+ consuming a queue or one run by `RunTask` does. So application code inside it builds an SDK client
1158
+ from `process.env` and is authorized by simulated IAM against the role the task definition declared.
1159
+
1160
+ The container sees the request the client made, with the headers a load balancer writes in front of
1161
+ a target. Those are the `host` the client asked for, `x-forwarded-for`, `x-forwarded-proto`,
1162
+ `x-forwarded-port` and `x-amzn-trace-id`. The URL is the AWS-facing one, and `new URL(request.url)`
1163
+ reads the name the client asked for and never a localhost one.
1164
+
1165
+ #### Which container of a task answers
1166
+
1167
+ Real ECS sends the request to the container the registration names, on the port it names. Yulin
1168
+ diverges from that on purpose, and it is worth knowing why.
1169
+
1170
+ A great many deployed services put a proxy container, usually nginx, on the port the service
1171
+ registers, with the application listening behind it. Yulin has nothing to run in place of that
1172
+ proxy. There is no image to run and nothing a test could bind to it. Routing strictly by name and
1173
+ port would therefore send every request to a container that is absent here, and the service would
1174
+ answer no request at all, however carefully it was set up.
1175
+
1176
+ So the request goes to a container that is bound, chosen in this order:
1177
+
1178
+ - the container the registration names, when that container is bound
1179
+ - otherwise the bound container that declared the registration's `containerPort`, which settles a
1180
+ choice between two containers that both answer
1181
+ - otherwise the first bound container of the task
1182
+
1183
+ ```typescript
1184
+ // A registration naming the proxy still reaches the application behind it.
1185
+ loadBalancers: [{ targetGroupArn, containerName: "nginx", containerPort: 80 }];
1186
+ ```
1187
+
1188
+ A target group whose service has no bound container at all is answered with a 503 by the load
1189
+ balancer. That is the honest answer, since the tasks are registered and there is nothing behind
1190
+ them.
1191
+
1192
+ `RunTask` cannot run a serving container, for the same reason it cannot run a consuming one. It has
1193
+ no handler that ends and no request to send it. A task started from the same definition records the
1194
+ container as not simulated, with a reason saying to create a service instead.
1195
+
1196
+ ### Updating and deleting a service
1197
+
1198
+ `UpdateService` changes the desired count, the task definition, or both. A new count starts or stops
1199
+ tasks to reach it. A new revision moves the service onto it and replaces every task the service is
1200
+ running. Real ECS replaces them a few at a time under a deployment configuration, and Yulin replaces
1201
+ them at once because nothing here takes any time to start.
1202
+
1203
+ ```typescript sim-ecs-update-service
1204
+ /**
1205
+ * Scaling a simulated ECS service and moving it to a new revision.
1206
+ */
1207
+
1208
+ import {
1209
+ CreateClusterCommand,
1210
+ CreateServiceCommand,
1211
+ ListTasksCommand,
1212
+ RegisterTaskDefinitionCommand,
1213
+ UpdateServiceCommand,
1214
+ } from "@aws-sdk/client-ecs";
1215
+
1216
+ import { SimAws } from "@kensio/yulin";
1217
+
1218
+ const simAws = new SimAws();
1219
+ const ecs = simAws.ecs();
1220
+
1221
+ await ecs.createCluster(new CreateClusterCommand({ clusterName: "orders" }));
1222
+ await ecs.registerTaskDefinition(
1223
+ new RegisterTaskDefinitionCommand({
1224
+ family: "checkout",
1225
+ containerDefinitions: [{ name: "app", image: "checkout:1" }],
1226
+ }),
1227
+ );
1228
+ await ecs.createService(
1229
+ new CreateServiceCommand({
1230
+ cluster: "orders",
1231
+ serviceName: "checkout",
1232
+ taskDefinition: "checkout",
1233
+ desiredCount: 1,
1234
+ }),
1235
+ );
1236
+
1237
+ const scaled = await ecs.updateService(
1238
+ new UpdateServiceCommand({
1239
+ cluster: "orders",
1240
+ service: "checkout",
1241
+ desiredCount: 4,
1242
+ }),
1243
+ );
1244
+
1245
+ console.log(scaled.service?.desiredCount); // 4
1246
+
1247
+ const second = await ecs.registerTaskDefinition(
1248
+ new RegisterTaskDefinitionCommand({
1249
+ family: "checkout",
1250
+ containerDefinitions: [{ name: "app", image: "checkout:2" }],
1251
+ }),
1252
+ );
1253
+
1254
+ const deployed = await ecs.updateService(
1255
+ new UpdateServiceCommand({
1256
+ cluster: "orders",
1257
+ service: "checkout",
1258
+ taskDefinition: "checkout:2",
1259
+ }),
1260
+ );
1261
+
1262
+ console.log(
1263
+ deployed.service?.taskDefinition === second.taskDefinition?.taskDefinitionArn,
1264
+ ); // true
1265
+
1266
+ await simAws.backgroundTasksComplete();
1267
+
1268
+ const listed = await ecs.listTasks(
1269
+ new ListTasksCommand({ cluster: "orders", serviceName: "checkout" }),
1270
+ );
1271
+
1272
+ console.log(listed.taskArns?.length); // 4, all of them on the new revision
1273
+ ```
1274
+
1275
+ `DeleteService` stops the service and its tasks. A service still scaled above zero is refused unless
1276
+ the request forces it, as real ECS refuses one. Scaling to zero first is the ordinary way round. A
1277
+ deleted service is still describable as `INACTIVE`, and its name is free to create again.
1278
+
1279
+ ```typescript sim-ecs-delete-service
1280
+ /**
1281
+ * Deleting a simulated ECS service and the tasks it was keeping running.
1282
+ */
1283
+
1284
+ import {
1285
+ CreateClusterCommand,
1286
+ CreateServiceCommand,
1287
+ DeleteServiceCommand,
1288
+ DescribeServicesCommand,
1289
+ ListTasksCommand,
1290
+ RegisterTaskDefinitionCommand,
1291
+ } from "@aws-sdk/client-ecs";
1292
+
1293
+ import { SimAws } from "@kensio/yulin";
1294
+
1295
+ const simAws = new SimAws();
1296
+ const ecs = simAws.ecs();
1297
+
1298
+ await ecs.createCluster(new CreateClusterCommand({ clusterName: "orders" }));
1299
+ await ecs.registerTaskDefinition(
1300
+ new RegisterTaskDefinitionCommand({
1301
+ family: "checkout",
1302
+ containerDefinitions: [{ name: "app", image: "checkout:1" }],
1303
+ }),
1304
+ );
1305
+ await ecs.createService(
1306
+ new CreateServiceCommand({
1307
+ cluster: "orders",
1308
+ serviceName: "checkout",
1309
+ taskDefinition: "checkout",
1310
+ desiredCount: 2,
1311
+ }),
1312
+ );
1313
+ await simAws.backgroundTasksComplete();
1314
+
1315
+ const deleted = await ecs.deleteService(
1316
+ new DeleteServiceCommand({
1317
+ cluster: "orders",
1318
+ service: "checkout",
1319
+ force: true,
1320
+ }),
1321
+ );
1322
+
1323
+ console.log(deleted.service?.status); // "INACTIVE"
1324
+
1325
+ const listed = await ecs.listTasks(
1326
+ new ListTasksCommand({ cluster: "orders", serviceName: "checkout" }),
1327
+ );
1328
+
1329
+ console.log(listed.taskArns?.length); // 0
1330
+
1331
+ const described = await ecs.describeServices(
1332
+ new DescribeServicesCommand({ cluster: "orders", services: ["checkout"] }),
1333
+ );
1334
+
1335
+ console.log(described.services?.[0]?.runningCount); // 0
1336
+ ```
1337
+
1338
+ Closing the simulated environment with `simAws.close()` stops the tasks of every service in it. A
1339
+ service is kept as state, and nothing is left scheduled either way. A test that finishes with a
1340
+ service running leaves nothing behind it.
1341
+
1342
+ ## Deploying ECS from CloudFormation
1343
+
1344
+ `AWS::ECS::Cluster` creates a simulated cluster, `AWS::ECS::TaskDefinition` registers a simulated
1345
+ task definition revision, and `AWS::ECS::Service` creates a simulated service running it. A test can
1346
+ start from the stack the application is actually defined in, without `RegisterTaskDefinition` and
1347
+ `CreateService` calls written for the test.
1348
+
1349
+ `Ref` on a cluster returns the cluster name and `Fn::GetAtt` `Arn` returns its ARN. `Ref` on a task
1350
+ definition returns the task definition ARN, revision and all, and so does `Fn::GetAtt`
1351
+ `TaskDefinitionArn`. Each deployment registers a new revision, as real CloudFormation does, because
1352
+ a revision is immutable and a changed one is a new revision of the same family.
1353
+
1354
+ Containers are stored as declared, whatever their image, and what makes one of them run is an
1355
+ executable binding supplied at deploy time, in the same `bindings` list a Lambda function handler is
1356
+ supplied in. A container binding targets a container by family and container name, by the logical ID
1357
+ of the task definition that declares it, or by the repository its image comes from.
1358
+
1359
+ ```typescript sim-ecs-cloudformation-task-definition
1360
+ /**
1361
+ * Deploying an ECS stack and binding a handler to one of its containers.
1362
+ */
1363
+
1364
+ import { RunTaskCommand } from "@aws-sdk/client-ecs";
1365
+
1366
+ import { SimAws } from "@kensio/yulin";
1367
+
1368
+ const simAws = new SimAws();
1369
+
1370
+ const processedOrders: string[] = [];
1371
+
1372
+ const stack = await simAws.cloudFormation().deployTemplate({
1373
+ stackName: "orders",
1374
+ template: {
1375
+ Resources: {
1376
+ OrdersCluster: {
1377
+ Type: "AWS::ECS::Cluster",
1378
+ Properties: { ClusterName: "orders" },
1379
+ },
1380
+ WorkerTaskDefinition: {
1381
+ Type: "AWS::ECS::TaskDefinition",
1382
+ Properties: {
1383
+ Family: "orders-worker",
1384
+ Cpu: "512",
1385
+ Memory: "1024",
1386
+ NetworkMode: "awsvpc",
1387
+ RequiresCompatibilities: ["FARGATE"],
1388
+ ContainerDefinitions: [
1389
+ {
1390
+ Name: "app",
1391
+ Image: "example.dkr.ecr.eu-west-2.amazonaws.com/orders-worker:1",
1392
+ Essential: true,
1393
+ Environment: [{ Name: "LOG_LEVEL", Value: "debug" }],
1394
+ },
1395
+ ],
1396
+ },
1397
+ },
1398
+ },
1399
+ Outputs: {
1400
+ TaskDefinition: { Value: { Ref: "WorkerTaskDefinition" } },
1401
+ },
1402
+ },
1403
+ bindings: [
1404
+ {
1405
+ family: "orders-worker",
1406
+ containerName: "app",
1407
+ run: async (): Promise<void> => {
1408
+ await Promise.resolve();
1409
+ processedOrders.push("outstanding orders");
1410
+ },
1411
+ },
1412
+ ],
1413
+ });
1414
+
1415
+ await stack.waitForDeployComplete();
1416
+
1417
+ console.log(stack.output("TaskDefinition"));
1418
+ // "arn:aws:ecs:us-east-1:888888888888:task-definition/orders-worker:1"
1419
+
1420
+ // Running a task from the deployed task definition runs the bound handler.
1421
+ await simAws
1422
+ .ecs()
1423
+ .runTask(
1424
+ new RunTaskCommand({ cluster: "orders", taskDefinition: "orders-worker" }),
1425
+ );
1426
+
1427
+ await simAws.backgroundTasksComplete();
1428
+
1429
+ console.log(processedOrders); // ["outstanding orders"]
1430
+ ```
1431
+
1432
+ A binding can name the task definition Resource instead, which is what a CDK stack gives a test to
1433
+ name. The construct ID is accepted as well as the synthesized logical ID, and the container name can
1434
+ be left out where the task definition declares one container. A binding naming an image repository
1435
+ matches any container running an image from it, whichever family declares it, which covers a tag
1436
+ that changes with every build.
1437
+
1438
+ ```typescript sim-ecs-cloudformation-binding-targets
1439
+ /**
1440
+ * Binding a container by the task definition Resource and by its repository.
1441
+ */
1442
+
1443
+ import { SimAws } from "@kensio/yulin";
1444
+
1445
+ const simAws = new SimAws();
1446
+
1447
+ const stack = await simAws.cloudFormation().deployTemplate({
1448
+ stackName: "orders",
1449
+ template: {
1450
+ Resources: {
1451
+ WorkerTaskDefinition: {
1452
+ Type: "AWS::ECS::TaskDefinition",
1453
+ Metadata: { "aws:cdk:path": "OrdersStack/WorkerTask/Resource" },
1454
+ Properties: {
1455
+ Family: "orders-worker",
1456
+ ContainerDefinitions: [
1457
+ {
1458
+ Name: "app",
1459
+ Image: "example.dkr.ecr.eu-west-2.amazonaws.com/orders-worker:1",
1460
+ },
1461
+ {
1462
+ Name: "log-router",
1463
+ Image: "public.ecr.aws/aws-observability/aws-for-fluent-bit:1",
1464
+ },
1465
+ ],
1466
+ },
1467
+ },
1468
+ CheckoutTaskDefinition: {
1469
+ Type: "AWS::ECS::TaskDefinition",
1470
+ Properties: {
1471
+ Family: "orders-checkout",
1472
+ ContainerDefinitions: [
1473
+ {
1474
+ Name: "app",
1475
+ Image: "example.dkr.ecr.eu-west-2.amazonaws.com/checkout:9f21c0",
1476
+ },
1477
+ ],
1478
+ },
1479
+ },
1480
+ },
1481
+ },
1482
+ bindings: [
1483
+ // The CDK construct ID, naming the container because this task definition
1484
+ // declares more than one.
1485
+ {
1486
+ logicalId: "WorkerTask",
1487
+ containerName: "app",
1488
+ run: (): void => {
1489
+ // Whatever the worker does.
1490
+ },
1491
+ },
1492
+ // Any container running an image from this repository, whatever its tag.
1493
+ {
1494
+ imageRepository: "example.dkr.ecr.eu-west-2.amazonaws.com/checkout",
1495
+ run: (): void => {
1496
+ // Whatever the checkout container does.
1497
+ },
1498
+ },
1499
+ ],
1500
+ });
1501
+
1502
+ await stack.waitForDeployComplete();
1503
+
1504
+ console.log(simAws.ecs().taskDefinition("orders-worker").revision); // 1
1505
+ ```
1506
+
1507
+ A binding that resolves to no Resource in the stack fails the deployment naming the binding, since
1508
+ the usual cause is a container renamed in the template and not in the test. A container with no
1509
+ binding is a different thing. The stack deploys, the container is stored as declared, and it is
1510
+ recorded as not simulated when a task runs. That is what lets a task definition holding a log router
1511
+ and an observability agent alongside the application deploy and run.
1512
+
1513
+ A task definition's `TaskRoleArn` and `ExecutionRoleArn` resolve whether the template gives an ARN
1514
+ or a `Ref` to an `AWS::IAM::Role` of the same stack, and a container's AWS calls are authorized as
1515
+ the role the stack deploys.
1516
+
1517
+ ### Deploying a service
1518
+
1519
+ `AWS::ECS::Service` creates a service in its cluster, keeping `DesiredCount` tasks of the task
1520
+ definition it names running. `Cluster` takes a `Ref` to a cluster of the same stack or a cluster
1521
+ ARN, and `TaskDefinition` takes a `Ref` to a task definition of the same stack, which pins the
1522
+ revision the deployment registered, or an ARN, or a family. A container bound at deploy time is
1523
+ running once the stack has deployed, because the service names the task definition and is created
1524
+ after it.
1525
+
1526
+ `Ref` on a service returns the service ARN, and so does `Fn::GetAtt` `ServiceArn`. `Fn::GetAtt`
1527
+ `Name` returns the service name. `simAws.ecs().service(name, cluster)` reads the simulated service
1528
+ itself, by name in a cluster or by its full ARN.
1529
+
1530
+ ```typescript sim-ecs-cloudformation-service
1531
+ /**
1532
+ * Deploying an ECS service and reading what it is keeping running.
1533
+ */
1534
+
1535
+ import { ListTasksCommand } from "@aws-sdk/client-ecs";
1536
+
1537
+ import { SimAws } from "@kensio/yulin";
1538
+
1539
+ const simAws = new SimAws();
1540
+
1541
+ const stack = await simAws.cloudFormation().deployTemplate({
1542
+ stackName: "orders",
1543
+ template: {
1544
+ Resources: {
1545
+ OrdersCluster: {
1546
+ Type: "AWS::ECS::Cluster",
1547
+ Properties: { ClusterName: "orders" },
1548
+ },
1549
+ WorkerTaskDefinition: {
1550
+ Type: "AWS::ECS::TaskDefinition",
1551
+ Properties: {
1552
+ Family: "orders-worker",
1553
+ ContainerDefinitions: [
1554
+ {
1555
+ Name: "app",
1556
+ Image: "example.dkr.ecr.eu-west-2.amazonaws.com/orders-worker:1",
1557
+ },
1558
+ ],
1559
+ },
1560
+ },
1561
+ WorkerService: {
1562
+ Type: "AWS::ECS::Service",
1563
+ Properties: {
1564
+ ServiceName: "orders-worker",
1565
+ Cluster: { Ref: "OrdersCluster" },
1566
+ TaskDefinition: { Ref: "WorkerTaskDefinition" },
1567
+ DesiredCount: 2,
1568
+ LaunchType: "FARGATE",
1569
+ },
1570
+ },
1571
+ },
1572
+ Outputs: {
1573
+ Service: { Value: { Ref: "WorkerService" } },
1574
+ ServiceName: { Value: { "Fn::GetAtt": ["WorkerService", "Name"] } },
1575
+ },
1576
+ },
1577
+ bindings: [
1578
+ {
1579
+ logicalId: "WorkerTaskDefinition",
1580
+ run: (): void => {
1581
+ // Whatever the worker container does when something reaches it.
1582
+ },
1583
+ },
1584
+ ],
1585
+ });
1586
+
1587
+ await stack.waitForDeployComplete();
1588
+ await simAws.backgroundTasksComplete();
1589
+
1590
+ console.log(stack.output("Service"));
1591
+ // "arn:aws:ecs:us-east-1:888888888888:service/orders/orders-worker"
1592
+ console.log(stack.output("ServiceName")); // "orders-worker"
1593
+
1594
+ const service = simAws.ecs().service("orders-worker", "orders");
1595
+
1596
+ console.log(service.desiredCount); // 2
1597
+
1598
+ const listed = await simAws
1599
+ .ecs()
1600
+ .listTasks(
1601
+ new ListTasksCommand({ cluster: "orders", serviceName: "orders-worker" }),
1602
+ );
1603
+
1604
+ console.log(listed.taskArns?.length); // 2
1605
+ ```
1606
+
1607
+ An unnamed service is named after the stack and the logical ID, as a cluster and a family are. A
1608
+ service declaring no `DesiredCount` keeps one task running, which is what real CloudFormation gives
1609
+ a new service.
1610
+
1611
+ Updating the stack moves the service. A changed `DesiredCount` scales it, and a changed task
1612
+ definition moves it onto the revision the update registered, replacing the tasks it was keeping.
1613
+ Tearing the stack down deletes the service, stopping its tasks and leaving it `INACTIVE`, whatever
1614
+ it was scaled to.
1615
+
1616
+ `LoadBalancers` registers the service's tasks into the target group it names. A template that
1617
+ declares a load balancer, a target group and a service therefore deploys something that answers. The
1618
+ target group has to exist and be an `ip` one, or the deployment fails naming it, and what the
1619
+ template declared is readable on the simulated service:
1620
+
1621
+ ```typescript
1622
+ console.log(simAws.ecs().service("orders-worker", "orders").loadBalancers);
1623
+ ```
1624
+
1625
+ Everything else the three Resource types declare is stored as declared, or recorded as ignored where
1626
+ this simulation has no use for it. `CapacityProviders`, `DefaultCapacityProviderStrategy` and
1627
+ `ServiceConnectDefaults` on a cluster, `InferenceAccelerators` and `EnableFaultInjection` on a task
1628
+ definition, and a service's `NetworkConfiguration`, `CapacityProviderStrategy`,
1629
+ `DeploymentConfiguration` and `ServiceRegistries` among the rest, are read and ignored while the
1630
+ stack deploys, and each one is reported on the Resource:
1631
+
1632
+ ```typescript
1633
+ console.log(stack.getResource("OrdersCluster")?.ignoredProperties);
1634
+ ```
1635
+
1636
+ ## Authorization
1637
+
1638
+ Every operation is authorized by simulated IAM against the real ECS action.
1639
+
1640
+ Real ECS gives the task definition operations no resource type at all, and gives `CreateCluster`,
1641
+ `ListClusters` and `ListTasks` none either, and all of those authorize against `*`. A policy naming
1642
+ a task definition ARN grants none of them, here as on AWS. The operations that do take a resource
1643
+ are `DescribeClusters` and `DeleteCluster`, which take the cluster's ARN, `RunTask`, which takes the
1644
+ task definition revision it would run, `DescribeTasks` and `StopTask`, which take the task's ARN,
1645
+ and the service operations, which take the service's ARN. `CreateService` authorizes against the ARN
1646
+ the service is about to have, and a policy can therefore name one service by name before it exists.
1647
+
1648
+ ```typescript sim-ecs-iam-policy
1649
+ /**
1650
+ * A simulated IAM policy allowing a Role to register task definitions.
1651
+ */
1652
+
1653
+ import { RegisterTaskDefinitionCommand } from "@aws-sdk/client-ecs";
1654
+ import { CreateRoleCommand, PutRolePolicyCommand } from "@aws-sdk/client-iam";
1655
+
1656
+ import { SimAws } from "@kensio/yulin";
1657
+
1658
+ const simAws = new SimAws();
1659
+ const accountId = simAws.defaultAccountId;
1660
+
1661
+ const role = await simAws.iam().createRole(
1662
+ new CreateRoleCommand({
1663
+ RoleName: "Deployer",
1664
+ AssumeRolePolicyDocument: JSON.stringify({
1665
+ Version: "2012-10-17",
1666
+ Statement: {
1667
+ Effect: "Allow",
1668
+ Principal: { AWS: `arn:aws:iam::${accountId}:root` },
1669
+ Action: "sts:AssumeRole",
1670
+ },
1671
+ }),
1672
+ }),
1673
+ );
1674
+
1675
+ await simAws.iam().putRolePolicy(
1676
+ new PutRolePolicyCommand({
1677
+ RoleName: "Deployer",
1678
+ PolicyName: "RegisterTaskDefinitions",
1679
+ PolicyDocument: JSON.stringify({
1680
+ Version: "2012-10-17",
1681
+ Statement: {
1682
+ Effect: "Allow",
1683
+ Action: "ecs:RegisterTaskDefinition",
1684
+ // ECS gives this action no resource type, so only `*` grants it.
1685
+ Resource: "*",
1686
+ },
1687
+ }),
1688
+ }),
1689
+ );
1690
+
1691
+ const registered = await simAws.ecs().registerTaskDefinition(
1692
+ new RegisterTaskDefinitionCommand({
1693
+ family: "checkout",
1694
+ containerDefinitions: [{ name: "app", image: "checkout:1" }],
1695
+ }),
1696
+ { caller: { kind: "arn", arn: role.Role.Arn } },
1697
+ );
1698
+
1699
+ console.log(registered.taskDefinition?.registeredBy); // the Role ARN
1700
+ ```
1701
+
1702
+ A registered revision records the caller that registered it as `registeredBy`, as real ECS does.
1703
+
1704
+ ## Scoping by account and region
1705
+
1706
+ Clusters and task definitions belong to one account and region. A family registered in one region
1707
+ has its own revision numbering and its own ARNs.
1708
+
1709
+ ```typescript sim-ecs-scoping
1710
+ /**
1711
+ * Simulated ECS state in two account and region scopes.
1712
+ */
1713
+
1714
+ import {
1715
+ ListTaskDefinitionsCommand,
1716
+ RegisterTaskDefinitionCommand,
1717
+ } from "@aws-sdk/client-ecs";
1718
+
1719
+ import { SimAws } from "@kensio/yulin";
1720
+
1721
+ const simAws = new SimAws();
1722
+
1723
+ const registered = await simAws
1724
+ .account("222222222222")
1725
+ .region("us-east-1")
1726
+ .ecs()
1727
+ .registerTaskDefinition(
1728
+ new RegisterTaskDefinitionCommand({
1729
+ family: "checkout",
1730
+ containerDefinitions: [{ name: "app", image: "checkout:1" }],
1731
+ }),
1732
+ );
1733
+
1734
+ console.log(registered.taskDefinition?.taskDefinitionArn);
1735
+ // "arn:aws:ecs:us-east-1:222222222222:task-definition/checkout:1"
1736
+
1737
+ const elsewhere = await simAws
1738
+ .account("222222222222")
1739
+ .region("eu-west-2")
1740
+ .ecs()
1741
+ .listTaskDefinitions(new ListTaskDefinitionsCommand({}));
1742
+
1743
+ console.log(elsewhere.taskDefinitionArns?.length); // 0
1744
+ ```
1745
+
1746
+ ## Using it through an ECS SDK client
1747
+
1748
+ Ordinary SDK code reaches simulated ECS through `SimSdk`, without a Yulin type appearing in the code
1749
+ under test.
1750
+
1751
+ ```typescript sim-ecs-sdk-interception
1752
+ /**
1753
+ * Reaching simulated ECS through an intercepted ECS SDK client.
1754
+ */
1755
+
1756
+ import {
1757
+ DescribeTaskDefinitionCommand,
1758
+ ECSClient,
1759
+ RegisterTaskDefinitionCommand,
1760
+ } from "@aws-sdk/client-ecs";
1761
+
1762
+ import { SimSdk } from "@kensio/yulin/sdk";
1763
+
1764
+ using simSdk = new SimSdk();
1765
+ simSdk.intercept(ECSClient);
1766
+
1767
+ const ecs = new ECSClient({ region: "eu-west-2" });
1768
+
1769
+ await ecs.send(
1770
+ new RegisterTaskDefinitionCommand({
1771
+ family: "checkout",
1772
+ containerDefinitions: [{ name: "app", image: "checkout:1" }],
1773
+ }),
1774
+ );
1775
+
1776
+ const described = await ecs.send(
1777
+ new DescribeTaskDefinitionCommand({ taskDefinition: "checkout" }),
1778
+ );
1779
+
1780
+ console.log(described.taskDefinition?.revision); // 1
1781
+ ```
1782
+
1783
+ ## Available functionality
1784
+
1785
+ - `RegisterTaskDefinitionCommand`, with revisions numbered per family from one
1786
+ - `DeregisterTaskDefinitionCommand`, marking one revision `INACTIVE`
1787
+ - `DescribeTaskDefinitionCommand`, taking a family, a `family:revision` or a full ARN
1788
+ - `ListTaskDefinitionsCommand` and `ListTaskDefinitionFamiliesCommand`, with prefix, status and
1789
+ paging
1790
+ - `CreateClusterCommand`, `DescribeClustersCommand`, `ListClustersCommand` and
1791
+ `DeleteClusterCommand`
1792
+ - `RunTaskCommand`, running the handlers bound to a task definition's containers, up to a `count` of
1793
+ ten tasks at a time
1794
+ - `DescribeTasksCommand`, `ListTasksCommand` and `StopTaskCommand`
1795
+ - Tasks run from an EventBridge rule target or a Scheduler schedule target, through that target's
1796
+ role
1797
+ - `CreateServiceCommand`, keeping a desired count of tasks running from a task definition
1798
+ - `UpdateServiceCommand`, changing the desired count, the task definition, or both
1799
+ - `DescribeServicesCommand` and `DeleteServiceCommand`, with the `force` a scaled-up service needs
1800
+ - `ListTasks` filtering by `serviceName`, listing a service's tasks on their own
1801
+ - `bindContainer`, targeting a container by family and container name or by image repository
1802
+ - A container binding that `consumes` a simulated SQS queue, with Yulin driving the polling loop on
1803
+ the simulated clock while the service is running
1804
+ - `AWS::ECS::Cluster`, answering `Ref` with the cluster name and `Fn::GetAtt` with `Arn`
1805
+ - `AWS::ECS::TaskDefinition`, registering a revision and answering `Ref` with its ARN
1806
+ - `AWS::ECS::Service`, running the task definition it names at the desired count it declares, and
1807
+ answering `Ref` with the service ARN and `Fn::GetAtt` with `Name` and `ServiceArn`
1808
+ - A stack update that scales a service or moves it onto a new revision, and a teardown that deletes
1809
+ it
1810
+ - A container binding that answers `http` requests, called once per request a load balancer routes
1811
+ to the container
1812
+ - `loadBalancers` on a service, registering each of its tasks into the target group it names and
1813
+ deregistering them as they stop
1814
+ - `LoadBalancers` on an `AWS::ECS::Service`, deploying into the same registration
1815
+ - `simAws.ecs().service()`, reading a simulated service by name in a cluster or by its ARN
1816
+ - Deploy-time container bindings, targeting a container by family and container name, by the task
1817
+ definition's logical ID or CDK construct ID, or by its image repository, and declaring `run` or
1818
+ `consumes` as a directly bound container does
1819
+ - Task and execution roles resolved from a `Ref` to a same-stack role or from an ARN
1820
+ - Container environment variables and `RunTask` container overrides, through `process.env`
1821
+ - Container AWS calls authorized as the task role, including a `RunTask` `taskRoleArn` override
1822
+ - Container `secrets` resolved from simulated Secrets Manager and SSM Parameter Store as the
1823
+ execution role, including a JSON key selector and a `SecureString` parameter
1824
+ - Container definitions stored and reported as declared, whatever their image
1825
+ - Task definition tags, reported under `include: ["TAGS"]`
1826
+ - Cluster settings, configuration and tags, reported under their matching `include` values
1827
+ - Authorization of every operation by simulated IAM, against the real IAM action and resource
1828
+ - Clusters, task definitions, tasks and services scoped by account and region
1829
+ - ECS SDK clients intercepted by `SimSdk`
1830
+
1831
+ ## Limitations
1832
+
1833
+ Current documented limitations:
1834
+
1835
+ - Image contents are never read. An image URI is an identifier and nothing more. No image is pulled,
1836
+ inspected or run, and a tag no registry holds is stored without complaint.
1837
+ - A container definition field the `SimEcsContainerDefinitionType` shape leaves unnamed, such as
1838
+ `hostname` or `links`, is still stored and reported back. The shape names the fields the docs
1839
+ describe rather than every field ECS has, and it deliberately carries no index signature, since
1840
+ one would stop a real SDK command input being passed straight in.
1841
+ - `ListTaskDefinitions` refuses a `status` of `DELETE_IN_PROGRESS`, which real ECS accepts. Nothing
1842
+ deletes a task definition here, and the answer would always be an empty listing. Refusing says so,
1843
+ where a result would look like an answer.
1844
+ - Yulin creates no cluster on its own, including the `default` one. A `RunTask` request naming no
1845
+ cluster needs one to have been created already. An AWS account often has a `default` cluster,
1846
+ created the first time ECS was used from the console.
1847
+ - A container of a task definition with no `taskRoleArn` is denied every AWS call. It never falls
1848
+ back to whatever credentials a container instance role might have supplied.
1849
+ - Only a bound container runs. A container with no binding never starts and is reported with a
1850
+ reason saying so, and a task with no binding at all stops with `TaskFailedToStart`.
1851
+ - Containers run one after another in the order the task definition declares them, and never
1852
+ alongside each other. `dependsOn`, `essential`, health checks and `startTimeout` are stored and
1853
+ ignored.
1854
+ - `RunTask` refuses `networkConfiguration`, `capacityProviderStrategy`, `platformVersion`, `tags`,
1855
+ `placementConstraints`, `placementStrategy` and the rest of what it takes. There is no network and
1856
+ no capacity here for any of them to apply to.
1857
+ - A `RunTask` override may name `taskRoleArn` and a container's `environment`. A `command`, `cpu` or
1858
+ `memory` override is refused, since Yulin never runs an image and has no capacity to allocate.
1859
+ - The execution role resolves container `secrets` and does nothing else. There is no image to pull
1860
+ and no log driver to write to.
1861
+ - A container secret can only come from simulated Secrets Manager or simulated SSM Parameter Store.
1862
+ A `valueFrom` naming anything else stops the task outright. Secret rotation is absent, and a task
1863
+ always reads the version that is current when it starts.
1864
+ - A JSON key selector resolves only where the key holds a string. A key holding a number, a boolean
1865
+ or a nested object is refused, because an environment variable is text and real ECS does not
1866
+ document which text it would become.
1867
+ - A secret holding a binary value is refused. Real ECS cannot put one in an environment variable
1868
+ either, but it reports the problem differently.
1869
+ - `secretOptions` on a `logConfiguration` are stored and never resolved. There is no log driver here
1870
+ for them to configure.
1871
+ - A task definition declaring `secrets` and no `executionRoleArn` fails when a task is run, and not
1872
+ when the revision is registered. Real ECS refuses the registration.
1873
+ - An EventBridge or Scheduler target's `EcsParameters` takes `TaskDefinitionArn` and `TaskCount`
1874
+ only, taking and ignoring the launch type, platform version, network configuration and capacity
1875
+ provider strategy for the same reason `RunTask` refuses them, which is that there is no placement
1876
+ and no network here. A `TaskCount` above one runs that many simulated tasks, and a bound container
1877
+ handler runs once for each of them, in this process and one after another.
1878
+ - A rule or schedule target's `Input` is read as the task's overrides, since a task has nowhere to
1879
+ receive a payload. Container environment variables are set with a `containerOverrides` list naming
1880
+ the container.
1881
+ - A service's desired count is simulated as state, and not as concurrency. Three tasks exist and are
1882
+ reported as running, and the handler bound to a container is called once per request or per poll.
1883
+ Yulin runs in one Node.js process, with nothing to copy.
1884
+ - A bound container of a service is treated as running and stays available until the service is
1885
+ scaled to zero, deleted, or its `SimAws` is closed. What calls its handler is whatever reaches it,
1886
+ either a queue it consumes or a request a load balancer routes to it.
1887
+ - A consuming container's loop belongs to Yulin, and not to the container. A binding supplies what
1888
+ happens to a batch, and a polling loop written inside your own container code is never run,
1889
+ because an endless loop in a single Node.js process never yields to the test running it.
1890
+ - A container can only consume a simulated SQS queue, in the same account and region as the service.
1891
+ Nothing else is a source, and a queue URL naming another scope reaches no queue.
1892
+ - Yulin polls once per service and container, and never once per task, and a desired count above one
1893
+ hands a batch over exactly once. It also polls in response to something, and never continuously.
1894
+ The triggers are a message arriving, a batch coming back, or a full batch suggesting there is more
1895
+ waiting.
1896
+ - Long polling is absent. A `WaitTimeSeconds` is meaningless when the queue itself says when there
1897
+ is something to poll for, and the setting is gone.
1898
+ - A consuming container handles a batch all or nothing. There is no partial batch response, since
1899
+ that is a Lambda event source feature and not something a worker container has.
1900
+ - A consuming or serving container of a task started by `RunTask` records the same not-simulated
1901
+ reason an unbound container does. Neither has a handler that ends, and a run task has to end.
1902
+ - A service's tasks come up all at once and are replaced all at once. There are no deployments,
1903
+ deployment controllers, circuit breakers or rolling replacement, so `DescribeServices` reports no
1904
+ `deployments` and no `events`, and `UpdateService` refuses `forceNewDeployment`.
1905
+ - A service whose tasks fail to start never starts replacements for them. Real ECS keeps trying,
1906
+ which in a test would be an endless retry with nothing to assert on. The service here reports the
1907
+ running count it actually has.
1908
+ - A `loadBalancers` entry needs a `targetGroupArn`, a `containerName` and a `containerPort` between
1909
+ 1 and 65535, and the target group has to be an `ip` one in the service's own account and region. A
1910
+ `loadBalancerName` is the Classic Load Balancer form, and is refused, as is a target group that
1911
+ was never created.
1912
+ - Which container of a task a request reaches diverges from real ECS on purpose. Real ECS routes to
1913
+ the container the registration names, on the port it names. Here the request goes to a bound
1914
+ container, which is the one the registration names where that one is bound, otherwise the bound
1915
+ container declaring the registration's port, otherwise the first bound one. The common real task
1916
+ puts an unsimulated proxy on the registered port, and routing strictly would reach a container
1917
+ absent here.
1918
+ - A service registered into a target group with no bound container is answered with a 503 by the
1919
+ load balancer. The tasks are registered and there is nothing behind them.
1920
+ - A task is registered as a target as soon as the service starts it and deregistered as soon as it
1921
+ stops. There are no health checks, no target health states, no deregistration delay and no
1922
+ connection draining. The address a task is registered under is counted, since there is no network
1923
+ interface to take one from.
1924
+ - Requests are never shared between a service's tasks. The desired count is state and not
1925
+ concurrency, and a target group holding three targets calls one handler.
1926
+ - `CreateService` refuses `serviceRegistries`, `networkConfiguration`, `deploymentConfiguration`,
1927
+ `capacityProviderStrategy` and the rest of what it takes. There is no network or capacity here for
1928
+ any of them to apply to.
1929
+ - `UpdateService` changes the desired count and the task definition alone. A service's load balancer
1930
+ registration stays as it was created, and its tasks are registered and deregistered as the count
1931
+ changes.
1932
+ - `CreateService` refuses a `schedulingStrategy` of `DAEMON`, which places one task on each
1933
+ container instance. There are no container instances here to place one on each of.
1934
+ - `CreateService` needs a `desiredCount`, as a replica service does on real ECS, and it can be zero.
1935
+ - A service's tasks are `startedBy` `ecs-svc/` and the service name. Real ECS uses `ecs-svc/` and a
1936
+ number, which would name nothing here.
1937
+ - Service autoscaling, service discovery and `ListServices` are all absent.
1938
+ - `StartTask` is absent.
1939
+ - Closing a `SimAws` stops the tasks of every service in it. The services stay describable with the
1940
+ desired count they had, and their tasks are not brought back.
1941
+ - `DescribeTasks` refuses `include`, and a task carries no tags, so `RunTask` refuses `tags` too.
1942
+ - `ListTasks` refuses a `desiredStatus` of `PENDING`, which real ECS accepts. A simulated task is
1943
+ wanted either running or stopped, so the answer would always be an empty listing.
1944
+ - A stopped task is kept for as long as the simulation lasts. Real ECS stops reporting one about an
1945
+ hour after it stops.
1946
+ - A task reports no `cpu`, `memory`, `connectivity`, `attachments` or `availabilityZone`. There is
1947
+ no capacity and no network here to report.
1948
+ - A described task definition reports neither `compatibilities` nor `requiresAttributes`. Real ECS
1949
+ works both out from what the definition declares, which would mean reading a container
1950
+ definition's meaning.
1951
+ - `RegisterTaskDefinition` refuses a setting this simulation has no room for, and never drops one
1952
+ silently. `inferenceAccelerators` and `enableFaultInjection` are refused for that reason.
1953
+ - Nothing is defaulted. A revision registered without `networkMode` describes without one, where
1954
+ real ECS would have chosen `bridge`, because that value would be made up here.
1955
+ - `CreateCluster` refuses `capacityProviders`, `defaultCapacityProviderStrategy` and
1956
+ `serviceConnectDefaults`. There is no capacity and no service discovery here to attach them to.
1957
+ - `DescribeClusters` refuses `include` values of `ATTACHMENTS` and `STATISTICS`, and always reports
1958
+ zero for every count, tasks in the cluster included. `ListTasks` is what reports the tasks a
1959
+ cluster holds.
1960
+ - `ListClusters` leaves out a deleted cluster, which is still describable by name or ARN as
1961
+ `INACTIVE`.
1962
+ - `CreateCluster` with a name an active cluster already has hands that cluster back, as real ECS
1963
+ does. The settings, configuration and tags on the second request are ignored.
1964
+ - Deleting a cluster is immediate, and never fails for a cluster still holding running tasks or
1965
+ active services, which real ECS refuses. The services in it go on reporting what they are keeping.
1966
+ - `DescribeClusters` reports zero services and zero tasks whatever the cluster holds.
1967
+ `DescribeServices` and `ListTasks` are what report those.
1968
+ - Task definition and cluster tags are stored and reported, but `TagResource`, `UntagResource` and
1969
+ `ListTagsForResource` are absent.
1970
+ - An unnamed cluster, task definition or service gets a name composed from the stack name and the
1971
+ logical ID, without the random part real CloudFormation adds, which makes it predictable in a
1972
+ test. A stack deployed twice under different names therefore gets two different families.
1973
+ - A service that declares no `DesiredCount` keeps one task running. That is the default real
1974
+ CloudFormation documents for a new service. A `DesiredCount` written as the text of a number is
1975
+ taken as that number, since a String Parameter resolves to text, and anything else is refused
1976
+ naming the Resource and the property, a fraction of a task included.
1977
+ - An update replaces a task definition Resource in full, registering a new revision and
1978
+ deregistering the one it replaced. The family accumulates revisions with only the newest one
1979
+ `ACTIVE`, where real CloudFormation leaves the earlier revisions active.
1980
+ - An update replaces a service Resource as well, and a scaled or redeployed service is deleted and
1981
+ created again. Its tasks stop and new ones start, and its ARN is unchanged as long as its name is,
1982
+ since a service ARN is its cluster and its name.
1983
+ - A stack teardown deletes its cluster, leaving it `INACTIVE`, deregisters the revision it
1984
+ registered, leaving that `INACTIVE`, and deletes its service, leaving that `INACTIVE` too. None of
1985
+ them is removed, and revision numbers are not freed. The service is deleted with force, because
1986
+ real CloudFormation scales one to zero on its way out and nothing here needs the time that takes.
1987
+ - CloudFormation property names are translated to the API's by lowering the first letter of each of
1988
+ them, all the way down, apart from `EFSVolumeConfiguration`,
1989
+ `FSxWindowsFileServerVolumeConfiguration` and `ProxyConfigurationProperties`, which the API spells
1990
+ differently. `DockerLabels`, `Options`, `DriverOpts` and `Labels` hold keys the template wrote, so
1991
+ those are left alone. A name outside all of that is stored under the name lowering gives it.
1992
+ - `CapacityProviders`, `DefaultCapacityProviderStrategy` and `ServiceConnectDefaults` on a cluster,
1993
+ `InferenceAccelerators` and `EnableFaultInjection` on a task definition, and everything a service
1994
+ declares beyond its cluster, task definition, count, launch type, scheduling strategy and load
1995
+ balancers, are read and recorded as ignored, where the equivalent SDK request is refused. A stack
1996
+ that fails to deploy is worth less to a test than a Resource missing a property this simulation
1997
+ has no use for.
1998
+ - A deploy-time binding is checked against the template as the stack is built. A family, a container
1999
+ name or an image repository built from another Resource's attribute, in place of a plain string,
2000
+ resolves to no target and fails the deployment.
2001
+ - A `TaskRoleArn` or `ExecutionRoleArn` given as a `Ref` to a role resolves to the role name, and
2002
+ that name is turned into the ARN it would have at the default path. A role declaring a `Path` of
2003
+ its own therefore resolves to an ARN without it. An `AWS::Lambda::Function` `Role` already behaves
2004
+ this way, so the two agree, and naming the role by `Fn::GetAtt` `Arn` gets the real ARN either
2005
+ way.
2006
+ - Cluster and family names are validated to the 255 letters, numbers, hyphens and underscores real
2007
+ ECS accepts, but error messages differ from the real ones.
2008
+ - Account-wide limits are absent. No request fails for having registered too many task definitions
2009
+ or created too many clusters.
2010
+ - `serveSimAws` serves no ECS HTTP API.