Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
---

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', () => {
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