@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,1897 @@
1
+ # Simulated Elastic Load Balancing
2
+
3
+ Yulin includes a simulated Application Load Balancer for tests and local development. Load
4
+ balancers, target groups, listeners and listener rules are held in memory and every operation is
5
+ authorized by simulated IAM. ELBv2-specific types are imported from the `@kensio/yulin/elbv2`
6
+ subpath.
7
+
8
+ A load balancer created here has a DNS name of the shape real ELB issues, and a
9
+ [Route53](https://yulinsim.dev/services/route53/) record pointing at that name resolves to it. A request made to your own
10
+ hostname reaches the load balancer as it would deployed. A request is matched to a listener by port
11
+ and then to one of that listener's rules, and a `forward` action sends it to a target group, where a
12
+ registered [Lambda](https://yulinsim.dev/services/lambda/) function is invoked with the request and its response becomes the
13
+ HTTP response.
14
+
15
+ Only the application load balancer is simulated. A network or gateway load balancer routes below
16
+ HTTP, which nothing here speaks, and `Type: "network"` is refused outright.
17
+
18
+ No TLS is performed anywhere in this. An HTTPS listener holds a certificate and is checked against
19
+ simulated ACM, and everything travels in the clear. See
20
+ [HTTPS listeners and certificates](#https-listeners-and-certificates) for what that leaves a test
21
+ able to conclude.
22
+
23
+ ## Creating a load balancer
24
+
25
+ ```typescript sim-elbv2-create-load-balancer
26
+ /**
27
+ * Creating a load balancer and reading the DNS name it is issued.
28
+ *
29
+ * That name is what a Route53 record points at, and what a request addressed
30
+ * to the load balancer directly names. A request that a Route53 record brought
31
+ * here names the record instead.
32
+ */
33
+
34
+ import {
35
+ CreateLoadBalancerCommand,
36
+ DescribeLoadBalancersCommand,
37
+ } from "@aws-sdk/client-elastic-load-balancing-v2";
38
+
39
+ import { SimAws } from "@kensio/yulin";
40
+
41
+ const simAws = new SimAws();
42
+ const elbV2 = simAws.account("888888888888").region("eu-west-1").elbV2();
43
+
44
+ const created = await elbV2.createLoadBalancer(
45
+ new CreateLoadBalancerCommand({
46
+ Name: "shop-alb",
47
+ Scheme: "internet-facing",
48
+ // Subnets and security groups are accepted and not modelled: there is no
49
+ // network here to place a load balancer on.
50
+ Subnets: ["subnet-a", "subnet-b"],
51
+ }),
52
+ );
53
+
54
+ console.log(created.LoadBalancers?.[0]?.DNSName);
55
+ // "shop-alb-0000000001.eu-west-1.elb.amazonaws.com"
56
+
57
+ console.log(created.LoadBalancers?.[0]?.LoadBalancerArn);
58
+ // "arn:aws:elasticloadbalancing:eu-west-1:888888888888:loadbalancer/app/shop-alb/0000000000000001"
59
+
60
+ const described = await elbV2.describeLoadBalancers(
61
+ new DescribeLoadBalancersCommand({ Names: ["shop-alb"] }),
62
+ );
63
+
64
+ console.log(described.LoadBalancers?.[0]?.State.Code); // "active"
65
+ ```
66
+
67
+ A load balancer is `active` as soon as it is created, where real ELB leaves one in `provisioning`
68
+ for a few minutes first. A name is unique within one account and region. The same name can be used
69
+ in another region, and the two stay separate.
70
+
71
+ An internal load balancer's host name carries the `internal-` prefix real ELB gives it. That is also
72
+ why a load balancer cannot be named starting with `internal-`.
73
+
74
+ ## Target groups hold functions or addresses
75
+
76
+ A target group names what it holds through its `TargetType`, and that decides the rest. It sets how
77
+ many targets the group takes, what a target's `Id` has to look like, and whether the group carries a
78
+ protocol and port at all.
79
+
80
+ ```typescript sim-elbv2-lambda-target-group
81
+ /**
82
+ * A target group holding one Lambda function.
83
+ */
84
+
85
+ import {
86
+ CreateTargetGroupCommand,
87
+ DescribeTargetHealthCommand,
88
+ RegisterTargetsCommand,
89
+ } from "@aws-sdk/client-elastic-load-balancing-v2";
90
+
91
+ import { SimAws } from "@kensio/yulin";
92
+
93
+ const simAws = new SimAws();
94
+ const elbV2 = simAws.account("888888888888").region("eu-west-1").elbV2();
95
+
96
+ const targetGroup = await elbV2.createTargetGroup(
97
+ new CreateTargetGroupCommand({
98
+ Name: "checkout-tg",
99
+ // A lambda target group takes no Protocol or Port: the load balancer
100
+ // invokes the function rather than connecting to it.
101
+ TargetType: "lambda",
102
+ }),
103
+ );
104
+
105
+ const targetGroupArn = targetGroup.TargetGroups?.[0]?.TargetGroupArn;
106
+
107
+ await elbV2.registerTargets(
108
+ new RegisterTargetsCommand({
109
+ TargetGroupArn: targetGroupArn,
110
+ Targets: [
111
+ { Id: "arn:aws:lambda:eu-west-1:888888888888:function:checkout" },
112
+ ],
113
+ }),
114
+ );
115
+
116
+ const health = await elbV2.describeTargetHealth(
117
+ new DescribeTargetHealthCommand({ TargetGroupArn: targetGroupArn }),
118
+ );
119
+
120
+ console.log(health.TargetHealthDescriptions?.[0]?.Target.Id);
121
+ // "arn:aws:lambda:eu-west-1:888888888888:function:checkout"
122
+ console.log(health.TargetHealthDescriptions?.[0]?.TargetHealth.State);
123
+ // "healthy"
124
+ ```
125
+
126
+ A `lambda` target group takes exactly one function, as real ELB does, and registering a second is
127
+ refused. An `ip` target group is what a container service registers itself as, takes many addresses,
128
+ and requires the `Protocol` and `Port` its targets are reached on.
129
+
130
+ ```typescript sim-elbv2-ip-target-group
131
+ /**
132
+ * A target group holding addresses, and taking one out again.
133
+ */
134
+
135
+ import {
136
+ CreateTargetGroupCommand,
137
+ DeregisterTargetsCommand,
138
+ DescribeTargetHealthCommand,
139
+ RegisterTargetsCommand,
140
+ } from "@aws-sdk/client-elastic-load-balancing-v2";
141
+
142
+ import { SimAws } from "@kensio/yulin";
143
+
144
+ const simAws = new SimAws();
145
+ const elbV2 = simAws.elbV2();
146
+
147
+ const targetGroup = await elbV2.createTargetGroup(
148
+ new CreateTargetGroupCommand({
149
+ Name: "web-tg",
150
+ TargetType: "ip",
151
+ Protocol: "HTTP",
152
+ Port: 8080,
153
+ }),
154
+ );
155
+
156
+ const targetGroupArn = targetGroup.TargetGroups?.[0]?.TargetGroupArn;
157
+
158
+ await elbV2.registerTargets(
159
+ new RegisterTargetsCommand({
160
+ TargetGroupArn: targetGroupArn,
161
+ // A target naming no port takes the group's, as it does on real ELB.
162
+ Targets: [{ Id: "10.0.1.5" }, { Id: "10.0.1.6", Port: 9090 }],
163
+ }),
164
+ );
165
+
166
+ await elbV2.deregisterTargets(
167
+ new DeregisterTargetsCommand({
168
+ TargetGroupArn: targetGroupArn,
169
+ Targets: [{ Id: "10.0.1.6", Port: 9090 }],
170
+ }),
171
+ );
172
+
173
+ const health = await elbV2.describeTargetHealth(
174
+ new DescribeTargetHealthCommand({ TargetGroupArn: targetGroupArn }),
175
+ );
176
+
177
+ console.log(health.TargetHealthDescriptions?.length); // 1
178
+ console.log(health.TargetHealthDescriptions?.[0]?.Target.Port); // 8080
179
+ ```
180
+
181
+ `TargetType: "instance"` is refused outright, because there are no EC2 instances here for it to mean
182
+ anything about. A group created as one would look configured and route nowhere. A request naming no
183
+ target type at all is refused for the same reason, since real ELB defaults it to `instance`.
184
+
185
+ ## Listeners and the rules on them
186
+
187
+ A listener answers on a port and holds the default actions for a request no rule claims. Rules carry
188
+ a priority, and that is what decides which of several matching rules claims a request. Two rules on
189
+ one listener cannot hold the same priority.
190
+
191
+ ```typescript sim-elbv2-listener-rules
192
+ /**
193
+ * A listener and a rule sending one host name to a different target group.
194
+ */
195
+
196
+ import {
197
+ CreateListenerCommand,
198
+ CreateLoadBalancerCommand,
199
+ CreateRuleCommand,
200
+ CreateTargetGroupCommand,
201
+ DescribeRulesCommand,
202
+ } from "@aws-sdk/client-elastic-load-balancing-v2";
203
+
204
+ import { SimAws } from "@kensio/yulin";
205
+
206
+ const simAws = new SimAws();
207
+ const elbV2 = simAws.elbV2();
208
+
209
+ const loadBalancer = await elbV2.createLoadBalancer(
210
+ new CreateLoadBalancerCommand({ Name: "shop-alb" }),
211
+ );
212
+ const web = await elbV2.createTargetGroup(
213
+ new CreateTargetGroupCommand({
214
+ Name: "web-tg",
215
+ TargetType: "ip",
216
+ Protocol: "HTTP",
217
+ Port: 8080,
218
+ }),
219
+ );
220
+ const admin = await elbV2.createTargetGroup(
221
+ new CreateTargetGroupCommand({ Name: "admin-tg", TargetType: "lambda" }),
222
+ );
223
+
224
+ const listener = await elbV2.createListener(
225
+ new CreateListenerCommand({
226
+ LoadBalancerArn: loadBalancer.LoadBalancers?.[0]?.LoadBalancerArn,
227
+ Protocol: "HTTP",
228
+ Port: 80,
229
+ DefaultActions: [
230
+ {
231
+ Type: "forward",
232
+ TargetGroupArn: web.TargetGroups?.[0]?.TargetGroupArn,
233
+ },
234
+ ],
235
+ }),
236
+ );
237
+
238
+ const listenerArn = listener.Listeners?.[0]?.ListenerArn;
239
+
240
+ await elbV2.createRule(
241
+ new CreateRuleCommand({
242
+ ListenerArn: listenerArn,
243
+ Priority: 10,
244
+ Conditions: [{ Field: "host-header", Values: ["admin.example.com"] }],
245
+ Actions: [
246
+ {
247
+ Type: "forward",
248
+ TargetGroupArn: admin.TargetGroups?.[0]?.TargetGroupArn,
249
+ },
250
+ ],
251
+ }),
252
+ );
253
+
254
+ const rules = await elbV2.describeRules(
255
+ new DescribeRulesCommand({ ListenerArn: listenerArn }),
256
+ );
257
+
258
+ // The listener's rules come back in evaluation order, ending in the default
259
+ // rule, which is the listener's own default actions.
260
+ console.log(rules.Rules?.map((rule) => rule.Priority)); // ["10", "default"]
261
+ ```
262
+
263
+ Conditions are `host-header` and `path-pattern`. What is checked when a rule is written is that it
264
+ could match something. The field has to be one of those two, and it has to have values to compare
265
+ against. A forward action naming a target group that was never created is refused for the same
266
+ reason. The other four condition fields real ELB has are refused at write time, ahead of any
267
+ request. [Matching a request against the rules](#matching-a-request-against-the-rules) covers how
268
+ the values are compared.
269
+
270
+ `ModifyRule` changes a rule's conditions and actions but not its priority, as on real ELB. Moving
271
+ rules about is `SetRulePriorities`, which reorders a whole listener. It judges a request against the
272
+ order it would leave behind, and two rules can therefore swap places in one request.
273
+
274
+ ## HTTPS listeners and certificates
275
+
276
+ A listener on `HTTPS` carries a certificate from simulated ACM. The certificate has to be one that
277
+ exists and has been issued, and one in the load balancer's own account and region, or the listener
278
+ is refused with the reason. That refusal is the point of connecting the two simulations. A test can
279
+ prove that a stack's certificate and its listener line up, ahead of the deploy that would otherwise
280
+ find out.
281
+
282
+ **No TLS is performed here.** Everything travels in the clear, with no handshake and no certificate
283
+ presented to anything. What is simulated is the configuration relationship between a listener and a
284
+ certificate, and the protocol a request is treated as having arrived on.
285
+
286
+ ```typescript sim-elbv2-https-listener
287
+ /**
288
+ * An HTTPS listener holding a certificate simulated ACM issued.
289
+ *
290
+ * No TLS is performed: the certificate is checked and held, and nothing is
291
+ * encrypted or presented to a client.
292
+ */
293
+
294
+ import { RequestCertificateCommand } from "@aws-sdk/client-acm";
295
+ import {
296
+ type Action,
297
+ CreateListenerCommand,
298
+ CreateLoadBalancerCommand,
299
+ CreateTargetGroupCommand,
300
+ } from "@aws-sdk/client-elastic-load-balancing-v2";
301
+
302
+ import { SimAws } from "@kensio/yulin";
303
+
304
+ const simAws = new SimAws();
305
+ const elbV2 = simAws.elbV2();
306
+
307
+ const certificate = await simAws
308
+ .acm()
309
+ .requestCertificate(
310
+ new RequestCertificateCommand({ DomainName: "shop.example.com" }),
311
+ );
312
+
313
+ // A certificate no hosted zone covers issues on its own, once the simulation's
314
+ // background work has run.
315
+ await simAws.backgroundTasksComplete();
316
+
317
+ const loadBalancer = await elbV2.createLoadBalancer(
318
+ new CreateLoadBalancerCommand({ Name: "shop-alb" }),
319
+ );
320
+ const targetGroup = await elbV2.createTargetGroup(
321
+ new CreateTargetGroupCommand({ Name: "checkout-tg", TargetType: "lambda" }),
322
+ );
323
+
324
+ const loadBalancerArn = loadBalancer.LoadBalancers?.[0]?.LoadBalancerArn;
325
+ const forward: Action = {
326
+ Type: "forward",
327
+ TargetGroupArn: targetGroup.TargetGroups?.[0]?.TargetGroupArn,
328
+ };
329
+
330
+ const listener = await elbV2.createListener(
331
+ new CreateListenerCommand({
332
+ LoadBalancerArn: loadBalancerArn,
333
+ Protocol: "HTTPS",
334
+ Port: 443,
335
+ Certificates: [{ CertificateArn: certificate.CertificateArn }],
336
+ DefaultActions: [forward],
337
+ }),
338
+ );
339
+
340
+ // A listener that named no security policy gets the one real ELB gives it.
341
+ console.log(listener.Listeners?.[0]?.SslPolicy); // "ELBSecurityPolicy-2016-08"
342
+
343
+ try {
344
+ await elbV2.createListener(
345
+ new CreateListenerCommand({
346
+ LoadBalancerArn: loadBalancerArn,
347
+ Protocol: "HTTPS",
348
+ Port: 8443,
349
+ Certificates: [
350
+ {
351
+ CertificateArn:
352
+ "arn:aws:acm:us-east-1:888888888888:certificate/00000009",
353
+ },
354
+ ],
355
+ DefaultActions: [forward],
356
+ }),
357
+ );
358
+ } catch (error) {
359
+ // Certificate arn:aws:acm:us-east-1:888888888888:certificate/00000009 was
360
+ // not found in simulated ACM
361
+ console.log((error as Error).message);
362
+ }
363
+ ```
364
+
365
+ A certificate that is still `PENDING_VALIDATION` is refused the same way, since a listener
366
+ presenting one could serve nothing. That is what makes a test of the whole issuance path worth
367
+ writing. The certificate has to have been validated for the listener that uses it to be created.
368
+
369
+ An HTTPS listener with no certificate at all is refused, and so is a listener that names a
370
+ certificate and speaks something other than HTTPS, which would otherwise look configured for HTTPS
371
+ while answering plain HTTP. Moving a listener to HTTP without naming a certificate drops the ones it
372
+ was carrying, since only an HTTPS listener presents a certificate.
373
+
374
+ ### The certificate list
375
+
376
+ A listener's default certificate is the one `CreateListener` and `ModifyListener` name, and it is
377
+ the one a described listener reports. The rest of the list is `AddListenerCertificates`,
378
+ `RemoveListenerCertificates` and `DescribeListenerCertificates`, which are the certificates a real
379
+ listener would choose between by the host name a client asked for.
380
+
381
+ ```typescript sim-elbv2-listener-certificates
382
+ /**
383
+ * The certificates a listener carries beyond its default one.
384
+ */
385
+
386
+ import { RequestCertificateCommand } from "@aws-sdk/client-acm";
387
+ import {
388
+ AddListenerCertificatesCommand,
389
+ CreateListenerCommand,
390
+ CreateLoadBalancerCommand,
391
+ CreateTargetGroupCommand,
392
+ DescribeListenerCertificatesCommand,
393
+ RemoveListenerCertificatesCommand,
394
+ } from "@aws-sdk/client-elastic-load-balancing-v2";
395
+
396
+ import { SimAws } from "@kensio/yulin";
397
+
398
+ const simAws = new SimAws();
399
+ const acm = simAws.acm();
400
+ const elbV2 = simAws.elbV2();
401
+
402
+ async function issuedCertificateArn(domainName: string): Promise<string> {
403
+ const requested = await acm.requestCertificate(
404
+ new RequestCertificateCommand({ DomainName: domainName }),
405
+ );
406
+
407
+ await simAws.backgroundTasksComplete();
408
+
409
+ return requested.CertificateArn ?? "";
410
+ }
411
+
412
+ const shop = await issuedCertificateArn("shop.example.com");
413
+ const admin = await issuedCertificateArn("admin.example.com");
414
+
415
+ const loadBalancer = await elbV2.createLoadBalancer(
416
+ new CreateLoadBalancerCommand({ Name: "shop-alb" }),
417
+ );
418
+ const targetGroup = await elbV2.createTargetGroup(
419
+ new CreateTargetGroupCommand({ Name: "checkout-tg", TargetType: "lambda" }),
420
+ );
421
+
422
+ const listener = await elbV2.createListener(
423
+ new CreateListenerCommand({
424
+ LoadBalancerArn: loadBalancer.LoadBalancers?.[0]?.LoadBalancerArn,
425
+ Protocol: "HTTPS",
426
+ Port: 443,
427
+ Certificates: [{ CertificateArn: shop }],
428
+ DefaultActions: [
429
+ {
430
+ Type: "forward",
431
+ TargetGroupArn: targetGroup.TargetGroups?.[0]?.TargetGroupArn,
432
+ },
433
+ ],
434
+ }),
435
+ );
436
+
437
+ const listenerArn = listener.Listeners?.[0]?.ListenerArn;
438
+
439
+ await elbV2.addListenerCertificates(
440
+ new AddListenerCertificatesCommand({
441
+ ListenerArn: listenerArn,
442
+ Certificates: [{ CertificateArn: admin }],
443
+ }),
444
+ );
445
+
446
+ const carried = await elbV2.describeListenerCertificates(
447
+ new DescribeListenerCertificatesCommand({ ListenerArn: listenerArn }),
448
+ );
449
+
450
+ // The default certificate comes first and is the only one flagged as such.
451
+ console.log(carried.Certificates?.map((each) => each.IsDefault)); // [true, false]
452
+
453
+ await elbV2.removeListenerCertificates(
454
+ new RemoveListenerCertificatesCommand({
455
+ ListenerArn: listenerArn,
456
+ Certificates: [{ CertificateArn: admin }],
457
+ }),
458
+ );
459
+ ```
460
+
461
+ The default certificate cannot be removed this way. Replacing it is `ModifyListener`, as on real
462
+ ELB, and trying to remove it is refused, naming it.
463
+
464
+ ### Serving a request over HTTPS
465
+
466
+ A request to `https://<dns-name>/orders` reaches the listener on port 443, and from there it is the
467
+ same request as any other. The same rules are evaluated in the same order, and the same target
468
+ groups answer. The listener's protocol is what the target is told the request arrived on, and a
469
+ function behind an HTTPS listener sees `x-forwarded-proto: https` and `x-forwarded-port: 443`.
470
+
471
+ Because no TLS happens, the URL scheme goes unchecked against the listener it reaches. What decides
472
+ the listener is the port, and what decides the protocol in the event is the listener. A test can
473
+ therefore conclude that a request treated as arriving over HTTPS is routed and forwarded the way the
474
+ configuration says. Certificates, ciphers and handshakes stay out of reach.
475
+
476
+ ## Carrying a request to a Lambda function
477
+
478
+ `simElbV2Fetch` sends a request to whichever load balancer its host name names, in process and
479
+ without a socket. The port in the URL is the listener's, and `http://<dns-name>/orders` reaches the
480
+ listener on port 80.
481
+
482
+ The listener then evaluates its rules, and the first one to claim the request says what happens to
483
+ it. A request no rule claims is answered by the listener's default action. A `forward` action sends
484
+ the request to its target group, and a `lambda` target group invokes the function registered in it.
485
+
486
+ ```typescript sim-elbv2-serve-lambda-target
487
+ /**
488
+ * A request carried through a load balancer to a Lambda function.
489
+ */
490
+
491
+ import {
492
+ CreateListenerCommand,
493
+ CreateLoadBalancerCommand,
494
+ CreateTargetGroupCommand,
495
+ RegisterTargetsCommand,
496
+ } from "@aws-sdk/client-elastic-load-balancing-v2";
497
+ import {
498
+ AddPermissionCommand,
499
+ CreateFunctionCommand,
500
+ } from "@aws-sdk/client-lambda";
501
+
502
+ import { SimAws } from "@kensio/yulin";
503
+ import type { SimElbV2Event, SimElbV2Result } from "@kensio/yulin/elbv2";
504
+ import { simElbV2Fetch } from "@kensio/yulin/elbv2";
505
+ import { makeLambdaZipFileInput } from "@kensio/yulin/lambda";
506
+
507
+ const simAws = new SimAws();
508
+ const region = simAws.account("888888888888").region("eu-west-1");
509
+
510
+ const created = await region.lambda().createFunction(
511
+ new CreateFunctionCommand({
512
+ FunctionName: "checkout",
513
+ Role: "arn:aws:iam::888888888888:role/CheckoutRole",
514
+ Code: {
515
+ ZipFile: makeLambdaZipFileInput(
516
+ (event: SimElbV2Event): SimElbV2Result => ({
517
+ statusCode: 200,
518
+ statusDescription: "200 OK",
519
+ headers: { "content-type": "application/json" },
520
+ body: JSON.stringify({ path: event.path, method: event.httpMethod }),
521
+ isBase64Encoded: false,
522
+ }),
523
+ ),
524
+ },
525
+ }),
526
+ );
527
+
528
+ const elbV2 = region.elbV2();
529
+
530
+ const loadBalancer = await elbV2.createLoadBalancer(
531
+ new CreateLoadBalancerCommand({ Name: "shop-alb" }),
532
+ );
533
+ const targetGroup = await elbV2.createTargetGroup(
534
+ new CreateTargetGroupCommand({ Name: "checkout-tg", TargetType: "lambda" }),
535
+ );
536
+
537
+ const targetGroupArn = targetGroup.TargetGroups?.[0]?.TargetGroupArn;
538
+
539
+ // Without this the load balancer cannot invoke the function, and every request
540
+ // gets a 502.
541
+ await region.lambda().addPermission(
542
+ new AddPermissionCommand({
543
+ FunctionName: "checkout",
544
+ StatementId: "elb-invoke",
545
+ Action: "lambda:InvokeFunction",
546
+ Principal: "elasticloadbalancing.amazonaws.com",
547
+ SourceArn: targetGroupArn,
548
+ }),
549
+ );
550
+
551
+ await elbV2.registerTargets(
552
+ new RegisterTargetsCommand({
553
+ TargetGroupArn: targetGroupArn,
554
+ Targets: [{ Id: created.FunctionArn }],
555
+ }),
556
+ );
557
+
558
+ await elbV2.createListener(
559
+ new CreateListenerCommand({
560
+ LoadBalancerArn: loadBalancer.LoadBalancers?.[0]?.LoadBalancerArn,
561
+ Protocol: "HTTP",
562
+ Port: 80,
563
+ DefaultActions: [{ Type: "forward", TargetGroupArn: targetGroupArn }],
564
+ }),
565
+ );
566
+
567
+ const dnsName = loadBalancer.LoadBalancers?.[0]?.DNSName;
568
+
569
+ const response = await simElbV2Fetch(simAws, `http://${dnsName}/orders`);
570
+
571
+ console.log(response.status); // 200
572
+ console.log(response.statusText); // "OK"
573
+ console.log(await response.json()); // { path: "/orders", method: "GET" }
574
+ ```
575
+
576
+ ### Matching a request against the rules
577
+
578
+ Rules are evaluated in priority order, lowest number first, and the first one whose conditions all
579
+ hold claims the request. A rule later in the order that would also have matched never sees it. A
580
+ request no rule claims falls through to the listener's default action.
581
+
582
+ A rule with more than one condition claims a request only when every one of them holds. Within one
583
+ condition, a list of values is satisfied when any one of them matches.
584
+
585
+ Both fields support the two wildcards real ELB has, `*` for zero or more characters and `?` for
586
+ exactly one, and both compare the pattern against the whole value. Neither looks for the pattern
587
+ inside a longer value. That last part is the one worth knowing, because a pattern can read as though
588
+ it covers more than it does:
589
+
590
+ - `/api/*` claims `/api/orders` and `/api/v1/orders`, and leaves `/api` alone. The pattern has a
591
+ slash the bare path lacks. Real ELB behaves the same way, and a rule meant to cover both is
592
+ written as `["/api", "/api/*"]`.
593
+ - `*.example.com` claims `admin.example.com` and `a.b.example.com`, and leaves `example.com` alone,
594
+ for the same reason.
595
+ - `*` covers slashes and dots, so `/api/*` claims paths any number of segments deep.
596
+
597
+ A path pattern is compared with regard to case and a host name without, as on real ELB. A path
598
+ pattern is compared against the path alone, and a query string plays no part in it.
599
+
600
+ ```typescript sim-elbv2-serve-listener-rules
601
+ /**
602
+ * An application split across two services by path, with the rules matched
603
+ * when a request arrives.
604
+ */
605
+
606
+ import {
607
+ CreateListenerCommand,
608
+ CreateLoadBalancerCommand,
609
+ CreateRuleCommand,
610
+ CreateTargetGroupCommand,
611
+ RegisterTargetsCommand,
612
+ } from "@aws-sdk/client-elastic-load-balancing-v2";
613
+ import {
614
+ AddPermissionCommand,
615
+ CreateFunctionCommand,
616
+ } from "@aws-sdk/client-lambda";
617
+
618
+ import { SimAws } from "@kensio/yulin";
619
+ import type { SimElbV2Result } from "@kensio/yulin/elbv2";
620
+ import { simElbV2Fetch, simElbV2ServicePrincipal } from "@kensio/yulin/elbv2";
621
+ import { makeLambdaZipFileInput } from "@kensio/yulin/lambda";
622
+
623
+ const simAws = new SimAws();
624
+ const elbV2 = simAws.elbV2();
625
+ const lambda = simAws.lambda();
626
+
627
+ /**
628
+ * Create a function answering with its own name, a target group holding it,
629
+ * and the permission the load balancer needs to invoke it.
630
+ */
631
+ async function makeTargetGroup(name: string): Promise<string> {
632
+ const created = await lambda.createFunction(
633
+ new CreateFunctionCommand({
634
+ FunctionName: name,
635
+ Role: `arn:aws:iam::888888888888:role/${name}-role`,
636
+ Code: {
637
+ ZipFile: makeLambdaZipFileInput((): SimElbV2Result => ({
638
+ statusCode: 200,
639
+ body: name,
640
+ })),
641
+ },
642
+ }),
643
+ );
644
+
645
+ const group = await elbV2.createTargetGroup(
646
+ new CreateTargetGroupCommand({ Name: `${name}-tg`, TargetType: "lambda" }),
647
+ );
648
+ const groupArn = group.TargetGroups?.[0]?.TargetGroupArn;
649
+
650
+ await lambda.addPermission(
651
+ new AddPermissionCommand({
652
+ FunctionName: name,
653
+ StatementId: "elb-invoke",
654
+ Action: "lambda:InvokeFunction",
655
+ Principal: simElbV2ServicePrincipal,
656
+ SourceArn: groupArn,
657
+ }),
658
+ );
659
+ await elbV2.registerTargets(
660
+ new RegisterTargetsCommand({
661
+ TargetGroupArn: groupArn,
662
+ Targets: [{ Id: created.FunctionArn }],
663
+ }),
664
+ );
665
+
666
+ return groupArn ?? "";
667
+ }
668
+
669
+ const web = await makeTargetGroup("web");
670
+ const api = await makeTargetGroup("api");
671
+
672
+ const loadBalancer = await elbV2.createLoadBalancer(
673
+ new CreateLoadBalancerCommand({ Name: "shop-alb" }),
674
+ );
675
+ const listener = await elbV2.createListener(
676
+ new CreateListenerCommand({
677
+ LoadBalancerArn: loadBalancer.LoadBalancers?.[0]?.LoadBalancerArn,
678
+ Protocol: "HTTP",
679
+ Port: 80,
680
+ DefaultActions: [{ Type: "forward", TargetGroupArn: web }],
681
+ }),
682
+ );
683
+
684
+ await elbV2.createRule(
685
+ new CreateRuleCommand({
686
+ ListenerArn: listener.Listeners?.[0]?.ListenerArn,
687
+ Priority: 10,
688
+ Conditions: [{ Field: "path-pattern", Values: ["/api/*"] }],
689
+ Actions: [{ Type: "forward", TargetGroupArn: api }],
690
+ }),
691
+ );
692
+
693
+ const dnsName = loadBalancer.LoadBalancers?.[0]?.DNSName ?? "";
694
+
695
+ const toApi = await simElbV2Fetch(simAws, `http://${dnsName}/api/orders`);
696
+ console.log(await toApi.text()); // "api"
697
+
698
+ // The pattern has a slash the bare path does not, so this request is not one
699
+ // the rule claims, and the listener's default action answers it.
700
+ const toWeb = await simElbV2Fetch(simAws, `http://${dnsName}/api`);
701
+ console.log(await toWeb.text()); // "web"
702
+ ```
703
+
704
+ A `host-header` condition is matched against the request's Host header, falling back to the host
705
+ name in the URL when the request carries none. On real AWS those are the same thing, since DNS is
706
+ what brought the request to the load balancer. Here a request reaches one at its own DNS name, so
707
+ sending a Host header is how a test says which name the client asked for. Any port in the header is
708
+ left out of the comparison, since a condition value cannot carry one.
709
+
710
+ ### Answering without a target
711
+
712
+ A `fixed-response` action answers with the status, content type and body it holds, and a `redirect`
713
+ action answers with a status and a `Location`. Neither touches a target group, and a listener
714
+ holding one serves with nothing registered behind it.
715
+
716
+ ```typescript sim-elbv2-serve-fixed-response
717
+ /**
718
+ * A health endpoint and an HTTP to HTTPS redirect, neither of which needs a
719
+ * target group.
720
+ */
721
+
722
+ import {
723
+ CreateListenerCommand,
724
+ CreateLoadBalancerCommand,
725
+ CreateRuleCommand,
726
+ } from "@aws-sdk/client-elastic-load-balancing-v2";
727
+
728
+ import { SimAws } from "@kensio/yulin";
729
+ import { simElbV2Fetch } from "@kensio/yulin/elbv2";
730
+
731
+ const simAws = new SimAws();
732
+ const elbV2 = simAws.elbV2();
733
+
734
+ const loadBalancer = await elbV2.createLoadBalancer(
735
+ new CreateLoadBalancerCommand({ Name: "shop-alb" }),
736
+ );
737
+
738
+ // Nothing is registered behind this listener, and nothing needs to be: both
739
+ // actions are answered by the load balancer itself.
740
+ const listener = await elbV2.createListener(
741
+ new CreateListenerCommand({
742
+ LoadBalancerArn: loadBalancer.LoadBalancers?.[0]?.LoadBalancerArn,
743
+ Protocol: "HTTP",
744
+ Port: 80,
745
+ DefaultActions: [
746
+ {
747
+ Type: "redirect",
748
+ RedirectConfig: {
749
+ Protocol: "HTTPS",
750
+ Port: "443",
751
+ StatusCode: "HTTP_301",
752
+ },
753
+ },
754
+ ],
755
+ }),
756
+ );
757
+
758
+ await elbV2.createRule(
759
+ new CreateRuleCommand({
760
+ ListenerArn: listener.Listeners?.[0]?.ListenerArn,
761
+ Priority: 10,
762
+ Conditions: [{ Field: "path-pattern", Values: ["/health"] }],
763
+ Actions: [
764
+ {
765
+ Type: "fixed-response",
766
+ FixedResponseConfig: {
767
+ StatusCode: "200",
768
+ ContentType: "application/json",
769
+ MessageBody: '{"ok":true}',
770
+ },
771
+ },
772
+ ],
773
+ }),
774
+ );
775
+
776
+ const dnsName = loadBalancer.LoadBalancers?.[0]?.DNSName ?? "";
777
+
778
+ const health = await simElbV2Fetch(simAws, `http://${dnsName}/health`);
779
+ console.log(health.status); // 200
780
+ console.log(await health.text()); // '{"ok":true}'
781
+
782
+ const redirected = await simElbV2Fetch(simAws, `http://${dnsName}/orders`, {
783
+ headers: { host: "shop.example.com" },
784
+ });
785
+ console.log(redirected.status); // 301
786
+ console.log(redirected.headers.get("location"));
787
+ // "https://shop.example.com:443/orders"
788
+ ```
789
+
790
+ A redirect keeps every component it leaves unnamed, and the five reserved keywords put a component
791
+ back where one is named. They are `#{protocol}`, `#{host}`, `#{port}`, `#{path}` and `#{query}`.
792
+ `#{path}` comes without its leading slash, and a redirect keeping the path writes it as `/#{path}`.
793
+ `#{query}` comes without its leading question mark, which the load balancer adds. A redirect that
794
+ changes none of the protocol, host, port or path is refused when it is written, since it would
795
+ redirect to the request's own URI.
796
+
797
+ The port is always in the `Location`, including when it is the protocol's own. A redirect to HTTPS
798
+ on 443 therefore answers with a `Location` ending `:443`, as real ELB sends it.
799
+
800
+ ### The event and the response
801
+
802
+ The event is ELB's own shape, distinct from both API Gateway payload formats. A handler can tell
803
+ them apart by the request context. An ALB event's request context holds one `elb` block, carrying
804
+ the target group ARN.
805
+
806
+ ```typescript
807
+ {
808
+ requestContext: { elb: { targetGroupArn: "arn:aws:elasticloadbalancing:..." } },
809
+ httpMethod: "GET",
810
+ path: "/orders",
811
+ queryStringParameters: { page: "2" },
812
+ headers: { host: "shop-alb-0000000001.eu-west-1.elb.amazonaws.com", ... },
813
+ body: "",
814
+ isBase64Encoded: false,
815
+ }
816
+ ```
817
+
818
+ Every field is always there. A request with no query string carries an empty
819
+ `queryStringParameters`, and one with no body carries an empty `body`. Cookies stay in the `cookie`
820
+ header they arrived in, and never move into a field of their own. Query string values arrive as they
821
+ were sent, since real ELB leaves percent escapes for the function to decode.
822
+
823
+ The load balancer writes `host`, `x-amzn-trace-id`, `x-forwarded-port` and `x-forwarded-proto`
824
+ itself, and overwrites whatever a client sent under those names. `x-forwarded-for` is the exception.
825
+ The client's address is appended to what the request already carried. That is what makes that header
826
+ a chain of proxies.
827
+
828
+ A body is passed through as text for `text/*`, `application/json`, `application/javascript` and
829
+ `application/xml`, and base64 encoded otherwise, with `isBase64Encoded` saying which happened. A
830
+ request carrying a `content-encoding` header is always base64. That list is shorter than API
831
+ Gateway's, and a form post is text to API Gateway and base64 to a load balancer. A body called text
832
+ that turns out to be invalid UTF-8 fails the invocation, ahead of any handler seeing replacement
833
+ characters.
834
+
835
+ The response has to carry a `statusCode`. `statusDescription`, `headers`, `body` and
836
+ `isBase64Encoded` are all optional, and a `statusDescription` of `200 OK` becomes the reason phrase
837
+ `OK`, since the status line already has the code. Hop-by-hop headers and `content-length` are
838
+ dropped, because the load balancer writes those itself.
839
+
840
+ ### What a load balancer answers itself
841
+
842
+ ```typescript sim-elbv2-serve-errors
843
+ /**
844
+ * What a load balancer answers when its target cannot serve the request.
845
+ */
846
+
847
+ import {
848
+ CreateListenerCommand,
849
+ CreateLoadBalancerCommand,
850
+ CreateTargetGroupCommand,
851
+ RegisterTargetsCommand,
852
+ } from "@aws-sdk/client-elastic-load-balancing-v2";
853
+ import {
854
+ AddPermissionCommand,
855
+ CreateFunctionCommand,
856
+ } from "@aws-sdk/client-lambda";
857
+
858
+ import { SimAws } from "@kensio/yulin";
859
+ import { simElbV2Fetch } from "@kensio/yulin/elbv2";
860
+ import { makeLambdaZipFileInput } from "@kensio/yulin/lambda";
861
+
862
+ const simAws = new SimAws();
863
+ const simLambda = simAws.lambda();
864
+ const elbV2 = simAws.elbV2();
865
+
866
+ const created = await simLambda.createFunction(
867
+ new CreateFunctionCommand({
868
+ FunctionName: "checkout",
869
+ Role: "arn:aws:iam::888888888888:role/CheckoutRole",
870
+ // A handler written for an API Gateway proxy integration, which returns no
871
+ // status code of its own.
872
+ Code: { ZipFile: makeLambdaZipFileInput(() => ({ body: "checkout" })) },
873
+ }),
874
+ );
875
+
876
+ const loadBalancer = await elbV2.createLoadBalancer(
877
+ new CreateLoadBalancerCommand({ Name: "shop-alb" }),
878
+ );
879
+ const targetGroup = await elbV2.createTargetGroup(
880
+ new CreateTargetGroupCommand({ Name: "checkout-tg", TargetType: "lambda" }),
881
+ );
882
+
883
+ const targetGroupArn = targetGroup.TargetGroups?.[0]?.TargetGroupArn;
884
+ const dnsName = loadBalancer.LoadBalancers?.[0]?.DNSName;
885
+
886
+ await elbV2.createListener(
887
+ new CreateListenerCommand({
888
+ LoadBalancerArn: loadBalancer.LoadBalancers?.[0]?.LoadBalancerArn,
889
+ Protocol: "HTTP",
890
+ Port: 80,
891
+ DefaultActions: [{ Type: "forward", TargetGroupArn: targetGroupArn }],
892
+ }),
893
+ );
894
+
895
+ // Nothing is registered yet, so there is no target to send the request to.
896
+ const empty = await simElbV2Fetch(simAws, `http://${dnsName}/orders`);
897
+
898
+ console.log(empty.status); // 503
899
+
900
+ await simLambda.addPermission(
901
+ new AddPermissionCommand({
902
+ FunctionName: "checkout",
903
+ StatementId: "elb-invoke",
904
+ Action: "lambda:InvokeFunction",
905
+ Principal: "elasticloadbalancing.amazonaws.com",
906
+ SourceArn: targetGroupArn,
907
+ }),
908
+ );
909
+ await elbV2.registerTargets(
910
+ new RegisterTargetsCommand({
911
+ TargetGroupArn: targetGroupArn,
912
+ Targets: [{ Id: created.FunctionArn }],
913
+ }),
914
+ );
915
+
916
+ // The function runs now, and what it returns is not a response ELB can send.
917
+ const malformed = await simElbV2Fetch(simAws, `http://${dnsName}/orders`);
918
+
919
+ console.log(malformed.status); // 502
920
+ ```
921
+
922
+ A 503 means there was no target to send the request to, such as an empty target group.
923
+
924
+ A 502 means there was a target and it failed to produce a response. A missing invoke permission, a
925
+ function that was never created, a handler that threw, a result with no usable `statusCode`, and a
926
+ response over 1 MB are all 502, as they are on real ELB, where the difference between them shows up
927
+ only in the load balancer's own logs. The response limit is on the whole response document, and a
928
+ base64 body counts at its encoded size.
929
+
930
+ A request body over 1 MB is a 413, the limit on what real ELB sends to a Lambda target.
931
+
932
+ A host name no load balancer answers on, and a port no listener holds, both throw a
933
+ `SimElbV2ConnectionRefusedError`. Neither reaches a load balancer on real AWS either. One resolves
934
+ to nothing and the other refuses the connection, and there is no status for either.
935
+
936
+ ### The invoke permission
937
+
938
+ A load balancer invokes a Lambda function through the function's resource-based policy, exactly as
939
+ real ELB does. The grant names `elasticloadbalancing.amazonaws.com` as the principal and the target
940
+ group as the source ARN, and the load balancer supplies the target group's own Account as the source
941
+ Account. A policy written with the `aws:SourceAccount` condition the ELB documentation recommends
942
+ therefore matches.
943
+
944
+ Forgetting the grant is a common way to end up with a load balancer that looks configured and serves
945
+ nothing but 502s. Real ELB refuses to register a Lambda target at all until the permission is there.
946
+ Here the permission is checked when the request arrives. A target group can be built in any order,
947
+ and a policy that is later removed stops the requests.
948
+
949
+ ## Carrying a request to an ECS service
950
+
951
+ An `ip` target group is answered by the simulated [ECS](https://yulinsim.dev/services/ecs/) service registered into it. A
952
+ service declares `loadBalancers` naming a target group, a container and a container port. Each task
953
+ it keeps running is registered into that group as an address, and a request forwarded there reaches
954
+ the handler bound to the service's container.
955
+
956
+ That is the whole path an application takes. A client asks for a name, Route53 resolves it to the
957
+ load balancer, a rule picks the target group, and the container's own code answers. This is a stack
958
+ deployed from a template, which is how one usually arrives.
959
+
960
+ ```typescript sim-elbv2-serve-ecs-service
961
+ /**
962
+ * A request reaching an ECS service's container through a load balancer.
963
+ */
964
+
965
+ import {
966
+ DynamoDBClient,
967
+ GetItemCommand,
968
+ PutItemCommand,
969
+ } from "@aws-sdk/client-dynamodb";
970
+
971
+ import { SimSdk } from "@kensio/yulin/sdk";
972
+ import { SimAwsHttp } from "@kensio/yulin/serve";
973
+
974
+ using simSdk = new SimSdk();
975
+ const { simAws } = simSdk;
976
+
977
+ // The application's own SDK clients, intercepted as they would be in any test.
978
+ simSdk.intercept(DynamoDBClient);
979
+
980
+ const stack = await simAws.cloudFormation().deployTemplate({
981
+ stackName: "orders",
982
+ template: {
983
+ Resources: {
984
+ OrdersTable: {
985
+ Type: "AWS::DynamoDB::Table",
986
+ Properties: {
987
+ TableName: "orders",
988
+ BillingMode: "PAY_PER_REQUEST",
989
+ AttributeDefinitions: [
990
+ { AttributeName: "orderId", AttributeType: "S" },
991
+ ],
992
+ KeySchema: [{ AttributeName: "orderId", KeyType: "HASH" }],
993
+ },
994
+ },
995
+ OrdersTaskRole: {
996
+ Type: "AWS::IAM::Role",
997
+ Properties: {
998
+ RoleName: "OrdersTaskRole",
999
+ AssumeRolePolicyDocument: {
1000
+ Version: "2012-10-17",
1001
+ Statement: [
1002
+ {
1003
+ Effect: "Allow",
1004
+ Principal: { Service: "ecs-tasks.amazonaws.com" },
1005
+ Action: "sts:AssumeRole",
1006
+ },
1007
+ ],
1008
+ },
1009
+ Policies: [
1010
+ {
1011
+ PolicyName: "ReadWriteOrders",
1012
+ PolicyDocument: {
1013
+ Version: "2012-10-17",
1014
+ Statement: [
1015
+ {
1016
+ Effect: "Allow",
1017
+ Action: ["dynamodb:PutItem", "dynamodb:GetItem"],
1018
+ Resource: { "Fn::GetAtt": ["OrdersTable", "Arn"] },
1019
+ },
1020
+ ],
1021
+ },
1022
+ },
1023
+ ],
1024
+ },
1025
+ },
1026
+ OrdersAlb: {
1027
+ Type: "AWS::ElasticLoadBalancingV2::LoadBalancer",
1028
+ Properties: { Name: "orders-alb", Scheme: "internet-facing" },
1029
+ },
1030
+ OrdersTargetGroup: {
1031
+ Type: "AWS::ElasticLoadBalancingV2::TargetGroup",
1032
+ Properties: {
1033
+ Name: "orders-tg",
1034
+ TargetType: "ip",
1035
+ Protocol: "HTTP",
1036
+ Port: 80,
1037
+ },
1038
+ },
1039
+ HttpListener: {
1040
+ Type: "AWS::ElasticLoadBalancingV2::Listener",
1041
+ Properties: {
1042
+ LoadBalancerArn: { Ref: "OrdersAlb" },
1043
+ Protocol: "HTTP",
1044
+ Port: 80,
1045
+ DefaultActions: [
1046
+ { Type: "forward", TargetGroupArn: { Ref: "OrdersTargetGroup" } },
1047
+ ],
1048
+ },
1049
+ },
1050
+ OrdersZone: {
1051
+ Type: "AWS::Route53::HostedZone",
1052
+ Properties: { Name: "example.test" },
1053
+ },
1054
+ OrdersRecord: {
1055
+ Type: "AWS::Route53::RecordSet",
1056
+ Properties: {
1057
+ HostedZoneId: { Ref: "OrdersZone" },
1058
+ Name: "orders.example.test",
1059
+ Type: "A",
1060
+ AliasTarget: {
1061
+ DNSName: { "Fn::GetAtt": ["OrdersAlb", "DNSName"] },
1062
+ HostedZoneId: {
1063
+ "Fn::GetAtt": ["OrdersAlb", "CanonicalHostedZoneID"],
1064
+ },
1065
+ },
1066
+ },
1067
+ },
1068
+ OrdersCluster: {
1069
+ Type: "AWS::ECS::Cluster",
1070
+ Properties: { ClusterName: "orders" },
1071
+ },
1072
+ OrdersTaskDefinition: {
1073
+ Type: "AWS::ECS::TaskDefinition",
1074
+ Properties: {
1075
+ Family: "orders-api",
1076
+ NetworkMode: "awsvpc",
1077
+ TaskRoleArn: { "Fn::GetAtt": ["OrdersTaskRole", "Arn"] },
1078
+ ContainerDefinitions: [
1079
+ // The proxy the service registers, which Yulin has nothing to run.
1080
+ {
1081
+ Name: "nginx",
1082
+ Image: "public.ecr.aws/nginx/nginx:1.27",
1083
+ PortMappings: [{ ContainerPort: 80 }],
1084
+ },
1085
+ {
1086
+ Name: "app",
1087
+ Image: "example.dkr.ecr.eu-west-2.amazonaws.com/orders-api:1",
1088
+ PortMappings: [{ ContainerPort: 8080 }],
1089
+ Environment: [{ Name: "ORDERS_TABLE", Value: "orders" }],
1090
+ },
1091
+ ],
1092
+ },
1093
+ },
1094
+ OrdersService: {
1095
+ Type: "AWS::ECS::Service",
1096
+ Properties: {
1097
+ ServiceName: "orders-api",
1098
+ Cluster: { Ref: "OrdersCluster" },
1099
+ TaskDefinition: { Ref: "OrdersTaskDefinition" },
1100
+ DesiredCount: 2,
1101
+ LaunchType: "FARGATE",
1102
+ LoadBalancers: [
1103
+ {
1104
+ TargetGroupArn: { Ref: "OrdersTargetGroup" },
1105
+ ContainerName: "nginx",
1106
+ ContainerPort: 80,
1107
+ },
1108
+ ],
1109
+ },
1110
+ },
1111
+ },
1112
+ },
1113
+ bindings: [
1114
+ {
1115
+ logicalId: "OrdersTaskDefinition",
1116
+ containerName: "app",
1117
+ http: async (request: Request): Promise<Response> => {
1118
+ const dynamoDb = new DynamoDBClient({});
1119
+ const orderId = new URL(request.url).pathname.split("/").at(-1) ?? "";
1120
+
1121
+ if (request.method === "POST") {
1122
+ await dynamoDb.send(
1123
+ new PutItemCommand({
1124
+ TableName: process.env["ORDERS_TABLE"],
1125
+ Item: { orderId: { S: orderId }, item: { S: "flat white" } },
1126
+ }),
1127
+ );
1128
+
1129
+ return new Response("", { status: 201 });
1130
+ }
1131
+
1132
+ const read = await dynamoDb.send(
1133
+ new GetItemCommand({
1134
+ TableName: process.env["ORDERS_TABLE"],
1135
+ Key: { orderId: { S: orderId } },
1136
+ }),
1137
+ );
1138
+
1139
+ return Response.json({ item: read.Item?.["item"]?.S ?? null });
1140
+ },
1141
+ },
1142
+ ],
1143
+ });
1144
+
1145
+ await stack.waitForDeployComplete();
1146
+ await simAws.backgroundTasksComplete();
1147
+
1148
+ // The name Route53 answers for, reached in process rather than over a socket.
1149
+ const client = new SimAwsHttp({ simAws });
1150
+ const url = "http://orders.example.test.sim-aws.localhost:52341/orders/42";
1151
+
1152
+ const placed = await client.fetch(url, { method: "POST" });
1153
+
1154
+ console.log(placed.status); // 201
1155
+
1156
+ const read = await client.fetch(url);
1157
+
1158
+ console.log(await read.json()); // { item: "flat white" }
1159
+ ```
1160
+
1161
+ The container's own AWS calls are authorized as the task role the task definition declared. A policy
1162
+ that would break the deployed service breaks the test. Everything in the handler is ordinary
1163
+ application code, with an SDK client, `process.env`, a route and a response.
1164
+
1165
+ ### What the container is given
1166
+
1167
+ The request is the one the client made, with the headers a load balancer writes in front of a
1168
+ target. Those are the `host` the client asked for, `x-forwarded-for`, `x-forwarded-proto`,
1169
+ `x-forwarded-port` and `x-amzn-trace-id`. Its URL is the AWS-facing one, carrying the listener's
1170
+ scheme and port. A container reading `request.url` sees the name a client asked for, and never the
1171
+ localhost one a served request arrived at. A Lambda target behind the same listener gets the same
1172
+ values in its event, since both are written by the same rules.
1173
+
1174
+ ### Which container answers, and what a 503 means
1175
+
1176
+ Which container of a task a request reaches is a deliberate divergence, documented in full under
1177
+ [the ECS service docs](https://yulinsim.dev/services/ecs/#which-container-of-a-task-answers). In short, real ECS routes to the
1178
+ container the registration names on the port it names, the common real task puts an unsimulated
1179
+ proxy on that port, and the request here goes to a container that is bound.
1180
+
1181
+ Three things are all the same 503 real ELB answers when no target is in service:
1182
+
1183
+ - a target group with nothing registered in it
1184
+ - a target group whose registered service has no container bound to an HTTP handler
1185
+ - an address registered by hand, since only an ECS service registration puts something behind an
1186
+ address in this simulation
1187
+
1188
+ A container whose handler throws is a 502, as a Lambda target that throws is, and so is one
1189
+ answering with something other than a `Response`. The error goes no further than the load balancer,
1190
+ as it goes no further on real AWS.
1191
+
1192
+ ## Reaching a load balancer by name
1193
+
1194
+ A load balancer's DNS name resolves through simulated [Route53](https://yulinsim.dev/services/route53/), and a record pointing
1195
+ at it reaches its listeners and rules. An alias record is the usual way, and a CNAME below the apex
1196
+ works too, the same as on real AWS. The record's value is `DNSName` exactly as a describe reported
1197
+ it.
1198
+
1199
+ A `host-header` condition then sees the name the request was made to, and never the load balancer's
1200
+ own. A rule on `api.example.test` claims a request that a Route53 record for `api.example.test`
1201
+ brought to the load balancer. Host-based routing and DNS agree, and a stack with one load balancer
1202
+ behind several names behaves here as it does deployed.
1203
+
1204
+ Under `serveSimAws` the same name is served over real localhost HTTP, and a DNS lookup for it
1205
+ answers with the address the local server listens on. A request made under the Yulin-local suffix
1206
+ reaches the listener on port 80, since the port such a request carries belongs to the local server
1207
+ and not to a client's choice.
1208
+
1209
+ ```typescript sim-elbv2-route53-alias
1210
+ /**
1211
+ * Reaching a load balancer through the Route53 name pointing at it.
1212
+ */
1213
+
1214
+ import {
1215
+ CreateListenerCommand,
1216
+ CreateLoadBalancerCommand,
1217
+ CreateRuleCommand,
1218
+ } from "@aws-sdk/client-elastic-load-balancing-v2";
1219
+ import {
1220
+ ChangeResourceRecordSetsCommand,
1221
+ CreateHostedZoneCommand,
1222
+ } from "@aws-sdk/client-route-53";
1223
+
1224
+ import { SimAws } from "@kensio/yulin";
1225
+ import { serveSimAws } from "@kensio/yulin/serve";
1226
+
1227
+ const simAws = new SimAws();
1228
+ const elbV2 = simAws.elbV2();
1229
+
1230
+ const created = await elbV2.createLoadBalancer(
1231
+ new CreateLoadBalancerCommand({ Name: "shop-alb" }),
1232
+ );
1233
+
1234
+ const loadBalancerArn = created.LoadBalancers?.[0]?.LoadBalancerArn;
1235
+ const dnsName = created.LoadBalancers?.[0]?.DNSName;
1236
+
1237
+ const listener = await elbV2.createListener(
1238
+ new CreateListenerCommand({
1239
+ LoadBalancerArn: loadBalancerArn,
1240
+ Protocol: "HTTP",
1241
+ Port: 80,
1242
+ DefaultActions: [
1243
+ {
1244
+ Type: "fixed-response",
1245
+ FixedResponseConfig: {
1246
+ StatusCode: "404",
1247
+ ContentType: "text/plain",
1248
+ MessageBody: "no such site",
1249
+ },
1250
+ },
1251
+ ],
1252
+ }),
1253
+ );
1254
+
1255
+ // The rule matches on the name a client asks for, not on the load balancer's.
1256
+ await elbV2.createRule(
1257
+ new CreateRuleCommand({
1258
+ ListenerArn: listener.Listeners?.[0]?.ListenerArn,
1259
+ Priority: 10,
1260
+ Conditions: [{ Field: "host-header", Values: ["api.example.test"] }],
1261
+ Actions: [
1262
+ {
1263
+ Type: "fixed-response",
1264
+ FixedResponseConfig: {
1265
+ StatusCode: "200",
1266
+ ContentType: "text/plain",
1267
+ MessageBody: "orders",
1268
+ },
1269
+ },
1270
+ ],
1271
+ }),
1272
+ );
1273
+
1274
+ const zone = await simAws.route53().createHostedZone(
1275
+ new CreateHostedZoneCommand({
1276
+ Name: "example.test",
1277
+ CallerReference: "shop-zone",
1278
+ }),
1279
+ );
1280
+
1281
+ await simAws.route53().changeResourceRecordSets(
1282
+ new ChangeResourceRecordSetsCommand({
1283
+ HostedZoneId: zone.HostedZone?.Id,
1284
+ ChangeBatch: {
1285
+ Changes: [
1286
+ {
1287
+ Action: "CREATE",
1288
+ ResourceRecordSet: {
1289
+ Name: "api.example.test",
1290
+ Type: "A",
1291
+ AliasTarget: {
1292
+ DNSName: dnsName,
1293
+ // The load balancer's CanonicalHostedZoneId, which sim Route53
1294
+ // does not resolve by.
1295
+ HostedZoneId: "Z0000000000000",
1296
+ EvaluateTargetHealth: false,
1297
+ },
1298
+ },
1299
+ },
1300
+ ],
1301
+ },
1302
+ }),
1303
+ );
1304
+
1305
+ await simAws.backgroundTasksComplete();
1306
+
1307
+ const srv = await serveSimAws({ simAws });
1308
+
1309
+ try {
1310
+ const response = await fetch(srv.localUrl("http://api.example.test/orders"));
1311
+
1312
+ console.log(response.status); // 200
1313
+ console.log(await response.text()); // "orders"
1314
+
1315
+ // The same name answers a DNS lookup with the address serving it.
1316
+ console.log(`dig @127.0.0.1 -p ${srv.dnsPort} api.example.test`);
1317
+ } finally {
1318
+ await srv.close();
1319
+ }
1320
+ ```
1321
+
1322
+ A name pointing at a load balancer that has since been deleted fails. Nothing holds that host name
1323
+ any more, and the failure names it. A DNS lookup for the name still answers, because the shape of a
1324
+ load balancer host name is what a lookup recognises, in the same way a lookup for a deleted bucket's
1325
+ website name does.
1326
+
1327
+ ## Deleting
1328
+
1329
+ Deleting a load balancer takes its listeners and their rules with it, and leaves its target groups
1330
+ where they are, as real ELB does. A target group is a resource in its own right, and a replacement
1331
+ load balancer's listeners forward to the same ones. A target group a listener or rule still forwards
1332
+ to cannot be deleted until that forward has gone.
1333
+
1334
+ ```typescript sim-elbv2-delete-load-balancer
1335
+ /**
1336
+ * Deleting a load balancer, and what survives it.
1337
+ */
1338
+
1339
+ import {
1340
+ CreateListenerCommand,
1341
+ CreateLoadBalancerCommand,
1342
+ CreateTargetGroupCommand,
1343
+ DeleteLoadBalancerCommand,
1344
+ DeleteTargetGroupCommand,
1345
+ DescribeListenersCommand,
1346
+ DescribeTargetGroupsCommand,
1347
+ } from "@aws-sdk/client-elastic-load-balancing-v2";
1348
+
1349
+ import { SimAws } from "@kensio/yulin";
1350
+ import {
1351
+ SimElbV2LoadBalancerNotFoundException,
1352
+ SimElbV2ResourceInUseException,
1353
+ } from "@kensio/yulin/elbv2";
1354
+
1355
+ const simAws = new SimAws();
1356
+ const elbV2 = simAws.elbV2();
1357
+
1358
+ const loadBalancer = await elbV2.createLoadBalancer(
1359
+ new CreateLoadBalancerCommand({ Name: "shop-alb" }),
1360
+ );
1361
+ const targetGroup = await elbV2.createTargetGroup(
1362
+ new CreateTargetGroupCommand({ Name: "checkout-tg", TargetType: "lambda" }),
1363
+ );
1364
+
1365
+ const loadBalancerArn = loadBalancer.LoadBalancers?.[0]?.LoadBalancerArn;
1366
+ const targetGroupArn = targetGroup.TargetGroups?.[0]?.TargetGroupArn;
1367
+
1368
+ await elbV2.createListener(
1369
+ new CreateListenerCommand({
1370
+ LoadBalancerArn: loadBalancerArn,
1371
+ Protocol: "HTTP",
1372
+ Port: 80,
1373
+ DefaultActions: [{ Type: "forward", TargetGroupArn: targetGroupArn }],
1374
+ }),
1375
+ );
1376
+
1377
+ // While the listener forwards to it, the target group cannot go.
1378
+ try {
1379
+ await elbV2.deleteTargetGroup(
1380
+ new DeleteTargetGroupCommand({ TargetGroupArn: targetGroupArn }),
1381
+ );
1382
+ } catch (error) {
1383
+ console.log(error instanceof SimElbV2ResourceInUseException); // true
1384
+ }
1385
+
1386
+ await elbV2.deleteLoadBalancer(
1387
+ new DeleteLoadBalancerCommand({ LoadBalancerArn: loadBalancerArn }),
1388
+ );
1389
+
1390
+ // The listener went with the load balancer; the target group did not.
1391
+ const remaining = await elbV2.describeTargetGroups(
1392
+ new DescribeTargetGroupsCommand({}),
1393
+ );
1394
+
1395
+ console.log(remaining.TargetGroups?.length); // 1
1396
+ console.log(remaining.TargetGroups?.[0]?.LoadBalancerArns.length); // 0
1397
+
1398
+ try {
1399
+ await elbV2.describeListeners(
1400
+ new DescribeListenersCommand({ LoadBalancerArn: loadBalancerArn }),
1401
+ );
1402
+ } catch (error) {
1403
+ // The listener went with the load balancer, and so did the load balancer.
1404
+ console.log(error instanceof SimElbV2LoadBalancerNotFoundException); // true
1405
+ }
1406
+ ```
1407
+
1408
+ ## Deploying a load balancer from CloudFormation
1409
+
1410
+ `AWS::ElasticLoadBalancingV2::LoadBalancer`, `TargetGroup`, `Listener` and `ListenerRule` create
1411
+ their simulated counterparts, and a test can start from the stack the routing is actually defined
1412
+ in. Each one goes through the same command an SDK caller would use. A template's listener rule
1413
+ matches requests the way the same rule created by hand does, and a declaration real ELB would refuse
1414
+ fails the deployment.
1415
+
1416
+ `Ref` returns the ARN of all four, and that is what a listener's `LoadBalancerArn`, a rule's
1417
+ `ListenerArn` and a forward action's `TargetGroupArn` each take. `Fn::GetAtt` answers with:
1418
+
1419
+ - `DNSName`, `LoadBalancerArn`, `LoadBalancerName`, `LoadBalancerFullName` and
1420
+ `CanonicalHostedZoneID` on a load balancer
1421
+ - `TargetGroupArn`, `TargetGroupName` and `TargetGroupFullName` on a target group
1422
+ - `ListenerArn` on a listener, and `RuleArn` and `IsDefault` on a rule
1423
+
1424
+ An unnamed load balancer or target group is named after the stack and the logical ID, trimmed to the
1425
+ 32 characters ELB allows. A target group declaring `Targets` has them registered as part of creating
1426
+ it, and the group routes as soon as the stack has deployed.
1427
+
1428
+ ```typescript sim-elbv2-cloudformation
1429
+ /**
1430
+ * Deploying a load balancer, target group, listener and rule from a template.
1431
+ */
1432
+
1433
+ import { SimAws } from "@kensio/yulin";
1434
+ import type { SimElbV2Event, SimElbV2Result } from "@kensio/yulin/elbv2";
1435
+ import { simElbV2Fetch } from "@kensio/yulin/elbv2";
1436
+
1437
+ const simAws = new SimAws();
1438
+
1439
+ const stack = await simAws.cloudFormation().deployTemplate({
1440
+ stackName: "shop",
1441
+ template: {
1442
+ Resources: {
1443
+ CheckoutFunction: {
1444
+ Type: "AWS::Lambda::Function",
1445
+ Properties: {
1446
+ FunctionName: "checkout",
1447
+ Role: "arn:aws:iam::888888888888:role/CheckoutRole",
1448
+ },
1449
+ },
1450
+ ShopAlb: {
1451
+ Type: "AWS::ElasticLoadBalancingV2::LoadBalancer",
1452
+ Properties: {
1453
+ Name: "shop-alb",
1454
+ Scheme: "internet-facing",
1455
+ // Accepted and left out: there is no VPC here to place one in.
1456
+ Subnets: ["subnet-1111", "subnet-2222"],
1457
+ },
1458
+ },
1459
+ CheckoutTargets: {
1460
+ Type: "AWS::ElasticLoadBalancingV2::TargetGroup",
1461
+ Properties: {
1462
+ Name: "checkout-tg",
1463
+ TargetType: "lambda",
1464
+ // Registered at deploy time, so the group routes straight away.
1465
+ Targets: [{ Id: { "Fn::GetAtt": ["CheckoutFunction", "Arn"] } }],
1466
+ },
1467
+ },
1468
+ InvokePermission: {
1469
+ Type: "AWS::Lambda::Permission",
1470
+ Properties: {
1471
+ FunctionName: { Ref: "CheckoutFunction" },
1472
+ Action: "lambda:InvokeFunction",
1473
+ Principal: "elasticloadbalancing.amazonaws.com",
1474
+ SourceArn: { Ref: "CheckoutTargets" },
1475
+ },
1476
+ },
1477
+ HttpListener: {
1478
+ Type: "AWS::ElasticLoadBalancingV2::Listener",
1479
+ Properties: {
1480
+ LoadBalancerArn: { Ref: "ShopAlb" },
1481
+ Protocol: "HTTP",
1482
+ Port: 80,
1483
+ DefaultActions: [
1484
+ {
1485
+ Type: "fixed-response",
1486
+ FixedResponseConfig: {
1487
+ StatusCode: "404",
1488
+ ContentType: "text/plain",
1489
+ MessageBody: "no such site",
1490
+ },
1491
+ },
1492
+ ],
1493
+ },
1494
+ },
1495
+ CheckoutRule: {
1496
+ Type: "AWS::ElasticLoadBalancingV2::ListenerRule",
1497
+ Properties: {
1498
+ ListenerArn: { Ref: "HttpListener" },
1499
+ Priority: 10,
1500
+ Conditions: [{ Field: "path-pattern", Values: ["/checkout*"] }],
1501
+ Actions: [
1502
+ { Type: "forward", TargetGroupArn: { Ref: "CheckoutTargets" } },
1503
+ ],
1504
+ },
1505
+ },
1506
+ },
1507
+ Outputs: {
1508
+ DnsName: { Value: { "Fn::GetAtt": ["ShopAlb", "DNSName"] } },
1509
+ FullName: {
1510
+ Value: { "Fn::GetAtt": ["ShopAlb", "LoadBalancerFullName"] },
1511
+ },
1512
+ },
1513
+ },
1514
+ bindings: [
1515
+ {
1516
+ logicalId: "CheckoutFunction",
1517
+ handler: (event: SimElbV2Event): SimElbV2Result => ({
1518
+ statusCode: 200,
1519
+ statusDescription: "200 OK",
1520
+ headers: { "content-type": "text/plain" },
1521
+ body: `checkout ${event.path}`,
1522
+ isBase64Encoded: false,
1523
+ }),
1524
+ },
1525
+ ],
1526
+ });
1527
+
1528
+ await stack.waitForDeployComplete();
1529
+ await simAws.backgroundTasksComplete();
1530
+
1531
+ const dnsName = stack.output("DnsName");
1532
+
1533
+ console.log(dnsName); // "shop-alb-0000000001.us-east-1.elb.amazonaws.com"
1534
+ console.log(stack.output("FullName"));
1535
+ // "app/shop-alb/0000000001"
1536
+
1537
+ const claimed = await simElbV2Fetch(simAws, `http://${dnsName}/checkout/42`);
1538
+
1539
+ console.log(claimed.status); // 200
1540
+ console.log(await claimed.text()); // "checkout /checkout/42"
1541
+
1542
+ // A request no rule claims falls through to the listener's default action.
1543
+ const unclaimed = await simElbV2Fetch(simAws, `http://${dnsName}/other`);
1544
+
1545
+ console.log(unclaimed.status); // 404
1546
+ ```
1547
+
1548
+ A listener's `Certificates` resolves against simulated [ACM](https://yulinsim.dev/services/acm/), and a stack that creates a
1549
+ certificate and attaches it to an HTTPS listener works end to end. A certificate that was never
1550
+ issued, or that belongs to another account or region, fails the deployment outright. A `Fn::GetAtt`
1551
+ on `DNSName` is a name a Route53 alias in the same stack can point at and reach.
1552
+
1553
+ ```typescript sim-elbv2-cloudformation-certificate
1554
+ /**
1555
+ * An HTTPS listener holding a certificate the same stack created.
1556
+ */
1557
+
1558
+ import { DescribeListenersCommand } from "@aws-sdk/client-elastic-load-balancing-v2";
1559
+
1560
+ import { SimAws } from "@kensio/yulin";
1561
+
1562
+ const simAws = new SimAws();
1563
+
1564
+ const stack = await simAws.cloudFormation().deployTemplate({
1565
+ stackName: "shop",
1566
+ template: {
1567
+ Resources: {
1568
+ ShopZone: {
1569
+ Type: "AWS::Route53::HostedZone",
1570
+ Properties: { Name: "example.test" },
1571
+ },
1572
+ SiteCertificate: {
1573
+ Type: "AWS::CertificateManager::Certificate",
1574
+ Properties: {
1575
+ DomainName: "shop.example.test",
1576
+ ValidationMethod: "DNS",
1577
+ },
1578
+ },
1579
+ ShopAlb: {
1580
+ Type: "AWS::ElasticLoadBalancingV2::LoadBalancer",
1581
+ Properties: { Name: "shop-alb" },
1582
+ },
1583
+ HttpsListener: {
1584
+ Type: "AWS::ElasticLoadBalancingV2::Listener",
1585
+ Properties: {
1586
+ LoadBalancerArn: { Ref: "ShopAlb" },
1587
+ Protocol: "HTTPS",
1588
+ Port: 443,
1589
+ Certificates: [{ CertificateArn: { Ref: "SiteCertificate" } }],
1590
+ DefaultActions: [
1591
+ {
1592
+ Type: "fixed-response",
1593
+ FixedResponseConfig: {
1594
+ StatusCode: "200",
1595
+ ContentType: "text/plain",
1596
+ MessageBody: "shop",
1597
+ },
1598
+ },
1599
+ ],
1600
+ },
1601
+ },
1602
+ ShopRecord: {
1603
+ Type: "AWS::Route53::RecordSet",
1604
+ Properties: {
1605
+ HostedZoneId: { Ref: "ShopZone" },
1606
+ Name: "shop.example.test",
1607
+ Type: "A",
1608
+ AliasTarget: {
1609
+ DNSName: { "Fn::GetAtt": ["ShopAlb", "DNSName"] },
1610
+ HostedZoneId: {
1611
+ "Fn::GetAtt": ["ShopAlb", "CanonicalHostedZoneID"],
1612
+ },
1613
+ },
1614
+ },
1615
+ },
1616
+ },
1617
+ Outputs: {
1618
+ ListenerArn: { Value: { Ref: "HttpsListener" } },
1619
+ },
1620
+ },
1621
+ });
1622
+
1623
+ await stack.waitForDeployComplete();
1624
+ await simAws.backgroundTasksComplete();
1625
+
1626
+ const listenerArn = stack.output("ListenerArn");
1627
+
1628
+ const described = await simAws
1629
+ .elbV2()
1630
+ .describeListeners(
1631
+ new DescribeListenersCommand({ ListenerArns: [listenerArn] }),
1632
+ );
1633
+
1634
+ const listener = described.Listeners?.[0];
1635
+
1636
+ console.log(listener?.Certificates[0]?.CertificateArn);
1637
+ // the ARN of the certificate the stack created
1638
+
1639
+ console.log(listener?.SslPolicy); // "ELBSecurityPolicy-2016-08"
1640
+ ```
1641
+
1642
+ Properties Yulin has no use for are read and left out, and the stack still deploys. Subnets,
1643
+ security groups, load balancer and target group attributes, listener attributes, mutual
1644
+ authentication and health check configuration are all in that group. Each one is recorded on the
1645
+ Resource, where a reader can see which parts of the deployed load balancer are inert:
1646
+
1647
+ ```typescript
1648
+ const ignored = stack.getResource("ShopAlb")?.ignoredProperties;
1649
+ ```
1650
+
1651
+ Tearing the stack down removes all four in reverse dependency order. A rule comes down before its
1652
+ listener, a listener before its load balancer, and a target group after everything forwarding to it.
1653
+
1654
+ ## IAM authorization
1655
+
1656
+ Every operation is authorized by simulated [IAM](https://yulinsim.dev/services/iam/) as the caller making it, against the
1657
+ `elasticloadbalancing:` action and the ARN of whatever it names. An operation that names no existing
1658
+ resource, such as `CreateLoadBalancer` or a describe, is authorized against `*`, and only a policy
1659
+ whose Resource is `*` allows it.
1660
+
1661
+ ```typescript sim-elbv2-iam-policy
1662
+ /**
1663
+ * A Role allowed to describe load balancers but not to create one.
1664
+ */
1665
+
1666
+ import { CreateRoleCommand, PutRolePolicyCommand } from "@aws-sdk/client-iam";
1667
+ import {
1668
+ CreateLoadBalancerCommand,
1669
+ DescribeLoadBalancersCommand,
1670
+ } from "@aws-sdk/client-elastic-load-balancing-v2";
1671
+
1672
+ import { SimAws } from "@kensio/yulin";
1673
+ import { SimElbV2AccessDeniedException } from "@kensio/yulin/elbv2";
1674
+
1675
+ const simAws = new SimAws();
1676
+ const account = simAws.account("888888888888");
1677
+ const elbV2 = account.region("eu-west-1").elbV2();
1678
+
1679
+ await account.iam().createRole(
1680
+ new CreateRoleCommand({
1681
+ RoleName: "ReadOnlyRole",
1682
+ AssumeRolePolicyDocument: JSON.stringify({
1683
+ Version: "2012-10-17",
1684
+ Statement: {
1685
+ Effect: "Allow",
1686
+ Principal: { AWS: "arn:aws:iam::888888888888:root" },
1687
+ Action: "sts:AssumeRole",
1688
+ },
1689
+ }),
1690
+ }),
1691
+ );
1692
+
1693
+ await account.iam().putRolePolicy(
1694
+ new PutRolePolicyCommand({
1695
+ RoleName: "ReadOnlyRole",
1696
+ PolicyName: "describe-only",
1697
+ PolicyDocument: JSON.stringify({
1698
+ Version: "2012-10-17",
1699
+ Statement: {
1700
+ Effect: "Allow",
1701
+ Action: "elasticloadbalancing:DescribeLoadBalancers",
1702
+ Resource: "*",
1703
+ },
1704
+ }),
1705
+ }),
1706
+ );
1707
+
1708
+ const caller = {
1709
+ kind: "arn",
1710
+ arn: "arn:aws:iam::888888888888:role/ReadOnlyRole",
1711
+ } as const;
1712
+
1713
+ const described = await elbV2.describeLoadBalancers(
1714
+ new DescribeLoadBalancersCommand({}),
1715
+ { caller },
1716
+ );
1717
+
1718
+ console.log(described.LoadBalancers?.length); // 0
1719
+
1720
+ try {
1721
+ await elbV2.createLoadBalancer(
1722
+ new CreateLoadBalancerCommand({ Name: "shop-alb" }),
1723
+ { caller },
1724
+ );
1725
+ } catch (error) {
1726
+ console.log(error instanceof SimElbV2AccessDeniedException); // true
1727
+ }
1728
+ ```
1729
+
1730
+ ## AWS SDK interception
1731
+
1732
+ An `ElasticLoadBalancingV2Client` can be intercepted so ordinary SDK code reaches the simulation
1733
+ without being handed a simulator object. See [AWS SDK interception](https://yulinsim.dev/sdk/) for how that works.
1734
+
1735
+ ```typescript sim-elbv2-sdk-interception
1736
+ /**
1737
+ * Intercepting an ELBv2 SDK client.
1738
+ */
1739
+
1740
+ import {
1741
+ CreateLoadBalancerCommand,
1742
+ ElasticLoadBalancingV2Client,
1743
+ } from "@aws-sdk/client-elastic-load-balancing-v2";
1744
+
1745
+ import { SimSdk } from "@kensio/yulin/sdk";
1746
+
1747
+ using simSdk = new SimSdk();
1748
+ simSdk.intercept(ElasticLoadBalancingV2Client);
1749
+
1750
+ const client = new ElasticLoadBalancingV2Client({ region: "eu-west-2" });
1751
+
1752
+ const created = await client.send(
1753
+ new CreateLoadBalancerCommand({ Name: "shop-alb" }),
1754
+ );
1755
+
1756
+ console.log(created.LoadBalancers?.[0]?.DNSName);
1757
+ // "shop-alb-0000000001.eu-west-2.elb.amazonaws.com"
1758
+ ```
1759
+
1760
+ ## Available functionality
1761
+
1762
+ - `CreateLoadBalancer`, `DescribeLoadBalancers` and `DeleteLoadBalancer`, with a DNS name, ARN,
1763
+ canonical hosted zone id, scheme and state on every load balancer.
1764
+ - `CreateTargetGroup`, `DescribeTargetGroups`, `ModifyTargetGroup` and `DeleteTargetGroup`, with the
1765
+ `lambda` and `ip` target types.
1766
+ - `RegisterTargets`, `DeregisterTargets` and `DescribeTargetHealth`.
1767
+ - `CreateListener`, `DescribeListeners`, `ModifyListener` and `DeleteListener`, on HTTP and HTTPS.
1768
+ - An HTTPS listener's default certificate resolved against simulated ACM, refusing a missing one,
1769
+ one whose status falls short of `ISSUED`, and one outside the load balancer's own account and
1770
+ region.
1771
+ - `AddListenerCertificates`, `RemoveListenerCertificates` and `DescribeListenerCertificates`, with
1772
+ the default certificate reported first and refused removal.
1773
+ - `CreateRule`, `DescribeRules`, `ModifyRule`, `DeleteRule` and `SetRulePriorities`, with priorities
1774
+ unique within a listener and the listener's default rule reported last.
1775
+ - `forward`, `fixed-response` and `redirect` actions, and `host-header` and `path-pattern`
1776
+ conditions. A condition is read through its own field's configuration, so one carrying another
1777
+ field's is refused.
1778
+ - Paged describes with `PageSize` and `Marker`.
1779
+ - Carrying a request through `simElbV2Fetch`, with a listener matched by port, its rules evaluated
1780
+ in priority order with the first match winning, and a fall through to the default action.
1781
+ - Resolving a load balancer's DNS name through sim Route53, where an alias record or a CNAME
1782
+ pointing at it reaches its listeners and rules, and a `host-header` condition sees the name the
1783
+ request was made to.
1784
+ - Serving a load balancer under `serveSimAws`, over real localhost HTTP and with a DNS lookup for
1785
+ the name answering with the address the local server listens on.
1786
+ - `host-header` and `path-pattern` matching with ELB's own wildcard semantics, and a rule claiming a
1787
+ request only when all of its conditions hold.
1788
+ - `forward` to a `lambda` target group, which invokes its function with an ALB-shaped event, and the
1789
+ `fixed-response` and `redirect` actions, which the load balancer answers itself.
1790
+ - `forward` to an `ip` target group, which reaches the container of the simulated ECS service
1791
+ registered into it, with the request carrying the forwarding headers a load balancer writes.
1792
+ - The load balancer's own 503, 502 and 413, and the invoke permission the function's resource policy
1793
+ has to grant `elasticloadbalancing.amazonaws.com`.
1794
+ - Deploying `AWS::ElasticLoadBalancingV2::LoadBalancer`, `TargetGroup`, `Listener` and
1795
+ `ListenerRule` from a CloudFormation template, with `Ref` returning ARNs, `Fn::GetAtt` answering
1796
+ `DNSName`, `LoadBalancerFullName` and `TargetGroupFullName` among others, a listener's
1797
+ `Certificates` resolved against simulated ACM, and a target group's `Targets` registered at deploy
1798
+ time.
1799
+ - IAM authorization against the ARN of whatever an operation names.
1800
+ - SDK interception of `ElasticLoadBalancingV2Client`.
1801
+
1802
+ ## Limitations
1803
+
1804
+ - Only `host-header` and `path-pattern` conditions exist. The `http-header`, `http-request-method`,
1805
+ `query-string` and `source-ip` fields real ELB has are refused when the rule is written. Storing
1806
+ them would leave a rule that looks configured while claiming no request at all. Regular expression
1807
+ condition values, which real ELB takes in `RegexValues`, go unmatched too.
1808
+ - A `host-header` condition is matched against the request's Host header, falling back to the host
1809
+ name in the URL. On real AWS those are the same thing, because DNS is what brought the request to
1810
+ the load balancer. A request served under the Yulin-local suffix is matched against the name
1811
+ inside that suffix, so `api.example.test.sim-aws.localhost` is matched as `api.example.test`.
1812
+ - A `ForwardConfig` naming several target groups by weight is refused when the request arrives.
1813
+ Nothing here reads the weights, and which group takes a request is a question this cannot answer.
1814
+ - An `ip` target group is answered by the simulated ECS service registered into it, and by that
1815
+ alone. An address registered by hand is a 503, since only an ECS service registration puts
1816
+ something behind an address here.
1817
+ - Requests are never shared between the targets of a group. An ECS service's desired count is state
1818
+ and not concurrency, and a group holding three targets calls one container handler.
1819
+ - Which container of an ECS task a request reaches diverges from real ECS on purpose, and is
1820
+ documented under [the ECS docs](https://yulinsim.dev/services/ecs/#which-container-of-a-task-answers).
1821
+ - A redirect goes unchecked against the listener it is on, and redirecting HTTPS to HTTP is accepted
1822
+ where real ELB refuses it. Nothing here performs TLS, and the listener's protocol says nothing
1823
+ about what a request really arrived over.
1824
+ - No TLS is performed on an HTTPS listener. Everything travels in the clear, with no handshake and
1825
+ no certificate presented to a client. What a test can conclude is that a listener's certificate
1826
+ exists, was issued, and is in the load balancer's account and region, and that a request treated
1827
+ as arriving over HTTPS is routed and forwarded the way the configuration says. Out of reach are a
1828
+ client trusting the certificate, expiry, protocol versions and ciphers, and whether a request
1829
+ really arrived over a secure connection.
1830
+ - The URL scheme a request is written with goes unchecked against the listener it reaches. The port
1831
+ decides the listener, and the listener decides the protocol the event and the forwarding headers
1832
+ report. So `http://<dns-name>:443/` reaching an HTTPS listener is served as an HTTPS request,
1833
+ where a real handshake would have failed.
1834
+ - SNI certificate selection by host name is absent. The certificates beyond the default are held and
1835
+ reported, and nothing chooses between them when a request arrives, since there is no handshake to
1836
+ choose in. The default certificate is the one every request is served under.
1837
+ - Security policies and cipher suites are accepted and ignored. A listener that names no `SslPolicy`
1838
+ is given the one real ELB defaults to, the value is reported back, and nothing acts on it.
1839
+ - `IsDefault` is ignored on `AddListenerCertificates`, as real ELB documents that it should not be
1840
+ set there. The default certificate is replaced with `ModifyListener`, which drops the certificate
1841
+ that was the default instead of moving it into the rest of the list.
1842
+ - A certificate is only ever an ACM one. `ImportCertificate`, IAM server certificates and mutual TLS
1843
+ are all absent, and there is no trust store to give a listener.
1844
+ - The length limits real ELB puts on a fixed response's message body and a redirect's components go
1845
+ unenforced. A condition value is held to ELB's own 128 characters.
1846
+ - A request served under the Yulin-local suffix reaches the listener on port 80, or on 443 for an
1847
+ `https:` URL. The port such a request carries belongs to the local server, and cannot say which
1848
+ listener it is for. To reach a listener on another port over localhost, serve on that port and
1849
+ request the hostname without the suffix, which needs a resolver pointed at the simulator as
1850
+ described in [Route53](https://yulinsim.dev/services/route53/#ports).
1851
+ - A DNS lookup for a name pointing at a load balancer that has been deleted still answers with the
1852
+ local server address, because a load balancer host name is recognised by its shape. The request
1853
+ that follows is the thing that fails, naming the host name nothing answers on.
1854
+ - The invoke permission is checked when the request arrives, where real ELB checks it when a Lambda
1855
+ target is registered and refuses `RegisterTargets` without it. A target naming a function in
1856
+ another Account or Region is registered here and then answers 502, where real ELB refuses the
1857
+ registration.
1858
+ - Multi-value headers are absent. `lambda.multi_value_headers.enabled` cannot be set, because target
1859
+ group attributes are absent too, and the event and the accepted response always use the
1860
+ single-value `headers` and `queryStringParameters` fields. A repeated query string key keeps its
1861
+ last value, as real ELB does with the attribute off, while repeated request headers arrive already
1862
+ joined with commas.
1863
+ - Health check requests never reach a target. A Lambda function or a container here will never see
1864
+ the `ELB-HealthChecker/2.0` request real ELB sends when health checks are enabled.
1865
+ - Network and gateway load balancers are absent. `Type: "network"` and `"gateway"` are refused, as
1866
+ are the `TCP`, `TLS`, `UDP`, `TCP_UDP` and `GENEVE` protocols.
1867
+ - `TargetType: "instance"` and `"alb"` are refused outright, and so is a target group naming no
1868
+ target type at all, where real ELB defaults it to `instance`.
1869
+ - Health checks never run. Health check settings are held and reported, and every registered target
1870
+ is `healthy` however it is configured, so a test cannot watch a deployment come up.
1871
+ - A load balancer is `active` immediately, where real ELB spends minutes in `provisioning`, and
1872
+ deregistration is immediate, where real ELB drains connections first.
1873
+ - Subnets, security groups, availability zones and cross-zone configuration are accepted and left
1874
+ out of a describe, and `AvailabilityZones` and `SecurityGroups` are therefore absent from a
1875
+ described load balancer.
1876
+ - `CanonicalHostedZoneId` is one value everywhere, where the real one varies by region. Simulated
1877
+ Route53 resolves an alias by looking its target up, so only the shape is load-bearing, and copying
1878
+ this value into a real template would be copying the wrong one.
1879
+ - ARN ids and DNS name suffixes count from one, where real ones are random, and a test can therefore
1880
+ assert on an ARN it never captured. The shape is the one real ELB issues either way.
1881
+ - `authenticate-oidc` and `authenticate-cognito` actions are refused. Nothing here performs that
1882
+ exchange, and treating one as a plain forward would quietly skip authentication. Since those are
1883
+ the only actions that may precede a routing action, a listener or rule takes exactly one action
1884
+ here and a longer list is refused.
1885
+ - Load balancer and target group attributes, access logs, and tags as a readable resource are all
1886
+ absent.
1887
+ - `AWS::ElasticLoadBalancingV2::TrustStore`, `TrustStoreRevocation` and `ListenerCertificate` are
1888
+ never deployed, and a stack declaring one records it as unsupported and carries on. The first two
1889
+ have nothing to attach to, with mutual TLS absent, and a listener's additional certificates are
1890
+ added with `AddListenerCertificates`.
1891
+ - `Fn::GetAtt` `SecurityGroups` on a load balancer and `LoadBalancerArns` on a target group are both
1892
+ refused. Nothing places a simulated load balancer behind a security group, and a target group
1893
+ keeps no record of which load balancers forward to it, which `DescribeTargetGroups` reads back out
1894
+ of the listeners instead.
1895
+ - Sim CloudFormation has no in-place resource update. A changed load balancer, target group,
1896
+ listener or rule is deleted and created again, and everything naming it is replaced too. A
1897
+ replaced load balancer gets a new DNS name.