Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/ready-cities-wonder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@aws-amplify/backend-function': minor
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to bump @aws-amplify/backend too.

---

feat: add timezone support to scheduling Lambda functions
22 changes: 20 additions & 2 deletions packages/backend-function/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ export type AddEnvironmentFactory = {
};

// @public (undocumented)
export type CronSchedule = `${string} ${string} ${string} ${string} ${string}` | `${string} ${string} ${string} ${string} ${string} ${string}`;
export type CronSchedule = CronScheduleExpression | ZonedCronSchedule;

// @public (undocumented)
export type CronScheduleExpression = `${string} ${string} ${string} ${string} ${string}` | `${string} ${string} ${string} ${string} ${string} ${string}`;

// @public (undocumented)
type DataClientConfig = {
Expand Down Expand Up @@ -141,7 +144,22 @@ type ResourceConfig = {
};

// @public (undocumented)
export type TimeInterval = `every ${number}m` | `every ${number}h` | `every day` | `every week` | `every month` | `every year`;
export type TimeInterval = ZonedTimeInterval | TimeIntervalExpression;

// @public (undocumented)
export type TimeIntervalExpression = `every ${number}m` | `every ${number}h` | `every day` | `every week` | `every month` | `every year`;

// @public (undocumented)
export type ZonedCronSchedule = {
cron: CronScheduleExpression;
timezone: string;
};

// @public (undocumented)
export type ZonedTimeInterval = {
rate: TimeIntervalExpression;
timezone: string;
};

// (No @packageDocumentation comment for this package)

Expand Down
80 changes: 58 additions & 22 deletions packages/backend-function/src/factory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -495,17 +495,34 @@ void describe('AmplifyFunctionFactory', () => {
}).getInstance(getInstanceProps);
const template = Template.fromStack(lambda.stack);

template.hasResourceProperties('AWS::Events::Rule', {
template.hasResourceProperties('AWS::Scheduler::Schedule', {
ScheduleExpression: 'cron(*/5 * * * ? *)',
Targets: [
{
Arn: {
// eslint-disable-next-line spellcheck/spell-checker
'Fn::GetAtt': ['handlerlambdaE29D1580', 'Arn'],
},
Id: 'Target0',
ScheduleExpressionTimezone: 'UTC',
Target: {
Arn: {
// eslint-disable-next-line spellcheck/spell-checker
'Fn::GetAtt': ['handlerlambdaE29D1580', 'Arn'],
},
],
},
});
});

void it('sets valid schedule - rate with timezone', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add some test cases for invalid inputs for rate and timezone (invalid timezone)?

const lambda = defineFunction({
entry: './test-assets/default-lambda/handler.ts',
schedule: { rate: 'every 5m', timezone: 'America/New_York' },
}).getInstance(getInstanceProps);
const template = Template.fromStack(lambda.stack);

template.hasResourceProperties('AWS::Scheduler::Schedule', {
ScheduleExpression: 'cron(*/5 * * * ? *)',
ScheduleExpressionTimezone: 'America/New_York',
Target: {
Arn: {
// eslint-disable-next-line spellcheck/spell-checker
'Fn::GetAtt': ['handlerlambdaE29D1580', 'Arn'],
},
},
});
});

Expand All @@ -516,17 +533,34 @@ void describe('AmplifyFunctionFactory', () => {
}).getInstance(getInstanceProps);
const template = Template.fromStack(lambda.stack);

template.hasResourceProperties('AWS::Events::Rule', {
template.hasResourceProperties('AWS::Scheduler::Schedule', {
ScheduleExpression: 'cron(0 1 * * ? *)',
Targets: [
{
Arn: {
// eslint-disable-next-line spellcheck/spell-checker
'Fn::GetAtt': ['handlerlambdaE29D1580', 'Arn'],
},
Id: 'Target0',
ScheduleExpressionTimezone: 'UTC',
Target: {
Arn: {
// eslint-disable-next-line spellcheck/spell-checker
'Fn::GetAtt': ['handlerlambdaE29D1580', 'Arn'],
},
],
},
});
});

void it('sets valid schedule - cron with timezone', () => {
const lambda = defineFunction({
entry: './test-assets/default-lambda/handler.ts',
schedule: { cron: '0 1 * * ?', timezone: 'America/New_York' },
}).getInstance(getInstanceProps);
const template = Template.fromStack(lambda.stack);

template.hasResourceProperties('AWS::Scheduler::Schedule', {
ScheduleExpression: 'cron(0 1 * * ? *)',
ScheduleExpressionTimezone: 'America/New_York',
Target: {
Arn: {
// eslint-disable-next-line spellcheck/spell-checker
'Fn::GetAtt': ['handlerlambdaE29D1580', 'Arn'],
},
},
});
});

Expand All @@ -537,14 +571,16 @@ void describe('AmplifyFunctionFactory', () => {
}).getInstance(getInstanceProps);
const template = Template.fromStack(lambda.stack);

template.resourceCountIs('AWS::Events::Rule', 2);
template.resourceCountIs('AWS::Scheduler::Schedule', 2);

template.hasResourceProperties('AWS::Events::Rule', {
template.hasResourceProperties('AWS::Scheduler::Schedule', {
ScheduleExpression: 'cron(0 1 * * ? *)',
ScheduleExpressionTimezone: 'UTC',
});

template.hasResourceProperties('AWS::Events::Rule', {
template.hasResourceProperties('AWS::Scheduler::Schedule', {
ScheduleExpression: 'cron(*/5 * * * ? *)',
ScheduleExpressionTimezone: 'UTC',
});
});

Expand All @@ -554,7 +590,7 @@ void describe('AmplifyFunctionFactory', () => {
}).getInstance(getInstanceProps);
const template = Template.fromStack(lambda.stack);

template.resourceCountIs('AWS::Events::Rule', 0);
template.resourceCountIs('AWS::Scheduler::Schedule', 0);
});
});

Expand Down
42 changes: 29 additions & 13 deletions packages/backend-function/src/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ import {
StackProvider,
} from '@aws-amplify/plugin-types';
import { Duration, Size, Stack, Tags } from 'aws-cdk-lib';
import { Rule } from 'aws-cdk-lib/aws-events';
import * as targets from 'aws-cdk-lib/aws-events-targets';
import * as scheduler from 'aws-cdk-lib/aws-scheduler';
import * as targets from 'aws-cdk-lib/aws-scheduler-targets';
import {
Architecture,
CfnFunction,
Expand All @@ -47,7 +47,7 @@ import { FunctionEnvironmentTranslator } from './function_env_translator.js';
import { FunctionEnvironmentTypeGenerator } from './function_env_type_generator.js';
import { FunctionLayerArnParser } from './layer_parser.js';
import { convertLoggingOptionsToCDK } from './logging_options_parser.js';
import { convertFunctionSchedulesToRuleSchedules } from './schedule_parser.js';
import { convertFunctionSchedulesToScheduleExpressions } from './schedule_parser.js';
import {
ProvidedFunctionFactory,
ProvidedFunctionProps,
Expand All @@ -59,16 +59,32 @@ export type AddEnvironmentFactory = {
addEnvironment: (key: string, value: string | BackendSecret) => void;
};

export type CronSchedule =
export type CronScheduleExpression =
| `${string} ${string} ${string} ${string} ${string}`
| `${string} ${string} ${string} ${string} ${string} ${string}`;
export type TimeInterval =

export type ZonedCronSchedule = {
cron: CronScheduleExpression;
timezone: string;
};

export type CronSchedule = CronScheduleExpression | ZonedCronSchedule;

export type TimeIntervalExpression =
| `every ${number}m`
| `every ${number}h`
| `every day`
| `every week`
| `every month`
| `every year`;

export type ZonedTimeInterval = {
rate: TimeIntervalExpression;
timezone: string;
};

export type TimeInterval = ZonedTimeInterval | TimeIntervalExpression;

export type FunctionSchedule = TimeInterval | CronSchedule;

export type FunctionLogLevel = Extract<
Expand Down Expand Up @@ -626,19 +642,19 @@ class AmplifyFunction
}

try {
const schedules = convertFunctionSchedulesToRuleSchedules(
const expressions = convertFunctionSchedulesToScheduleExpressions(
functionLambda,
props.schedule,
);
const lambdaTarget = new targets.LambdaFunction(functionLambda);

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

rule.addTarget(lambdaTarget);
expressions.forEach((expression, index) => {
// Lambda name will be prepended to schedule id, so only using index here for uniqueness
new scheduler.Schedule(functionLambda, `schedule${index}`, {
schedule: expression,
target: lambdaTarget,
});
});
} catch (error) {
throw new AmplifyUserError(
Expand Down
Loading
Loading