Skip to content

Commit 603e831

Browse files
fossamagnaiankhou
andauthored
feat: add timezone support to scheduling for Lambda functions (#3016)
<!-- Thank you for your Pull Request! Please describe the problem this PR fixes and a summary of the changes made. Link to any relevant issues, code snippets, or other PRs. For trivial changes, this template can be ignored in favor of a short description of the changes. --> ## Problem <!-- Describe the issue this PR is solving --> Function schedules are powered by Amazon EventBridge rules, so not supported timezone. Migrate EventBridge Scheduler to support timezone. **Issue number, if available:** fix #3008 ## Changes <!-- Summarize the changes introduced in this PR. This is a good place to call out critical or potentially problematic parts of the change. --> Added a function schedule format that specifies the timezone. We migrated from EventBridge rules to EventBridge Scheduler to support time zones. **Corresponding docs PR, if applicable:** aws-amplify/docs#8448 ## Validation <!-- Describe how changes in this PR have been validated. This may include added or updated unit, integration and/or E2E tests, test workflow runs, or manual verification. If manual verification is the only way changes in this PR have been validated, you will need to write some automated tests before this PR is ready to merge. For changes to test infra, or non-functional changes, tests are not always required. Instead, you should call out _why_ you think tests are not required here. If changes affect a GitHub workflow that is not included in the PR checks, include a link to a passing test run of the modified workflow. ---> ## Checklist <!-- These items must be completed before a PR is ready to be merged. Feel free to publish a draft PR before these items are complete. --> - [x] If this PR includes a functional change to the runtime behavior of the code, I have added or updated automated test coverage for this change. - [ ] If this PR requires a change to the [Project Architecture README](../PROJECT_ARCHITECTURE.md), I have included that update in this PR. - [x] If this PR requires a docs update, I have linked to that docs PR above. - [ ] If this PR modifies E2E tests, makes changes to resource provisioning, or makes SDK calls, I have run the PR checks with the `run-e2e` label set. _By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license._ --------- Co-authored-by: Ian Hou <45278651+iankhou@users.noreply.github.com>
1 parent 6e0027d commit 603e831

File tree

6 files changed

+358
-90
lines changed

6 files changed

+358
-90
lines changed

.changeset/ready-cities-wonder.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@aws-amplify/backend-function': minor
3+
'@aws-amplify/backend': minor
4+
---
5+
6+
feat: add timezone support to scheduling Lambda functions

packages/backend-function/API.md

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@ export type AddEnvironmentFactory = {
3434
};
3535

3636
// @public (undocumented)
37-
export type CronSchedule = `${string} ${string} ${string} ${string} ${string}` | `${string} ${string} ${string} ${string} ${string} ${string}`;
37+
export type CronSchedule = CronScheduleExpression | ZonedCronSchedule;
38+
39+
// @public (undocumented)
40+
export type CronScheduleExpression = `${string} ${string} ${string} ${string} ${string}` | `${string} ${string} ${string} ${string} ${string} ${string}`;
3841

3942
// @public (undocumented)
4043
type DataClientConfig = {
@@ -141,7 +144,22 @@ type ResourceConfig = {
141144
};
142145

143146
// @public (undocumented)
144-
export type TimeInterval = `every ${number}m` | `every ${number}h` | `every day` | `every week` | `every month` | `every year`;
147+
export type TimeInterval = ZonedTimeInterval | TimeIntervalExpression;
148+
149+
// @public (undocumented)
150+
export type TimeIntervalExpression = `every ${number}m` | `every ${number}h` | `every day` | `every week` | `every month` | `every year`;
151+
152+
// @public (undocumented)
153+
export type ZonedCronSchedule = {
154+
cron: CronScheduleExpression;
155+
timezone: string;
156+
};
157+
158+
// @public (undocumented)
159+
export type ZonedTimeInterval = {
160+
rate: TimeIntervalExpression;
161+
timezone: string;
162+
};
145163

146164
// (No @packageDocumentation comment for this package)
147165

packages/backend-function/src/factory.test.ts

Lines changed: 98 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -495,17 +495,54 @@ void describe('AmplifyFunctionFactory', () => {
495495
}).getInstance(getInstanceProps);
496496
const template = Template.fromStack(lambda.stack);
497497

498-
template.hasResourceProperties('AWS::Events::Rule', {
498+
template.hasResourceProperties('AWS::Scheduler::Schedule', {
499499
ScheduleExpression: 'cron(*/5 * * * ? *)',
500-
Targets: [
501-
{
502-
Arn: {
503-
// eslint-disable-next-line spellcheck/spell-checker
504-
'Fn::GetAtt': ['handlerlambdaE29D1580', 'Arn'],
505-
},
506-
Id: 'Target0',
500+
ScheduleExpressionTimezone: 'UTC',
501+
Target: {
502+
Arn: {
503+
// eslint-disable-next-line spellcheck/spell-checker
504+
'Fn::GetAtt': ['handlerlambdaE29D1580', 'Arn'],
507505
},
508-
],
506+
},
507+
});
508+
});
509+
510+
void it('sets valid schedule - rate with timezone', () => {
511+
const lambda = defineFunction({
512+
entry: './test-assets/default-lambda/handler.ts',
513+
schedule: { rate: 'every 5m', timezone: 'America/New_York' },
514+
}).getInstance(getInstanceProps);
515+
const template = Template.fromStack(lambda.stack);
516+
517+
template.hasResourceProperties('AWS::Scheduler::Schedule', {
518+
ScheduleExpression: 'cron(*/5 * * * ? *)',
519+
ScheduleExpressionTimezone: 'America/New_York',
520+
Target: {
521+
Arn: {
522+
// eslint-disable-next-line spellcheck/spell-checker
523+
'Fn::GetAtt': ['handlerlambdaE29D1580', 'Arn'],
524+
},
525+
},
526+
});
527+
});
528+
529+
void it('sets schedule - rate with invalid timezone', () => {
530+
const lambda = defineFunction({
531+
entry: './test-assets/default-lambda/handler.ts',
532+
schedule: { rate: 'every 5m', timezone: 'Invalid/Timezone' },
533+
}).getInstance(getInstanceProps);
534+
const template = Template.fromStack(lambda.stack);
535+
536+
// CDK does not validate timezone
537+
template.hasResourceProperties('AWS::Scheduler::Schedule', {
538+
ScheduleExpression: 'cron(*/5 * * * ? *)',
539+
ScheduleExpressionTimezone: 'Invalid/Timezone',
540+
Target: {
541+
Arn: {
542+
// eslint-disable-next-line spellcheck/spell-checker
543+
'Fn::GetAtt': ['handlerlambdaE29D1580', 'Arn'],
544+
},
545+
},
509546
});
510547
});
511548

@@ -516,17 +553,54 @@ void describe('AmplifyFunctionFactory', () => {
516553
}).getInstance(getInstanceProps);
517554
const template = Template.fromStack(lambda.stack);
518555

519-
template.hasResourceProperties('AWS::Events::Rule', {
556+
template.hasResourceProperties('AWS::Scheduler::Schedule', {
520557
ScheduleExpression: 'cron(0 1 * * ? *)',
521-
Targets: [
522-
{
523-
Arn: {
524-
// eslint-disable-next-line spellcheck/spell-checker
525-
'Fn::GetAtt': ['handlerlambdaE29D1580', 'Arn'],
526-
},
527-
Id: 'Target0',
558+
ScheduleExpressionTimezone: 'UTC',
559+
Target: {
560+
Arn: {
561+
// eslint-disable-next-line spellcheck/spell-checker
562+
'Fn::GetAtt': ['handlerlambdaE29D1580', 'Arn'],
528563
},
529-
],
564+
},
565+
});
566+
});
567+
568+
void it('sets valid schedule - cron with timezone', () => {
569+
const lambda = defineFunction({
570+
entry: './test-assets/default-lambda/handler.ts',
571+
schedule: { cron: '0 1 * * ?', timezone: 'America/New_York' },
572+
}).getInstance(getInstanceProps);
573+
const template = Template.fromStack(lambda.stack);
574+
575+
template.hasResourceProperties('AWS::Scheduler::Schedule', {
576+
ScheduleExpression: 'cron(0 1 * * ? *)',
577+
ScheduleExpressionTimezone: 'America/New_York',
578+
Target: {
579+
Arn: {
580+
// eslint-disable-next-line spellcheck/spell-checker
581+
'Fn::GetAtt': ['handlerlambdaE29D1580', 'Arn'],
582+
},
583+
},
584+
});
585+
});
586+
587+
void it('sets schedule - cron with invalid timezone', () => {
588+
const lambda = defineFunction({
589+
entry: './test-assets/default-lambda/handler.ts',
590+
schedule: { cron: '0 1 * * ?', timezone: 'Invalid/Timezone' },
591+
}).getInstance(getInstanceProps);
592+
const template = Template.fromStack(lambda.stack);
593+
594+
// CDK does not validate timezone
595+
template.hasResourceProperties('AWS::Scheduler::Schedule', {
596+
ScheduleExpression: 'cron(0 1 * * ? *)',
597+
ScheduleExpressionTimezone: 'Invalid/Timezone',
598+
Target: {
599+
Arn: {
600+
// eslint-disable-next-line spellcheck/spell-checker
601+
'Fn::GetAtt': ['handlerlambdaE29D1580', 'Arn'],
602+
},
603+
},
530604
});
531605
});
532606

@@ -537,14 +611,16 @@ void describe('AmplifyFunctionFactory', () => {
537611
}).getInstance(getInstanceProps);
538612
const template = Template.fromStack(lambda.stack);
539613

540-
template.resourceCountIs('AWS::Events::Rule', 2);
614+
template.resourceCountIs('AWS::Scheduler::Schedule', 2);
541615

542-
template.hasResourceProperties('AWS::Events::Rule', {
616+
template.hasResourceProperties('AWS::Scheduler::Schedule', {
543617
ScheduleExpression: 'cron(0 1 * * ? *)',
618+
ScheduleExpressionTimezone: 'UTC',
544619
});
545620

546-
template.hasResourceProperties('AWS::Events::Rule', {
621+
template.hasResourceProperties('AWS::Scheduler::Schedule', {
547622
ScheduleExpression: 'cron(*/5 * * * ? *)',
623+
ScheduleExpressionTimezone: 'UTC',
548624
});
549625
});
550626

@@ -554,7 +630,7 @@ void describe('AmplifyFunctionFactory', () => {
554630
}).getInstance(getInstanceProps);
555631
const template = Template.fromStack(lambda.stack);
556632

557-
template.resourceCountIs('AWS::Events::Rule', 0);
633+
template.resourceCountIs('AWS::Scheduler::Schedule', 0);
558634
});
559635
});
560636

packages/backend-function/src/factory.ts

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ import {
2323
StackProvider,
2424
} from '@aws-amplify/plugin-types';
2525
import { Duration, Size, Stack, Tags } from 'aws-cdk-lib';
26-
import { Rule } from 'aws-cdk-lib/aws-events';
27-
import * as targets from 'aws-cdk-lib/aws-events-targets';
26+
import * as scheduler from 'aws-cdk-lib/aws-scheduler';
27+
import * as targets from 'aws-cdk-lib/aws-scheduler-targets';
2828
import {
2929
Architecture,
3030
CfnFunction,
@@ -47,7 +47,7 @@ import { FunctionEnvironmentTranslator } from './function_env_translator.js';
4747
import { FunctionEnvironmentTypeGenerator } from './function_env_type_generator.js';
4848
import { FunctionLayerArnParser } from './layer_parser.js';
4949
import { convertLoggingOptionsToCDK } from './logging_options_parser.js';
50-
import { convertFunctionSchedulesToRuleSchedules } from './schedule_parser.js';
50+
import { convertFunctionSchedulesToScheduleExpressions } from './schedule_parser.js';
5151
import {
5252
ProvidedFunctionFactory,
5353
ProvidedFunctionProps,
@@ -59,16 +59,32 @@ export type AddEnvironmentFactory = {
5959
addEnvironment: (key: string, value: string | BackendSecret) => void;
6060
};
6161

62-
export type CronSchedule =
62+
export type CronScheduleExpression =
6363
| `${string} ${string} ${string} ${string} ${string}`
6464
| `${string} ${string} ${string} ${string} ${string} ${string}`;
65-
export type TimeInterval =
65+
66+
export type ZonedCronSchedule = {
67+
cron: CronScheduleExpression;
68+
timezone: string;
69+
};
70+
71+
export type CronSchedule = CronScheduleExpression | ZonedCronSchedule;
72+
73+
export type TimeIntervalExpression =
6674
| `every ${number}m`
6775
| `every ${number}h`
6876
| `every day`
6977
| `every week`
7078
| `every month`
7179
| `every year`;
80+
81+
export type ZonedTimeInterval = {
82+
rate: TimeIntervalExpression;
83+
timezone: string;
84+
};
85+
86+
export type TimeInterval = ZonedTimeInterval | TimeIntervalExpression;
87+
7288
export type FunctionSchedule = TimeInterval | CronSchedule;
7389

7490
export type FunctionLogLevel = Extract<
@@ -626,19 +642,19 @@ class AmplifyFunction
626642
}
627643

628644
try {
629-
const schedules = convertFunctionSchedulesToRuleSchedules(
645+
const expressions = convertFunctionSchedulesToScheduleExpressions(
630646
functionLambda,
631647
props.schedule,
632648
);
633-
const lambdaTarget = new targets.LambdaFunction(functionLambda);
634649

635-
schedules.forEach((schedule, index) => {
636-
// Lambda name will be prepended to rule id, so only using index here for uniqueness
637-
const rule = new Rule(functionLambda, `schedule${index}`, {
638-
schedule,
639-
});
650+
const lambdaTarget = new targets.LambdaInvoke(functionLambda);
640651

641-
rule.addTarget(lambdaTarget);
652+
expressions.forEach((expression, index) => {
653+
// Lambda name will be prepended to schedule id, so only using index here for uniqueness
654+
new scheduler.Schedule(functionLambda, `schedule${index}`, {
655+
schedule: expression,
656+
target: lambdaTarget,
657+
});
642658
});
643659
} catch (error) {
644660
throw new AmplifyUserError(

0 commit comments

Comments
 (0)