@kensio/yulin 1.20.15 → 1.20.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -0
- package/dist/sdk/module/sim-sdk-module-client-interceptor.d.ts +5 -0
- package/dist/sdk/module/sim-sdk-module-client-interceptor.js +14 -0
- package/dist/sdk/module/sim-sdk-static-client-factory.d.ts +26 -0
- package/dist/sdk/module/sim-sdk-static-client-factory.js +59 -0
- package/dist/sdk/send-patch.d.ts +8 -0
- package/dist/sdk/send-patch.js +11 -0
- package/dist/service/aws/caller/sim-aws-caller-resolver.d.ts +28 -6
- package/dist/service/aws/caller/sim-aws-caller-resolver.js +18 -11
- package/dist/service/aws/caller/sim-aws-caller.d.ts +9 -0
- package/dist/service/aws/caller/sim-aws-resolved-caller.d.ts +19 -0
- package/dist/service/aws/caller/sim-aws-resolved-caller.js +26 -0
- package/dist/service/aws/factory/sim-aws-account-region-service-builder.d.ts +6 -0
- package/dist/service/aws/factory/sim-aws-account-region-service-builder.js +3 -0
- package/dist/service/aws/factory/sim-aws-account-service-cache.d.ts +6 -0
- package/dist/service/aws/factory/sim-aws-account-service-cache.js +5 -0
- package/dist/service/aws/factory/sim-aws-service-factory.d.ts +5 -0
- package/dist/service/aws/factory/sim-aws-service-factory.js +6 -0
- package/dist/service/aws/sim-aws-properties.d.ts +15 -0
- package/dist/service/aws/sim-aws.js +2 -1
- package/dist/service/iam/authorize/context/sim-iam-auth-z-caller-context-builder.d.ts +30 -7
- package/dist/service/iam/authorize/context/sim-iam-auth-z-caller-context-builder.js +20 -11
- package/dist/service/iam/authorize/context/sim-iam-auth-z-context-builder.d.ts +2 -1
- package/dist/service/iam/authorize/context/sim-iam-auth-z-context-builder.js +5 -1
- package/dist/service/iam/authorize/match/sim-iam-policy-principal-matcher.d.ts +10 -0
- package/dist/service/iam/authorize/match/sim-iam-policy-principal-matcher.js +15 -3
- package/dist/service/iam/authorize/sim-iam-account-auth-z.d.ts +11 -4
- package/dist/service/iam/authorize/sim-iam-account-auth-z.js +5 -3
- package/dist/service/iam/authorize/sim-iam-authorizer.d.ts +2 -1
- package/dist/service/iam/authorize/sim-iam-authorizer.js +1 -0
- package/dist/service/iam/role/sim-iam-role-with-policy.factory.d.ts +9 -0
- package/dist/service/iam/role/sim-iam-role-with-policy.factory.js +4 -2
- package/dist/service/iam/sim-iam-account-parts.d.ts +8 -0
- package/dist/service/iam/sim-iam-account-parts.js +1 -0
- package/dist/service/sts/auth-z/assume-role-auth-z-coordinator.d.ts +9 -2
- package/dist/service/sts/auth-z/assume-role-auth-z-coordinator.js +2 -2
- package/dist/service/sts/auth-z/assume-role-source-account-auth-z.d.ts +10 -4
- package/dist/service/sts/auth-z/assume-role-source-account-auth-z.js +8 -7
- package/dist/service/sts/auth-z/assume-role-target-auth-z.d.ts +2 -2
- package/dist/service/sts/auth-z/assume-role-trust-policy-authorizer.d.ts +6 -2
- package/dist/service/sts/auth-z/assume-role-trust-policy-authorizer.js +14 -11
- package/dist/service/sts/command/assume-role/assume-role.handler.d.ts +5 -1
- package/dist/service/sts/command/assume-role/assume-role.handler.js +5 -2
- package/dist/service/sts/command/get-caller-identity/get-caller-identity.handler.d.ts +5 -1
- package/dist/service/sts/command/get-caller-identity/get-caller-identity.handler.js +4 -1
- package/dist/service/sts/service-role/sim-service-role.js +5 -1
- package/dist/service/sts/sim-sts.d.ts +9 -1
- package/dist/service/sts/sim-sts.js +4 -0
- package/docs/README.md +54 -0
- package/docs/ai-skill/README.md +69 -0
- package/docs/cli/README.md +284 -0
- package/docs/factories/README.md +140 -0
- package/docs/lint/README.md +176 -0
- package/docs/non-aws-dependencies/README.md +261 -0
- package/docs/sdk/README.md +272 -0
- package/docs/serve/README.md +1126 -0
- package/docs/services/acm/README.md +823 -0
- package/docs/services/apigateway/README.md +1919 -0
- package/docs/services/apigatewayv2/README.md +2678 -0
- package/docs/services/athena/README.md +1044 -0
- package/docs/services/bedrock/README.md +350 -0
- package/docs/services/cloudformation/README.md +3440 -0
- package/docs/services/cloudfront/README.md +2874 -0
- package/docs/services/cloudwatch/README.md +420 -0
- package/docs/services/cognito/README.md +4923 -0
- package/docs/services/dynamodb/README.md +3784 -0
- package/docs/services/ecr/README.md +302 -0
- package/docs/services/ecs/README.md +2010 -0
- package/docs/services/elbv2/README.md +1897 -0
- package/docs/services/eventbridge/README.md +1110 -0
- package/docs/services/firehose/README.md +805 -0
- package/docs/services/glue/README.md +501 -0
- package/docs/services/iam/README.md +1283 -0
- package/docs/services/kinesis/README.md +454 -0
- package/docs/services/kms/README.md +659 -0
- package/docs/services/lambda/README.md +3645 -0
- package/docs/services/logs/README.md +868 -0
- package/docs/services/organizations/README.md +574 -0
- package/docs/services/personalize/README.md +835 -0
- package/docs/services/rekognition/README.md +1070 -0
- package/docs/services/route53/README.md +1783 -0
- package/docs/services/s3/README.md +3024 -0
- package/docs/services/scheduler/README.md +806 -0
- package/docs/services/secretsmanager/README.md +667 -0
- package/docs/services/ses/README.md +1011 -0
- package/docs/services/sns/README.md +1904 -0
- package/docs/services/sqs/README.md +1185 -0
- package/docs/services/ssm/README.md +1072 -0
- package/docs/services/stepfunctions/README.md +1332 -0
- package/docs/services/sts/README.md +245 -0
- package/docs/services/wafv2/README.md +1378 -0
- package/docs/terraform/README.md +248 -0
- package/docs/time/README.md +284 -0
- package/llms.txt +59 -0
- package/package.json +7 -4
|
@@ -0,0 +1,1072 @@
|
|
|
1
|
+
# Simulated SSM Parameter Store
|
|
2
|
+
|
|
3
|
+
Yulin includes a simulated AWS Systems Manager Parameter Store for tests and local development.
|
|
4
|
+
Parameters are stored in memory, versioned on every write, and every operation is authorized by
|
|
5
|
+
simulated IAM.
|
|
6
|
+
|
|
7
|
+
Only Parameter Store is simulated.
|
|
8
|
+
|
|
9
|
+
SSM-specific types are imported from the `@kensio/yulin/ssm` subpath.
|
|
10
|
+
|
|
11
|
+
## Writing and reading a parameter
|
|
12
|
+
|
|
13
|
+
`PutParameter` needs a `Type` when the parameter is new. `GetParameter` returns the current version.
|
|
14
|
+
|
|
15
|
+
```typescript sim-ssm-put-and-get
|
|
16
|
+
/**
|
|
17
|
+
* Writing a simulated parameter and reading it back.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { GetParameterCommand, PutParameterCommand } from "@aws-sdk/client-ssm";
|
|
21
|
+
|
|
22
|
+
import { SimAws } from "@kensio/yulin";
|
|
23
|
+
|
|
24
|
+
const simAws = new SimAws();
|
|
25
|
+
const ssm = simAws.ssm();
|
|
26
|
+
|
|
27
|
+
await ssm.putParameter(
|
|
28
|
+
new PutParameterCommand({
|
|
29
|
+
Name: "/myapp/prod/db-host",
|
|
30
|
+
Type: "String",
|
|
31
|
+
Value: "db.internal",
|
|
32
|
+
}),
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const read = await ssm.getParameter(
|
|
36
|
+
new GetParameterCommand({ Name: "/myapp/prod/db-host" }),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
console.log(read.Parameter?.Value); // "db.internal"
|
|
40
|
+
console.log(read.Parameter?.Version); // 1
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
A parameter written without a leading slash and one read with it are the same parameter, because
|
|
44
|
+
both name the same ARN.
|
|
45
|
+
|
|
46
|
+
## Parameter ARNs and IAM policies
|
|
47
|
+
|
|
48
|
+
A parameter ARN drops the leading slash from the name. `/myapp/prod/db-host` becomes
|
|
49
|
+
`arn:aws:ssm:eu-west-2:111111111111:parameter/myapp/prod/db-host`, with one slash after `parameter`
|
|
50
|
+
rather than two. A policy written with the doubled slash matches nothing.
|
|
51
|
+
|
|
52
|
+
```typescript sim-ssm-iam-policy
|
|
53
|
+
/**
|
|
54
|
+
* A simulated IAM policy allowing a Role to read one parameter.
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
import { CreateRoleCommand, PutRolePolicyCommand } from "@aws-sdk/client-iam";
|
|
58
|
+
import { GetParameterCommand, PutParameterCommand } from "@aws-sdk/client-ssm";
|
|
59
|
+
|
|
60
|
+
import { SimAws } from "@kensio/yulin";
|
|
61
|
+
|
|
62
|
+
const simAws = new SimAws();
|
|
63
|
+
const accountId = simAws.defaultAccountId;
|
|
64
|
+
const regionName = simAws.defaultRegionName;
|
|
65
|
+
|
|
66
|
+
const role = await simAws.iam().createRole(
|
|
67
|
+
new CreateRoleCommand({
|
|
68
|
+
RoleName: "ConfigReader",
|
|
69
|
+
AssumeRolePolicyDocument: JSON.stringify({
|
|
70
|
+
Version: "2012-10-17",
|
|
71
|
+
Statement: {
|
|
72
|
+
Effect: "Allow",
|
|
73
|
+
Principal: { AWS: `arn:aws:iam::${accountId}:root` },
|
|
74
|
+
Action: "sts:AssumeRole",
|
|
75
|
+
},
|
|
76
|
+
}),
|
|
77
|
+
}),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
await simAws.iam().putRolePolicy(
|
|
81
|
+
new PutRolePolicyCommand({
|
|
82
|
+
RoleName: "ConfigReader",
|
|
83
|
+
PolicyName: "ReadDbHost",
|
|
84
|
+
PolicyDocument: JSON.stringify({
|
|
85
|
+
Version: "2012-10-17",
|
|
86
|
+
Statement: {
|
|
87
|
+
Effect: "Allow",
|
|
88
|
+
Action: "ssm:GetParameter",
|
|
89
|
+
// One slash after `parameter`, not two, whatever the name looks like.
|
|
90
|
+
Resource: `arn:aws:ssm:${regionName}:${accountId}:parameter/myapp/prod/db-host`,
|
|
91
|
+
},
|
|
92
|
+
}),
|
|
93
|
+
}),
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
await simAws.ssm().putParameter(
|
|
97
|
+
new PutParameterCommand({
|
|
98
|
+
Name: "/myapp/prod/db-host",
|
|
99
|
+
Type: "String",
|
|
100
|
+
Value: "db.internal",
|
|
101
|
+
}),
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const read = await simAws
|
|
105
|
+
.ssm()
|
|
106
|
+
.getParameter(new GetParameterCommand({ Name: "/myapp/prod/db-host" }), {
|
|
107
|
+
caller: { kind: "arn", arn: role.Role.Arn },
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
console.log(read.Parameter?.Value); // "db.internal"
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`DescribeParameters` is the exception. Real Parameter Store gives that action no resource-level
|
|
114
|
+
permissions. It authorizes against `*` here, and a policy naming individual parameter ARNs grants
|
|
115
|
+
nothing.
|
|
116
|
+
|
|
117
|
+
`GetParametersByPath` authorizes against the path rather than against each parameter it returns.
|
|
118
|
+
Access to a path is access to everything under it. A recursive listing of `/myapp` returns
|
|
119
|
+
`/myapp/prod/db-host` even where a policy explicitly denies that parameter.
|
|
120
|
+
|
|
121
|
+
## Versions
|
|
122
|
+
|
|
123
|
+
Every write makes a new version. `PutParameter` refuses a name that is already taken unless the
|
|
124
|
+
request sets `Overwrite`, and an earlier version stays readable by number.
|
|
125
|
+
|
|
126
|
+
```typescript sim-ssm-parameter-versions
|
|
127
|
+
/**
|
|
128
|
+
* Overwriting a simulated parameter and reading an earlier version.
|
|
129
|
+
*/
|
|
130
|
+
|
|
131
|
+
import { GetParameterCommand, PutParameterCommand } from "@aws-sdk/client-ssm";
|
|
132
|
+
|
|
133
|
+
import { SimAws } from "@kensio/yulin";
|
|
134
|
+
|
|
135
|
+
const simAws = new SimAws();
|
|
136
|
+
const ssm = simAws.ssm();
|
|
137
|
+
|
|
138
|
+
await ssm.putParameter(
|
|
139
|
+
new PutParameterCommand({
|
|
140
|
+
Name: "/myapp/prod/db-host",
|
|
141
|
+
Type: "String",
|
|
142
|
+
Value: "db.internal",
|
|
143
|
+
}),
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const overwritten = await ssm.putParameter(
|
|
147
|
+
new PutParameterCommand({
|
|
148
|
+
Name: "/myapp/prod/db-host",
|
|
149
|
+
Value: "db2.internal",
|
|
150
|
+
Overwrite: true,
|
|
151
|
+
}),
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
console.log(overwritten.Version); // 2
|
|
155
|
+
|
|
156
|
+
const first = await ssm.getParameter(
|
|
157
|
+
new GetParameterCommand({ Name: "/myapp/prod/db-host:1" }),
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
console.log(first.Parameter?.Value); // "db.internal"
|
|
161
|
+
console.log(first.Parameter?.Selector); // ":1"
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
A parameter's type cannot change. Overwriting a `String` parameter as a `StringList` fails with
|
|
165
|
+
`HierarchyTypeMismatchException`, as it does on real AWS. Delete the parameter and create a new one
|
|
166
|
+
instead.
|
|
167
|
+
|
|
168
|
+
## Reading a hierarchy
|
|
169
|
+
|
|
170
|
+
`GetParametersByPath` reads a whole level of the hierarchy. Without `Recursive` it returns only the
|
|
171
|
+
level immediately below the path.
|
|
172
|
+
|
|
173
|
+
```typescript sim-ssm-parameters-by-path
|
|
174
|
+
/**
|
|
175
|
+
* Reading a hierarchy of simulated parameters as application configuration.
|
|
176
|
+
*/
|
|
177
|
+
|
|
178
|
+
import {
|
|
179
|
+
GetParametersByPathCommand,
|
|
180
|
+
PutParameterCommand,
|
|
181
|
+
} from "@aws-sdk/client-ssm";
|
|
182
|
+
|
|
183
|
+
import { SimAws } from "@kensio/yulin";
|
|
184
|
+
|
|
185
|
+
const simAws = new SimAws();
|
|
186
|
+
const ssm = simAws.ssm();
|
|
187
|
+
|
|
188
|
+
await ssm.putParameter(
|
|
189
|
+
new PutParameterCommand({
|
|
190
|
+
Name: "/myapp/prod/db-host",
|
|
191
|
+
Type: "String",
|
|
192
|
+
Value: "db.internal",
|
|
193
|
+
}),
|
|
194
|
+
);
|
|
195
|
+
await ssm.putParameter(
|
|
196
|
+
new PutParameterCommand({
|
|
197
|
+
Name: "/myapp/prod/db-port",
|
|
198
|
+
Type: "String",
|
|
199
|
+
Value: "5432",
|
|
200
|
+
}),
|
|
201
|
+
);
|
|
202
|
+
await ssm.putParameter(
|
|
203
|
+
new PutParameterCommand({
|
|
204
|
+
Name: "/myapp/test/db-host",
|
|
205
|
+
Type: "String",
|
|
206
|
+
Value: "db.test.internal",
|
|
207
|
+
}),
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
const listed = await ssm.getParametersByPath(
|
|
211
|
+
new GetParametersByPathCommand({ Path: "/myapp/prod" }),
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
console.log(listed.Parameters?.map((parameter) => parameter.Name));
|
|
215
|
+
// [ "/myapp/prod/db-host", "/myapp/prod/db-port" ]
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
A page holds ten parameters, as it does on real AWS. Follow `NextToken` to read the rest.
|
|
219
|
+
|
|
220
|
+
## Reading several parameters at once
|
|
221
|
+
|
|
222
|
+
`GetParameters` takes up to ten names. A name that resolves to nothing comes back in
|
|
223
|
+
`InvalidParameters`, and the request still succeeds. That is what makes a typo easy to miss.
|
|
224
|
+
|
|
225
|
+
```typescript sim-ssm-get-parameters-batch
|
|
226
|
+
/**
|
|
227
|
+
* Reading several simulated parameters, including one name with a typo.
|
|
228
|
+
*/
|
|
229
|
+
|
|
230
|
+
import { GetParametersCommand, PutParameterCommand } from "@aws-sdk/client-ssm";
|
|
231
|
+
|
|
232
|
+
import { SimAws } from "@kensio/yulin";
|
|
233
|
+
|
|
234
|
+
const simAws = new SimAws();
|
|
235
|
+
const ssm = simAws.ssm();
|
|
236
|
+
|
|
237
|
+
await ssm.putParameter(
|
|
238
|
+
new PutParameterCommand({
|
|
239
|
+
Name: "/myapp/prod/db-host",
|
|
240
|
+
Type: "String",
|
|
241
|
+
Value: "db.internal",
|
|
242
|
+
}),
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
const read = await ssm.getParameters(
|
|
246
|
+
new GetParametersCommand({
|
|
247
|
+
Names: ["/myapp/prod/db-host", "/myapp/prod/db-hostt"],
|
|
248
|
+
}),
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
console.log(read.Parameters?.map((parameter) => parameter.Name));
|
|
252
|
+
// [ "/myapp/prod/db-host" ]
|
|
253
|
+
console.log(read.InvalidParameters); // [ "/myapp/prod/db-hostt" ]
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
`GetParameters` still authorizes each name. One name the caller may not read fails the whole
|
|
257
|
+
request.
|
|
258
|
+
|
|
259
|
+
## String lists
|
|
260
|
+
|
|
261
|
+
A `StringList` value comes back as one comma-separated string, on read as well as on write, never
|
|
262
|
+
as an array.
|
|
263
|
+
|
|
264
|
+
```typescript sim-ssm-string-list
|
|
265
|
+
/**
|
|
266
|
+
* Reading a simulated StringList parameter.
|
|
267
|
+
*/
|
|
268
|
+
|
|
269
|
+
import { GetParameterCommand, PutParameterCommand } from "@aws-sdk/client-ssm";
|
|
270
|
+
|
|
271
|
+
import { SimAws } from "@kensio/yulin";
|
|
272
|
+
|
|
273
|
+
const simAws = new SimAws();
|
|
274
|
+
const ssm = simAws.ssm();
|
|
275
|
+
|
|
276
|
+
await ssm.putParameter(
|
|
277
|
+
new PutParameterCommand({
|
|
278
|
+
Name: "/myapp/prod/allowed-origins",
|
|
279
|
+
Type: "StringList",
|
|
280
|
+
Value: "https://one.example,https://two.example",
|
|
281
|
+
}),
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
const read = await ssm.getParameter(
|
|
285
|
+
new GetParameterCommand({ Name: "/myapp/prod/allowed-origins" }),
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
const origins = read.Parameter?.Value?.split(",") ?? [];
|
|
289
|
+
|
|
290
|
+
console.log(origins.length); // 2
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
## Parameter names
|
|
294
|
+
|
|
295
|
+
Name validation matches real Parameter Store, because a name it accepts and AWS refuses is a
|
|
296
|
+
deployment failure a passing test would have hidden. A name:
|
|
297
|
+
|
|
298
|
+
- may contain letters, digits, `_`, `.`, `-` and `/`
|
|
299
|
+
- must start with `/` if it contains a hierarchy at all
|
|
300
|
+
- may not have more than fifteen hierarchy levels
|
|
301
|
+
- may not start with `aws` or `ssm` in any case, which Parameter Store reserves
|
|
302
|
+
- may not contain spaces between characters, though surrounding spaces are stripped
|
|
303
|
+
- may not make an ARN longer than 1011 characters, counting the ARN prefix for the account and region
|
|
304
|
+
|
|
305
|
+
A `String` or `StringList` value holds at most 4KB, the standard tier limit. This is the one people
|
|
306
|
+
hit, usually by putting a whole JSON configuration blob in one parameter.
|
|
307
|
+
|
|
308
|
+
## Deploying a parameter from CloudFormation
|
|
309
|
+
|
|
310
|
+
Simulated CloudFormation creates a parameter from an `AWS::SSM::Parameter` resource, in the stack's
|
|
311
|
+
account and region. The parameter is written through `PutParameter`. A template-created parameter is
|
|
312
|
+
the same thing an SDK caller would get, with the same name validation, the same ARN, and version 1.
|
|
313
|
+
|
|
314
|
+
`Ref` on the resource gives the parameter name, as it does on real AWS, and it can be handed straight
|
|
315
|
+
to `GetParameter`. `Fn::GetAtt … Type` and `Fn::GetAtt … Value` give those
|
|
316
|
+
properties.
|
|
317
|
+
|
|
318
|
+
```typescript sim-ssm-cloudformation-parameter
|
|
319
|
+
/**
|
|
320
|
+
* Deploying a parameter from a CloudFormation template and reading it back.
|
|
321
|
+
*/
|
|
322
|
+
|
|
323
|
+
import { GetParameterCommand } from "@aws-sdk/client-ssm";
|
|
324
|
+
|
|
325
|
+
import { SimAws } from "@kensio/yulin";
|
|
326
|
+
|
|
327
|
+
const simAws = new SimAws();
|
|
328
|
+
|
|
329
|
+
const stack = await simAws.cloudFormation().deployTemplate({
|
|
330
|
+
stackName: "config-stack",
|
|
331
|
+
template: {
|
|
332
|
+
Resources: {
|
|
333
|
+
DbHost: {
|
|
334
|
+
Type: "AWS::SSM::Parameter",
|
|
335
|
+
Properties: {
|
|
336
|
+
Name: "/myapp/prod/db-host",
|
|
337
|
+
Type: "String",
|
|
338
|
+
Value: "db.internal",
|
|
339
|
+
Description: "Where the application database lives",
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
Outputs: {
|
|
344
|
+
DbHostParameter: {
|
|
345
|
+
Value: { Ref: "DbHost" },
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
await stack.waitForDeployComplete();
|
|
352
|
+
|
|
353
|
+
// Ref resolves to the parameter name, so it works as a GetParameter Name.
|
|
354
|
+
const parameterName = stack.output("DbHostParameter");
|
|
355
|
+
|
|
356
|
+
const read = await simAws
|
|
357
|
+
.ssm()
|
|
358
|
+
.getParameter(new GetParameterCommand({ Name: parameterName }));
|
|
359
|
+
|
|
360
|
+
console.log(read.Parameter?.Value); // "db.internal"
|
|
361
|
+
console.log(read.Parameter?.Version); // 1
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
A parameter with no `Name` is named after its logical ID. Real CloudFormation generates a name from
|
|
365
|
+
the stack name and its own random characters. A template cannot rely on the exact generated name
|
|
366
|
+
either way.
|
|
367
|
+
|
|
368
|
+
An IAM policy granting access to a template-created parameter needs the ARN rather than the `Ref`.
|
|
369
|
+
Build it with `Fn::Sub`, remembering that the ARN drops the name's leading slash:
|
|
370
|
+
|
|
371
|
+
```yaml
|
|
372
|
+
Resource: !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/myapp/prod/db-host"
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
CDK does this for you. `ssm.StringParameter` with `grantRead(fn)` synthesises a template that deploys
|
|
376
|
+
here without hand-editing.
|
|
377
|
+
|
|
378
|
+
## Reading a parameter with a dynamic reference
|
|
379
|
+
|
|
380
|
+
A template reads a parameter that already exists through a `{{resolve:ssm:...}}` dynamic reference.
|
|
381
|
+
The reference is replaced with the parameter's value as the resource holding it is created.
|
|
382
|
+
|
|
383
|
+
`{{resolve:ssm:name}}` reads the current version, and `{{resolve:ssm:name:3}}` reads version 3. A
|
|
384
|
+
reference can sit inside a longer string, where only the reference itself is replaced.
|
|
385
|
+
|
|
386
|
+
```typescript sim-ssm-dynamic-reference
|
|
387
|
+
/**
|
|
388
|
+
* Reading an existing parameter from a template with a dynamic reference.
|
|
389
|
+
*/
|
|
390
|
+
|
|
391
|
+
import { GetParameterCommand, PutParameterCommand } from "@aws-sdk/client-ssm";
|
|
392
|
+
|
|
393
|
+
import { SimAws } from "@kensio/yulin";
|
|
394
|
+
|
|
395
|
+
const simAws = new SimAws();
|
|
396
|
+
|
|
397
|
+
await simAws.ssm().putParameter(
|
|
398
|
+
new PutParameterCommand({
|
|
399
|
+
Name: "/myapp/prod/db-host",
|
|
400
|
+
Type: "String",
|
|
401
|
+
Value: "db.internal",
|
|
402
|
+
}),
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
const stack = await simAws.cloudFormation().deployTemplate({
|
|
406
|
+
stackName: "app-stack",
|
|
407
|
+
template: {
|
|
408
|
+
Resources: {
|
|
409
|
+
DbUrl: {
|
|
410
|
+
Type: "AWS::SSM::Parameter",
|
|
411
|
+
Properties: {
|
|
412
|
+
Name: "/myapp/prod/db-url",
|
|
413
|
+
Type: "String",
|
|
414
|
+
Value: "postgres://{{resolve:ssm:/myapp/prod/db-host}}:5432/app",
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
await stack.waitForDeployComplete();
|
|
422
|
+
|
|
423
|
+
const read = await simAws
|
|
424
|
+
.ssm()
|
|
425
|
+
.getParameter(new GetParameterCommand({ Name: "/myapp/prod/db-url" }));
|
|
426
|
+
|
|
427
|
+
console.log(read.Parameter?.Value); // "postgres://db.internal:5432/app"
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
CDK emits one of these from `ssm.StringParameter.valueForStringParameter` when a version is given,
|
|
431
|
+
and from `fromStringParameterAttributes` with `forceDynamicReference`.
|
|
432
|
+
|
|
433
|
+
A reference inside `Fn::Sub` is read after the variables around it are substituted, so
|
|
434
|
+
`!Sub "{{resolve:ssm:/myapp/${Environment}/db-host}}"` looks up the name the substitution produced.
|
|
435
|
+
|
|
436
|
+
A `StringList` parameter resolves to the comma-separated string Parameter Store holds, which
|
|
437
|
+
`Fn::Split` then splits.
|
|
438
|
+
|
|
439
|
+
Real CloudFormation makes no dependency out of a dynamic reference, and neither does this. A
|
|
440
|
+
parameter another resource of the same stack creates is only there in time when the template says
|
|
441
|
+
`DependsOn`.
|
|
442
|
+
|
|
443
|
+
The parameter is read with `GetParameter`, as the caller deploying the stack. A policy denying that
|
|
444
|
+
read fails the resource holding the reference, the way it fails a real deployment. A stack deployed
|
|
445
|
+
without a caller reads as the account root.
|
|
446
|
+
|
|
447
|
+
A `SecureString` parameter is read through `{{resolve:ssm-secure:...}}`, which the next section
|
|
448
|
+
covers. `{{resolve:secretsmanager:...}}` reads a secret, and
|
|
449
|
+
[simulated Secrets Manager](https://yulinsim.dev/services/secretsmanager/#reading-a-secret-with-a-dynamic-reference)
|
|
450
|
+
covers that one.
|
|
451
|
+
|
|
452
|
+
### A reference the simulation cannot answer
|
|
453
|
+
|
|
454
|
+
Simulated CloudFormation deploys what it can. A reference naming a parameter that was never created
|
|
455
|
+
resolves to `dummy-value-for-<name>`, and the stack carries on deploying. A template reading
|
|
456
|
+
configuration a test does not care about is still worth deploying for everything else in it.
|
|
457
|
+
|
|
458
|
+
The substitution is recorded on
|
|
459
|
+
[`stack.ignoredProperties`](https://yulinsim.dev/services/cloudformation/#properties-a-resource-was-created-without),
|
|
460
|
+
naming the property that held the reference and why the value is a stand-in. A version the parameter
|
|
461
|
+
never had, a `SecureString`, and a body that is not a name and an optional integer version are all
|
|
462
|
+
recorded the same way.
|
|
463
|
+
|
|
464
|
+
## Reading a SecureString with an ssm-secure reference
|
|
465
|
+
|
|
466
|
+
`{{resolve:ssm-secure:name}}` reads a `SecureString` parameter and resolves to its decrypted value.
|
|
467
|
+
`{{resolve:ssm-secure:name:3}}` reads version 3. The string scanning is the one plain `ssm`
|
|
468
|
+
references use, so a reference can sit inside a longer value and inside `Fn::Sub`.
|
|
469
|
+
|
|
470
|
+
Decryption goes to simulated KMS under the key the parameter was written with, as the caller
|
|
471
|
+
deploying the stack. A caller lacking `kms:Decrypt` on that key fails the resource with the
|
|
472
|
+
`AccessDenied` a decrypting `GetParameter` raises (see
|
|
473
|
+
[A customer managed key needs its own permission](#a-customer-managed-key-needs-its-own-permission)).
|
|
474
|
+
|
|
475
|
+
CDK writes one of these from `SecretValue.ssmSecure`, from
|
|
476
|
+
`ssm.StringParameter.fromSecureStringParameterAttributes` and from the deprecated
|
|
477
|
+
`valueForSecureStringParameter`.
|
|
478
|
+
|
|
479
|
+
### Where a reference is accepted
|
|
480
|
+
|
|
481
|
+
Real CloudFormation reads an `ssm-secure` reference in eleven resource properties and refuses it in
|
|
482
|
+
every other one. Simulated CloudFormation holds a template to the same list.
|
|
483
|
+
|
|
484
|
+
| Resource | Property |
|
|
485
|
+
| -------------------------------------- | ------------------------------------------- |
|
|
486
|
+
| `AWS::DirectoryService::MicrosoftAD` | `Password` |
|
|
487
|
+
| `AWS::DirectoryService::SimpleAD` | `Password` |
|
|
488
|
+
| `AWS::ElastiCache::ReplicationGroup` | `AuthToken` |
|
|
489
|
+
| `AWS::IAM::User` | `LoginProfile.Password` |
|
|
490
|
+
| `AWS::KinesisFirehose::DeliveryStream` | `RedshiftDestinationConfiguration.Password` |
|
|
491
|
+
| `AWS::OpsWorks::App` | `Source.Password` |
|
|
492
|
+
| `AWS::OpsWorks::Stack` | `CustomCookbooksSource.Password` |
|
|
493
|
+
| `AWS::OpsWorks::Stack` | `RdsDbInstances.DbPassword` |
|
|
494
|
+
| `AWS::RDS::DBCluster` | `MasterUserPassword` |
|
|
495
|
+
| `AWS::RDS::DBInstance` | `MasterUserPassword` |
|
|
496
|
+
| `AWS::Redshift::Cluster` | `MasterUserPassword` |
|
|
497
|
+
|
|
498
|
+
A reference anywhere else fails the resource, naming the property that held it. A template breaking
|
|
499
|
+
this rule is broken on real CloudFormation too.
|
|
500
|
+
|
|
501
|
+
`AWS::IAM::User` `LoginProfile.Password` is the pair a simulated resource holds today, and it is
|
|
502
|
+
where CDK writes `SecretValue.ssmSecure`. The other ten name resource types this simulation has yet
|
|
503
|
+
to reach.
|
|
504
|
+
|
|
505
|
+
```typescript sim-ssm-secure-dynamic-reference
|
|
506
|
+
/**
|
|
507
|
+
* Reading a SecureString parameter into a resource property from a template.
|
|
508
|
+
*/
|
|
509
|
+
|
|
510
|
+
import { PutParameterCommand } from "@aws-sdk/client-ssm";
|
|
511
|
+
|
|
512
|
+
import { SimAws } from "@kensio/yulin";
|
|
513
|
+
|
|
514
|
+
const simAws = new SimAws();
|
|
515
|
+
|
|
516
|
+
await simAws.ssm().putParameter(
|
|
517
|
+
new PutParameterCommand({
|
|
518
|
+
Name: "/myapp/prod/console-password",
|
|
519
|
+
Type: "SecureString",
|
|
520
|
+
Value: "hunter2",
|
|
521
|
+
}),
|
|
522
|
+
);
|
|
523
|
+
|
|
524
|
+
const stack = await simAws.cloudFormation().deployTemplate({
|
|
525
|
+
stackName: "console-stack",
|
|
526
|
+
template: {
|
|
527
|
+
Resources: {
|
|
528
|
+
ConsoleUser: {
|
|
529
|
+
Type: "AWS::IAM::User",
|
|
530
|
+
Properties: {
|
|
531
|
+
UserName: "ConsoleUser",
|
|
532
|
+
LoginProfile: {
|
|
533
|
+
Password: "{{resolve:ssm-secure:/myapp/prod/console-password}}",
|
|
534
|
+
},
|
|
535
|
+
},
|
|
536
|
+
},
|
|
537
|
+
},
|
|
538
|
+
},
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
await stack.waitForDeployComplete();
|
|
542
|
+
|
|
543
|
+
const user = simAws
|
|
544
|
+
.iam()
|
|
545
|
+
.users.values()
|
|
546
|
+
.find((each) => each.userName === "ConsoleUser");
|
|
547
|
+
|
|
548
|
+
console.log(user?.loginProfile?.password); // "hunter2"
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
A reference naming a `String` or a `StringList` parameter fails the resource, as real CloudFormation
|
|
552
|
+
fails it. Read a parameter stored in the clear with a plain `{{resolve:ssm:...}}` reference.
|
|
553
|
+
|
|
554
|
+
### An ssm-secure reference the simulation cannot answer
|
|
555
|
+
|
|
556
|
+
The best-effort path is the one plain `ssm` references take. A reference naming a parameter that was
|
|
557
|
+
never created, or a version the parameter never had, resolves to `dummy-value-for-<name>` and the
|
|
558
|
+
stack carries on deploying. So does a body that is not a name and an optional integer version. Each
|
|
559
|
+
substitution is recorded on
|
|
560
|
+
[`stack.ignoredProperties`](https://yulinsim.dev/services/cloudformation/#properties-a-resource-was-created-without).
|
|
561
|
+
|
|
562
|
+
## Reading a parameter through a template Parameter
|
|
563
|
+
|
|
564
|
+
A template `Parameters` entry declared as `AWS::SSM::Parameter::Value<String>` is given a parameter
|
|
565
|
+
name, and `Ref` on it gives the value held under that name in the stack's account and region. The
|
|
566
|
+
name comes from the value passed to `CreateStack`, and from the template `Default` when no value is
|
|
567
|
+
passed.
|
|
568
|
+
|
|
569
|
+
```typescript sim-ssm-template-parameter-value
|
|
570
|
+
/**
|
|
571
|
+
* Reading configuration into a template through a Parameter Store value type.
|
|
572
|
+
*/
|
|
573
|
+
|
|
574
|
+
import { PutParameterCommand } from "@aws-sdk/client-ssm";
|
|
575
|
+
|
|
576
|
+
import { SimAws } from "@kensio/yulin";
|
|
577
|
+
|
|
578
|
+
const simAws = new SimAws();
|
|
579
|
+
|
|
580
|
+
await simAws.ssm().putParameter(
|
|
581
|
+
new PutParameterCommand({
|
|
582
|
+
Name: "/myapp/prod/uploads-bucket",
|
|
583
|
+
Type: "String",
|
|
584
|
+
Value: "myapp-prod-uploads",
|
|
585
|
+
}),
|
|
586
|
+
);
|
|
587
|
+
|
|
588
|
+
const stack = await simAws.cloudFormation().deployTemplate({
|
|
589
|
+
stackName: "app-stack",
|
|
590
|
+
template: {
|
|
591
|
+
Parameters: {
|
|
592
|
+
UploadsBucketName: {
|
|
593
|
+
Type: "AWS::SSM::Parameter::Value<String>",
|
|
594
|
+
Default: "/myapp/prod/uploads-bucket",
|
|
595
|
+
},
|
|
596
|
+
},
|
|
597
|
+
Resources: {
|
|
598
|
+
UploadsBucket: {
|
|
599
|
+
Type: "AWS::S3::Bucket",
|
|
600
|
+
Properties: { BucketName: { Ref: "UploadsBucketName" } },
|
|
601
|
+
},
|
|
602
|
+
},
|
|
603
|
+
},
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
await stack.waitForDeployComplete();
|
|
607
|
+
|
|
608
|
+
// The Bucket was created under the name the parameter holds.
|
|
609
|
+
console.log(simAws.s3().getSimBucketByName("myapp-prod-uploads")?.bucketName);
|
|
610
|
+
// "myapp-prod-uploads"
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
CDK emits this Parameter from `ssm.StringParameter.valueForStringParameter(scope, name)` called
|
|
614
|
+
without a version, carrying the name as the `Default`.
|
|
615
|
+
|
|
616
|
+
`AWS::SSM::Parameter::Value<List<String>>` resolves to the stored comma-separated string split into
|
|
617
|
+
a list, which `Fn::Select` then reads by index. `AWS::SSM::Parameter::Value<CommaDelimitedList>`
|
|
618
|
+
resolves the same way. `ssm.StringListParameter.fromListParameterAttributes` without a version emits
|
|
619
|
+
the first of the two.
|
|
620
|
+
|
|
621
|
+
The `Parameters` section is read before any resource is created, as it is on real AWS. A name a
|
|
622
|
+
resource of the same stack goes on to create is never there in time, whatever the template says
|
|
623
|
+
about `DependsOn`.
|
|
624
|
+
|
|
625
|
+
The stored value goes unchecked against the inner type. A name held against something other than an
|
|
626
|
+
image ID resolves under `AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>`, where real CloudFormation
|
|
627
|
+
refuses the stack.
|
|
628
|
+
|
|
629
|
+
### A name the simulation cannot answer
|
|
630
|
+
|
|
631
|
+
The same best-effort answer a dynamic reference gets. A name Parameter Store has never held resolves
|
|
632
|
+
to `dummy-value-for-<name>`, and the stack carries on deploying. A `SecureString` resolves that way
|
|
633
|
+
too, since real CloudFormation refuses to read one into a template Parameter.
|
|
634
|
+
|
|
635
|
+
The substitution is recorded on
|
|
636
|
+
[`stack.ignoredProperties`](https://yulinsim.dev/services/cloudformation/#properties-a-resource-was-created-without).
|
|
637
|
+
The `logicalId` names the template Parameter, the `resourceType` gives its declared type, and the
|
|
638
|
+
`path` is `Parameters.<parameter name>`.
|
|
639
|
+
|
|
640
|
+
## Reading configuration in a Lambda handler
|
|
641
|
+
|
|
642
|
+
Function code that reads its configuration on cold start needs no special treatment. Any
|
|
643
|
+
`@aws-sdk/client-ssm` client the handler creates is intercepted and dispatched with the function's
|
|
644
|
+
execution role as the caller. The role's policy decides whether the read succeeds.
|
|
645
|
+
|
|
646
|
+
```typescript sim-ssm-lambda-handler-config
|
|
647
|
+
/**
|
|
648
|
+
* A simulated Lambda handler reading its configuration from Parameter Store.
|
|
649
|
+
*/
|
|
650
|
+
|
|
651
|
+
import { CreateRoleCommand, PutRolePolicyCommand } from "@aws-sdk/client-iam";
|
|
652
|
+
import { CreateFunctionCommand, InvokeCommand } from "@aws-sdk/client-lambda";
|
|
653
|
+
import { PutParameterCommand } from "@aws-sdk/client-ssm";
|
|
654
|
+
|
|
655
|
+
import { SimAws } from "@kensio/yulin";
|
|
656
|
+
import { makeLambdaCodeZip } from "@kensio/yulin/lambda";
|
|
657
|
+
|
|
658
|
+
const simAws = new SimAws();
|
|
659
|
+
const accountId = simAws.defaultAccountId;
|
|
660
|
+
const regionName = simAws.defaultRegionName;
|
|
661
|
+
|
|
662
|
+
await simAws.ssm().putParameter(
|
|
663
|
+
new PutParameterCommand({
|
|
664
|
+
Name: "/myapp/prod/db-host",
|
|
665
|
+
Type: "String",
|
|
666
|
+
Value: "db.internal",
|
|
667
|
+
}),
|
|
668
|
+
);
|
|
669
|
+
|
|
670
|
+
const role = await simAws.iam().createRole(
|
|
671
|
+
new CreateRoleCommand({
|
|
672
|
+
RoleName: "ConfigReaderRole",
|
|
673
|
+
AssumeRolePolicyDocument: JSON.stringify({
|
|
674
|
+
Version: "2012-10-17",
|
|
675
|
+
Statement: {
|
|
676
|
+
Effect: "Allow",
|
|
677
|
+
Principal: { Service: "lambda.amazonaws.com" },
|
|
678
|
+
Action: "sts:AssumeRole",
|
|
679
|
+
},
|
|
680
|
+
}),
|
|
681
|
+
}),
|
|
682
|
+
);
|
|
683
|
+
|
|
684
|
+
await simAws.iam().putRolePolicy(
|
|
685
|
+
new PutRolePolicyCommand({
|
|
686
|
+
RoleName: "ConfigReaderRole",
|
|
687
|
+
PolicyName: "ReadConfig",
|
|
688
|
+
PolicyDocument: JSON.stringify({
|
|
689
|
+
Version: "2012-10-17",
|
|
690
|
+
Statement: {
|
|
691
|
+
Effect: "Allow",
|
|
692
|
+
Action: "ssm:GetParameter",
|
|
693
|
+
Resource: `arn:aws:ssm:${regionName}:${accountId}:parameter/myapp/prod/*`,
|
|
694
|
+
},
|
|
695
|
+
}),
|
|
696
|
+
}),
|
|
697
|
+
);
|
|
698
|
+
|
|
699
|
+
const handlerCode = [
|
|
700
|
+
'const { SSMClient, GetParameterCommand } = require("@aws-sdk/client-ssm");',
|
|
701
|
+
"exports.handler = async () => {",
|
|
702
|
+
" const client = new SSMClient({});",
|
|
703
|
+
' const command = new GetParameterCommand({ Name: "/myapp/prod/db-host" });',
|
|
704
|
+
" const out = await client.send(command);",
|
|
705
|
+
" return out.Parameter.Value;",
|
|
706
|
+
"};",
|
|
707
|
+
].join("\n");
|
|
708
|
+
|
|
709
|
+
const zipFile = makeLambdaCodeZip({ "index.js": handlerCode });
|
|
710
|
+
|
|
711
|
+
await simAws.lambda().createFunction(
|
|
712
|
+
new CreateFunctionCommand({
|
|
713
|
+
FunctionName: "config-reader",
|
|
714
|
+
Role: role.Role.Arn,
|
|
715
|
+
Handler: "index.handler",
|
|
716
|
+
Code: { ZipFile: zipFile },
|
|
717
|
+
}),
|
|
718
|
+
);
|
|
719
|
+
|
|
720
|
+
await simAws.backgroundTasksComplete();
|
|
721
|
+
|
|
722
|
+
const invoked = await simAws
|
|
723
|
+
.lambda()
|
|
724
|
+
.invoke(new InvokeCommand({ FunctionName: "config-reader" }));
|
|
725
|
+
|
|
726
|
+
console.log(Buffer.from(invoked.Payload ?? []).toString("utf8")); // "db.internal"
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
## Account and region scoping
|
|
730
|
+
|
|
731
|
+
A parameter belongs to one account and region, as it does on real AWS. The same name in another
|
|
732
|
+
scope is another parameter.
|
|
733
|
+
|
|
734
|
+
```typescript sim-ssm-scoping
|
|
735
|
+
/**
|
|
736
|
+
* The same simulated parameter name in two Account and Region scopes.
|
|
737
|
+
*/
|
|
738
|
+
|
|
739
|
+
import { GetParameterCommand, PutParameterCommand } from "@aws-sdk/client-ssm";
|
|
740
|
+
|
|
741
|
+
import { SimAws } from "@kensio/yulin";
|
|
742
|
+
|
|
743
|
+
const simAws = new SimAws();
|
|
744
|
+
|
|
745
|
+
await simAws
|
|
746
|
+
.account("111111111111")
|
|
747
|
+
.region("eu-west-2")
|
|
748
|
+
.ssm()
|
|
749
|
+
.putParameter(
|
|
750
|
+
new PutParameterCommand({
|
|
751
|
+
Name: "/myapp/db-host",
|
|
752
|
+
Type: "String",
|
|
753
|
+
Value: "eu.db.internal",
|
|
754
|
+
}),
|
|
755
|
+
);
|
|
756
|
+
|
|
757
|
+
await simAws
|
|
758
|
+
.account("222222222222")
|
|
759
|
+
.region("us-east-1")
|
|
760
|
+
.ssm()
|
|
761
|
+
.putParameter(
|
|
762
|
+
new PutParameterCommand({
|
|
763
|
+
Name: "/myapp/db-host",
|
|
764
|
+
Type: "String",
|
|
765
|
+
Value: "us.db.internal",
|
|
766
|
+
}),
|
|
767
|
+
);
|
|
768
|
+
|
|
769
|
+
const read = await simAws
|
|
770
|
+
.account("111111111111")
|
|
771
|
+
.region("eu-west-2")
|
|
772
|
+
.ssm()
|
|
773
|
+
.getParameter(new GetParameterCommand({ Name: "/myapp/db-host" }));
|
|
774
|
+
|
|
775
|
+
console.log(read.Parameter?.ARN);
|
|
776
|
+
// "arn:aws:ssm:eu-west-2:111111111111:parameter/myapp/db-host"
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
## SecureString parameters
|
|
780
|
+
|
|
781
|
+
A `SecureString` value is encrypted through simulated KMS, under the `aws/ssm` AWS managed key unless
|
|
782
|
+
the request names a key of its own. Simulated KMS creates that managed key the first time something
|
|
783
|
+
asks for it, with no setup needed.
|
|
784
|
+
|
|
785
|
+
A read returns the ciphertext unless it asks for decryption. This is the mistake that is easy to make
|
|
786
|
+
and hard to see. A handler that forgets `WithDecryption` parses a base64 blob as if it were a
|
|
787
|
+
password.
|
|
788
|
+
|
|
789
|
+
```typescript sim-ssm-secure-string
|
|
790
|
+
/**
|
|
791
|
+
* Writing and reading a simulated SecureString parameter.
|
|
792
|
+
*/
|
|
793
|
+
|
|
794
|
+
import { GetParameterCommand, PutParameterCommand } from "@aws-sdk/client-ssm";
|
|
795
|
+
|
|
796
|
+
import { SimAws } from "@kensio/yulin";
|
|
797
|
+
|
|
798
|
+
const simAws = new SimAws();
|
|
799
|
+
const ssm = simAws.ssm();
|
|
800
|
+
|
|
801
|
+
await ssm.putParameter(
|
|
802
|
+
new PutParameterCommand({
|
|
803
|
+
Name: "/myapp/prod/db-password",
|
|
804
|
+
Type: "SecureString",
|
|
805
|
+
Value: "hunter2",
|
|
806
|
+
}),
|
|
807
|
+
);
|
|
808
|
+
|
|
809
|
+
const encrypted = await ssm.getParameter(
|
|
810
|
+
new GetParameterCommand({ Name: "/myapp/prod/db-password" }),
|
|
811
|
+
);
|
|
812
|
+
|
|
813
|
+
console.log(encrypted.Parameter?.Value); // a base64 ciphertext, not "hunter2"
|
|
814
|
+
|
|
815
|
+
const decrypted = await ssm.getParameter(
|
|
816
|
+
new GetParameterCommand({
|
|
817
|
+
Name: "/myapp/prod/db-password",
|
|
818
|
+
WithDecryption: true,
|
|
819
|
+
}),
|
|
820
|
+
);
|
|
821
|
+
|
|
822
|
+
console.log(decrypted.Parameter?.Value); // "hunter2"
|
|
823
|
+
```
|
|
824
|
+
|
|
825
|
+
`WithDecryption` on a `String` or `StringList` parameter is ignored, as real Parameter Store ignores
|
|
826
|
+
it.
|
|
827
|
+
|
|
828
|
+
Pass `KeyId` to encrypt under a customer managed key instead. A `KeyId` naming a key that is absent,
|
|
829
|
+
disabled, or pending deletion fails with `InvalidKeyId`. That is how real Parameter Store reports
|
|
830
|
+
every KMS key problem.
|
|
831
|
+
|
|
832
|
+
Each value is bound to its own parameter's ARN as the KMS encryption context, under the
|
|
833
|
+
`PARAMETER_ARN` key. A ciphertext lifted out of one parameter cannot be decrypted as another.
|
|
834
|
+
|
|
835
|
+
### A customer managed key needs its own permission
|
|
836
|
+
|
|
837
|
+
Encrypting and decrypting go to simulated KMS as the caller, not as the service. Under a customer
|
|
838
|
+
managed key a write needs `kms:Encrypt` on the key on top of `ssm:PutParameter` on the parameter, and
|
|
839
|
+
a decrypting read needs `kms:Decrypt` on top of `ssm:GetParameter`. A role granted one and not the
|
|
840
|
+
other fails here, ahead of a deployment.
|
|
841
|
+
|
|
842
|
+
```typescript sim-ssm-secure-string-permissions
|
|
843
|
+
/**
|
|
844
|
+
* A Role allowed to read a simulated SecureString but not to decrypt it.
|
|
845
|
+
*/
|
|
846
|
+
|
|
847
|
+
import { CreateRoleCommand, PutRolePolicyCommand } from "@aws-sdk/client-iam";
|
|
848
|
+
import { CreateKeyCommand } from "@aws-sdk/client-kms";
|
|
849
|
+
import { GetParameterCommand, PutParameterCommand } from "@aws-sdk/client-ssm";
|
|
850
|
+
|
|
851
|
+
import { SimAws } from "@kensio/yulin";
|
|
852
|
+
|
|
853
|
+
const simAws = new SimAws();
|
|
854
|
+
const accountId = simAws.defaultAccountId;
|
|
855
|
+
|
|
856
|
+
const key = await simAws
|
|
857
|
+
.kms()
|
|
858
|
+
.createKey(new CreateKeyCommand({ Description: "Parameter key" }));
|
|
859
|
+
|
|
860
|
+
await simAws.ssm().putParameter(
|
|
861
|
+
new PutParameterCommand({
|
|
862
|
+
Name: "/myapp/prod/db-password",
|
|
863
|
+
Type: "SecureString",
|
|
864
|
+
Value: "hunter2",
|
|
865
|
+
KeyId: key.KeyMetadata?.Arn,
|
|
866
|
+
}),
|
|
867
|
+
);
|
|
868
|
+
|
|
869
|
+
const role = await simAws.iam().createRole(
|
|
870
|
+
new CreateRoleCommand({
|
|
871
|
+
RoleName: "ConfigReader",
|
|
872
|
+
AssumeRolePolicyDocument: JSON.stringify({
|
|
873
|
+
Version: "2012-10-17",
|
|
874
|
+
Statement: {
|
|
875
|
+
Effect: "Allow",
|
|
876
|
+
Principal: { AWS: `arn:aws:iam::${accountId}:root` },
|
|
877
|
+
Action: "sts:AssumeRole",
|
|
878
|
+
},
|
|
879
|
+
}),
|
|
880
|
+
}),
|
|
881
|
+
);
|
|
882
|
+
|
|
883
|
+
// The parameter is allowed, the key is not.
|
|
884
|
+
await simAws.iam().putRolePolicy(
|
|
885
|
+
new PutRolePolicyCommand({
|
|
886
|
+
RoleName: "ConfigReader",
|
|
887
|
+
PolicyName: "ReadDbPassword",
|
|
888
|
+
PolicyDocument: JSON.stringify({
|
|
889
|
+
Version: "2012-10-17",
|
|
890
|
+
Statement: {
|
|
891
|
+
Effect: "Allow",
|
|
892
|
+
Action: "ssm:GetParameter",
|
|
893
|
+
Resource: "*",
|
|
894
|
+
},
|
|
895
|
+
}),
|
|
896
|
+
}),
|
|
897
|
+
);
|
|
898
|
+
|
|
899
|
+
const caller = { kind: "arn", arn: role.Role.Arn } as const;
|
|
900
|
+
|
|
901
|
+
try {
|
|
902
|
+
await simAws.ssm().getParameter(
|
|
903
|
+
new GetParameterCommand({
|
|
904
|
+
Name: "/myapp/prod/db-password",
|
|
905
|
+
WithDecryption: true,
|
|
906
|
+
}),
|
|
907
|
+
{ caller },
|
|
908
|
+
);
|
|
909
|
+
} catch (error) {
|
|
910
|
+
console.log((error as Error).name); // "AccessDenied"
|
|
911
|
+
}
|
|
912
|
+
```
|
|
913
|
+
|
|
914
|
+
### The aws/ssm managed key needs no permission
|
|
915
|
+
|
|
916
|
+
A parameter naming no key is encrypted under the `aws/ssm` AWS managed key, and that key asks the
|
|
917
|
+
caller for nothing. Parameter Store supplies `kms:ViaService`, and the managed key's policy allows
|
|
918
|
+
the account's principals to use it through Systems Manager. A role holding only `ssm:GetParameter`
|
|
919
|
+
therefore reads the decrypted value, as it does on real AWS.
|
|
920
|
+
|
|
921
|
+
The same role cannot take the ciphertext to KMS itself, because that policy allows nothing to a
|
|
922
|
+
request arriving directly.
|
|
923
|
+
|
|
924
|
+
```typescript sim-ssm-secure-string-managed-key
|
|
925
|
+
/**
|
|
926
|
+
* Reading a simulated SecureString under the aws/ssm managed key.
|
|
927
|
+
*/
|
|
928
|
+
|
|
929
|
+
import { CreateRoleCommand, PutRolePolicyCommand } from "@aws-sdk/client-iam";
|
|
930
|
+
import { GetParameterCommand, PutParameterCommand } from "@aws-sdk/client-ssm";
|
|
931
|
+
|
|
932
|
+
import { SimAws } from "@kensio/yulin";
|
|
933
|
+
|
|
934
|
+
const simAws = new SimAws();
|
|
935
|
+
const accountId = simAws.defaultAccountId;
|
|
936
|
+
|
|
937
|
+
await simAws.ssm().putParameter(
|
|
938
|
+
new PutParameterCommand({
|
|
939
|
+
Name: "/myapp/prod/db-password",
|
|
940
|
+
Type: "SecureString",
|
|
941
|
+
Value: "hunter2",
|
|
942
|
+
}),
|
|
943
|
+
);
|
|
944
|
+
|
|
945
|
+
const role = await simAws.iam().createRole(
|
|
946
|
+
new CreateRoleCommand({
|
|
947
|
+
RoleName: "ConfigReader",
|
|
948
|
+
AssumeRolePolicyDocument: JSON.stringify({
|
|
949
|
+
Version: "2012-10-17",
|
|
950
|
+
Statement: {
|
|
951
|
+
Effect: "Allow",
|
|
952
|
+
Principal: { AWS: `arn:aws:iam::${accountId}:root` },
|
|
953
|
+
Action: "sts:AssumeRole",
|
|
954
|
+
},
|
|
955
|
+
}),
|
|
956
|
+
}),
|
|
957
|
+
);
|
|
958
|
+
|
|
959
|
+
// The parameter, and no KMS permission at all.
|
|
960
|
+
await simAws.iam().putRolePolicy(
|
|
961
|
+
new PutRolePolicyCommand({
|
|
962
|
+
RoleName: "ConfigReader",
|
|
963
|
+
PolicyName: "ReadDbPassword",
|
|
964
|
+
PolicyDocument: JSON.stringify({
|
|
965
|
+
Version: "2012-10-17",
|
|
966
|
+
Statement: {
|
|
967
|
+
Effect: "Allow",
|
|
968
|
+
Action: "ssm:GetParameter",
|
|
969
|
+
Resource: "*",
|
|
970
|
+
},
|
|
971
|
+
}),
|
|
972
|
+
}),
|
|
973
|
+
);
|
|
974
|
+
|
|
975
|
+
const read = await simAws.ssm().getParameter(
|
|
976
|
+
new GetParameterCommand({
|
|
977
|
+
Name: "/myapp/prod/db-password",
|
|
978
|
+
WithDecryption: true,
|
|
979
|
+
}),
|
|
980
|
+
{ caller: { kind: "arn", arn: role.Role.Arn } },
|
|
981
|
+
);
|
|
982
|
+
|
|
983
|
+
console.log(read.Parameter?.Value); // "hunter2"
|
|
984
|
+
```
|
|
985
|
+
|
|
986
|
+
`DescribeParameters` reports the key each `SecureString` is encrypted under as `KeyId`.
|
|
987
|
+
|
|
988
|
+
## Available functionality
|
|
989
|
+
|
|
990
|
+
Sim SSM currently supports:
|
|
991
|
+
|
|
992
|
+
- `PutParameterCommand`, creating a parameter or overwriting one
|
|
993
|
+
- `GetParameterCommand`, by name, by `name:version` or by `name:label`
|
|
994
|
+
- `GetParametersCommand`, reporting names it could not resolve in `InvalidParameters`
|
|
995
|
+
- `GetParametersByPathCommand`, with and without `Recursive`
|
|
996
|
+
- `DeleteParameterCommand` and `DeleteParametersCommand`
|
|
997
|
+
- `DescribeParametersCommand`
|
|
998
|
+
- `String`, `StringList` and `SecureString` parameter types
|
|
999
|
+
- `SecureString` values encrypted through simulated KMS, decrypted only with `WithDecryption`
|
|
1000
|
+
- The `AWS::SSM::Parameter` CloudFormation resource, including `Ref` and `Fn::GetAtt`
|
|
1001
|
+
- `{{resolve:ssm:...}}` dynamic references in CloudFormation resource properties, by version or by
|
|
1002
|
+
current value, embedded in a longer string and inside `Fn::Sub`
|
|
1003
|
+
- `{{resolve:ssm-secure:...}}` dynamic references, decrypting the `SecureString` through the KMS key
|
|
1004
|
+
it was written under, in the eleven resource properties CloudFormation reads one in
|
|
1005
|
+
- `StringList` parameters read through a dynamic reference as the comma-separated string `Fn::Split`
|
|
1006
|
+
then splits
|
|
1007
|
+
- `AWS::SSM::Parameter::Value<String>` template parameters, resolving `Ref` to the stored value
|
|
1008
|
+
- `AWS::SSM::Parameter::Value<List<String>>` and `<CommaDelimitedList>` template parameters,
|
|
1009
|
+
resolving `Ref` to the stored string split into a list
|
|
1010
|
+
- Parameter name validation, including hierarchy depth and the reserved `aws` and `ssm` prefixes
|
|
1011
|
+
- Authorization of every operation by simulated IAM, against the real IAM action and ARN
|
|
1012
|
+
- Calls made from inside a simulated Lambda handler, authorized as the function's execution role
|
|
1013
|
+
|
|
1014
|
+
## Limitations
|
|
1015
|
+
|
|
1016
|
+
Current documented limitations:
|
|
1017
|
+
|
|
1018
|
+
- Only standard tier `SecureString` encryption is simulated, which encrypts under the KMS key
|
|
1019
|
+
directly. The advanced tier's envelope encryption through the AWS Encryption SDK is left out, and
|
|
1020
|
+
`kms:GenerateDataKey` is never needed.
|
|
1021
|
+
- Parameter labels are left out. `LabelParameterVersion` is unimplemented, so a label can never be
|
|
1022
|
+
created, and a `name:label` selector is refused with an error saying so.
|
|
1023
|
+
- `GetParameterHistory` is absent, though earlier versions stay readable by number.
|
|
1024
|
+
- The advanced tier is left out. `Tier: Advanced` and `Tier: Intelligent-Tiering` are refused, and
|
|
1025
|
+
every parameter reports `Tier: Standard` with the 4KB standard tier value limit.
|
|
1026
|
+
- Parameter policies (expiration and notification) are left out. `Policies` is refused, and
|
|
1027
|
+
`DescribeParameters` always reports an empty `Policies` list.
|
|
1028
|
+
- Tags are left out. `Tags` on `PutParameter` is refused, and `AddTagsToResource`,
|
|
1029
|
+
`RemoveTagsFromResource` and `ListTagsForResource` are absent.
|
|
1030
|
+
- `AllowedPattern` is refused outright. Ignoring it would store a value it was meant to reject,
|
|
1031
|
+
without complaint.
|
|
1032
|
+
- `KeyId` on a `String` or `StringList` parameter is refused, since nothing would encrypt a value
|
|
1033
|
+
stored in the clear.
|
|
1034
|
+
- `DataType` other than `text` is refused. Real Parameter Store validates an `aws:ec2:image` value
|
|
1035
|
+
against EC2, which this simulation cannot do.
|
|
1036
|
+
- Filters are refused outright. `GetParametersByPath` refuses `ParameterFilters`, and
|
|
1037
|
+
`DescribeParameters` refuses `Filters` and `ParameterFilters`. Parameters are listed in name order.
|
|
1038
|
+
- `DescribeParameters` refuses `Shared`. Parameters cannot be shared between simulated accounts, and
|
|
1039
|
+
there is no resource policy support, so cross-account access to a parameter cannot be granted.
|
|
1040
|
+
- Every version is kept. Real Parameter Store keeps the hundred most recent versions and deletes the
|
|
1041
|
+
oldest as new ones are made, which can fail with `ParameterMaxVersionLimitExceeded`.
|
|
1042
|
+
- There is no per-account parameter count limit, so `ParameterLimitExceeded` never happens.
|
|
1043
|
+
- Deletion is immediate. Real Parameter Store asks for thirty seconds before a deleted name is
|
|
1044
|
+
reused, where here the name is free straight away.
|
|
1045
|
+
- `AWS::SSM::Parameter` supports `Name`, `Type`, `Value`, `Description` and `Tier`. `AllowedPattern`,
|
|
1046
|
+
`DataType`, `Policies` and `Tags` reach `PutParameter`, which refuses them for the reasons above.
|
|
1047
|
+
`Type: SecureString` is refused, as real CloudFormation refuses it for this resource type. The
|
|
1048
|
+
plaintext value would sit in the template.
|
|
1049
|
+
- The other `AWS::SSM::*` resource types (`Document`, `Association`, `MaintenanceWindow`,
|
|
1050
|
+
`PatchBaseline`, `ResourceDataSync` and the rest) are reported as unsupported and skipped.
|
|
1051
|
+
- Every deployment of an `AWS::SSM::Parameter` is a create. A name another stack already used is
|
|
1052
|
+
refused. A stack update that changes `Value` deletes the parameter and creates it again, where
|
|
1053
|
+
real CloudFormation overwrites it in place, so the parameter's version starts from 1 again.
|
|
1054
|
+
- A template parameter typed as `AWS::SSM::Parameter::Value<...>` is read once, while the
|
|
1055
|
+
`Parameters` section is. Real CloudFormation does the same, so a name that only exists once the
|
|
1056
|
+
stack has deployed resolves to a stand-in value here and fails the stack there.
|
|
1057
|
+
- The value a template parameter resolves to goes unvalidated against the inner type. The
|
|
1058
|
+
`AWS::EC2::*` and `AWS::Route53::HostedZone::Id` inner types name EC2 and Route53 resources, which
|
|
1059
|
+
this simulation holds none of.
|
|
1060
|
+
- A `{{resolve:ssm:...}}` or `{{resolve:ssm-secure:...}}` reference naming a parameter, or a
|
|
1061
|
+
version, that simulated Parameter Store has never held resolves to `dummy-value-for-<name>` and
|
|
1062
|
+
records the substitution. Real CloudFormation fails the stack. A template parameter naming one
|
|
1063
|
+
gets the same stand-in value.
|
|
1064
|
+
- An `ssm-secure` reference reads the stack's own account and region. A parameter in another account
|
|
1065
|
+
is out of reach on real CloudFormation as well.
|
|
1066
|
+
- Public parameters under `/aws/service/...` do not exist, and names under the reserved `aws` and
|
|
1067
|
+
`ssm` prefixes are refused, as they are on real AWS.
|
|
1068
|
+
- The Parameters and Secrets Lambda extension HTTP endpoint is absent. Handler code has to use the
|
|
1069
|
+
SDK.
|
|
1070
|
+
- Systems Manager is otherwise left out. Run Command, Session Manager, Patch Manager, State Manager,
|
|
1071
|
+
Automation, inventory and maintenance windows are all absent.
|
|
1072
|
+
- SSM is not served as an HTTP API by `serveSimAws`.
|