Skip to content

Commit 741ab20

Browse files
authored
add asynchronous job infrastructure (#45)
*Issue #, if available:* *Description of changes:* * Implement aynschronous job infrastructure using Lambda * Add AppSync Events API for realtime notification (e.g. on job completion) * Update docs By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
1 parent 7058217 commit 741ab20

40 files changed

Lines changed: 3157 additions & 516 deletions

.github/workflows/build.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
name: Build
2-
on: [push, workflow_dispatch]
2+
on:
3+
push:
4+
branches:
5+
- main
6+
workflow_dispatch:
7+
pull_request:
38
jobs:
49
Build-and-Test-CDK:
510
runs-on: ubuntu-latest

README.md

Lines changed: 38 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,35 @@
11
# Serverless Full Stack WebApp Starter Kit
22
[![Build](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/actions/workflows/build.yml/badge.svg)](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/actions/workflows/build.yml)
33

4-
[日本語の解説記事はこちら](https://tmokmss.hatenablog.com/entry/20220611/1654931458)。 (Additional article about this kit in Japanese.)
5-
64
This is a full stack webapp kit for starters who want to leverage the power of AWS serverless services!
75

86
Features include:
97

10-
* Express API endpoint (both with and without authentication)
11-
* React.js frontend (assets are delivered via CDN)
12-
* E-mail authentication
8+
* Next.js App Router on AWS Lambda
9+
* CloudFront + Lambda function URL with response stream support
10+
* End to end type safety from client to server
11+
* Cognito authentication
1312
* Asynchronous job queue
14-
* Scheduled job runner
15-
* Instant deployment of the entire app
16-
17-
**NOTE:** You can also refer to [aws-samples/trpc-nextjs-ssr-prisma-lambda](https://github.com/aws-samples/trpc-nextjs-ssr-prisma-lambda) for end-to-end type safety, server side rendering, or relational database examples.
13+
* Instant deployment of the entire app with a single command
1814

1915
## Overview
2016
Here is the architecture of this kit. We use:
2117

22-
* [Amazon DynamoDB](https://aws.amazon.com/dynamodb/), a serverless scalable NoSQL database
23-
* [Amazon API Gateway HTTP API](https://aws.amazon.com/api-gateway/) + [AWS Lambda](https://aws.amazon.com/lambda/) to build serverless API endpoint ([`serverless-express`](https://github.com/vendia/serverless-express))
24-
* [Amazon CloudFront](https://aws.amazon.com/cloudfront/) + [S3](https://aws.amazon.com/s3/) to distribute frontend assets (React.js, Amplify libraries, MUI)
18+
* [Amazon Aurora PostgreSQL Serverless v2](https://aws.amazon.com/rds/aurora/serverless/), a serverless relational database with Prisma ORM
19+
* [Next.js App Router](https://nextjs.org/docs/app) on [AWS Lambda](https://aws.amazon.com/lambda/) for a unified frontend and backend solution
20+
* [Amazon CloudFront](https://aws.amazon.com/cloudfront/) + Lambda Function URL with response streaming support for efficient content delivery
2521
* [Amazon Cognito](https://aws.amazon.com/cognito/) for authentication. By default, you can sign in/up by email, but you can federate with other OIDC providers such as Google, Facebook, and more with a little modification.
26-
* [Amazon SQS](https://aws.amazon.com/sqs/) + AWS Lambda for asynchronous job queue.
27-
* [Amazon EventBridge](https://aws.amazon.com/eventbridge/) to run scheduled jobs.
22+
* [AWS AppSync Events](https://docs.aws.amazon.com/appsync/latest/eventapi/event-api-welcome.html) + AWS Lambda for asynchronous job and realtime notification.
23+
* [Amazon EventBridge](https://aws.amazon.com/eventbridge/) to run scheduled jobs.
2824
* [Amazon CloudWatch](https://aws.amazon.com/cloudwatch/) + S3 for access logging.
2925
* [AWS CDK](https://aws.amazon.com/cdk/) for Infrastructure as Code. It enables you to deploy the entire application with the simplest commands.
3026

31-
![architecture](imgs/architecture.svg)
27+
![architecture](imgs/architecture.png)
3228

3329
Since it fully leverages AWS serverless services, you can use it with high cost efficiency, scalability, and almost no heavy lifting of managing servers! In terms of cost, we further discuss how much it costs in the below [#Cost](#cost) section.
3430

3531
### About the sample app
36-
To show how this kit works, we include a sample web app to write and store your memos.
32+
To show how this kit works, we include a sample web app to manage your todo list.
3733
With this sample, you can easily understand how each component works with other ones, and what the overall experience will be like.
3834

3935
<img align="right" width="300" src="./imgs/signin.png">
@@ -51,9 +47,19 @@ You can further improve this sample or remove all the specific code and write yo
5147
## Deploy
5248
You need the following tools to deploy this sample:
5349

54-
* [Node.js](https://nodejs.org/en/download/) (>= v16)
50+
* [Node.js](https://nodejs.org/en/download/) (>= v20)
5551
* [Docker](https://docs.docker.com/get-docker/)
5652
* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) and a configured IAM profile
53+
* A public domain name configured as a Hosted Zone in Amazon Route53
54+
55+
Before deployment, you need to update the domain name in [`bin/cdk.ts`](cdk/bin/cdk.ts) to use your own domain that is configured as a Hosted Zone in Route53:
56+
57+
```typescript
58+
const props: EnvironmentProps = {
59+
// ...
60+
domainName: 'your-domain.example.com', // Replace with your domain name
61+
};
62+
```
5763

5864
Then run the following commands:
5965

@@ -64,53 +70,45 @@ npx cdk bootstrap
6470
npx cdk deploy
6571
```
6672

67-
Initial deployment usually takes about 10 minutes. You can also use `npx cdk deploy` command to deploy when you modified your CDK templates in the future.
73+
Initial deployment usually takes about 20 minutes. You can also use `npx cdk deploy` command to deploy when you modified your CDK templates in the future.
6874

6975
After a successful deployment, you will get a CLI output like the below:
7076

7177
```
72-
ServerlessFullstackWebappStarterKitStack
78+
ServerlessWebappStarterKitStack
7379
74-
✨ Deployment time: 235.21s
80+
✨ Deployment time: 407.12s
7581
7682
Outputs:
77-
ServerlessFullstackWebappStarterKitStack.AuthUserPoolClientId8216BF9A = xxxxxxxxxxxx
78-
ServerlessFullstackWebappStarterKitStack.AuthUserPoolIdC0605E59 = ap-northeast-1_xxxxxxx
79-
ServerlessFullstackWebappStarterKitStack.BackendApiBackendApiUrl4A0A7879 = https://xxxxxx.execute-api.ap-northeast-1.amazonaws.com
80-
ServerlessFullstackWebappStarterKitStack.FrontendDomainName = https://xxxxxxxxxx.cloudfront.net
81-
Stack ARN:
82-
arn:aws:cloudformation:ap-northeast-1:123456789012:stack/ServerlessFullstackWebappStarterKitStack/e47a02d0-e40a-11ec-8ea7-0ed955f95f17
83+
ServerlessWebappStarterKitStack.AuthUserPoolClientId8216BF9A = xxxxxxxxxxxxxxxxxx
84+
ServerlessWebappStarterKitStack.AuthUserPoolIdC0605E59 = us-west-2_xxxxxx
85+
ServerlessWebappStarterKitStack.DatabaseDatabaseSecretsCommandF4A622EB = aws secretsmanager get-secret-value --secret-id DatabaseClusterSecretD1FB63-xxxxxxx --region us-west-2
86+
ServerlessWebappStarterKitStack.DatabasePortForwardCommandC3718B89 = aws ssm start-session --region us-west-2 --target i-xxxxxxxxxx --document-name AWS-StartPortForwardingSessionToRemoteHost --parameters '{"portNumber":["5432"], "localPortNumber":["5432"], "host": ["foo.cluster-bar.us-west-2.rds.amazonaws.com"]}'
87+
ServerlessWebappStarterKitStack.FrontendDomainName = https://web.mtomooka.people.aws.dev
8388
```
8489

8590
Opening the URL in `FrontendDomainName` output, you can now try the sample app on your browser.
8691

8792
## Add your own features
88-
To implement your own features, you may want to add frontend pages, backend API endpoints, or async jobs. The frontend is an ordinary React.js application, so you can follow the conventional ways to add pages to it. As for backend, there are step-by-step guides to add features in [backend/README.md](backend/README.md), so please follow the guide.
93+
To implement your own features, you may want to add frontend pages, API routes, or async jobs. The project uses Next.js App Router, which provides a unified approach for both frontend and backend development. You can follow the conventional Next.js patterns to add pages and API routes. For more details, please refer to the [`webapp/README.md`](./webapp/README.md) guide.
8994

9095
If you want to add another authentication method such as Google or Facebook federation, you can follow this document: [Add social sign-in to a user pool](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-configuring-federation-with-social-idp.html).
9196

9297
## Local development
93-
To develop frontend or backend locally, please refer to each `README.md` in the subdirectories:
98+
To develop the webapp locally, please refer to the [`webapp/README.md`](./webapp/README.md).
9499

95-
* [Frontend](./frontend/README.md)
96-
* [Backend](./backend/README.md)
97-
98-
Instead of running the backend API on your local environment, you can use `cdk watch` feature for development by just running the following command:
100+
Before running webapp locally, you have to populate .env.local by the following steps:
99101

100102
```sh
101-
cd cdk
102-
npx cdk watch
103+
cp .env.local.example .env.local
104+
# edit .env.local using the stack outputs shown after cdk deploy
103105
```
104106

105-
`cdk watch` allows you to instantly deploy your backend as soon as you change the code and "tail" logs from your Lambda functions, enabling rapid iteration cycles. See [this blog for more details](https://aws.amazon.com/blogs/developer/increasing-development-speed-with-cdk-watch/).
106-
107-
Using `cdk watch`, you will access the deployed AWS resources from your local frontend. You have to configure environment variables in [frontend/.env](./frontend/.env) file to properly access these resources. Please refer to [Frontend README](./frontend/README.md) for more details.
108-
109107
## Cost
110-
API Gateway, Lambda, SQS, CloudWatch, CloudFront, and S3 offer free tier plans, which allows you to use those services almost freely for small businesses.
108+
Lambda, SQS, CloudWatch, CloudFront, and S3 offer free tier plans, which allows you to use those services almost freely for small businesses.
111109
Up to one million requests per month, most of the costs related to those services are free. See [this page for more details](https://aws.amazon.com/free/).
112110

113-
DynamoDB is billed basically by how many read and write counts processed. See [this page for the current prices](https://aws.amazon.com/dynamodb/pricing/on-demand/). DynamoDB provisioned capacity mode also offers free tier plans, so if you want to pay the minimal cost, you can switch the billing mode (see [database.ts](cdk/lib/constructs/database.ts)).
111+
Aurora PostgreSQL Serverless v2 is billed based on compute and storage usage. The database automatically scales up and down based on workload, and you only pay for the resources you use. See [this page for the current prices](https://aws.amazon.com/rds/aurora/pricing/). Aurora Serverless v2 can scale down to almost zero during periods of inactivity, helping to minimize costs (see [database.ts](cdk/lib/constructs/database.ts) for configuration details).
114112

115113
Other costs will be derived from data transfer and Elastic Container Repository (used for Docker Lambda). Although it usually does not cost much compared to other services, you may want to continuously monitor the billing metrics. Please refer to [the document to set CloudWatch alarm for AWS charges](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/monitor_estimated_charges_with_cloudwatch.html).
116114

cdk/bin/serverless-fullstack-webapp-starter-kit.ts renamed to cdk/bin/cdk.ts

File renamed without changes.

cdk/cdk.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"app": "npx ts-node --prefer-ts-exts bin/serverless-fullstack-webapp-starter-kit.ts",
2+
"app": "npx ts-node --prefer-ts-exts bin/cdk.ts",
33
"watch": {
44
"include": [
55
"**"

cdk/lib/constructs/async-job.ts

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,44 @@
11
import { Construct } from 'constructs';
2-
import { Duration } from 'aws-cdk-lib';
3-
import { ITable } from 'aws-cdk-lib/aws-dynamodb';
4-
import { DockerImageCode, DockerImageFunction } from 'aws-cdk-lib/aws-lambda';
5-
import { Queue, QueueEncryption } from 'aws-cdk-lib/aws-sqs';
6-
import { SqsEventSource } from 'aws-cdk-lib/aws-lambda-event-sources';
2+
import { CfnOutput, Duration } from 'aws-cdk-lib';
3+
import { Architecture, DockerImageCode, DockerImageFunction, IFunction } from 'aws-cdk-lib/aws-lambda';
74
import { Platform } from 'aws-cdk-lib/aws-ecr-assets';
5+
import { Database } from './database';
6+
import { EventBus } from './event-bus';
87

98
export interface AsyncJobProps {
10-
readonly database: ITable;
9+
readonly database: Database;
10+
readonly eventBus: EventBus;
1111
}
1212

1313
export class AsyncJob extends Construct {
14-
readonly queue: Queue;
14+
readonly handler: IFunction;
15+
1516
constructor(scope: Construct, id: string, props: AsyncJobProps) {
1617
super(scope, id);
17-
const { database } = props;
18-
19-
const visibilityTimeout = Duration.minutes(10);
20-
21-
const queue = new Queue(this, 'Queue', {
22-
visibilityTimeout,
23-
encryption: QueueEncryption.KMS_MANAGED,
24-
});
18+
const { database, eventBus } = props;
2519

2620
const handler = new DockerImageFunction(this, 'Handler', {
27-
code: DockerImageCode.fromImageAsset('../backend', {
28-
cmd: ['handler-job.handler'],
29-
platform: Platform.LINUX_AMD64,
21+
code: DockerImageCode.fromImageAsset('../webapp', {
22+
cmd: ['async-job-runner.handler'],
23+
platform: Platform.LINUX_ARM64,
24+
file: 'job.Dockerfile',
3025
}),
3126
memorySize: 256,
32-
timeout: visibilityTimeout,
27+
timeout: Duration.minutes(10),
28+
architecture: Architecture.ARM_64,
3329
environment: {
34-
TABLE_NAME: database.tableName,
30+
...database.getLambdaEnvironment('main'),
31+
EVENT_HTTP_ENDPOINT: eventBus.httpEndpoint,
3532
},
36-
// limit concurrency to mitigate any possible EDoS attacks
33+
vpc: database.cluster.vpc,
34+
// limit concurrency to mitigate any possible EDoS attacks
3735
reservedConcurrentExecutions: 1,
3836
});
3937

40-
database.grantReadWriteData(handler);
41-
handler.addEventSource(new SqsEventSource(queue, { maxBatchingWindow: Duration.seconds(5) }));
38+
handler.connections.allowToDefaultPort(database);
39+
eventBus.api.grantPublish(handler);
4240

43-
this.queue = queue;
41+
new CfnOutput(this, 'HandlerArn', { value: handler.functionArn });
42+
this.handler = handler;
4443
}
4544
}

cdk/lib/constructs/cf-lambda-furl-service/service.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,5 @@ export class CloudFrontLambdaFunctionUrlService extends Construct {
168168
});
169169

170170
this.url = `https://${domainName}`;
171-
new CfnOutput(this, 'CloudFrontUrl', { value: this.url });
172171
}
173172
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { util } from '@aws-appsync/utils';
2+
3+
/**
4+
* Allow subscription only for the channels that
5+
* 1. begin with /public
6+
* 2. begin with /user/<userId>
7+
* https://docs.aws.amazon.com/appsync/latest/eventapi/channel-namespace-handlers.html
8+
*/
9+
export function onSubscribe(ctx) {
10+
if (ctx.info.channel.path.startsWith(`/event-bus/public`)) {
11+
return;
12+
}
13+
if (ctx.info.channel.path.startsWith(`/event-bus/user/${ctx.identity.username}`)) {
14+
return;
15+
}
16+
console.log(`user ${ctx.identity.username} tried connecting to wrong channel: ${ctx.channel}`);
17+
util.unauthorized();
18+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { Construct } from 'constructs';
2+
import * as appsync from 'aws-cdk-lib/aws-appsync';
3+
import { CfnOutput, CfnResource, Names, Stack } from 'aws-cdk-lib';
4+
import { IUserPool } from 'aws-cdk-lib/aws-cognito';
5+
import { join } from 'path';
6+
7+
export interface EventBusProps {}
8+
9+
export class EventBus extends Construct {
10+
public readonly httpEndpoint: string;
11+
public readonly api: appsync.EventApi;
12+
13+
private userPoolCount = 0;
14+
15+
constructor(scope: Construct, id: string, props: EventBusProps) {
16+
super(scope, id);
17+
18+
const api = new appsync.EventApi(this, 'Api', {
19+
apiName: Names.uniqueResourceName(this, { maxLength: 30 }),
20+
authorizationConfig: {
21+
authProviders: [
22+
{
23+
authorizationType: appsync.AppSyncAuthorizationType.IAM,
24+
},
25+
],
26+
connectionAuthModeTypes: [appsync.AppSyncAuthorizationType.IAM],
27+
defaultPublishAuthModeTypes: [appsync.AppSyncAuthorizationType.IAM],
28+
defaultSubscribeAuthModeTypes: [appsync.AppSyncAuthorizationType.IAM],
29+
},
30+
});
31+
32+
new appsync.ChannelNamespace(this, 'Namespace', {
33+
api,
34+
channelNamespaceName: 'event-bus',
35+
code: appsync.Code.fromAsset(join(__dirname, 'handler.mjs')),
36+
});
37+
38+
this.httpEndpoint = `https://${api.httpDns}`;
39+
this.api = api;
40+
41+
new CfnOutput(this, 'HttpEndpoint', { value: this.httpEndpoint });
42+
}
43+
44+
public addUserPoolProvider(userPool: IUserPool) {
45+
if (this.userPoolCount == 0) {
46+
(this.api.node.defaultChild as CfnResource).addPropertyOverride('EventConfig.ConnectionAuthModes.1', {
47+
AuthType: 'AMAZON_COGNITO_USER_POOLS',
48+
});
49+
(this.api.node.defaultChild as CfnResource).addPropertyOverride('EventConfig.DefaultSubscribeAuthModes.1', {
50+
AuthType: 'AMAZON_COGNITO_USER_POOLS',
51+
});
52+
}
53+
54+
this.userPoolCount += 1;
55+
(this.api.node.defaultChild as CfnResource).addPropertyOverride(`EventConfig.AuthProviders.${this.userPoolCount}`, {
56+
AuthType: 'AMAZON_COGNITO_USER_POOLS',
57+
CognitoConfig: {
58+
AwsRegion: Stack.of(this).region,
59+
UserPoolId: userPool.userPoolId,
60+
},
61+
});
62+
}
63+
}

0 commit comments

Comments
 (0)