@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,806 @@
|
|
|
1
|
+
# Simulated EventBridge Scheduler
|
|
2
|
+
|
|
3
|
+
Yulin includes a simulated Amazon EventBridge Scheduler for tests and local development. Schedules
|
|
4
|
+
are held in memory and every operation is authorized by simulated IAM. Scheduler-specific types are
|
|
5
|
+
imported from the `@kensio/yulin/scheduler` subpath.
|
|
6
|
+
|
|
7
|
+
Scheduler is a separate service from [EventBridge](https://yulinsim.dev/services/eventbridge/), not a corner of it. It has its
|
|
8
|
+
own SDK client, its own ARN shape, and its own way of reaching a target. A schedule assumes an IAM
|
|
9
|
+
execution role, where an EventBridge rule relies on a resource policy admitting a service principal.
|
|
10
|
+
A project using Scheduler cannot be tested against simulated EventBridge rules. That is why this
|
|
11
|
+
exists separately.
|
|
12
|
+
|
|
13
|
+
## Creating a schedule
|
|
14
|
+
|
|
15
|
+
```typescript sim-scheduler-create-schedule
|
|
16
|
+
/**
|
|
17
|
+
* Creating a schedule that invokes a function every night.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
CreateScheduleCommand,
|
|
22
|
+
GetScheduleCommand,
|
|
23
|
+
} from "@aws-sdk/client-scheduler";
|
|
24
|
+
|
|
25
|
+
import { SimAws } from "@kensio/yulin";
|
|
26
|
+
|
|
27
|
+
const simAws = new SimAws();
|
|
28
|
+
const scheduler = simAws.scheduler();
|
|
29
|
+
|
|
30
|
+
const created = await scheduler.createSchedule(
|
|
31
|
+
new CreateScheduleCommand({
|
|
32
|
+
Name: "nightly-report",
|
|
33
|
+
ScheduleExpression: "cron(0 2 * * ? *)",
|
|
34
|
+
FlexibleTimeWindow: { Mode: "OFF" },
|
|
35
|
+
Target: {
|
|
36
|
+
Arn: "arn:aws:lambda:us-east-1:888888888888:function:report",
|
|
37
|
+
RoleArn: "arn:aws:iam::888888888888:role/SchedulerRole",
|
|
38
|
+
},
|
|
39
|
+
}),
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
console.log(created.ScheduleArn);
|
|
43
|
+
// "arn:aws:scheduler:us-east-1:888888888888:schedule/default/nightly-report"
|
|
44
|
+
|
|
45
|
+
const described = await scheduler.getSchedule(
|
|
46
|
+
new GetScheduleCommand({ Name: "nightly-report" }),
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
console.log(described.ScheduleExpression); // "cron(0 2 * * ? *)"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`FlexibleTimeWindow` and `Target` are both required, as AWS requires them, and a target carries both
|
|
53
|
+
an `Arn` and the `RoleArn` it is invoked as. A schedule ARN always names its group, even the
|
|
54
|
+
`default` one. An EventBridge rule ARN differs, showing the bus only when it is not the default. An
|
|
55
|
+
IAM policy naming a schedule needs the group in it, or it matches no schedule.
|
|
56
|
+
|
|
57
|
+
## Writing the schedule expression
|
|
58
|
+
|
|
59
|
+
Three forms, and the same parser as an [EventBridge scheduled
|
|
60
|
+
rule](https://yulinsim.dev/services/eventbridge/#rules-that-fire-on-a-schedule) with two differences:
|
|
61
|
+
|
|
62
|
+
- `at(yyyy-mm-ddThh:mm:ss)` runs once, at that instant. The timezone is a separate setting on the
|
|
63
|
+
schedule, outside the expression, and a trailing `Z` is refused.
|
|
64
|
+
- `rate(<value> <unit>)` runs from when the schedule was created. The unit is `minute`, `hour` or
|
|
65
|
+
`day`, and Scheduler lets it disagree with its value. `rate(1 hours)` is an hour here and a
|
|
66
|
+
refusal on an EventBridge rule.
|
|
67
|
+
- `cron(<six fields>)` names absolute instants in UTC. Minutes, hours, day-of-month, month,
|
|
68
|
+
day-of-week and year, so every day at two in the morning is `cron(0 2 * * ? *)`. The day-of-month
|
|
69
|
+
and day-of-week fields cannot both say something. Whichever is not deciding the day is written
|
|
70
|
+
`?`.
|
|
71
|
+
|
|
72
|
+
## Firing a schedule
|
|
73
|
+
|
|
74
|
+
A schedule fires on the simulation's clock. Advancing simulated time past a due
|
|
75
|
+
instant invokes the target. Leave time alone and the target is never invoked. A nightly job takes no
|
|
76
|
+
time at all to test.
|
|
77
|
+
|
|
78
|
+
```typescript sim-scheduler-firing
|
|
79
|
+
/**
|
|
80
|
+
* A schedule invoking a function three times in three simulated hours.
|
|
81
|
+
*/
|
|
82
|
+
|
|
83
|
+
import { CreateRoleCommand, PutRolePolicyCommand } from "@aws-sdk/client-iam";
|
|
84
|
+
import { CreateScheduleCommand } from "@aws-sdk/client-scheduler";
|
|
85
|
+
|
|
86
|
+
import { SimAws, SimFixedClock } from "@kensio/yulin";
|
|
87
|
+
import { makeLambdaZipFileInput } from "@kensio/yulin/lambda";
|
|
88
|
+
|
|
89
|
+
const simAws = new SimAws({
|
|
90
|
+
clock: new SimFixedClock(new Date("2026-07-26T09:00:00.000Z")),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const functionArn = "arn:aws:lambda:us-east-1:888888888888:function:report";
|
|
94
|
+
const runs: string[] = [];
|
|
95
|
+
|
|
96
|
+
await simAws.lambda().createFunction({
|
|
97
|
+
input: {
|
|
98
|
+
FunctionName: "report",
|
|
99
|
+
Role: "arn:aws:iam::888888888888:role/ReportRole",
|
|
100
|
+
Code: {
|
|
101
|
+
ZipFile: makeLambdaZipFileInput(() => {
|
|
102
|
+
runs.push("ran");
|
|
103
|
+
return { ok: true };
|
|
104
|
+
}),
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// The execution role has to trust Scheduler, and be allowed to invoke.
|
|
110
|
+
await simAws.iam().createRole(
|
|
111
|
+
new CreateRoleCommand({
|
|
112
|
+
RoleName: "SchedulerRole",
|
|
113
|
+
AssumeRolePolicyDocument: JSON.stringify({
|
|
114
|
+
Version: "2012-10-17",
|
|
115
|
+
Statement: {
|
|
116
|
+
Effect: "Allow",
|
|
117
|
+
Principal: { Service: "scheduler.amazonaws.com" },
|
|
118
|
+
Action: "sts:AssumeRole",
|
|
119
|
+
},
|
|
120
|
+
}),
|
|
121
|
+
}),
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
await simAws.iam().putRolePolicy(
|
|
125
|
+
new PutRolePolicyCommand({
|
|
126
|
+
RoleName: "SchedulerRole",
|
|
127
|
+
PolicyName: "InvokeReport",
|
|
128
|
+
PolicyDocument: JSON.stringify({
|
|
129
|
+
Version: "2012-10-17",
|
|
130
|
+
Statement: {
|
|
131
|
+
Effect: "Allow",
|
|
132
|
+
Action: "lambda:InvokeFunction",
|
|
133
|
+
Resource: functionArn,
|
|
134
|
+
},
|
|
135
|
+
}),
|
|
136
|
+
}),
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
await simAws.scheduler().createSchedule(
|
|
140
|
+
new CreateScheduleCommand({
|
|
141
|
+
Name: "hourly-report",
|
|
142
|
+
ScheduleExpression: "rate(1 hour)",
|
|
143
|
+
FlexibleTimeWindow: { Mode: "OFF" },
|
|
144
|
+
Target: {
|
|
145
|
+
Arn: functionArn,
|
|
146
|
+
RoleArn: "arn:aws:iam::888888888888:role/SchedulerRole",
|
|
147
|
+
},
|
|
148
|
+
}),
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
await simAws.clock().advanceBy({ hours: 3 });
|
|
152
|
+
|
|
153
|
+
console.log(runs.length); // 3
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Firing is per due instant. Advancing an hour with a `rate(1 minute)` schedule
|
|
157
|
+
invokes the target sixty times, at sixty distinct simulated instants. `advanceBy(...)` returns once
|
|
158
|
+
every one of those invocations has settled, leaving the next line free to assert.
|
|
159
|
+
|
|
160
|
+
A target with an `Input` receives that text. One without receives an empty JSON object, which AWS
|
|
161
|
+
documents for a function with no payload. There is no envelope, since a schedule has no event of its
|
|
162
|
+
own to describe.
|
|
163
|
+
|
|
164
|
+
### The execution role
|
|
165
|
+
|
|
166
|
+
This is the part that differs most from an [EventBridge rule](https://yulinsim.dev/services/eventbridge/), and the part that
|
|
167
|
+
most often goes wrong in a real account. A rule reaches its target as the `events.amazonaws.com`
|
|
168
|
+
service principal, and the target's own resource policy decides. A schedule assumes the `RoleArn` on
|
|
169
|
+
its target, and that role's policies decide. No resource policy on the target is involved at all.
|
|
170
|
+
|
|
171
|
+
Two things therefore have to be right, and they are fixed in different places:
|
|
172
|
+
|
|
173
|
+
- The role's **trust policy** has to let `scheduler.amazonaws.com` assume it. A role copied from an
|
|
174
|
+
EventBridge rule trusts `events.amazonaws.com` and fails here.
|
|
175
|
+
- A policy **on the role** has to allow the action on the target, being `lambda:InvokeFunction`,
|
|
176
|
+
`sqs:SendMessage`, `sns:Publish` or `ecs:RunTask`.
|
|
177
|
+
|
|
178
|
+
A trust policy may also carry the condition AWS recommends against the confused deputy problem. The
|
|
179
|
+
schedule's group ARN is supplied as `aws:SourceArn`, and the Account the schedule is in as
|
|
180
|
+
`aws:SourceAccount`. A role scoped to one schedule group is assumable by schedules in that group and
|
|
181
|
+
by nothing else. CDK writes that condition into the execution roles it generates for a schedule
|
|
182
|
+
target, so a role taken from a synthesized template works here unchanged.
|
|
183
|
+
|
|
184
|
+
When either is missing the target goes uninvoked and no error is thrown, exactly as on AWS, where the
|
|
185
|
+
failure goes to CloudWatch and nowhere the caller can see. `advanceBy(...)` still returns normally. A
|
|
186
|
+
test asserting on a failed invocation reads `deliveryFailures`:
|
|
187
|
+
|
|
188
|
+
```typescript sim-scheduler-delivery-failures
|
|
189
|
+
/**
|
|
190
|
+
* Finding out why a schedule's target was never invoked.
|
|
191
|
+
*/
|
|
192
|
+
|
|
193
|
+
import { CreateRoleCommand } from "@aws-sdk/client-iam";
|
|
194
|
+
import { CreateScheduleCommand } from "@aws-sdk/client-scheduler";
|
|
195
|
+
|
|
196
|
+
import { SimAws, SimFixedClock } from "@kensio/yulin";
|
|
197
|
+
|
|
198
|
+
const simAws = new SimAws({
|
|
199
|
+
clock: new SimFixedClock(new Date("2026-07-26T09:00:00.000Z")),
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// A role that trusts EventBridge rules rather than Scheduler.
|
|
203
|
+
await simAws.iam().createRole(
|
|
204
|
+
new CreateRoleCommand({
|
|
205
|
+
RoleName: "SchedulerRole",
|
|
206
|
+
AssumeRolePolicyDocument: JSON.stringify({
|
|
207
|
+
Version: "2012-10-17",
|
|
208
|
+
Statement: {
|
|
209
|
+
Effect: "Allow",
|
|
210
|
+
Principal: { Service: "events.amazonaws.com" },
|
|
211
|
+
Action: "sts:AssumeRole",
|
|
212
|
+
},
|
|
213
|
+
}),
|
|
214
|
+
}),
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
await simAws.scheduler().createSchedule(
|
|
218
|
+
new CreateScheduleCommand({
|
|
219
|
+
Name: "hourly-report",
|
|
220
|
+
ScheduleExpression: "rate(1 hour)",
|
|
221
|
+
FlexibleTimeWindow: { Mode: "OFF" },
|
|
222
|
+
Target: {
|
|
223
|
+
Arn: "arn:aws:lambda:us-east-1:888888888888:function:report",
|
|
224
|
+
RoleArn: "arn:aws:iam::888888888888:role/SchedulerRole",
|
|
225
|
+
},
|
|
226
|
+
}),
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
await simAws.clock().advanceBy({ hours: 1 });
|
|
230
|
+
|
|
231
|
+
const [failure] = simAws.scheduler().deliveryFailures;
|
|
232
|
+
|
|
233
|
+
console.log(failure?.message);
|
|
234
|
+
// "The trust policy of arn:aws:iam::888888888888:role/SchedulerRole does not
|
|
235
|
+
// allow scheduler.amazonaws.com to assume it, ..."
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
`JSON.stringify` on a failure carries the message alongside the schedule, the target, the role and
|
|
239
|
+
the instant it names.
|
|
240
|
+
|
|
241
|
+
### One-time schedules and what happens after
|
|
242
|
+
|
|
243
|
+
An `at(...)` schedule fires once and then stops. By default it stays in the Account afterwards, which
|
|
244
|
+
surprises people who expected it to clean up. It keeps counting against the schedule quota and keeps
|
|
245
|
+
turning up in listings. `ActionAfterCompletion: "DELETE"` is what removes it, and after that
|
|
246
|
+
`GetSchedule` reports it gone.
|
|
247
|
+
|
|
248
|
+
A schedule that is disabled when its only instant passes has not completed, since nothing was
|
|
249
|
+
invoked. It is still there afterwards whatever `ActionAfterCompletion` says.
|
|
250
|
+
|
|
251
|
+
`State: "DISABLED"` stops a recurring schedule firing while it is off, and an `UpdateSchedule`
|
|
252
|
+
enabling it picks up from the next due instant. What it missed is never replayed. An update that
|
|
253
|
+
changes the expression reschedules from the new one.
|
|
254
|
+
|
|
255
|
+
## Running an ECS task on a schedule
|
|
256
|
+
|
|
257
|
+
A target whose ARN names an ECS cluster runs a [simulated ECS](https://yulinsim.dev/services/ecs/) task, in place of being
|
|
258
|
+
invoked with a payload. That is the shape a nightly batch job usually has. A container runs, does
|
|
259
|
+
its work and stops.
|
|
260
|
+
|
|
261
|
+
```typescript sim-scheduler-ecs-target
|
|
262
|
+
/**
|
|
263
|
+
* A schedule running an ECS task every night.
|
|
264
|
+
*/
|
|
265
|
+
|
|
266
|
+
import {
|
|
267
|
+
CreateClusterCommand,
|
|
268
|
+
ListTasksCommand,
|
|
269
|
+
RegisterTaskDefinitionCommand,
|
|
270
|
+
} from "@aws-sdk/client-ecs";
|
|
271
|
+
import { CreateRoleCommand, PutRolePolicyCommand } from "@aws-sdk/client-iam";
|
|
272
|
+
import { CreateScheduleCommand } from "@aws-sdk/client-scheduler";
|
|
273
|
+
|
|
274
|
+
import { SimAws, SimFixedClock } from "@kensio/yulin";
|
|
275
|
+
|
|
276
|
+
const simAws = new SimAws({
|
|
277
|
+
clock: new SimFixedClock(new Date("2026-07-26T09:00:00.000Z")),
|
|
278
|
+
});
|
|
279
|
+
const ecs = simAws.ecs();
|
|
280
|
+
const imported: string[] = [];
|
|
281
|
+
|
|
282
|
+
await ecs.createCluster(new CreateClusterCommand({ clusterName: "orders" }));
|
|
283
|
+
|
|
284
|
+
ecs.bindContainer({
|
|
285
|
+
family: "nightly-import",
|
|
286
|
+
containerName: "app",
|
|
287
|
+
run: () => {
|
|
288
|
+
imported.push(process.env["IMPORT_MODE"] ?? "");
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
await ecs.registerTaskDefinition(
|
|
293
|
+
new RegisterTaskDefinitionCommand({
|
|
294
|
+
family: "nightly-import",
|
|
295
|
+
containerDefinitions: [{ name: "app", image: "nightly-import:1" }],
|
|
296
|
+
}),
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
// The schedule runs the task as this role, so the role trusts Scheduler and is
|
|
300
|
+
// allowed to run it.
|
|
301
|
+
await simAws.iam().createRole(
|
|
302
|
+
new CreateRoleCommand({
|
|
303
|
+
RoleName: "SchedulerRole",
|
|
304
|
+
AssumeRolePolicyDocument: JSON.stringify({
|
|
305
|
+
Version: "2012-10-17",
|
|
306
|
+
Statement: {
|
|
307
|
+
Effect: "Allow",
|
|
308
|
+
Principal: { Service: "scheduler.amazonaws.com" },
|
|
309
|
+
Action: "sts:AssumeRole",
|
|
310
|
+
},
|
|
311
|
+
}),
|
|
312
|
+
}),
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
await simAws.iam().putRolePolicy(
|
|
316
|
+
new PutRolePolicyCommand({
|
|
317
|
+
RoleName: "SchedulerRole",
|
|
318
|
+
PolicyName: "RunImport",
|
|
319
|
+
PolicyDocument: JSON.stringify({
|
|
320
|
+
Version: "2012-10-17",
|
|
321
|
+
Statement: { Effect: "Allow", Action: "ecs:RunTask", Resource: "*" },
|
|
322
|
+
}),
|
|
323
|
+
}),
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
await simAws.scheduler().createSchedule(
|
|
327
|
+
new CreateScheduleCommand({
|
|
328
|
+
Name: "nightly-import",
|
|
329
|
+
ScheduleExpression: "cron(0 2 * * ? *)",
|
|
330
|
+
FlexibleTimeWindow: { Mode: "OFF" },
|
|
331
|
+
Target: {
|
|
332
|
+
Arn: "arn:aws:ecs:us-east-1:888888888888:cluster/orders",
|
|
333
|
+
RoleArn: "arn:aws:iam::888888888888:role/SchedulerRole",
|
|
334
|
+
EcsParameters: {
|
|
335
|
+
TaskDefinitionArn: "nightly-import",
|
|
336
|
+
TaskCount: 1,
|
|
337
|
+
},
|
|
338
|
+
// An ECS target's Input is the task's overrides, since a task has
|
|
339
|
+
// nowhere to receive a payload.
|
|
340
|
+
Input: JSON.stringify({
|
|
341
|
+
containerOverrides: [
|
|
342
|
+
{
|
|
343
|
+
name: "app",
|
|
344
|
+
environment: [{ name: "IMPORT_MODE", value: "full" }],
|
|
345
|
+
},
|
|
346
|
+
],
|
|
347
|
+
}),
|
|
348
|
+
},
|
|
349
|
+
}),
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
// Advancing past 02:00 fires the schedule and runs the task.
|
|
353
|
+
await simAws.clock().advanceBy({ hours: 24 });
|
|
354
|
+
|
|
355
|
+
console.log(imported); // ["full"]
|
|
356
|
+
|
|
357
|
+
const tasks = await ecs.listTasks(
|
|
358
|
+
new ListTasksCommand({ cluster: "orders", desiredStatus: "STOPPED" }),
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
console.log(tasks.taskArns?.length); // 1
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
The target ARN names the cluster. An ARN naming anything else in ECS is refused when the schedule
|
|
365
|
+
is created. `EcsParameters` names the task definition, as a family, a `family:revision` or a full
|
|
366
|
+
ARN, and the same one `RunTask` would take.
|
|
367
|
+
|
|
368
|
+
An ECS target's `Input` is the task's overrides, since a task has nowhere to receive a payload. A
|
|
369
|
+
target with no `Input` runs the task with no overrides.
|
|
370
|
+
`EcsParameters` on a target whose ARN names anything else is refused, since it would do nothing.
|
|
371
|
+
|
|
372
|
+
[Simulated ECS](https://yulinsim.dev/services/ecs/) decides which containers actually run. A container
|
|
373
|
+
with a binding runs its handler, and a container without one is recorded as not simulated.
|
|
374
|
+
A target naming a task definition with nothing bound therefore records a task that never started,
|
|
375
|
+
and the schedule counts as invoked.
|
|
376
|
+
|
|
377
|
+
## Updating and deleting
|
|
378
|
+
|
|
379
|
+
```typescript sim-scheduler-update-schedule
|
|
380
|
+
/**
|
|
381
|
+
* An update replaces the whole schedule rather than merging into it.
|
|
382
|
+
*/
|
|
383
|
+
|
|
384
|
+
import {
|
|
385
|
+
CreateScheduleCommand,
|
|
386
|
+
GetScheduleCommand,
|
|
387
|
+
UpdateScheduleCommand,
|
|
388
|
+
} from "@aws-sdk/client-scheduler";
|
|
389
|
+
|
|
390
|
+
import { SimAws } from "@kensio/yulin";
|
|
391
|
+
|
|
392
|
+
const simAws = new SimAws();
|
|
393
|
+
const scheduler = simAws.scheduler();
|
|
394
|
+
const target = {
|
|
395
|
+
Arn: "arn:aws:lambda:us-east-1:888888888888:function:report",
|
|
396
|
+
RoleArn: "arn:aws:iam::888888888888:role/SchedulerRole",
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
await scheduler.createSchedule(
|
|
400
|
+
new CreateScheduleCommand({
|
|
401
|
+
Name: "nightly-report",
|
|
402
|
+
Description: "The nightly reconciliation",
|
|
403
|
+
ScheduleExpression: "cron(0 2 * * ? *)",
|
|
404
|
+
FlexibleTimeWindow: { Mode: "OFF" },
|
|
405
|
+
Target: target,
|
|
406
|
+
}),
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
// Meaning only to change the expression, and leaving the description out.
|
|
410
|
+
await scheduler.updateSchedule(
|
|
411
|
+
new UpdateScheduleCommand({
|
|
412
|
+
Name: "nightly-report",
|
|
413
|
+
ScheduleExpression: "rate(30 minutes)",
|
|
414
|
+
FlexibleTimeWindow: { Mode: "OFF" },
|
|
415
|
+
Target: target,
|
|
416
|
+
}),
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
const described = await scheduler.getSchedule(
|
|
420
|
+
new GetScheduleCommand({ Name: "nightly-report" }),
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
console.log(described.ScheduleExpression); // "rate(30 minutes)"
|
|
424
|
+
console.log(described.Description); // undefined, and not by accident
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
`UpdateSchedule` carries the whole of a schedule, and anything an earlier request set and this one
|
|
428
|
+
leaves out is gone. That is real behaviour and a common surprise. The schedule has to exist.
|
|
429
|
+
Updating one that is absent raises `ResourceNotFoundException`. EventBridge's `PutRule` creates it.
|
|
430
|
+
|
|
431
|
+
`CreateSchedule` for a name that already exists raises `ConflictException`. A deployment running it
|
|
432
|
+
twice fails the second time here as it does on AWS. `DeleteSchedule` for a schedule that is absent
|
|
433
|
+
raises `ResourceNotFoundException`, where EventBridge's `DeleteRule` succeeds.
|
|
434
|
+
|
|
435
|
+
## Listing schedules
|
|
436
|
+
|
|
437
|
+
`ListSchedules` reports the schedules of a group in creation order, narrowed by `NamePrefix` and
|
|
438
|
+
`State` and paged by `MaxResults` and `NextToken`.
|
|
439
|
+
|
|
440
|
+
A listing carries less than a describe, as it does on AWS. It has the target's ARN and no more of the target,
|
|
441
|
+
and no expression at all. Code reading `ScheduleExpression` off a listing gets
|
|
442
|
+
`undefined` from AWS, and gets `undefined` here too.
|
|
443
|
+
|
|
444
|
+
## Schedule groups
|
|
445
|
+
|
|
446
|
+
Every account and region starts with a `default` group. A schedule naming no group goes in it.
|
|
447
|
+
|
|
448
|
+
A schedule's name is unique within its group, and the group is in the schedule's ARN. Two
|
|
449
|
+
deployments of one construct into the same account and region collide on schedule names unless each
|
|
450
|
+
one brings its own group.
|
|
451
|
+
|
|
452
|
+
```typescript sim-scheduler-schedule-group
|
|
453
|
+
/**
|
|
454
|
+
* A schedule group scoping the names of one deployment's schedules.
|
|
455
|
+
*/
|
|
456
|
+
|
|
457
|
+
import {
|
|
458
|
+
CreateScheduleCommand,
|
|
459
|
+
CreateScheduleGroupCommand,
|
|
460
|
+
ListSchedulesCommand,
|
|
461
|
+
} from "@aws-sdk/client-scheduler";
|
|
462
|
+
|
|
463
|
+
import { SimAws } from "@kensio/yulin";
|
|
464
|
+
|
|
465
|
+
const simAws = new SimAws();
|
|
466
|
+
const scheduler = simAws.scheduler();
|
|
467
|
+
|
|
468
|
+
const group = await scheduler.createScheduleGroup(
|
|
469
|
+
new CreateScheduleGroupCommand({ Name: "reporting-pr-412" }),
|
|
470
|
+
);
|
|
471
|
+
|
|
472
|
+
console.log(group.ScheduleGroupArn);
|
|
473
|
+
// "arn:aws:scheduler:us-east-1:888888888888:schedule-group/reporting-pr-412"
|
|
474
|
+
|
|
475
|
+
const created = await scheduler.createSchedule(
|
|
476
|
+
new CreateScheduleCommand({
|
|
477
|
+
Name: "pageviews-hourly",
|
|
478
|
+
GroupName: "reporting-pr-412",
|
|
479
|
+
ScheduleExpression: "rate(1 hour)",
|
|
480
|
+
FlexibleTimeWindow: { Mode: "OFF" },
|
|
481
|
+
Target: {
|
|
482
|
+
Arn: "arn:aws:lambda:us-east-1:888888888888:function:report",
|
|
483
|
+
RoleArn: "arn:aws:iam::888888888888:role/SchedulerRole",
|
|
484
|
+
},
|
|
485
|
+
}),
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
// The group is in the schedule's ARN. The same schedule name is free in every
|
|
489
|
+
// other group, including default.
|
|
490
|
+
console.log(created.ScheduleArn);
|
|
491
|
+
// ".../schedule/reporting-pr-412/pageviews-hourly"
|
|
492
|
+
|
|
493
|
+
const listed = await scheduler.listSchedules(
|
|
494
|
+
new ListSchedulesCommand({ GroupName: "reporting-pr-412" }),
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
console.log(listed.Schedules?.length); // 1
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
A `GroupName` for a group that has yet to be created raises `ResourceNotFoundException`, and so
|
|
501
|
+
does a listing for one. Real Scheduler answers the same way. A schedule quietly moved into `default`
|
|
502
|
+
would carry an ARN naming a group it had never been put in.
|
|
503
|
+
|
|
504
|
+
`GetScheduleGroup` reports a group's ARN, state and timestamps. `ListScheduleGroups` reports them
|
|
505
|
+
all in creation order, narrowed by `NamePrefix` and paged by `MaxResults` and `NextToken`.
|
|
506
|
+
|
|
507
|
+
`DeleteScheduleGroup` deletes the schedules in the group along with it, as AWS does. It refuses the
|
|
508
|
+
`default` group, which comes with the account. Losing that group would leave every request naming
|
|
509
|
+
no group with nowhere to go.
|
|
510
|
+
|
|
511
|
+
## Permissions
|
|
512
|
+
|
|
513
|
+
Every operation is authorized against the schedule ARN, which carries the group:
|
|
514
|
+
|
|
515
|
+
```typescript sim-scheduler-iam-policy
|
|
516
|
+
/**
|
|
517
|
+
* A Role allowed to manage one schedule and no other.
|
|
518
|
+
*/
|
|
519
|
+
|
|
520
|
+
import { CreateRoleCommand, PutRolePolicyCommand } from "@aws-sdk/client-iam";
|
|
521
|
+
import { CreateScheduleCommand } from "@aws-sdk/client-scheduler";
|
|
522
|
+
|
|
523
|
+
import { SimAws } from "@kensio/yulin";
|
|
524
|
+
|
|
525
|
+
const simAws = new SimAws();
|
|
526
|
+
|
|
527
|
+
const role = await simAws.iam().createRole(
|
|
528
|
+
new CreateRoleCommand({
|
|
529
|
+
RoleName: "ScheduleAdministrator",
|
|
530
|
+
AssumeRolePolicyDocument: JSON.stringify({
|
|
531
|
+
Version: "2012-10-17",
|
|
532
|
+
Statement: {
|
|
533
|
+
Effect: "Allow",
|
|
534
|
+
Principal: { AWS: "arn:aws:iam::888888888888:root" },
|
|
535
|
+
Action: "sts:AssumeRole",
|
|
536
|
+
},
|
|
537
|
+
}),
|
|
538
|
+
}),
|
|
539
|
+
);
|
|
540
|
+
|
|
541
|
+
await simAws.iam().putRolePolicy(
|
|
542
|
+
new PutRolePolicyCommand({
|
|
543
|
+
RoleName: "ScheduleAdministrator",
|
|
544
|
+
PolicyName: "ManageNightlyReport",
|
|
545
|
+
PolicyDocument: JSON.stringify({
|
|
546
|
+
Version: "2012-10-17",
|
|
547
|
+
Statement: {
|
|
548
|
+
Effect: "Allow",
|
|
549
|
+
Action: "scheduler:CreateSchedule",
|
|
550
|
+
// The group is part of the ARN, so a policy without it matches nothing.
|
|
551
|
+
Resource:
|
|
552
|
+
"arn:aws:scheduler:us-east-1:888888888888:schedule/default/nightly-report",
|
|
553
|
+
},
|
|
554
|
+
}),
|
|
555
|
+
}),
|
|
556
|
+
);
|
|
557
|
+
|
|
558
|
+
const created = await simAws.scheduler().createSchedule(
|
|
559
|
+
new CreateScheduleCommand({
|
|
560
|
+
Name: "nightly-report",
|
|
561
|
+
ScheduleExpression: "rate(1 hour)",
|
|
562
|
+
FlexibleTimeWindow: { Mode: "OFF" },
|
|
563
|
+
Target: {
|
|
564
|
+
Arn: "arn:aws:lambda:us-east-1:888888888888:function:report",
|
|
565
|
+
RoleArn: "arn:aws:iam::888888888888:role/SchedulerRole",
|
|
566
|
+
},
|
|
567
|
+
}),
|
|
568
|
+
{ caller: { kind: "arn", arn: role.Role.Arn } },
|
|
569
|
+
);
|
|
570
|
+
|
|
571
|
+
console.log(created.ScheduleArn !== undefined); // true
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
`ListSchedules` names no schedule. IAM evaluates it against `*`, and only a policy whose `Resource`
|
|
575
|
+
is `*` allows it. A policy naming a schedule ARN allows no listing, here as on AWS.
|
|
576
|
+
|
|
577
|
+
That is the caller's own permission to manage schedules, and it is a separate question from whether a
|
|
578
|
+
schedule's execution role may invoke its target. The second is asked when the schedule fires,
|
|
579
|
+
against the `RoleArn` on the target.
|
|
580
|
+
|
|
581
|
+
## Deploying from a CloudFormation template
|
|
582
|
+
|
|
583
|
+
`AWS::Scheduler::Schedule` deploys through [simulated CloudFormation](https://yulinsim.dev/services/cloudformation/). A stack
|
|
584
|
+
that declares its schedules can be exercised end to end, with no SDK calls of its own. Everything the
|
|
585
|
+
Resource carries lines up with `CreateSchedule`, and a target ARN or execution role resolved by
|
|
586
|
+
`Fn::GetAtt` from the same template works as it would in a real deployment.
|
|
587
|
+
|
|
588
|
+
```typescript sim-scheduler-cloudformation
|
|
589
|
+
/**
|
|
590
|
+
* A schedule deployed from a template, firing as simulated time advances.
|
|
591
|
+
*/
|
|
592
|
+
|
|
593
|
+
import { CreateRoleCommand, PutRolePolicyCommand } from "@aws-sdk/client-iam";
|
|
594
|
+
import { ReceiveMessageCommand } from "@aws-sdk/client-sqs";
|
|
595
|
+
|
|
596
|
+
import { SimAws, SimFixedClock } from "@kensio/yulin";
|
|
597
|
+
|
|
598
|
+
const simAws = new SimAws({
|
|
599
|
+
clock: new SimFixedClock(new Date("2026-07-26T09:00:00.000Z")),
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
const queueArn = "arn:aws:sqs:us-east-1:888888888888:reports";
|
|
603
|
+
const roleArn = "arn:aws:iam::888888888888:role/SchedulerRole";
|
|
604
|
+
|
|
605
|
+
// The execution role has to trust Scheduler, and be allowed to send.
|
|
606
|
+
await simAws.iam().createRole(
|
|
607
|
+
new CreateRoleCommand({
|
|
608
|
+
RoleName: "SchedulerRole",
|
|
609
|
+
AssumeRolePolicyDocument: JSON.stringify({
|
|
610
|
+
Version: "2012-10-17",
|
|
611
|
+
Statement: {
|
|
612
|
+
Effect: "Allow",
|
|
613
|
+
Principal: { Service: "scheduler.amazonaws.com" },
|
|
614
|
+
Action: "sts:AssumeRole",
|
|
615
|
+
},
|
|
616
|
+
}),
|
|
617
|
+
}),
|
|
618
|
+
);
|
|
619
|
+
|
|
620
|
+
await simAws.iam().putRolePolicy(
|
|
621
|
+
new PutRolePolicyCommand({
|
|
622
|
+
RoleName: "SchedulerRole",
|
|
623
|
+
PolicyName: "SendReports",
|
|
624
|
+
PolicyDocument: JSON.stringify({
|
|
625
|
+
Version: "2012-10-17",
|
|
626
|
+
Statement: {
|
|
627
|
+
Effect: "Allow",
|
|
628
|
+
Action: "sqs:SendMessage",
|
|
629
|
+
Resource: queueArn,
|
|
630
|
+
},
|
|
631
|
+
}),
|
|
632
|
+
}),
|
|
633
|
+
);
|
|
634
|
+
|
|
635
|
+
const stack = await simAws.cloudFormation().deployTemplate({
|
|
636
|
+
stackName: "reporting-stack",
|
|
637
|
+
template: {
|
|
638
|
+
Resources: {
|
|
639
|
+
ReportQueue: {
|
|
640
|
+
Type: "AWS::SQS::Queue",
|
|
641
|
+
Properties: { QueueName: "reports" },
|
|
642
|
+
},
|
|
643
|
+
HourlyReport: {
|
|
644
|
+
Type: "AWS::Scheduler::Schedule",
|
|
645
|
+
Properties: {
|
|
646
|
+
Name: "hourly-report",
|
|
647
|
+
ScheduleExpression: "rate(1 hour)",
|
|
648
|
+
FlexibleTimeWindow: { Mode: "OFF" },
|
|
649
|
+
Target: {
|
|
650
|
+
Arn: { "Fn::GetAtt": ["ReportQueue", "Arn"] },
|
|
651
|
+
RoleArn: roleArn,
|
|
652
|
+
Input: JSON.stringify({ report: "hourly" }),
|
|
653
|
+
},
|
|
654
|
+
},
|
|
655
|
+
},
|
|
656
|
+
},
|
|
657
|
+
},
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
await stack.waitForDeployComplete();
|
|
661
|
+
|
|
662
|
+
// Three simulated hours on, the schedule has invoked its target three times.
|
|
663
|
+
await simAws.clock().advanceBy({ hours: 3 });
|
|
664
|
+
|
|
665
|
+
const received = await simAws.sqs().receiveMessage(
|
|
666
|
+
new ReceiveMessageCommand({
|
|
667
|
+
QueueUrl: "https://sqs.us-east-1.amazonaws.com/888888888888/reports",
|
|
668
|
+
MaxNumberOfMessages: 10,
|
|
669
|
+
}),
|
|
670
|
+
);
|
|
671
|
+
|
|
672
|
+
console.log(received.Messages?.length); // 3
|
|
673
|
+
|
|
674
|
+
// Nothing went wrong on the way, which is worth checking: a schedule that
|
|
675
|
+
// could not reach its target says so here rather than by throwing.
|
|
676
|
+
console.log(simAws.scheduler().deliveryFailures.length); // 0
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
`Ref` returns the schedule's **name** and `Fn::GetAtt ... Arn` its ARN, which carries the schedule
|
|
680
|
+
group as it always does. A schedule the template leaves unnamed gets one generated from the stack
|
|
681
|
+
name and the logical ID.
|
|
682
|
+
|
|
683
|
+
A property this simulation leaves out is refused at deploy time, naming the Resource. Deploying a
|
|
684
|
+
schedule that behaves differently from the one declared would be worse. Tearing the stack down
|
|
685
|
+
removes the schedules it created, and no schedule fires afterwards.
|
|
686
|
+
|
|
687
|
+
### Deploying a schedule group
|
|
688
|
+
|
|
689
|
+
`AWS::Scheduler::ScheduleGroup` deploys too, so a stack can bring the group its schedules go in.
|
|
690
|
+
|
|
691
|
+
```typescript sim-scheduler-cfn-schedule-group
|
|
692
|
+
/**
|
|
693
|
+
* A stack deploying its own schedule group, with a schedule in it.
|
|
694
|
+
*/
|
|
695
|
+
|
|
696
|
+
import { SimAws } from "@kensio/yulin";
|
|
697
|
+
|
|
698
|
+
const simAws = new SimAws();
|
|
699
|
+
|
|
700
|
+
const stack = await simAws.cloudFormation().deployTemplate({
|
|
701
|
+
stackName: "reporting-stack",
|
|
702
|
+
template: {
|
|
703
|
+
Resources: {
|
|
704
|
+
ReportGroup: {
|
|
705
|
+
Type: "AWS::Scheduler::ScheduleGroup",
|
|
706
|
+
Properties: { Name: "reporting-pr-412" },
|
|
707
|
+
},
|
|
708
|
+
HourlyReport: {
|
|
709
|
+
Type: "AWS::Scheduler::Schedule",
|
|
710
|
+
Properties: {
|
|
711
|
+
Name: "pageviews-hourly",
|
|
712
|
+
GroupName: { Ref: "ReportGroup" },
|
|
713
|
+
ScheduleExpression: "rate(1 hour)",
|
|
714
|
+
FlexibleTimeWindow: { Mode: "OFF" },
|
|
715
|
+
Target: {
|
|
716
|
+
Arn: "arn:aws:sqs:us-east-1:888888888888:reports",
|
|
717
|
+
RoleArn: "arn:aws:iam::888888888888:role/SchedulerRole",
|
|
718
|
+
},
|
|
719
|
+
},
|
|
720
|
+
},
|
|
721
|
+
},
|
|
722
|
+
Outputs: {
|
|
723
|
+
GroupArn: { Value: { "Fn::GetAtt": ["ReportGroup", "Arn"] } },
|
|
724
|
+
},
|
|
725
|
+
},
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
await stack.waitForDeployComplete();
|
|
729
|
+
|
|
730
|
+
// An identity policy granting `scheduler:` actions on the group names this
|
|
731
|
+
// ARN. So does the `aws:SourceArn` condition AWS recommends in a schedule
|
|
732
|
+
// execution role's trust policy, which the simulation does not supply.
|
|
733
|
+
console.log(stack.output("GroupArn"));
|
|
734
|
+
// "arn:aws:scheduler:us-east-1:888888888888:schedule-group/reporting-pr-412"
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
`Ref` returns the group's **name** and `Fn::GetAtt` answers `Arn`, `State`, `CreationDate` and
|
|
738
|
+
`LastModificationDate`. A group the template leaves unnamed gets one generated from the stack name
|
|
739
|
+
and the logical ID.
|
|
740
|
+
|
|
741
|
+
`Tags` on a group are recorded as an ignored property and the group deploys without them. The CDK
|
|
742
|
+
puts a stack's tags on every taggable Resource in it, so a template gains them without asking.
|
|
743
|
+
Failing the whole stack over a tag would refuse a deployment for a property the simulation ignores
|
|
744
|
+
anyway. Read the record back from `stack.getResource("<logicalId>")?.ignoredProperties`.
|
|
745
|
+
|
|
746
|
+
Tearing the stack down removes the group. Its schedules go with it, whether or not they are
|
|
747
|
+
Resources of the same stack.
|
|
748
|
+
|
|
749
|
+
## Available functionality
|
|
750
|
+
|
|
751
|
+
- `CreateSchedule`, `GetSchedule`, `UpdateSchedule`, `DeleteSchedule` and `ListSchedules`.
|
|
752
|
+
- `at(...)`, `rate(...)` and six-field `cron(...)` expressions, fired by advancing the simulation's
|
|
753
|
+
clock.
|
|
754
|
+
- Lambda, SQS and SNS targets, with a target `Input`, invoked as the target's execution role and
|
|
755
|
+
authorized against that role's own policies.
|
|
756
|
+
- ECS targets, running a simulated task as the execution role, with the task definition and
|
|
757
|
+
`TaskCount` from `EcsParameters` and container overrides from the target's `Input`.
|
|
758
|
+
- `ActionAfterCompletion`, and `deliveryFailures` for invocations that did not happen.
|
|
759
|
+
- `CreateScheduleGroup`, `GetScheduleGroup`, `DeleteScheduleGroup` and `ListScheduleGroups`, over
|
|
760
|
+
the `default` group every account and region starts with and any group created beside it.
|
|
761
|
+
- `AWS::Scheduler::Schedule` and `AWS::Scheduler::ScheduleGroup` deployed from a CloudFormation
|
|
762
|
+
template.
|
|
763
|
+
- Creation and modification timestamps from the simulation's clock, and prefix-narrowed,
|
|
764
|
+
state-narrowed, paged listings.
|
|
765
|
+
- IAM authorization against the schedule ARN.
|
|
766
|
+
- SDK interception of `SchedulerClient`.
|
|
767
|
+
|
|
768
|
+
## Limitations
|
|
769
|
+
|
|
770
|
+
- A schedule only fires while a test advances the simulation's clock. The host's clock drives none
|
|
771
|
+
of it, and a simulation left alone in real time never fires however long it is left.
|
|
772
|
+
- Firing is exact and exactly once. Real Scheduler invokes within a minute of the due time, and its
|
|
773
|
+
promise is at-least-once.
|
|
774
|
+
- An invocation is attempted once. There is no retry and no dead letter queue. A target that throws
|
|
775
|
+
is recorded as a failure, and never redelivered. A failed invocation never rejects
|
|
776
|
+
`advanceBy(...)`, and is read from `deliveryFailures`.
|
|
777
|
+
- A schedule group carries no tags. `CreateScheduleGroup` refuses `Tags`, since the simulation
|
|
778
|
+
stores them nowhere. A template's `Tags` are recorded as an ignored property and the group still
|
|
779
|
+
deploys.
|
|
780
|
+
- A schedule group is `ACTIVE` or gone. Deleting one removes its schedules in the same call, where
|
|
781
|
+
real Scheduler holds the group in `DELETING` until they have gone.
|
|
782
|
+
- The `default` schedule group cannot be deleted. AWS leaves the answer to that request
|
|
783
|
+
undocumented.
|
|
784
|
+
- `FlexibleTimeWindow` with `Mode: "FLEXIBLE"` is refused. Real Scheduler invokes the target at an
|
|
785
|
+
unpredictable moment inside the window, and firing at the exact due time instead would let a test
|
|
786
|
+
rely on timing AWS leaves unpromised.
|
|
787
|
+
- `ScheduleExpressionTimezone` other than `UTC` is refused outright, since running a schedule in the
|
|
788
|
+
wrong zone fires it at the wrong hour.
|
|
789
|
+
- `StartDate` and `EndDate` are refused outright.
|
|
790
|
+
- Targets are Lambda, SQS, SNS and ECS. The universal target
|
|
791
|
+
(`arn:aws:scheduler:::aws-sdk:<service>:<action>`) and every other target service are refused when
|
|
792
|
+
the schedule is created, ahead of the first due instant.
|
|
793
|
+
- A target `DeadLetterConfig`, `RetryPolicy`, `EventBridgeParameters`, `KinesisParameters`,
|
|
794
|
+
`SageMakerPipelineParameters` and `SqsParameters` are refused outright, as is `EcsParameters` on a
|
|
795
|
+
target whose ARN names something other than an ECS cluster.
|
|
796
|
+
- An ECS target's `EcsParameters` takes `TaskDefinitionArn` and `TaskCount`, and takes and ignores
|
|
797
|
+
`LaunchType`, `PlatformVersion`, `NetworkConfiguration` and `CapacityProviderStrategy`, since
|
|
798
|
+
there is no placement and no network here for them to apply to. Anything else it can carry, such
|
|
799
|
+
as `Group`, `Tags` or `PropagateTags`, is refused outright.
|
|
800
|
+
- An ECS target's `Input` is read as the task's overrides. A `containerOverrides` list is how a
|
|
801
|
+
schedule sets a container's environment. An `Input` that is anything but a JSON object is refused
|
|
802
|
+
on an ECS target, where every other target type takes any text.
|
|
803
|
+
- A `TaskCount` above one runs that many simulated tasks, and a bound container handler runs once
|
|
804
|
+
for each of them, in this process and one after another.
|
|
805
|
+
- `KmsKeyArn` is refused, and `ClientToken` is accepted and ignored. Nothing here retries, so it has
|
|
806
|
+
no request to make idempotent.
|