├── .npmignore ├── lib ├── 01-Lambda │ ├── myFirstLambda │ │ └── handler.ts │ ├── rollADice │ │ └── handler.ts │ ├── rollManyDices │ │ └── handler.ts │ └── stack.ts ├── 10-SNS │ ├── requestDelivery │ │ └── handler.ts │ ├── sendNotification │ │ └── handler.ts │ ├── executeOrder │ │ └── handler.ts │ ├── orderItem │ │ └── handler.ts │ └── stack.ts ├── 17-EventBridgeScheduler │ ├── executeMemo.ts │ ├── addMemo.ts │ └── stack.ts ├── 04-Cognito │ ├── secret │ │ └── handler.ts │ ├── confirm │ │ └── handler.ts │ ├── signup │ │ └── handler.ts │ ├── signin │ │ └── handler.ts │ └── stack.ts ├── 11-DynamoDBStreams │ ├── types.ts │ ├── onReservationConfirmed │ │ ├── handler.ts │ │ └── template.ts │ ├── confirmReservation │ │ └── handler.ts │ ├── onRestaurantBooked │ │ ├── handler.ts │ │ └── template.ts │ ├── bookRestaurant │ │ └── handler.ts │ ├── streamTarget │ │ └── handler.ts │ └── stack.ts ├── 15-UploadS3 │ └── README.md ├── 12-Frontend │ └── README.md ├── 13-LambdaTypes │ └── README.md ├── 16-LambdaDestinations │ ├── lambda.ts │ └── stack.ts ├── 05-StepFunctions │ ├── createOrder │ │ └── handler.ts │ ├── updateItemStock │ │ └── handler.ts │ ├── isItemInStock │ │ └── handler.ts │ ├── createStoreItem │ │ └── handler.ts │ └── stack.ts ├── 03-S3 │ ├── listArticles │ │ └── handler.ts │ ├── getArticle │ │ └── handler.ts │ ├── publishArticle │ │ └── handler.ts │ └── stack.ts ├── 07-EventBridge │ ├── registerBooking │ │ └── handler.ts │ ├── syncFlights │ │ └── handler.ts │ ├── sendBookingReceipt │ │ └── handler.ts │ ├── bookingReceiptHtmlTemplate.ts │ ├── bookFlight │ │ └── handler.ts │ └── stack.ts ├── 14-MasterDynamoDB │ ├── toolbox.ts │ ├── userEntity.ts │ ├── classic.ts │ ├── document.ts │ └── stack.ts ├── 08-SQS │ ├── executeOrder │ │ └── handler.ts │ ├── notifyOrderExecuted │ │ └── handler.ts │ ├── orderExecutedHtmlTemplate.ts │ ├── requestOrder │ │ └── handler.ts │ └── stack.ts ├── 02-DynamoDB │ ├── createNote │ │ └── handler.ts │ ├── getNote │ │ └── handler.ts │ └── stack.ts ├── 09-Aurora │ ├── getUsers │ │ └── handler.ts │ ├── addUser │ │ └── handler.ts │ ├── runMigrations │ │ └── handler.ts │ └── stack.ts └── 06-SES │ ├── emailHtmlTemplate.ts │ ├── sendEmail │ └── handler.ts │ └── stack.ts ├── .gitignore ├── jest.config.js ├── test └── learn-serverless.test.ts ├── tsconfig.json ├── package.json ├── cdk.json ├── bin └── learn-serverless.ts └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /lib/01-Lambda/myFirstLambda/handler.ts: -------------------------------------------------------------------------------- 1 | export const handler = (): Promise => Promise.resolve('Hello World!'); -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | -------------------------------------------------------------------------------- /lib/10-SNS/requestDelivery/handler.ts: -------------------------------------------------------------------------------- 1 | export const handler = async (): Promise => { 2 | console.log("DELIVERY REQUESTED"); 3 | } 4 | -------------------------------------------------------------------------------- /lib/10-SNS/sendNotification/handler.ts: -------------------------------------------------------------------------------- 1 | export const handler = async (): Promise => { 2 | console.log("NOTIFICATION SENT"); 3 | } 4 | -------------------------------------------------------------------------------- /lib/17-EventBridgeScheduler/executeMemo.ts: -------------------------------------------------------------------------------- 1 | export const handler = async ({ memo }: { memo: string }): Promise => { 2 | console.log(memo); 3 | 4 | return Promise.resolve(); 5 | } -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | roots: ['/test'], 4 | testMatch: ['**/*.test.ts'], 5 | transform: { 6 | '^.+\\.tsx?$': 'ts-jest' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /lib/04-Cognito/secret/handler.ts: -------------------------------------------------------------------------------- 1 | export const handler = async (): Promise<{ statusCode: number, body: string }> => { 2 | 3 | return Promise.resolve({ statusCode: 200, body: 'THIS IS VERY SECRET' }); 4 | } -------------------------------------------------------------------------------- /lib/11-DynamoDBStreams/types.ts: -------------------------------------------------------------------------------- 1 | export type Reservation = { 2 | id: string; 3 | firstName: string; 4 | lastName: string; 5 | email: string; 6 | dateTime: string; 7 | partySize: number; 8 | }; 9 | -------------------------------------------------------------------------------- /lib/01-Lambda/rollADice/handler.ts: -------------------------------------------------------------------------------- 1 | export const handler = async (): Promise<{ statusCode: number, body: number }> => { 2 | const randomNumber = Math.floor(Math.random() * 6) + 1; 3 | 4 | return Promise.resolve({ statusCode: 200, body: randomNumber }); 5 | } -------------------------------------------------------------------------------- /lib/15-UploadS3/README.md: -------------------------------------------------------------------------------- 1 | # Code is in a different repo! 2 | 3 | The code for article 15: "Upload files on S3" is not in this repo! (it needs a frontend) 4 | 5 | [➡️ Find it here](https://github.com/PChol22/learn-serverless-upload-s3) 6 | 7 | (Last commit on main) -------------------------------------------------------------------------------- /lib/12-Frontend/README.md: -------------------------------------------------------------------------------- 1 | # Code is in a different repo! 2 | 3 | The code for article 12: "Deploy a frontend" is not in this repo! (it needs a frontend) 4 | 5 | [➡️ Find it here](https://github.com/PChol22/learn-serverless-backendxfrontend/tree/episode-12) 6 | 7 | (Specific branch, continues in article 13) -------------------------------------------------------------------------------- /lib/13-LambdaTypes/README.md: -------------------------------------------------------------------------------- 1 | # Code is in a different repo! 2 | 3 | The code for article 13: "Strongly type Lambda functions" is not in this repo! (it needs a frontend) 4 | 5 | [➡️ Find it here](https://github.com/PChol22/learn-serverless-backendxfrontend) 6 | 7 | (Last commit on main, direct follow-up of article 12) -------------------------------------------------------------------------------- /lib/16-LambdaDestinations/lambda.ts: -------------------------------------------------------------------------------- 1 | export const handler = async (): Promise => { 2 | // 80% chance of going wrong 3 | // 64% counting the retry 4 | const success = Math.random() > 0.8; 5 | 6 | if (!success) { 7 | throw new Error('Something went wrong!'); 8 | } 9 | 10 | return Promise.resolve(); 11 | } 12 | -------------------------------------------------------------------------------- /lib/10-SNS/executeOrder/handler.ts: -------------------------------------------------------------------------------- 1 | export const handler = async (event: { 2 | Records: { 3 | Sns: { 4 | Message: string; 5 | }; 6 | }[]; 7 | }): Promise => { 8 | event.Records.forEach(({ Sns: { Message }}) => { 9 | const { item, quantity } = JSON.parse(Message) as { item: string; quantity: number; }; 10 | 11 | console.log(`ORDER EXECUTED - Item: ${item}, Quantity: ${quantity}`); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /lib/01-Lambda/rollManyDices/handler.ts: -------------------------------------------------------------------------------- 1 | type LambdaInput = { pathParameters: { nbOfDices: string } }; 2 | 3 | export const handler = async ({ pathParameters: { nbOfDices } }: LambdaInput): Promise<{ statusCode: number, body?: number }> => { 4 | if (Number.isNaN(+nbOfDices)) { 5 | return Promise.resolve({ statusCode: 400 }); 6 | } 7 | 8 | let total = 0; 9 | 10 | for (let i = 0; i < +nbOfDices; i++) { 11 | total += Math.floor(Math.random() * 6) + 1; 12 | } 13 | 14 | return Promise.resolve({ statusCode: 200, body: total }); 15 | } -------------------------------------------------------------------------------- /lib/05-StepFunctions/createOrder/handler.ts: -------------------------------------------------------------------------------- 1 | import { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb"; 2 | import { v4 as uuid } from 'uuid'; 3 | 4 | const client = new DynamoDBClient({}); 5 | 6 | export const handler = async ({ order }: { order: { itemId: string, quantity: number }[] }): Promise => { 7 | const tableName = process.env.TABLE_NAME; 8 | 9 | await client.send(new PutItemCommand({ 10 | TableName: tableName, 11 | Item: { 12 | PK: { S: 'Order' }, 13 | SK: { S: uuid() }, 14 | order: { L: order.map(({ itemId, quantity }) => ({ M: { itemId: { S: itemId }, quantity: { N: quantity.toString() } } })) } 15 | } 16 | })); 17 | } 18 | -------------------------------------------------------------------------------- /lib/05-StepFunctions/updateItemStock/handler.ts: -------------------------------------------------------------------------------- 1 | import { DynamoDBClient, UpdateItemCommand } from "@aws-sdk/client-dynamodb"; 2 | 3 | const client = new DynamoDBClient({}); 4 | 5 | export const handler = async ({ item : { itemId, quantity } }: { item: { itemId: string, quantity: number } }): Promise => { 6 | const tableName = process.env.TABLE_NAME; 7 | 8 | await client.send(new UpdateItemCommand({ 9 | TableName: tableName, 10 | Key: { 11 | PK: { S: 'StoreItem' }, 12 | SK: { S: itemId } 13 | }, 14 | UpdateExpression: 'SET stock = stock - :quantity', 15 | ExpressionAttributeValues: { 16 | ':quantity': { N: quantity.toString() } 17 | }, 18 | })); 19 | } 20 | -------------------------------------------------------------------------------- /test/learn-serverless.test.ts: -------------------------------------------------------------------------------- 1 | // import * as cdk from 'aws-cdk-lib'; 2 | // import { Template } from 'aws-cdk-lib/assertions'; 3 | // import * as LearnServerless from '../lib/learn-serverless-stack'; 4 | 5 | // example test. To run these tests, uncomment this file along with the 6 | // example resource in lib/learn-serverless-stack.ts 7 | test('SQS Queue Created', () => { 8 | // const app = new cdk.App(); 9 | // // WHEN 10 | // const stack = new LearnServerless.LearnServerlessStack(app, 'MyTestStack'); 11 | // // THEN 12 | // const template = Template.fromStack(stack); 13 | 14 | // template.hasResourceProperties('AWS::SQS::Queue', { 15 | // VisibilityTimeout: 300 16 | // }); 17 | }); 18 | -------------------------------------------------------------------------------- /lib/05-StepFunctions/isItemInStock/handler.ts: -------------------------------------------------------------------------------- 1 | import { DynamoDBClient, GetItemCommand } from "@aws-sdk/client-dynamodb"; 2 | 3 | const client = new DynamoDBClient({}); 4 | 5 | export const handler = async ({ item : { itemId, quantity } }: { item: { itemId: string, quantity: number } }): Promise => { 6 | const tableName = process.env.TABLE_NAME; 7 | 8 | const { Item } = await client.send(new GetItemCommand({ 9 | TableName: tableName, 10 | Key: { 11 | PK: { S: 'StoreItem' }, 12 | SK: { S: itemId } 13 | } 14 | })); 15 | 16 | const stock = Item?.stock.N; 17 | 18 | if (stock === undefined || +stock < quantity) { 19 | throw new Error('Item not in stock'); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": ["es2020"], 6 | "declaration": true, 7 | "strict": true, 8 | "noImplicitAny": true, 9 | "strictNullChecks": true, 10 | "noImplicitThis": true, 11 | "alwaysStrict": true, 12 | "noUnusedLocals": false, 13 | "noUnusedParameters": false, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": false, 16 | "inlineSourceMap": true, 17 | "inlineSources": true, 18 | "experimentalDecorators": true, 19 | "strictPropertyInitialization": false, 20 | "typeRoots": ["./node_modules/@types"], 21 | "esModuleInterop": true 22 | }, 23 | "exclude": ["node_modules", "cdk.out"] 24 | } 25 | -------------------------------------------------------------------------------- /lib/03-S3/listArticles/handler.ts: -------------------------------------------------------------------------------- 1 | import { DynamoDBClient, QueryCommand } from "@aws-sdk/client-dynamodb"; 2 | 3 | const client = new DynamoDBClient({}); 4 | 5 | export const handler = async (): Promise<{ statusCode: number, body: string }> => { 6 | const { Items } = await client.send(new QueryCommand({ 7 | TableName: process.env.TABLE_NAME, 8 | KeyConditionExpression: 'PK = :pk', 9 | ExpressionAttributeValues: { 10 | ':pk': { S: 'article' }, 11 | }, 12 | })); 13 | 14 | if (Items === undefined) { 15 | return { statusCode: 500, body: 'No articles found' }; 16 | } 17 | 18 | const articles = Items.map(item => ({ 19 | id: item.SK?.S, 20 | title: item.title?.S, 21 | author: item.author?.S, 22 | })); 23 | 24 | return { 25 | statusCode: 200, 26 | body: JSON.stringify({ articles }), 27 | }; 28 | } -------------------------------------------------------------------------------- /lib/03-S3/getArticle/handler.ts: -------------------------------------------------------------------------------- 1 | import { GetObjectCommand, GetObjectCommandOutput, S3Client } from "@aws-sdk/client-s3"; 2 | 3 | const client = new S3Client({}) 4 | 5 | export const handler = async ({ pathParameters: { id } }: { pathParameters: { id: string }}): Promise<{ statusCode: number, body: string }> => { 6 | let result: GetObjectCommandOutput | undefined; 7 | 8 | try { 9 | result = await client.send(new GetObjectCommand({ 10 | Bucket: process.env.BUCKET_NAME, 11 | Key: id, 12 | })); 13 | } catch { 14 | result = undefined; 15 | } 16 | 17 | if (result?.Body === undefined) { 18 | return { statusCode: 404, body: 'Article not found' }; 19 | } 20 | 21 | const content = await result.Body.transformToString(); 22 | 23 | return { 24 | statusCode: 200, 25 | body: JSON.stringify({ content }) 26 | }; 27 | } -------------------------------------------------------------------------------- /lib/07-EventBridge/registerBooking/handler.ts: -------------------------------------------------------------------------------- 1 | import { DynamoDBClient, UpdateItemCommand } from '@aws-sdk/client-dynamodb'; 2 | 3 | const ddbClient = new DynamoDBClient({}); 4 | 5 | export const handler = async (event: { 6 | detail: { 7 | destination: string, 8 | flightDate: string, 9 | numberOfSeats: number, 10 | } 11 | }): Promise => { 12 | const { destination, flightDate, numberOfSeats } = event.detail; 13 | 14 | await ddbClient.send( 15 | new UpdateItemCommand({ 16 | TableName: process.env.TABLE_NAME, 17 | Key: { 18 | PK: { S: `DESTINATION#${destination}` }, 19 | SK: { S: flightDate }, 20 | }, 21 | UpdateExpression: 'SET availableSeats = availableSeats - :numberOfSeats', 22 | ExpressionAttributeValues: { 23 | ':numberOfSeats': { N: `${numberOfSeats}` }, 24 | }, 25 | }), 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /lib/04-Cognito/confirm/handler.ts: -------------------------------------------------------------------------------- 1 | import { CognitoIdentityProviderClient, ConfirmSignUpCommand } from "@aws-sdk/client-cognito-identity-provider"; 2 | 3 | const client = new CognitoIdentityProviderClient({}); 4 | 5 | export const handler = async (event: { body: string }): Promise<{ statusCode: number, body: string }> => { 6 | const { username, code } = JSON.parse(event.body) as { username?: string; code?: string }; 7 | 8 | if (username === undefined || code === undefined) { 9 | return Promise.resolve({ statusCode: 400, body: 'Missing username or confirmation code' }); 10 | } 11 | 12 | const userPoolClientId = process.env.USER_POOL_CLIENT_ID; 13 | 14 | await client.send(new ConfirmSignUpCommand({ 15 | ClientId: userPoolClientId, 16 | Username: username, 17 | ConfirmationCode: code 18 | })); 19 | 20 | return { statusCode: 200, body: 'User confirmed' }; 21 | }; 22 | -------------------------------------------------------------------------------- /lib/07-EventBridge/syncFlights/handler.ts: -------------------------------------------------------------------------------- 1 | import { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb"; 2 | 3 | const DESTINATIONS = [ 4 | 'CDG', 5 | 'LHR', 6 | 'FRA', 7 | 'IST', 8 | 'AMS', 9 | 'FCO', 10 | 'LAX', 11 | ]; 12 | 13 | const client = new DynamoDBClient({}); 14 | 15 | export const handler = async (): Promise => { 16 | const tableName = process.env.TABLE_NAME; 17 | 18 | if (tableName === undefined) { 19 | throw new Error('Table name not set'); 20 | } 21 | 22 | const flightDate = new Date().toISOString().slice(0, 10); 23 | 24 | await Promise.all(DESTINATIONS.map( 25 | async (destination) => client.send( 26 | new PutItemCommand({ 27 | TableName: tableName, 28 | Item: { 29 | PK: { S: `DESTINATION#${destination}` }, 30 | SK: { S: flightDate }, 31 | availableSeats: { N: '2' }, 32 | }, 33 | }), 34 | ), 35 | )); 36 | }; -------------------------------------------------------------------------------- /lib/14-MasterDynamoDB/toolbox.ts: -------------------------------------------------------------------------------- 1 | import { UserEntity } from "./userEntity"; 2 | 3 | export const createUser = async (event: { body: string }) => { 4 | const { username, email, age, adult } = JSON.parse(event.body) as { username: string, email: string, age: number, adult: boolean }; 5 | 6 | await UserEntity.put({ 7 | username, 8 | email, 9 | age, 10 | adult, 11 | }); 12 | 13 | return { 14 | statusCode: 200, 15 | body: JSON.stringify({ 16 | message: 'User created successfully', 17 | }) 18 | } 19 | }; 20 | 21 | export const listUsers = async () => { 22 | const { Items = [] } = await UserEntity.query('USER'); 23 | 24 | return { 25 | statusCode: 200, 26 | body: JSON.stringify({ 27 | users: Items.map(user => ({ 28 | username: user.username, 29 | email: user.email, 30 | age: user.age, 31 | adult: user.adult, 32 | })) 33 | }) 34 | }; 35 | }; 36 | -------------------------------------------------------------------------------- /lib/08-SQS/executeOrder/handler.ts: -------------------------------------------------------------------------------- 1 | import { EventBridgeClient, PutEventsCommand } from "@aws-sdk/client-eventbridge"; 2 | 3 | const client = new EventBridgeClient({}); 4 | 5 | export const handler = async (event: { 6 | Records: { 7 | body: string; 8 | }[]; 9 | }): Promise => { 10 | const eventBusName = process.env.EVENT_BUS_NAME; 11 | 12 | if (eventBusName === undefined) { 13 | throw new Error('Missing environment variables'); 14 | } 15 | 16 | const { body } = event.Records[0]; 17 | 18 | console.log('Communication with external API started...'); 19 | await new Promise(resolve => setTimeout(resolve, 20000)); 20 | console.log('Communication with external API finished!'); 21 | 22 | await client.send(new PutEventsCommand({ 23 | Entries: [ 24 | { 25 | EventBusName: eventBusName, 26 | Source: 'notifyOrderExecuted', 27 | DetailType: 'orderExecuted', 28 | Detail: body, 29 | } 30 | ] 31 | })); 32 | }; 33 | -------------------------------------------------------------------------------- /lib/02-DynamoDB/createNote/handler.ts: -------------------------------------------------------------------------------- 1 | import { DynamoDBClient, PutItemCommand } from '@aws-sdk/client-dynamodb'; 2 | import { v4 as uuidv4 } from 'uuid'; 3 | 4 | const client = new DynamoDBClient({}); 5 | 6 | export const handler = async (event: { body: string, pathParameters: { userId?: string } }): Promise<{ statusCode: number, body: string }> => { 7 | const { content } = JSON.parse(event.body) as { content?: string }; 8 | const { userId } = event.pathParameters ?? {}; 9 | 10 | if (userId === undefined || content === undefined) { 11 | return { 12 | statusCode: 400, 13 | body: "bad request" 14 | } 15 | } 16 | 17 | const noteId = uuidv4(); 18 | 19 | await client.send(new PutItemCommand({ 20 | TableName: process.env.TABLE_NAME, 21 | Item: { 22 | PK: { S: userId }, 23 | SK: { S: noteId }, 24 | noteContent: { S: content }, 25 | }, 26 | })); 27 | return { 28 | statusCode: 200, 29 | body: JSON.stringify({ noteId }) 30 | }; 31 | } -------------------------------------------------------------------------------- /lib/05-StepFunctions/createStoreItem/handler.ts: -------------------------------------------------------------------------------- 1 | import { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb"; 2 | 3 | const client = new DynamoDBClient({}); 4 | 5 | export const handler = async ({ body }: { body: string}): Promise<{ statusCode: number, body: string }> => { 6 | const tableName = process.env.TABLE_NAME; 7 | 8 | const { itemId, quantity } = JSON.parse(body) as { itemId?: string, quantity?: number }; 9 | 10 | if (itemId === undefined || quantity === undefined) { 11 | return { 12 | statusCode: 200, 13 | body: JSON.stringify({ message: 'itemId or quantity is undefined' }), 14 | } 15 | } 16 | 17 | await client.send(new PutItemCommand({ 18 | TableName: tableName, 19 | Item: { 20 | PK: { S: 'StoreItem' }, 21 | SK: { S: itemId }, 22 | stock: { N: quantity.toString() } 23 | } 24 | })); 25 | 26 | return { 27 | statusCode: 200, 28 | body: JSON.stringify({ message: 'Store item created' }), 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/02-DynamoDB/getNote/handler.ts: -------------------------------------------------------------------------------- 1 | import { DynamoDBClient, GetItemCommand } from '@aws-sdk/client-dynamodb'; 2 | 3 | const client = new DynamoDBClient({}); 4 | 5 | export const handler = async (event: { pathParameters: { userId?: string, id?: string }}): Promise<{ statusCode: number, body: string }> => { 6 | const { userId, id: noteId } = event.pathParameters ?? {}; 7 | 8 | if (userId === undefined || noteId === undefined) { 9 | return { 10 | statusCode: 400, 11 | body: "bad request" 12 | } 13 | } 14 | 15 | const { Item } = await client.send(new GetItemCommand({ 16 | TableName: process.env.TABLE_NAME, 17 | Key: { 18 | PK: { S: userId }, 19 | SK: { S: noteId }, 20 | }, 21 | })); 22 | 23 | if (Item === undefined) { 24 | return { 25 | statusCode: 404, 26 | body: "not found" 27 | } 28 | } 29 | 30 | return { 31 | statusCode: 200, 32 | body: JSON.stringify({ 33 | id: noteId, 34 | content: Item.noteContent.S, 35 | }), 36 | }; 37 | } -------------------------------------------------------------------------------- /lib/04-Cognito/signup/handler.ts: -------------------------------------------------------------------------------- 1 | import { CognitoIdentityProviderClient, SignUpCommand } from "@aws-sdk/client-cognito-identity-provider"; 2 | 3 | const client = new CognitoIdentityProviderClient({}); 4 | 5 | export const handler = async (event: { body: string }): Promise<{ statusCode: number, body: string }> => { 6 | const { username, password, email } = JSON.parse(event.body) as { username?: string; password?: string; email?: string }; 7 | 8 | if (username === undefined || password === undefined || email === undefined) { 9 | return Promise.resolve({ statusCode: 400, body: 'Missing username or password' }); 10 | } 11 | 12 | const userPoolClientId = process.env.USER_POOL_CLIENT_ID; 13 | 14 | await client.send(new SignUpCommand({ 15 | ClientId: userPoolClientId, 16 | Username: username, 17 | Password: password, 18 | UserAttributes: [ 19 | { 20 | Name: 'email', 21 | Value: email 22 | } 23 | ] 24 | })); 25 | 26 | return { statusCode: 200, body: 'User created' }; 27 | }; 28 | -------------------------------------------------------------------------------- /lib/09-Aurora/getUsers/handler.ts: -------------------------------------------------------------------------------- 1 | import { ExecuteStatementCommand, RDSDataClient } from "@aws-sdk/client-rds-data"; 2 | 3 | const rdsDataClient = new RDSDataClient({}); 4 | 5 | export const handler = async (): Promise<{ statusCode: number; body: string }> => { 6 | const secretArn = process.env.SECRET_ARN; 7 | const resourceArn = process.env.CLUSTER_ARN; 8 | 9 | if (secretArn === undefined || resourceArn === undefined ) { 10 | throw new Error('Missing environment variables'); 11 | } 12 | 13 | const { records } = await rdsDataClient.send( 14 | new ExecuteStatementCommand({ 15 | secretArn, 16 | resourceArn, 17 | database: 'my_database', 18 | sql: 'SELECT * FROM users;', 19 | }), 20 | ); 21 | 22 | const users = records?.map(([{ stringValue: id }, { stringValue: firstName }, { stringValue: lastName }]) => ({ 23 | id, 24 | firstName, 25 | lastName, 26 | })) ?? []; 27 | 28 | return { 29 | statusCode: 200, 30 | body: JSON.stringify(users, null, 2), 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/08-SQS/notifyOrderExecuted/handler.ts: -------------------------------------------------------------------------------- 1 | import { SESv2Client, SendEmailCommand } from "@aws-sdk/client-sesv2"; 2 | 3 | const client = new SESv2Client({}); 4 | 5 | export const handler = async (event: { 6 | detail: { 7 | itemName: string, 8 | quantity: number, 9 | username: string, 10 | userEmail: string, 11 | } 12 | }): Promise => { 13 | const senderEmail = process.env.SENDER_EMAIL; 14 | const templateName = process.env.TEMPLATE_NAME; 15 | 16 | if (senderEmail === undefined || templateName === undefined) { 17 | throw new Error('Missing environment variables'); 18 | } 19 | 20 | const { itemName, quantity, username, userEmail } = event.detail; 21 | 22 | await client.send(new SendEmailCommand({ 23 | FromEmailAddress: senderEmail, 24 | Content: { 25 | Template: { 26 | TemplateName: templateName, 27 | TemplateData: JSON.stringify({ itemName, quantity, username }), 28 | } 29 | }, 30 | Destination: { 31 | ToAddresses: [userEmail], 32 | } 33 | })); 34 | }; 35 | -------------------------------------------------------------------------------- /lib/14-MasterDynamoDB/userEntity.ts: -------------------------------------------------------------------------------- 1 | import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; 2 | import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb"; 3 | import { Entity, Table } from "dynamodb-toolbox"; 4 | 5 | const ddbClient = new DynamoDBClient({}); 6 | const documentClient = DynamoDBDocumentClient.from(ddbClient); 7 | 8 | const table = new Table({ 9 | // WARNING: Only import this file in Lambda functions that have this environment variable 10 | name: process.env.TABLE_NAME ?? '', 11 | partitionKey: 'PK', 12 | sortKey: 'SK', 13 | DocumentClient: documentClient, 14 | }); 15 | 16 | export const UserEntity = new Entity({ 17 | table, 18 | name: 'USER', 19 | attributes: { 20 | PK: { partitionKey: true, default: 'USER', hidden: true }, 21 | SK: { sortKey: true, default: ({ username }: { username: string }) => username, hidden: true }, 22 | email: { type: 'string', required: true }, 23 | age: { type: 'number', required: true }, 24 | adult: { type: 'boolean', required: true }, 25 | username: { type: 'string', required: true }, 26 | }, 27 | }) -------------------------------------------------------------------------------- /lib/07-EventBridge/sendBookingReceipt/handler.ts: -------------------------------------------------------------------------------- 1 | import { SESv2Client, SendEmailCommand } from '@aws-sdk/client-sesv2'; 2 | 3 | const sesClient = new SESv2Client({}); 4 | 5 | export const handler = async (event: { 6 | detail: { 7 | destination: string, 8 | flightDate: string, 9 | numberOfSeats: number, 10 | bookerEmail: string, 11 | } 12 | }): Promise => { 13 | const { destination, flightDate, numberOfSeats, bookerEmail } = event.detail; 14 | 15 | const senderEmail = process.env.SENDER_EMAIL; 16 | const templateName = process.env.TEMPLATE_NAME; 17 | 18 | if (senderEmail === undefined || templateName === undefined) { 19 | throw new Error('Missing environment variables'); 20 | } 21 | 22 | await sesClient.send(new SendEmailCommand({ 23 | FromEmailAddress: senderEmail, 24 | Content: { 25 | Template: { 26 | TemplateName: templateName, 27 | TemplateData: JSON.stringify({ destination, flightDate, numberOfSeats }), 28 | } 29 | }, 30 | Destination: { 31 | ToAddresses: [bookerEmail], 32 | } 33 | })); 34 | }; 35 | -------------------------------------------------------------------------------- /lib/11-DynamoDBStreams/onReservationConfirmed/handler.ts: -------------------------------------------------------------------------------- 1 | import { SESv2Client, SendEmailCommand } from '@aws-sdk/client-sesv2'; 2 | import { Reservation } from '../types'; 3 | 4 | const client = new SESv2Client({}); 5 | 6 | export const handler = async ({ detail }: { detail: Reservation }): Promise => { 7 | const templateName = process.env.TEMPLATE_NAME; 8 | const fromEmailAddress = process.env.FROM_EMAIL_ADDRESS; 9 | 10 | if (templateName === undefined || fromEmailAddress === undefined) { 11 | throw new Error('TEMPLATE_NAME and FROM_EMAIL_ADDRESS environment variables must be defined'); 12 | } 13 | 14 | await client.send(new SendEmailCommand({ 15 | FromEmailAddress: fromEmailAddress, 16 | Destination: { 17 | ToAddresses: [detail.email], 18 | }, 19 | Content: { 20 | Template: { 21 | TemplateName: templateName, 22 | TemplateData: JSON.stringify({ 23 | firstName: detail.firstName, 24 | lastName: detail.lastName, 25 | dateTime: detail.dateTime, 26 | partySize: detail.partySize, 27 | }), 28 | }, 29 | }, 30 | })); 31 | } 32 | -------------------------------------------------------------------------------- /lib/06-SES/emailHtmlTemplate.ts: -------------------------------------------------------------------------------- 1 | export const emailHtmlTemplate = ` 2 | 3 | 32 | 33 | 34 |
35 |
36 |

Hello {{username}}!

37 |
38 |
39 |

{{message}}

40 |
41 |
42 | 43 | 44 | `; 45 | -------------------------------------------------------------------------------- /lib/04-Cognito/signin/handler.ts: -------------------------------------------------------------------------------- 1 | import { CognitoIdentityProviderClient, InitiateAuthCommand } from "@aws-sdk/client-cognito-identity-provider"; 2 | 3 | const client = new CognitoIdentityProviderClient({}); 4 | 5 | export const handler = async (event: { body: string }): Promise<{ statusCode: number, body: string }> => { 6 | const { username, password } = JSON.parse(event.body) as { username?: string; password?: string }; 7 | 8 | if (username === undefined || password === undefined) { 9 | return Promise.resolve({ statusCode: 400, body: 'Missing username or password' }); 10 | } 11 | 12 | const userPoolClientId = process.env.USER_POOL_CLIENT_ID; 13 | 14 | const result = await client.send(new InitiateAuthCommand({ 15 | AuthFlow: 'USER_PASSWORD_AUTH', 16 | ClientId: userPoolClientId, 17 | AuthParameters: { 18 | USERNAME: username, 19 | PASSWORD: password 20 | }, 21 | })); 22 | 23 | const idToken = result.AuthenticationResult?.IdToken; 24 | 25 | if (idToken === undefined) { 26 | return Promise.resolve({ statusCode: 401, body: 'Authentication failed' }); 27 | } 28 | 29 | return { statusCode: 200, body: idToken } 30 | } -------------------------------------------------------------------------------- /lib/08-SQS/orderExecutedHtmlTemplate.ts: -------------------------------------------------------------------------------- 1 | export const orderExecutedHtmlTemplate = ` 2 | 3 | 32 | 33 | 34 |
35 |
36 |

Hello {{username}}!

37 |
38 |
39 |

Your order of {{quantity}} {{itemName}} was passed to our provider!

40 |
41 |
42 | 43 | 44 | `; 45 | -------------------------------------------------------------------------------- /lib/11-DynamoDBStreams/confirmReservation/handler.ts: -------------------------------------------------------------------------------- 1 | import { DynamoDBClient, UpdateItemCommand } from '@aws-sdk/client-dynamodb'; 2 | 3 | const client = new DynamoDBClient({}); 4 | 5 | export const handler = async ({ pathParameters }: { pathParameters: { reservationId: string }}): Promise<{ statusCode: number, body: string, headers: unknown }> => { 6 | const tableName = process.env.TABLE_NAME; 7 | 8 | if (tableName === undefined) { 9 | throw new Error('TABLE_NAME environment variable must be defined'); 10 | } 11 | 12 | await client.send(new UpdateItemCommand({ 13 | TableName: tableName, 14 | Key: { 15 | PK: { S: `RESERVATION` }, 16 | SK: { S: pathParameters.reservationId }, 17 | }, 18 | UpdateExpression: 'SET #status = :status', 19 | ExpressionAttributeNames: { 20 | '#status': 'status', 21 | }, 22 | ExpressionAttributeValues: { 23 | ':status': { S: 'CONFIRMED' }, 24 | }, 25 | })); 26 | 27 | return { 28 | statusCode: 200, 29 | body: JSON.stringify({ 30 | reservationId: pathParameters.reservationId, 31 | }), 32 | headers: { 33 | 'Content-Type': 'application/json', 34 | 'Access-Control-Allow-Origin': '*', 35 | }, 36 | }; 37 | } -------------------------------------------------------------------------------- /lib/03-S3/publishArticle/handler.ts: -------------------------------------------------------------------------------- 1 | import { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb"; 2 | import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3' 3 | import { v4 as uuidv4 } from 'uuid'; 4 | 5 | const dynamoDBClient = new DynamoDBClient({}); 6 | const s3Client = new S3Client({}); 7 | 8 | export const handler = async (event: { body: string }): Promise<{ statusCode: number, body: string }> => { 9 | const { title, content, author } = JSON.parse(event.body) as { title?: string, content?: string, author?: string }; 10 | 11 | if (title === undefined || content === undefined || author === undefined) { 12 | return Promise.resolve({ statusCode: 400, body: 'Missing title, content or author' }); 13 | } 14 | 15 | const id = uuidv4(); 16 | 17 | await dynamoDBClient.send(new PutItemCommand({ 18 | TableName: process.env.TABLE_NAME, 19 | Item: { 20 | PK: { S: `article` }, 21 | SK: { S: id }, 22 | title: { S: title }, 23 | author: { S: author }, 24 | } 25 | })); 26 | 27 | await s3Client.send(new PutObjectCommand({ 28 | Bucket: process.env.BUCKET_NAME, 29 | Key: id, 30 | Body: content, 31 | })); 32 | 33 | return { statusCode: 200, body: JSON.stringify({ id }) }; 34 | } -------------------------------------------------------------------------------- /lib/06-SES/sendEmail/handler.ts: -------------------------------------------------------------------------------- 1 | import { SESv2Client, SendEmailCommand } from '@aws-sdk/client-sesv2' 2 | 3 | const sesClient = new SESv2Client({}); 4 | 5 | export const handler = async ({ body }: { body: string }) => { 6 | const { username, email, message } = JSON.parse(body) as { username?: string; email?: string; message?: string }; 7 | const senderEmail = process.env.SENDER_EMAIL; 8 | const templateName = process.env.TEMPLATE_NAME; 9 | 10 | if (!username || !email || !message || !senderEmail || !templateName) { 11 | return { 12 | statusCode: 400, 13 | body: JSON.stringify({ message: 'Missing parameters' }), 14 | }; 15 | } 16 | 17 | const formattedMessage = message.replace(/\n/g, '
'); 18 | 19 | const result = await sesClient.send(new SendEmailCommand({ 20 | FromEmailAddress: senderEmail, 21 | Content: { 22 | Template: { 23 | TemplateName: templateName, 24 | TemplateData: JSON.stringify({ username, message: formattedMessage }), 25 | } 26 | }, 27 | Destination: { 28 | ToAddresses: [email], 29 | } 30 | })); 31 | 32 | console.log(result); 33 | 34 | return { 35 | statusCode: 200, 36 | body: JSON.stringify({ message: 'Email sent' }), 37 | }; 38 | }; 39 | -------------------------------------------------------------------------------- /lib/07-EventBridge/bookingReceiptHtmlTemplate.ts: -------------------------------------------------------------------------------- 1 | export const bookingReceiptHtmlTemplate = ` 2 | 3 | 32 | 33 | 34 |
35 |
36 |

Your flight was booked!

37 |
38 |
39 |

Your flight was booked on {{flightDate}}, for {{numberOfSeats}} person(s), to {{destination}}!

40 |
41 |
42 | 43 | 44 | `; 45 | -------------------------------------------------------------------------------- /lib/11-DynamoDBStreams/onReservationConfirmed/template.ts: -------------------------------------------------------------------------------- 1 | export const reservationConfirmedTemplateHtml = ` 2 | 3 | 32 | 33 | 34 |
35 |
36 |

Reservation confirmed!

37 |
38 |
39 |

Hello {{firstName}} {{lastName}}!

40 |

Your reservation for a party of {{partySize}} on {{dateTime}} was confirmed.

41 |
42 |
43 | 44 | 45 | `; 46 | -------------------------------------------------------------------------------- /lib/08-SQS/requestOrder/handler.ts: -------------------------------------------------------------------------------- 1 | import { SQSClient, SendMessageCommand } from '@aws-sdk/client-sqs'; 2 | import { v4 as uuidv4 } from 'uuid'; 3 | 4 | const client = new SQSClient({}); 5 | 6 | export const handler = async ({ body }: { body: string }): Promise<{ statusCode: number; body: string }> => { 7 | const queueUrl = process.env.QUEUE_URL; 8 | 9 | if (queueUrl === undefined) { 10 | throw new Error('Missing environment variables'); 11 | } 12 | 13 | const { itemName, quantity, username, userEmail } = JSON.parse(body) as { 14 | itemName?: string, 15 | quantity?: number, 16 | username?: string, 17 | userEmail?: string, 18 | }; 19 | 20 | if (itemName === undefined || quantity === undefined || username === undefined || userEmail === undefined) { 21 | return Promise.resolve({ 22 | statusCode: 400, 23 | body: JSON.stringify({ message: 'Missing required parameters' }) 24 | }) 25 | } 26 | 27 | await client.send(new SendMessageCommand({ 28 | QueueUrl: queueUrl, 29 | MessageBody: JSON.stringify({ itemName, quantity, username, userEmail }), 30 | MessageGroupId: 'ORDER_REQUESTED', 31 | MessageDeduplicationId: uuidv4() 32 | })); 33 | 34 | return Promise.resolve({ 35 | statusCode: 200, 36 | body: JSON.stringify({ message: 'Order requested' }) 37 | }); 38 | }; 39 | -------------------------------------------------------------------------------- /lib/10-SNS/orderItem/handler.ts: -------------------------------------------------------------------------------- 1 | import { PublishCommand, SNSClient } from "@aws-sdk/client-sns"; 2 | 3 | const client = new SNSClient({}); 4 | 5 | export const handler = async (event: { body: string }): Promise<{ statusCode: number, body: string }> => { 6 | const topicArn = process.env.TOPIC_ARN; 7 | 8 | if (topicArn === undefined) { 9 | throw new Error("TOPIC_ARN is undefined"); 10 | } 11 | 12 | const { requestDelivery, sendNotification, item, quantity } = JSON.parse(event.body) as { requestDelivery?: boolean, sendNotification?: boolean; item?: string; quantity?: number; }; 13 | 14 | if (requestDelivery === undefined || sendNotification === undefined || item === undefined || quantity === undefined) { 15 | throw new Error("Bad request"); 16 | } 17 | 18 | await client.send( 19 | new PublishCommand({ 20 | Message: JSON.stringify({ item, quantity }), 21 | TopicArn: topicArn, 22 | MessageAttributes: { 23 | sendNotification: { 24 | DataType: 'String', 25 | StringValue: sendNotification.toString() 26 | }, 27 | requestDelivery: { 28 | DataType: 'String', 29 | StringValue: requestDelivery.toString() 30 | }, 31 | } 32 | }) 33 | ); 34 | 35 | return { 36 | statusCode: 200, 37 | body: 'Item ordered' 38 | }; 39 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "learn-serverless", 3 | "version": "0.1.0", 4 | "bin": { 5 | "learn-serverless": "bin/learn-serverless.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "test": "jest", 11 | "cdk": "cdk", 12 | "deploy": "npm run cdk deploy -- --all" 13 | }, 14 | "devDependencies": { 15 | "@types/jest": "^29.2.5", 16 | "@types/node": "18.11.18", 17 | "@types/uuid": "^9.0.1", 18 | "aws-cdk": "2.62.2", 19 | "esbuild": "^0.17.5", 20 | "jest": "^29.3.1", 21 | "ts-jest": "^29.0.3", 22 | "ts-node": "^10.9.1", 23 | "typescript": "~4.9.4" 24 | }, 25 | "dependencies": { 26 | "@aws-sdk/client-cognito-identity-provider": "^3.319.0", 27 | "@aws-sdk/client-dynamodb": "^3.279.0", 28 | "@aws-sdk/client-eventbridge": "^3.350.0", 29 | "@aws-sdk/client-rds-data": "^3.363.0", 30 | "@aws-sdk/client-s3": "^3.290.0", 31 | "@aws-sdk/client-scheduler": "^3.490.0", 32 | "@aws-sdk/client-sesv2": "^3.338.0", 33 | "@aws-sdk/client-sns": "^3.377.0", 34 | "@aws-sdk/client-sqs": "^3.354.0", 35 | "@aws-sdk/lib-dynamodb": "^3.435.0", 36 | "aws-cdk-lib": "2.62.2", 37 | "constructs": "^10.0.0", 38 | "dynamodb-toolbox": "^0.9.2", 39 | "source-map-support": "^0.5.21", 40 | "uuid": "^9.0.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/11-DynamoDBStreams/onRestaurantBooked/handler.ts: -------------------------------------------------------------------------------- 1 | import { SESv2Client, SendEmailCommand } from '@aws-sdk/client-sesv2'; 2 | import { Reservation } from '../types'; 3 | 4 | const client = new SESv2Client({}); 5 | const RESTAURANT_OWNER_EMAIL_ADDRESS = 'pierrech@theodo.fr'; 6 | 7 | export const handler = async ({ detail }: { detail: Reservation }): Promise => { 8 | const templateName = process.env.TEMPLATE_NAME; 9 | const apiURL = process.env.API_URL; 10 | const fromEmailAddress = process.env.FROM_EMAIL_ADDRESS; 11 | 12 | if (templateName === undefined || apiURL === undefined || fromEmailAddress === undefined) { 13 | throw new Error('TEMPLATE_NAME, API_URL and FROM_EMAIL_ADDRESS environment variables must be defined'); 14 | } 15 | 16 | await client.send(new SendEmailCommand({ 17 | FromEmailAddress: fromEmailAddress, 18 | Destination: { 19 | ToAddresses: [RESTAURANT_OWNER_EMAIL_ADDRESS], 20 | }, 21 | Content: { 22 | Template: { 23 | TemplateName: templateName, 24 | TemplateData: JSON.stringify({ 25 | firstName: detail.firstName, 26 | lastName: detail.lastName, 27 | dateTime: detail.dateTime, 28 | partySize: detail.partySize, 29 | apiURL, 30 | reservationId: detail.id, 31 | }), 32 | }, 33 | }, 34 | })); 35 | } 36 | -------------------------------------------------------------------------------- /lib/11-DynamoDBStreams/onRestaurantBooked/template.ts: -------------------------------------------------------------------------------- 1 | export const restaurantBookedTemplateHtml = ` 2 | 3 | 32 | 33 | 34 |
35 |
36 |

New booking received

37 |
38 |
39 |

{{firstName}} {{lastName}} booked a restaurant on {{dateTime}}

40 |

Party size: {{partySize}}

41 | Confirm reservation 42 |
43 |
44 | 45 | 46 | `; 47 | -------------------------------------------------------------------------------- /lib/14-MasterDynamoDB/classic.ts: -------------------------------------------------------------------------------- 1 | import { DynamoDBClient, PutItemCommand, QueryCommand } from "@aws-sdk/client-dynamodb"; 2 | 3 | const ddbClient = new DynamoDBClient({}); 4 | const tableName = process.env.TABLE_NAME ?? ''; 5 | 6 | export const createUser = async (event: { body: string }) => { 7 | const { username, email, age, adult } = JSON.parse(event.body) as { username: string, email: string, age: number, adult: boolean }; 8 | 9 | await ddbClient.send(new PutItemCommand({ 10 | TableName: tableName, 11 | Item: { 12 | PK: { S: 'USER' }, 13 | SK: { S: username }, 14 | email: { S: email }, 15 | age: { N: age.toString() }, 16 | adult: { BOOL: adult }, 17 | }, 18 | })); 19 | 20 | return { 21 | statusCode: 200, 22 | body: JSON.stringify({ 23 | message: 'User created successfully', 24 | }) 25 | } 26 | }; 27 | 28 | export const listUsers = async () => { 29 | const { Items = [] } = await ddbClient.send(new QueryCommand({ 30 | TableName: tableName, 31 | KeyConditionExpression: 'PK = :pk', 32 | ExpressionAttributeValues: { 33 | ':pk': { S: 'USER' }, 34 | }, 35 | })); 36 | 37 | return { 38 | statusCode: 200, 39 | body: JSON.stringify({ 40 | users: Items.map(user => ({ 41 | username: user.SK.S, 42 | email: user.email.S, 43 | age: +(user.age.N ?? '0'), 44 | adult: user.adult.BOOL ?? false, 45 | })) 46 | }) 47 | }; 48 | }; 49 | -------------------------------------------------------------------------------- /lib/14-MasterDynamoDB/document.ts: -------------------------------------------------------------------------------- 1 | import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; 2 | import { DynamoDBDocumentClient, PutCommand, QueryCommand } from "@aws-sdk/lib-dynamodb"; 3 | 4 | const ddbClient = new DynamoDBClient({}); 5 | const documentClient = DynamoDBDocumentClient.from(ddbClient); 6 | const tableName = process.env.TABLE_NAME ?? ''; 7 | 8 | export const createUser = async (event: { body: string }) => { 9 | const { username, email, age, adult } = JSON.parse(event.body) as { username: string, email: string, age: number, adult: boolean }; 10 | 11 | await documentClient.send(new PutCommand({ 12 | TableName: tableName, 13 | Item: { 14 | PK: 'USER', 15 | SK: username, 16 | email, 17 | age, 18 | adult, 19 | }, 20 | })); 21 | 22 | return { 23 | statusCode: 200, 24 | body: JSON.stringify({ 25 | message: 'User created successfully', 26 | }) 27 | } 28 | }; 29 | 30 | export const listUsers = async () => { 31 | const { Items = [] } = await documentClient.send(new QueryCommand({ 32 | TableName: tableName, 33 | KeyConditionExpression: 'PK = :pk', 34 | ExpressionAttributeValues: { 35 | ':pk': 'USER', 36 | }, 37 | })); 38 | 39 | return { 40 | statusCode: 200, 41 | body: JSON.stringify({ 42 | users: Items.map(user => ({ 43 | username: user.SK, 44 | email: user.email, 45 | age: user.age, 46 | adult: user.adult, 47 | })) 48 | }) 49 | }; 50 | }; 51 | -------------------------------------------------------------------------------- /lib/11-DynamoDBStreams/bookRestaurant/handler.ts: -------------------------------------------------------------------------------- 1 | import { DynamoDBClient, PutItemCommand } from '@aws-sdk/client-dynamodb'; 2 | 3 | import { v4 as uuid } from 'uuid'; 4 | import { Reservation } from '../types'; 5 | 6 | const client = new DynamoDBClient({}); 7 | 8 | export const handler = async ({ body }: { body: string}): Promise<{ statusCode: number, body: string }> => { 9 | const tableName = process.env.TABLE_NAME; 10 | 11 | if (tableName === undefined) { 12 | throw new Error('TABLE_NAME environment variable must be defined'); 13 | } 14 | 15 | const { 16 | firstName, 17 | lastName, 18 | email, 19 | dateTime, 20 | partySize, 21 | } = JSON.parse(body) as Partial; 22 | 23 | if (firstName === undefined || lastName === undefined || email === undefined || dateTime === undefined || partySize === undefined) { 24 | return { 25 | statusCode: 400, 26 | body: 'Bad request', 27 | }; 28 | } 29 | 30 | const reservationId = uuid(); 31 | 32 | await client.send(new PutItemCommand({ 33 | TableName: tableName, 34 | Item: { 35 | PK: { S: `RESERVATION` }, 36 | SK: { S: reservationId }, 37 | firstName: { S: firstName }, 38 | lastName: { S: lastName }, 39 | email: { S: email }, 40 | partySize: { N: partySize.toString() }, 41 | dateTime: { S: dateTime }, 42 | status: { S: 'PENDING' }, 43 | } 44 | })); 45 | 46 | return { 47 | statusCode: 200, 48 | body: JSON.stringify({ 49 | reservationId, 50 | }), 51 | }; 52 | } -------------------------------------------------------------------------------- /lib/17-EventBridgeScheduler/addMemo.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ActionAfterCompletion, 3 | CreateScheduleCommand, 4 | FlexibleTimeWindowMode, 5 | SchedulerClient, 6 | } from '@aws-sdk/client-scheduler'; 7 | 8 | import { v4 as uuidv4 } from 'uuid'; 9 | 10 | const client = new SchedulerClient({}); 11 | const scheduleTargetArn = process.env.SCHEDULE_TARGET_ARN as string; 12 | const scheduleRoleArn = process.env.SCHEDULE_ROLE_ARN as string; 13 | 14 | if (scheduleTargetArn === undefined || scheduleRoleArn === undefined) { 15 | throw new Error('Missing environment variables'); 16 | } 17 | 18 | export const handler = async ({ body }: { body: string }): Promise<{ 19 | statusCode: number, 20 | body: string, 21 | }> => { 22 | const { memo, date, time, timezone = 'Europe/Paris' } = JSON.parse(body) as { memo?: string, date?: string, timezone?: string, time?: string }; 23 | 24 | if (memo === undefined || date === undefined) { 25 | return { 26 | statusCode: 400, 27 | body: 'Bad Request', 28 | }; 29 | } 30 | 31 | const scheduleId = uuidv4(); 32 | 33 | await client.send(new CreateScheduleCommand({ 34 | Name: scheduleId, 35 | ScheduleExpressionTimezone: timezone, 36 | Target: { 37 | Arn: scheduleTargetArn, 38 | RoleArn: scheduleRoleArn, 39 | Input: JSON.stringify({ memo }), 40 | }, 41 | ScheduleExpression: `at(${date}T${time})`, 42 | FlexibleTimeWindow: { 43 | Mode: FlexibleTimeWindowMode.OFF, 44 | }, 45 | ActionAfterCompletion: ActionAfterCompletion.DELETE, 46 | })); 47 | 48 | return { 49 | statusCode: 200, 50 | body: 'Memo scheduled', 51 | } 52 | } -------------------------------------------------------------------------------- /lib/09-Aurora/addUser/handler.ts: -------------------------------------------------------------------------------- 1 | import { ExecuteStatementCommand, RDSDataClient } from "@aws-sdk/client-rds-data"; 2 | import { v4 as uuid } from 'uuid'; 3 | 4 | const rdsDataClient = new RDSDataClient({}); 5 | 6 | export const handler = async ({ body }: { body: string }): Promise<{ statusCode: number; body: string }> => { 7 | const secretArn = process.env.SECRET_ARN; 8 | const resourceArn = process.env.CLUSTER_ARN; 9 | 10 | if (secretArn === undefined || resourceArn === undefined ) { 11 | throw new Error('Missing environment variables'); 12 | } 13 | 14 | const { firstName, lastName } = JSON.parse(body) as { firstName?: string; lastName?: string; }; 15 | 16 | if (firstName === undefined || lastName === undefined) { 17 | return { 18 | statusCode: 400, 19 | body: 'Missing firstName or lastName', 20 | } 21 | } 22 | 23 | const userId = uuid(); 24 | 25 | await rdsDataClient.send( 26 | new ExecuteStatementCommand({ 27 | secretArn, 28 | resourceArn, 29 | database: 'my_database', 30 | sql: 'CREATE TABLE IF NOT EXISTS users (id VARCHAR(255) NOT NULL PRIMARY KEY, firstName VARCHAR(255) NOT NULL, lastName VARCHAR(255) NOT NULL); INSERT INTO users (id, firstName, lastName) VALUES (:id, :firstName, :lastName);', 31 | parameters: [ 32 | { name: 'id', value: { stringValue: userId } }, 33 | { name: 'firstName', value: { stringValue: firstName } }, 34 | { name: 'lastName', value: { stringValue: lastName } }, 35 | ], 36 | }), 37 | ); 38 | 39 | return { 40 | statusCode: 200, 41 | body: JSON.stringify({ 42 | userId 43 | }, null, 2), 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/learn-serverless.ts", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "**/*.d.ts", 11 | "**/*.js", 12 | "tsconfig.json", 13 | "package*.json", 14 | "yarn.lock", 15 | "node_modules", 16 | "test" 17 | ] 18 | }, 19 | "context": { 20 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 21 | "@aws-cdk/core:checkSecretUsage": true, 22 | "@aws-cdk/core:target-partitions": [ 23 | "aws", 24 | "aws-cn" 25 | ], 26 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 27 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 28 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 29 | "@aws-cdk/aws-iam:minimizePolicies": true, 30 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 31 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 32 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 33 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 34 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 35 | "@aws-cdk/core:enablePartitionLiterals": true, 36 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 37 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true, 38 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 39 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, 40 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, 41 | "@aws-cdk/aws-route53-patters:useCertificate": true, 42 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/16-LambdaDestinations/stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import { Construct } from 'constructs'; 3 | import { join } from 'path'; 4 | 5 | const DEVELOPERS_EMAILS = [ 6 | 'pchol.pro@gmail.com' 7 | ]; 8 | 9 | export class Part16LambdaDestinationsStack extends cdk.Stack { 10 | constructor(scope: Construct, id: string, props?: cdk.StackProps) { 11 | super(scope, id, props); 12 | 13 | const cronTrigger = new cdk.aws_events.Rule(this, 'CronTrigger', { 14 | schedule: cdk.aws_events.Schedule.expression('rate(1 minute)'), 15 | }); 16 | 17 | const onFailureTopic = new cdk.aws_sns.Topic(this, 'OnFailureTopic'); 18 | 19 | const lambdaFunction = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'LambdaFunction', { 20 | entry: join(__dirname, 'lambda.ts'), 21 | handler: 'handler', 22 | runtime: cdk.aws_lambda.Runtime.NODEJS_18_X, 23 | bundling: { 24 | externalModules: ['@aws-sdk'], 25 | }, 26 | onFailure: new cdk.aws_lambda_destinations.SnsDestination(onFailureTopic), 27 | }); 28 | 29 | cronTrigger.addTarget( 30 | new cdk.aws_events_targets.LambdaFunction(lambdaFunction, { 31 | event: cdk.aws_events.RuleTargetInput.fromObject({ 32 | veryImportantData: 'this should not be lost !!!', 33 | }), 34 | retryAttempts: 1, // 1 retry attempt 35 | }), 36 | ); 37 | 38 | const failedEventsQueue = new cdk.aws_sqs.Queue(this, 'FailedEventsQueue'); 39 | 40 | onFailureTopic.addSubscription( 41 | new cdk.aws_sns_subscriptions.SqsSubscription(failedEventsQueue) 42 | ); 43 | 44 | DEVELOPERS_EMAILS.forEach((email) => { 45 | onFailureTopic.addSubscription( 46 | new cdk.aws_sns_subscriptions.EmailSubscription(email) 47 | ); 48 | }); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/11-DynamoDBStreams/streamTarget/handler.ts: -------------------------------------------------------------------------------- 1 | import { EventBridgeClient, PutEventsCommand } from "@aws-sdk/client-eventbridge" 2 | import { Reservation } from "../types"; 3 | 4 | type InputProps = { 5 | Records: { 6 | eventName: string, 7 | dynamodb: { 8 | NewImage: { 9 | PK: { S: string }, 10 | SK: { S: string }, 11 | email: { S: string }, 12 | firstName: { S: string }, 13 | lastName: { S: string }, 14 | dateTime: { S: string }, 15 | partySize: { N: string }, 16 | status: { S: string }, 17 | }, 18 | } 19 | }[] 20 | } 21 | 22 | const client = new EventBridgeClient({}); 23 | 24 | export const handler = async ({ Records }: InputProps): Promise => { 25 | const eventBusName = process.env.EVENT_BUS_NAME; 26 | 27 | if (eventBusName === undefined) { 28 | throw new Error('EVENT_BUS_NAME environment variable is not set'); 29 | } 30 | 31 | await Promise.all(Records.map(async ({ dynamodb, eventName }) => { 32 | if (eventName !== 'INSERT' && eventName !== 'MODIFY') { 33 | return; 34 | } 35 | 36 | const { SK, email, firstName, lastName, dateTime, partySize } = dynamodb.NewImage; 37 | 38 | const eventDetail: Reservation = { 39 | id: SK.S, 40 | firstName: firstName.S, 41 | lastName: lastName.S, 42 | email: email.S, 43 | dateTime: dateTime.S, 44 | partySize: +partySize.N, 45 | } 46 | 47 | await client.send(new PutEventsCommand({ 48 | Entries: [ 49 | { 50 | EventBusName: eventBusName, 51 | Source: 'StreamTarget', 52 | DetailType: eventName === 'INSERT' ? 'OnRestaurantBooked' : 'OnReservationConfirmed', 53 | Detail: JSON.stringify(eventDetail), 54 | }, 55 | ], 56 | })); 57 | })); 58 | } 59 | -------------------------------------------------------------------------------- /lib/02-DynamoDB/stack.ts: -------------------------------------------------------------------------------- 1 | import { Stack, StackProps } from "aws-cdk-lib"; 2 | import { Construct } from "constructs"; 3 | 4 | import * as cdk from 'aws-cdk-lib'; 5 | import { join } from "path"; 6 | 7 | export class Part02DynamoDBStack extends Stack { 8 | constructor(scope: Construct, id: string, props?: StackProps) { 9 | super(scope, id, props); 10 | 11 | // Provision a new REST API Gateway 12 | const api = new cdk.aws_apigateway.RestApi(this, 'myFirstApi', { 13 | restApiName: 'Part02Service', 14 | }); 15 | 16 | const database = new cdk.aws_dynamodb.Table(this, 'myFirstDatabase', { 17 | partitionKey: { 18 | name: 'PK', 19 | type: cdk.aws_dynamodb.AttributeType.STRING, 20 | }, 21 | sortKey: { 22 | name: 'SK', 23 | type: cdk.aws_dynamodb.AttributeType.STRING, 24 | }, 25 | billingMode: cdk.aws_dynamodb.BillingMode.PAY_PER_REQUEST, 26 | }); 27 | 28 | const createNote = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'createNote', { 29 | entry: join(__dirname, 'createNote', 'handler.ts'), 30 | handler: 'handler', 31 | runtime: cdk.aws_lambda.Runtime.NODEJS_18_X, 32 | bundling: { 33 | externalModules: ['@aws-sdk'], 34 | }, 35 | environment: { 36 | TABLE_NAME: database.tableName, 37 | }, 38 | }); 39 | 40 | database.grantWriteData(createNote); 41 | 42 | const getNote = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'getNote', { 43 | entry: join(__dirname, 'getNote', 'handler.ts'), 44 | handler: 'handler', 45 | runtime: cdk.aws_lambda.Runtime.NODEJS_18_X, 46 | bundling: { 47 | externalModules: ['@aws-sdk'], 48 | }, 49 | environment: { 50 | TABLE_NAME: database.tableName, 51 | }, 52 | }); 53 | 54 | database.grantReadData(getNote); 55 | 56 | const usersResource = api.root.addResource('users').addResource('{userId}'); 57 | const notesResource = usersResource.addResource('notes'); 58 | notesResource.addMethod('POST', new cdk.aws_apigateway.LambdaIntegration(createNote)); 59 | notesResource.addResource('{id}').addMethod('GET', new cdk.aws_apigateway.LambdaIntegration(getNote)); 60 | } 61 | } -------------------------------------------------------------------------------- /lib/09-Aurora/runMigrations/handler.ts: -------------------------------------------------------------------------------- 1 | import { DynamoDBClient, GetItemCommand, PutItemCommand } from "@aws-sdk/client-dynamodb"; 2 | import { ExecuteStatementCommand, RDSDataClient } from "@aws-sdk/client-rds-data"; 3 | 4 | const migrations: { id: string, statement: string }[] = [ 5 | { 6 | id: 'migration-1', 7 | statement: 'CREATE TABLE IF NOT EXISTS users (id VARCHAR(255) NOT NULL PRIMARY KEY, firstName VARCHAR(255) NOT NULL, lastName VARCHAR(255) NOT NULL);', 8 | } 9 | ]; 10 | 11 | const rdsDataClient = new RDSDataClient({}); 12 | const dynamoDBClient = new DynamoDBClient({}); 13 | 14 | export const handler = async (): Promise => { 15 | const secretArn = process.env.SECRET_ARN; 16 | const resourceArn = process.env.CLUSTER_ARN; 17 | const dynamoDBTableName = process.env.DYNAMODB_TABLE_NAME; 18 | 19 | if (secretArn === undefined || resourceArn === undefined || dynamoDBTableName === undefined) { 20 | throw new Error('Missing environment variables'); 21 | } 22 | 23 | await rdsDataClient.send( 24 | new ExecuteStatementCommand({ 25 | secretArn, 26 | resourceArn, 27 | sql: `CREATE DATABASE IF NOT EXISTS my_database`, 28 | }), 29 | ); 30 | 31 | // Run migrations in order 32 | for (const { id, statement } of migrations) { 33 | // Check if migration has already been executed 34 | const { Item: migration } = await dynamoDBClient.send( 35 | new GetItemCommand({ 36 | TableName: dynamoDBTableName, 37 | Key: { 38 | PK: { S: 'MIGRATION' }, 39 | SK: { S: id }, 40 | }, 41 | }), 42 | ); 43 | 44 | if (migration !== undefined) { 45 | continue; 46 | } 47 | 48 | // Execute migration 49 | await rdsDataClient.send( 50 | new ExecuteStatementCommand({ 51 | secretArn, 52 | resourceArn, 53 | database: 'my_database', 54 | sql: statement, 55 | }), 56 | ); 57 | 58 | // Mark migration as executed 59 | await dynamoDBClient.send( 60 | new PutItemCommand({ 61 | TableName: dynamoDBTableName, 62 | Item: { 63 | PK: { S: 'MIGRATION' }, 64 | SK: { S: id }, 65 | }, 66 | }), 67 | ); 68 | 69 | console.log(`Migration ${id} executed successfully`) 70 | } 71 | } -------------------------------------------------------------------------------- /lib/01-Lambda/stack.ts: -------------------------------------------------------------------------------- 1 | import { Stack, StackProps } from "aws-cdk-lib"; 2 | import { Construct } from "constructs"; 3 | 4 | import * as cdk from 'aws-cdk-lib'; 5 | import { join } from "path"; 6 | 7 | export class Part01LambdaStack extends Stack { 8 | constructor(scope: Construct, id: string, props?: StackProps) { 9 | super(scope, id, props); 10 | 11 | // Provision a simple Lambda function 12 | new cdk.aws_lambda_nodejs.NodejsFunction(this, 'myFirstLambdaFunction', { 13 | entry: join(__dirname, 'myFirstLambda', 'handler.ts'), 14 | handler: 'handler', 15 | runtime: cdk.aws_lambda.Runtime.NODEJS_18_X, 16 | bundling: { 17 | externalModules: ['@aws-sdk'], 18 | }, 19 | }); 20 | 21 | // Provision a new Lambda function 22 | // Put the result inside a variable so we can use it later 23 | const rollADiceFunction = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'rollADiceFunction', { 24 | entry: join(__dirname, 'rollADice', 'handler.ts'), 25 | handler: 'handler', 26 | runtime: cdk.aws_lambda.Runtime.NODEJS_18_X, 27 | bundling: { 28 | externalModules: ['@aws-sdk'], 29 | }, 30 | }); 31 | 32 | // Provision a new REST API Gateway 33 | const myFirstApi = new cdk.aws_apigateway.RestApi(this, 'myFirstApi', { 34 | restApiName: 'Part01Service', 35 | }); 36 | 37 | // Add a new GET /dice resource to the API Gateway 38 | // Corresponding to the invocation of the rollADice function 39 | const diceResource = myFirstApi.root.addResource('dice'); 40 | diceResource.addMethod('GET', new cdk.aws_apigateway.LambdaIntegration(rollADiceFunction)); 41 | 42 | // Provision a new Lambda function 43 | // Put the result inside a variable so we can use it later 44 | const rollDicesFunction = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'rollDicesFunction', { 45 | entry: join(__dirname, 'rollManyDices', 'handler.ts'), 46 | handler: 'handler', 47 | runtime: cdk.aws_lambda.Runtime.NODEJS_18_X, 48 | bundling: { 49 | externalModules: ['@aws-sdk'], 50 | }, 51 | }); 52 | 53 | // Add a new GET /dice/:nbOfDices resource to the API Gateway 54 | // Corresponding to the invocation of the rollManyDices function 55 | diceResource.addResource('{nbOfDices}').addMethod('GET', new cdk.aws_apigateway.LambdaIntegration(rollDicesFunction)); 56 | } 57 | } -------------------------------------------------------------------------------- /lib/07-EventBridge/bookFlight/handler.ts: -------------------------------------------------------------------------------- 1 | import { DynamoDBClient, GetItemCommand } from "@aws-sdk/client-dynamodb"; 2 | import { EventBridgeClient, PutEventsCommand } from "@aws-sdk/client-eventbridge"; 3 | 4 | const ddbClient = new DynamoDBClient({}); 5 | const eventBridgeClient = new EventBridgeClient({}); 6 | 7 | 8 | export const handler = async ({ body }: { body: string }): Promise<{ statusCode: number, body: string}> => { 9 | const tableName = process.env.TABLE_NAME; 10 | const eventBusName = process.env.EVENT_BUS_NAME; 11 | 12 | if (tableName === undefined || eventBusName === undefined) { 13 | throw new Error('Missing environment variables'); 14 | } 15 | 16 | const { destination, flightDate, numberOfSeats, bookerEmail } = JSON.parse(body) as { destination?: string, flightDate?: string, numberOfSeats?: number, bookerEmail?: string }; 17 | 18 | if (destination === undefined || flightDate === undefined || numberOfSeats === undefined || bookerEmail === undefined) { 19 | return { 20 | statusCode: 400, 21 | body: JSON.stringify({ 22 | message: 'Missing required parameters', 23 | }), 24 | } 25 | } 26 | 27 | const { Item } = await ddbClient.send( 28 | new GetItemCommand({ 29 | TableName: tableName, 30 | Key: { 31 | PK: { S: `DESTINATION#${destination}` }, 32 | SK: { S: flightDate }, 33 | }, 34 | }), 35 | ); 36 | 37 | const availableSeats = Item?.availableSeats?.N; 38 | 39 | if (availableSeats === undefined) { 40 | return { 41 | statusCode: 404, 42 | body: JSON.stringify({ 43 | message: 'Flight not found', 44 | }), 45 | } 46 | } 47 | 48 | if (+availableSeats < numberOfSeats) { 49 | return { 50 | statusCode: 400, 51 | body: JSON.stringify({ 52 | message: 'Not enough seats for this flight', 53 | }), 54 | } 55 | } 56 | 57 | await eventBridgeClient.send(new PutEventsCommand({ 58 | Entries: [ 59 | { 60 | Source: 'bookFlight', 61 | DetailType: 'flightBooked', 62 | EventBusName: eventBusName, 63 | Detail: JSON.stringify({ 64 | destination, 65 | flightDate, 66 | numberOfSeats, 67 | bookerEmail, 68 | }), 69 | } 70 | ] 71 | })); 72 | 73 | return { 74 | statusCode: 200, 75 | body: JSON.stringify({ 76 | message: 'Processing flight booking', 77 | }), 78 | } 79 | }; -------------------------------------------------------------------------------- /lib/17-EventBridgeScheduler/stack.ts: -------------------------------------------------------------------------------- 1 | import { Stack, StackProps } from "aws-cdk-lib"; 2 | import { Construct } from "constructs"; 3 | 4 | import * as cdk from 'aws-cdk-lib'; 5 | import { join } from "path"; 6 | 7 | export class Part17EventBridgeSchedulerStack extends Stack { 8 | constructor(scope: Construct, id: string, props?: StackProps) { 9 | super(scope, id, props); 10 | 11 | // Lambda function triggered by scheduler 12 | const executeMemo = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'ExecuteMemo', { 13 | entry: join(__dirname, 'executeMemo.ts'), 14 | handler: 'handler', 15 | runtime: cdk.aws_lambda.Runtime.NODEJS_18_X, 16 | bundling: { 17 | externalModules: ['@aws-sdk'] 18 | }, 19 | }); 20 | 21 | // Create role for scheduler to invoke executeMemo 22 | const invokeExecuteMemoRole = new cdk.aws_iam.Role(this, 'InvokeMemoRole', { 23 | assumedBy: new cdk.aws_iam.ServicePrincipal('scheduler.amazonaws.com'), 24 | }); 25 | invokeExecuteMemoRole.addToPolicy(new cdk.aws_iam.PolicyStatement({ 26 | actions: ['lambda:InvokeFunction'], 27 | resources: [executeMemo.functionArn] 28 | })); 29 | 30 | // Lambda function that schedules executeMemo 31 | const addMemo = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'AddMemo', { 32 | entry: join(__dirname, 'addMemo.ts'), 33 | handler: 'handler', 34 | runtime: cdk.aws_lambda.Runtime.NODEJS_18_X, 35 | bundling: { 36 | externalModules: ['@aws-sdk'] 37 | }, 38 | environment: { 39 | SCHEDULE_TARGET_ARN: executeMemo.functionArn, 40 | SCHEDULE_ROLE_ARN: invokeExecuteMemoRole.roleArn, 41 | }, 42 | }); 43 | 44 | // Allow addMemo to create a scheduler 45 | addMemo.addToRolePolicy( 46 | new cdk.aws_iam.PolicyStatement({ 47 | actions: ['scheduler:CreateSchedule'], 48 | resources: ['*'], 49 | }), 50 | ); 51 | 52 | // Allow addMemo to pass the invokeExecuteMemoRole to the scheduler 53 | addMemo.addToRolePolicy( 54 | new cdk.aws_iam.PolicyStatement({ 55 | actions: ['iam:PassRole'], 56 | resources: [invokeExecuteMemoRole.roleArn], 57 | }), 58 | ); 59 | 60 | // Trigger addMemo via API Gateway 61 | const api = new cdk.aws_apigateway.RestApi(this, 'Api', { 62 | restApiName: 'Part17Service' 63 | }); 64 | api.root.addResource('addMemo').addMethod('POST', new cdk.aws_apigateway.LambdaIntegration(addMemo)); 65 | } 66 | } -------------------------------------------------------------------------------- /lib/10-SNS/stack.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from "constructs"; 2 | import * as cdk from "aws-cdk-lib"; 3 | 4 | import path from "path"; 5 | import { Stack } from "aws-cdk-lib"; 6 | 7 | export class Part10SNSStack extends Stack { 8 | constructor(scope: Construct, id: string, props?: cdk.StackProps) { 9 | super(scope, id, props); 10 | 11 | const api = new cdk.aws_apigateway.RestApi(this, 'api', { 12 | restApiName: 'Part10Service', 13 | }); 14 | 15 | const topic = new cdk.aws_sns.Topic(this, 'ArticleTopic'); 16 | 17 | const orderItem = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'OrderItem', { 18 | entry: path.join(__dirname, 'orderItem', 'handler.ts'), 19 | handler: 'handler', 20 | runtime: cdk.aws_lambda.Runtime.NODEJS_18_X, 21 | bundling: { 22 | externalModules: ['@aws-sdk'], 23 | }, 24 | environment: { 25 | TOPIC_ARN: topic.topicArn, 26 | } 27 | }); 28 | topic.grantPublish(orderItem); 29 | api.root.addResource('orderItem').addMethod('POST', new cdk.aws_apigateway.LambdaIntegration(orderItem)); 30 | 31 | const executeOrder = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'ExecuteOrder', { 32 | entry: path.join(__dirname, 'executeOrder', 'handler.ts'), 33 | handler: 'handler', 34 | runtime: cdk.aws_lambda.Runtime.NODEJS_18_X, 35 | bundling: { 36 | externalModules: ['@aws-sdk'], 37 | }, 38 | }); 39 | topic.addSubscription(new cdk.aws_sns_subscriptions.LambdaSubscription(executeOrder)); 40 | 41 | const requestDelivery = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'RequestDelivery', { 42 | entry: path.join(__dirname, 'requestDelivery', 'handler.ts'), 43 | handler: 'handler', 44 | runtime: cdk.aws_lambda.Runtime.NODEJS_18_X, 45 | bundling: { 46 | externalModules: ['@aws-sdk'], 47 | }, 48 | }); 49 | topic.addSubscription(new cdk.aws_sns_subscriptions.LambdaSubscription(requestDelivery, { filterPolicy: { 50 | requestDelivery: cdk.aws_sns.SubscriptionFilter.stringFilter({ allowlist: ["true"] }) 51 | }})); 52 | 53 | const sendNotification = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'SendNotification', { 54 | entry: path.join(__dirname, 'sendNotification', 'handler.ts'), 55 | handler: 'handler', 56 | runtime: cdk.aws_lambda.Runtime.NODEJS_18_X, 57 | bundling: { 58 | externalModules: ['@aws-sdk'], 59 | }, 60 | }); 61 | topic.addSubscription(new cdk.aws_sns_subscriptions.LambdaSubscription(sendNotification, { filterPolicy: { 62 | sendNotification: cdk.aws_sns.SubscriptionFilter.stringFilter({ allowlist: ["true"] }) 63 | }})); 64 | } 65 | } -------------------------------------------------------------------------------- /bin/learn-serverless.ts: -------------------------------------------------------------------------------- 1 | import 'source-map-support/register'; 2 | import * as cdk from 'aws-cdk-lib'; 3 | 4 | import { Part01LambdaStack } from '../lib/01-Lambda/stack'; 5 | import { Part02DynamoDBStack } from '../lib/02-DynamoDB/stack'; 6 | import { Part03S3Stack } from '../lib/03-S3/stack'; 7 | import { Part04CognitoStack } from '../lib/04-Cognito/stack'; 8 | import { Part05StepFunctionsStack } from '../lib/05-StepFunctions/stack'; 9 | import { Part06SESStack } from '../lib/06-SES/stack'; 10 | import { Part07EventBridgeStack } from '../lib/07-EventBridge/stack'; 11 | import { Part08SQSStack } from '../lib/08-SQS/stack'; 12 | import { Part09AuroraStack } from '../lib/09-Aurora/stack'; 13 | import { Part10SNSStack } from '../lib/10-SNS/stack'; 14 | import { Part11DynamoDBStreamsStack } from '../lib/11-DynamoDBStreams/stack'; 15 | // Part 12 not in this repo, more details below 16 | // Part 13 not in this repo, more details below 17 | import { Part14MasterDynamoDBStack } from '../lib/14-MasterDynamoDB/stack'; 18 | // Part 15 not in this repo, more details below 19 | import { Part16LambdaDestinationsStack } from '../lib/16-LambdaDestinations/stack'; 20 | import { Part17EventBridgeSchedulerStack } from '../lib/17-EventBridgeScheduler/stack'; 21 | 22 | const app = new cdk.App(); 23 | 24 | new Part01LambdaStack(app, 'Part01LambdaStack'); 25 | 26 | new Part02DynamoDBStack(app, 'Part02DynamoDBStack'); 27 | 28 | new Part03S3Stack(app, 'Part03S3Stack'); 29 | 30 | new Part04CognitoStack(app, 'Part04CognitoStack'); 31 | 32 | new Part05StepFunctionsStack(app, 'Part05StepFunctionsStack'); 33 | 34 | new Part06SESStack(app, 'Part06SESStack') 35 | 36 | new Part07EventBridgeStack(app, 'Part07EventBridgeStack'); 37 | 38 | new Part08SQSStack(app, 'Part08SQSStack'); 39 | 40 | new Part09AuroraStack(app, 'Part09AuroraStack'); 41 | 42 | new Part10SNSStack(app, 'Part10SNSStack'); 43 | 44 | new Part11DynamoDBStreamsStack(app, 'Part11DynamoDBStreamsStack'); 45 | 46 | // The code for article 12: "Deploy a frontend" is not in this repo! (it needs a frontend) 47 | // Find it here: https://github.com/PChol22/learn-serverless-backendxfrontend/tree/episode-12 48 | // (Specific branch, continues in article 13) 49 | 50 | // The code for article 13: "Strongly type Lambda functions" is not in this repo! (it needs a frontend) 51 | // Find it here: https://github.com/PChol22/learn-serverless-backendxfrontend 52 | // (Last commit on main, direct follow-up of article 12) 53 | 54 | new Part14MasterDynamoDBStack(app, 'Part14MasterDynamoDBStack'); 55 | 56 | // The code for article 15: "Upload files on S3" is not in this repo! (it needs a frontend) 57 | // Find it here: https://github.com/PChol22/learn-serverless-upload-s3 58 | // (Last commit on main) 59 | 60 | new Part16LambdaDestinationsStack(app, 'Part16LambdaDestinationsStack'); 61 | 62 | new Part17EventBridgeSchedulerStack(app, 'Part17SchedulerStack'); 63 | -------------------------------------------------------------------------------- /lib/06-SES/stack.ts: -------------------------------------------------------------------------------- 1 | import { Stack, StackProps } from "aws-cdk-lib"; 2 | import { Construct } from "constructs"; 3 | 4 | import * as cdk from 'aws-cdk-lib'; 5 | import { join } from "path"; 6 | 7 | import { emailHtmlTemplate } from './emailHtmlTemplate'; 8 | 9 | export class Part06SESStack extends Stack { 10 | constructor(scope: Construct, id: string, props?: StackProps) { 11 | super(scope, id, props); 12 | 13 | const api = new cdk.aws_apigateway.RestApi(this, 'api', { 14 | restApiName: 'Part06Service', 15 | }); 16 | 17 | // Use your own domain name 18 | const DOMAIN_NAME = 'pchol.fr'; 19 | 20 | // Uncomment this part to create a new hosted zone 21 | /*const hostedZone = new cdk.aws_route53.HostedZone(this, 'hostedZone', { 22 | zoneName: DOMAIN_NAME, 23 | });*/ 24 | 25 | // The hosted zone already exists on my personal account 26 | // I do not create it in this repo 27 | // Comment this part or use your own already existing hosted zone by changing the ID 28 | const HOSTED_ZONE_ID = 'Z03300451ZPNQ7JFRYW48'; 29 | const hostedZone = cdk.aws_route53.HostedZone.fromHostedZoneAttributes(this, 'hostedZone', { 30 | hostedZoneId: HOSTED_ZONE_ID, 31 | zoneName: DOMAIN_NAME, 32 | }); 33 | 34 | const identity = new cdk.aws_ses.EmailIdentity(this, 'sesIdentity', { 35 | identity: cdk.aws_ses.Identity.publicHostedZone(hostedZone) 36 | }); 37 | 38 | /* 39 | If you want to send emails for free, use a real email address to create the SES identity. 40 | 41 | const MY_EMAIL_ADDRESS = 'john@gmail.com'; 42 | const identity = new cdk.aws_ses.EmailIdentity(this, 'sesIdentity', { 43 | identity: cdk.aws_ses.Identity.email(MY_EMAIL_ADDRESS) 44 | }); 45 | */ 46 | 47 | const emailTemplate = new cdk.aws_ses.CfnTemplate(this, 'emailTemplate', { 48 | template: { 49 | htmlPart: emailHtmlTemplate, 50 | subjectPart: 'Hello {{username}}!', 51 | templateName: 'myFirstTemplate', 52 | } 53 | }); 54 | 55 | const sendEmail = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'sendEmail', { 56 | entry: join(__dirname, 'sendEmail', 'handler.ts'), 57 | handler: 'handler', 58 | runtime: cdk.aws_lambda.Runtime.NODEJS_18_X, 59 | bundling: { 60 | externalModules: ['@aws-sdk'], 61 | }, 62 | environment: { 63 | SENDER_EMAIL: `contact@${identity.emailIdentityName}`, 64 | TEMPLATE_NAME: emailTemplate.ref, 65 | }, 66 | }); 67 | 68 | sendEmail.addToRolePolicy( 69 | new cdk.aws_iam.PolicyStatement({ 70 | actions: ['ses:SendTemplatedEmail'], 71 | resources: [`*`], 72 | }) 73 | ); 74 | 75 | const sendEmailResource = api.root.addResource('send-email'); 76 | sendEmailResource.addMethod('POST', new cdk.aws_apigateway.LambdaIntegration(sendEmail)); 77 | } 78 | } -------------------------------------------------------------------------------- /lib/03-S3/stack.ts: -------------------------------------------------------------------------------- 1 | import { Stack, StackProps } from "aws-cdk-lib"; 2 | import { Construct } from "constructs"; 3 | 4 | import * as cdk from 'aws-cdk-lib'; 5 | import { join } from "path"; 6 | 7 | export class Part03S3Stack extends Stack { 8 | constructor(scope: Construct, id: string, props?: StackProps) { 9 | super(scope, id, props); 10 | 11 | // Provision a new REST API Gateway 12 | const api = new cdk.aws_apigateway.RestApi(this, 'myFirstApi', { 13 | restApiName: 'Part03Service', 14 | }); 15 | 16 | const articlesBucket = new cdk.aws_s3.Bucket(this, 'articlesBucket', { 17 | blockPublicAccess: cdk.aws_s3.BlockPublicAccess.BLOCK_ALL, 18 | lifecycleRules: [ 19 | { 20 | transitions: [ 21 | { 22 | storageClass: cdk.aws_s3.StorageClass.INTELLIGENT_TIERING, 23 | transitionAfter: cdk.Duration.days(0), 24 | } 25 | ] 26 | } 27 | ], 28 | enforceSSL: true, 29 | encryption: cdk.aws_s3.BucketEncryption.S3_MANAGED, 30 | }); 31 | 32 | const articlesDatabase = new cdk.aws_dynamodb.Table(this, 'articlesDatabase', { 33 | partitionKey: { 34 | name: 'PK', 35 | type: cdk.aws_dynamodb.AttributeType.STRING, 36 | }, 37 | sortKey: { 38 | name: 'SK', 39 | type: cdk.aws_dynamodb.AttributeType.STRING, 40 | }, 41 | billingMode: cdk.aws_dynamodb.BillingMode.PAY_PER_REQUEST, 42 | }); 43 | 44 | const publishArticle = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'publishArticle', { 45 | entry: join(__dirname, 'publishArticle', 'handler.ts'), 46 | handler: 'handler', 47 | runtime: cdk.aws_lambda.Runtime.NODEJS_18_X, 48 | bundling: { 49 | externalModules: ['@aws-sdk'], 50 | }, 51 | environment: { 52 | BUCKET_NAME: articlesBucket.bucketName, 53 | TABLE_NAME: articlesDatabase.tableName, 54 | }, 55 | }); 56 | 57 | const listArticles = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'listArticles', { 58 | entry: join(__dirname, 'listArticles', 'handler.ts'), 59 | handler: 'handler', 60 | runtime: cdk.aws_lambda.Runtime.NODEJS_18_X, 61 | bundling: { 62 | externalModules: ['@aws-sdk'], 63 | }, 64 | environment: { 65 | BUCKET_NAME: articlesBucket.bucketName, 66 | TABLE_NAME: articlesDatabase.tableName, 67 | }, 68 | }); 69 | 70 | const getArticle = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'getArticle', { 71 | entry: join(__dirname, 'getArticle', 'handler.ts'), 72 | handler: 'handler', 73 | runtime: cdk.aws_lambda.Runtime.NODEJS_18_X, 74 | bundling: { 75 | externalModules: ['@aws-sdk'], 76 | }, 77 | environment: { 78 | BUCKET_NAME: articlesBucket.bucketName, 79 | }, 80 | }); 81 | 82 | articlesBucket.grantWrite(publishArticle); 83 | articlesDatabase.grantWriteData(publishArticle); 84 | 85 | articlesDatabase.grantReadData(listArticles); 86 | 87 | articlesBucket.grantRead(getArticle); 88 | 89 | const articlesResource = api.root.addResource('articles'); 90 | articlesResource.addMethod('POST', new cdk.aws_apigateway.LambdaIntegration(publishArticle)); 91 | articlesResource.addMethod('GET', new cdk.aws_apigateway.LambdaIntegration(listArticles)); 92 | articlesResource.addResource('{id}').addMethod('GET', new cdk.aws_apigateway.LambdaIntegration(getArticle)); 93 | } 94 | } -------------------------------------------------------------------------------- /lib/14-MasterDynamoDB/stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import { Construct } from 'constructs'; 3 | import { join } from 'path'; 4 | 5 | // Use the Node18 runtime which provides the aws-sdk v3 natively 6 | // This way our Lambda functions bundles will be smaller 7 | const sharedLambdaConfig = { 8 | runtime: cdk.aws_lambda.Runtime.NODEJS_18_X, 9 | bundling: { 10 | externalModules: ['@aws-sdk'], 11 | }, 12 | }; 13 | 14 | export class Part14MasterDynamoDBStack extends cdk.Stack { 15 | constructor(scope: Construct, id: string, props?: cdk.StackProps) { 16 | super(scope, id, props); 17 | 18 | const api = new cdk.aws_apigateway.RestApi(this, 'Part14Service'); 19 | 20 | const table = new cdk.aws_dynamodb.Table(this, 'DdbTable', { 21 | partitionKey: { name: 'PK', type: cdk.aws_dynamodb.AttributeType.STRING }, 22 | sortKey: { name: 'SK', type: cdk.aws_dynamodb.AttributeType.STRING }, 23 | billingMode: cdk.aws_dynamodb.BillingMode.PAY_PER_REQUEST, 24 | }); 25 | 26 | const classicRoute = api.root.addResource('classic'); 27 | 28 | const classicCreateUser = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'ClassicCreateUser', { 29 | entry: join(__dirname, 'classic.ts'), 30 | handler: 'createUser', 31 | environment: { 32 | TABLE_NAME: table.tableName, 33 | }, 34 | ...sharedLambdaConfig, 35 | }); 36 | table.grantWriteData(classicCreateUser); 37 | classicRoute.addMethod('POST', new cdk.aws_apigateway.LambdaIntegration(classicCreateUser)); 38 | 39 | const classicListUsers = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'ClassicListUsers', { 40 | entry: join(__dirname, 'classic.ts'), 41 | handler: 'listUsers', 42 | environment: { 43 | TABLE_NAME: table.tableName, 44 | }, 45 | ...sharedLambdaConfig, 46 | }); 47 | table.grantReadData(classicListUsers); 48 | classicRoute.addMethod('GET', new cdk.aws_apigateway.LambdaIntegration(classicListUsers)); 49 | 50 | const documentRoute = api.root.addResource('document'); 51 | 52 | const documentCreateUser = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'DocumentCreateUser', { 53 | entry: join(__dirname, 'document.ts'), 54 | handler: 'createUser', 55 | environment: { 56 | TABLE_NAME: table.tableName, 57 | }, 58 | ...sharedLambdaConfig, 59 | }); 60 | table.grantWriteData(documentCreateUser); 61 | documentRoute.addMethod('POST', new cdk.aws_apigateway.LambdaIntegration(documentCreateUser)); 62 | 63 | const documentListUsers = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'DocumentListUsers', { 64 | entry: join(__dirname, 'document.ts'), 65 | handler: 'listUsers', 66 | environment: { 67 | TABLE_NAME: table.tableName, 68 | }, 69 | ...sharedLambdaConfig, 70 | }); 71 | table.grantReadData(documentListUsers); 72 | documentRoute.addMethod('GET', new cdk.aws_apigateway.LambdaIntegration(documentListUsers)); 73 | 74 | const toolboxRoute = api.root.addResource('toolbox'); 75 | 76 | const toolboxCreateUser = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'ToolboxCreateUser', { 77 | entry: join(__dirname, 'toolbox.ts'), 78 | handler: 'createUser', 79 | environment: { 80 | TABLE_NAME: table.tableName, 81 | }, 82 | ...sharedLambdaConfig, 83 | }); 84 | table.grantWriteData(toolboxCreateUser); 85 | toolboxRoute.addMethod('POST', new cdk.aws_apigateway.LambdaIntegration(toolboxCreateUser)); 86 | 87 | const toolboxListUsers = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'ToolboxListUsers', { 88 | entry: join(__dirname, 'toolbox.ts'), 89 | handler: 'listUsers', 90 | environment: { 91 | TABLE_NAME: table.tableName, 92 | }, 93 | ...sharedLambdaConfig, 94 | }); 95 | table.grantReadData(toolboxListUsers); 96 | toolboxRoute.addMethod('GET', new cdk.aws_apigateway.LambdaIntegration(toolboxListUsers)); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /lib/04-Cognito/stack.ts: -------------------------------------------------------------------------------- 1 | import { Stack, StackProps } from "aws-cdk-lib"; 2 | import { Construct } from "constructs"; 3 | 4 | import * as cdk from 'aws-cdk-lib'; 5 | import { join } from "path"; 6 | 7 | export class Part04CognitoStack extends Stack { 8 | constructor(scope: Construct, id: string, props?: StackProps) { 9 | super(scope, id, props); 10 | 11 | const api = new cdk.aws_apigateway.RestApi(this, 'api', { 12 | restApiName: 'Part04Service', 13 | }); 14 | 15 | const userPool = new cdk.aws_cognito.UserPool(this, 'myFirstUserPool', { 16 | selfSignUpEnabled: true, 17 | autoVerify: { 18 | email: true, 19 | }, 20 | }); 21 | 22 | const userPoolClient = new cdk.aws_cognito.UserPoolClient(this, 'myFirstUserPoolClient', { 23 | userPool, 24 | authFlows: { 25 | userPassword: true, 26 | }, 27 | }); 28 | 29 | const authorizer = new cdk.aws_apigateway.CognitoUserPoolsAuthorizer(this, 'myFirstAuthorizer', { 30 | cognitoUserPools: [userPool], 31 | identitySource: 'method.request.header.Authorization', 32 | }); 33 | 34 | const signup = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'signup', { 35 | entry: join(__dirname, 'signup', 'handler.ts'), 36 | handler: 'handler', 37 | runtime: cdk.aws_lambda.Runtime.NODEJS_18_X, 38 | bundling: { 39 | externalModules: ['@aws-sdk'], 40 | }, 41 | environment: { 42 | USER_POOL_CLIENT_ID: userPoolClient.userPoolClientId, 43 | }, 44 | }); 45 | 46 | signup.addToRolePolicy( 47 | new cdk.aws_iam.PolicyStatement({ 48 | actions: ['cognito-idp:SignUp'], 49 | resources: [userPool.userPoolArn], 50 | }) 51 | ); 52 | 53 | const signin = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'signin', { 54 | entry: join(__dirname, 'signin', 'handler.ts'), 55 | handler: 'handler', 56 | runtime: cdk.aws_lambda.Runtime.NODEJS_18_X, 57 | bundling: { 58 | externalModules: ['@aws-sdk'], 59 | }, 60 | environment: { 61 | USER_POOL_CLIENT_ID: userPoolClient.userPoolClientId, 62 | }, 63 | }); 64 | 65 | signin.addToRolePolicy( 66 | new cdk.aws_iam.PolicyStatement({ 67 | actions: ['cognito-idp:InitiateAuth', 'cognito-idp:RespondToAuthChallenge'], 68 | resources: [userPool.userPoolArn], 69 | }) 70 | ); 71 | 72 | const confirm = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'confirm', { 73 | entry: join(__dirname, 'confirm', 'handler.ts'), 74 | handler: 'handler', 75 | runtime: cdk.aws_lambda.Runtime.NODEJS_18_X, 76 | bundling: { 77 | externalModules: ['@aws-sdk'], 78 | }, 79 | environment: { 80 | USER_POOL_CLIENT_ID: userPoolClient.userPoolClientId, 81 | }, 82 | }); 83 | 84 | confirm.addToRolePolicy( 85 | new cdk.aws_iam.PolicyStatement({ 86 | actions: ['cognito-idp:ConfirmSignUp'], 87 | resources: [userPool.userPoolArn], 88 | }) 89 | ); 90 | 91 | const secret = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'secret', { 92 | entry: join(__dirname, 'secret', 'handler.ts'), 93 | handler: 'handler', 94 | runtime: cdk.aws_lambda.Runtime.NODEJS_18_X, 95 | bundling: { 96 | externalModules: ['@aws-sdk'], 97 | }, 98 | }); 99 | 100 | api.root.addResource('sign-up').addMethod('POST', new cdk.aws_apigateway.LambdaIntegration(signup)); 101 | api.root.addResource('sign-in').addMethod('POST', new cdk.aws_apigateway.LambdaIntegration(signin)); 102 | api.root.addResource('confirm').addMethod('POST', new cdk.aws_apigateway.LambdaIntegration(confirm)); 103 | api.root.addResource('secret').addMethod('GET', new cdk.aws_apigateway.LambdaIntegration(secret), { 104 | authorizer, 105 | authorizationType: cdk.aws_apigateway.AuthorizationType.COGNITO, 106 | }); 107 | } 108 | } -------------------------------------------------------------------------------- /lib/08-SQS/stack.ts: -------------------------------------------------------------------------------- 1 | import { Stack, StackProps } from "aws-cdk-lib"; 2 | import { Construct } from "constructs"; 3 | 4 | import * as cdk from 'aws-cdk-lib'; 5 | import { join } from "path"; 6 | 7 | import { orderExecutedHtmlTemplate } from './orderExecutedHtmlTemplate'; 8 | 9 | export class Part08SQSStack extends Stack { 10 | constructor(scope: Construct, id: string, props?: StackProps) { 11 | super(scope, id, props); 12 | 13 | const api = new cdk.aws_apigateway.RestApi(this, 'api', { 14 | restApiName: 'Part08Service', 15 | }); 16 | 17 | // Use your own domain name 18 | const DOMAIN_NAME = 'pchol.fr'; 19 | // I already created the SES identity in part 06 20 | const identity = cdk.aws_ses.EmailIdentity.fromEmailIdentityName(this, 'sesIdentity', DOMAIN_NAME); 21 | 22 | const ordersQueue = new cdk.aws_sqs.Queue(this, 'ordersQueue', { 23 | visibilityTimeout: cdk.Duration.seconds(120), 24 | fifo: true, 25 | }); 26 | 27 | const eventSource = new cdk.aws_lambda_event_sources.SqsEventSource(ordersQueue, { 28 | batchSize: 1, 29 | }); 30 | 31 | const ordersEventBus = new cdk.aws_events.EventBus(this, 'ordersEventBus'); 32 | 33 | const notifyOrderExecutedRule = new cdk.aws_events.Rule(this, 'notifyOrderExecutedRule', { 34 | eventBus: ordersEventBus, 35 | eventPattern: { 36 | source: ['notifyOrderExecuted'], 37 | detailType: ['orderExecuted'], 38 | }, 39 | }); 40 | 41 | const orderExecutedTemplate = new cdk.aws_ses.CfnTemplate(this, 'orderExecutedTemplate', { 42 | template: { 43 | htmlPart: orderExecutedHtmlTemplate, 44 | subjectPart: 'Your order was passed to our provider!', 45 | templateName: 'orderExecutedTemplate', 46 | } 47 | }); 48 | 49 | const requestOrder = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'requestOrder', { 50 | entry: join(__dirname, 'requestOrder', 'handler.ts'), 51 | handler: 'handler', 52 | runtime: cdk.aws_lambda.Runtime.NODEJS_18_X, 53 | bundling: { 54 | externalModules: ['@aws-sdk'], 55 | }, 56 | environment: { 57 | QUEUE_URL: ordersQueue.queueUrl, 58 | }, 59 | }); 60 | 61 | ordersQueue.grantSendMessages(requestOrder); 62 | api.root.addResource('request-order').addMethod('POST', new cdk.aws_apigateway.LambdaIntegration(requestOrder)); 63 | 64 | const executeOrder = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'executeOrder', { 65 | entry: join(__dirname, 'executeOrder', 'handler.ts'), 66 | handler: 'handler', 67 | environment: { 68 | EVENT_BUS_NAME: ordersEventBus.eventBusName, 69 | }, 70 | runtime: cdk.aws_lambda.Runtime.NODEJS_18_X, 71 | bundling: { 72 | externalModules: ['@aws-sdk'], 73 | }, 74 | reservedConcurrentExecutions: 1, 75 | timeout: cdk.Duration.seconds(30), 76 | }); 77 | 78 | executeOrder.addEventSource(eventSource); 79 | executeOrder.addToRolePolicy( 80 | new cdk.aws_iam.PolicyStatement({ 81 | actions: ['events:PutEvents'], 82 | resources: [ordersEventBus.eventBusArn], 83 | }) 84 | ); 85 | 86 | const notifyOrderExecuted = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'notifyOrderExecuted', { 87 | entry: join(__dirname, 'notifyOrderExecuted', 'handler.ts'), 88 | handler: 'handler', 89 | runtime: cdk.aws_lambda.Runtime.NODEJS_18_X, 90 | bundling: { 91 | externalModules: ['@aws-sdk'], 92 | }, 93 | environment: { 94 | SENDER_EMAIL: `contact@${identity.emailIdentityName}`, 95 | TEMPLATE_NAME: orderExecutedTemplate.ref, 96 | }, 97 | }); 98 | 99 | notifyOrderExecuted.addToRolePolicy( 100 | new cdk.aws_iam.PolicyStatement({ 101 | actions: ['ses:SendTemplatedEmail'], 102 | resources: ['*'], 103 | }), 104 | ); 105 | 106 | notifyOrderExecutedRule.addTarget(new cdk.aws_events_targets.LambdaFunction(notifyOrderExecuted)); 107 | } 108 | } -------------------------------------------------------------------------------- /lib/07-EventBridge/stack.ts: -------------------------------------------------------------------------------- 1 | import { Stack, StackProps } from "aws-cdk-lib"; 2 | import { Construct } from "constructs"; 3 | 4 | import * as cdk from 'aws-cdk-lib'; 5 | import { join } from "path"; 6 | 7 | import { bookingReceiptHtmlTemplate } from './bookingReceiptHtmlTemplate'; 8 | 9 | 10 | export class Part07EventBridgeStack extends Stack { 11 | constructor(scope: Construct, id: string, props?: StackProps) { 12 | super(scope, id, props); 13 | 14 | const api = new cdk.aws_apigateway.RestApi(this, 'api', { 15 | restApiName: 'Part07Service', 16 | }); 17 | 18 | // Use your own domain name 19 | const DOMAIN_NAME = 'pchol.fr'; 20 | // I already created the SES identity in part 06 21 | const identity = cdk.aws_ses.EmailIdentity.fromEmailIdentityName(this, 'sesIdentity', DOMAIN_NAME); 22 | 23 | const flightTable = new cdk.aws_dynamodb.Table(this, 'flightTable', { 24 | partitionKey: { 25 | name: 'PK', 26 | type: cdk.aws_dynamodb.AttributeType.STRING, 27 | }, 28 | sortKey: { 29 | name: 'SK', 30 | type: cdk.aws_dynamodb.AttributeType.STRING, 31 | }, 32 | billingMode: cdk.aws_dynamodb.BillingMode.PAY_PER_REQUEST, 33 | }); 34 | 35 | const eventBus = new cdk.aws_events.EventBus(this, 'eventBus'); 36 | const rule = new cdk.aws_events.Rule(this, 'bookFlightRule', { 37 | eventBus, 38 | eventPattern: { 39 | source: ['bookFlight'], 40 | detailType: ['flightBooked'], 41 | }, 42 | }); 43 | 44 | const bookFlight = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'bookFlight', { 45 | entry: join(__dirname, 'bookFlight', 'handler.ts'), 46 | handler: 'handler', 47 | runtime: cdk.aws_lambda.Runtime.NODEJS_18_X, 48 | bundling: { 49 | externalModules: ['@aws-sdk'], 50 | }, 51 | environment: { 52 | TABLE_NAME: flightTable.tableName, 53 | EVENT_BUS_NAME: eventBus.eventBusName, 54 | }, 55 | }); 56 | 57 | bookFlight.addToRolePolicy( 58 | new cdk.aws_iam.PolicyStatement({ 59 | actions: ['events:PutEvents'], 60 | resources: [eventBus.eventBusArn], 61 | }) 62 | ); 63 | flightTable.grantReadData(bookFlight); 64 | api.root.addResource('book-flight').addMethod('POST', new cdk.aws_apigateway.LambdaIntegration(bookFlight)); 65 | 66 | const registerBooking = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'registerBooking', { 67 | entry: join(__dirname, 'registerBooking', 'handler.ts'), 68 | handler: 'handler', 69 | runtime: cdk.aws_lambda.Runtime.NODEJS_18_X, 70 | bundling: { 71 | externalModules: ['@aws-sdk'], 72 | }, 73 | environment: { 74 | TABLE_NAME: flightTable.tableName, 75 | }, 76 | }); 77 | flightTable.grantReadWriteData(registerBooking); 78 | rule.addTarget(new cdk.aws_events_targets.LambdaFunction(registerBooking)); 79 | 80 | const bookingReceiptTemplate = new cdk.aws_ses.CfnTemplate(this, 'bookingReceiptTemplate', { 81 | template: { 82 | htmlPart: bookingReceiptHtmlTemplate, 83 | subjectPart: 'Your flight to {{destination}} was booked!', 84 | templateName: 'bookingReceiptTemplate', 85 | } 86 | }); 87 | 88 | const sendBookingReceipt = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'sendBookingReceipt', { 89 | entry: join(__dirname, 'sendBookingReceipt', 'handler.ts'), 90 | handler: 'handler', 91 | runtime: cdk.aws_lambda.Runtime.NODEJS_18_X, 92 | bundling: { 93 | externalModules: ['@aws-sdk'], 94 | }, 95 | environment: { 96 | SENDER_EMAIL: `contact@${identity.emailIdentityName}`, 97 | TEMPLATE_NAME: bookingReceiptTemplate.ref, 98 | }, 99 | }); 100 | sendBookingReceipt.addToRolePolicy( 101 | new cdk.aws_iam.PolicyStatement({ 102 | actions: ['ses:SendTemplatedEmail'], 103 | resources: [`*`], 104 | }) 105 | ); 106 | rule.addTarget(new cdk.aws_events_targets.LambdaFunction(sendBookingReceipt)); 107 | 108 | const syncFlights = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'syncFlights', { 109 | entry: join(__dirname, 'syncFlights', 'handler.ts'), 110 | handler: 'handler', 111 | runtime: cdk.aws_lambda.Runtime.NODEJS_18_X, 112 | bundling: { 113 | externalModules: ['@aws-sdk'], 114 | }, 115 | environment: { 116 | TABLE_NAME: flightTable.tableName, 117 | }, 118 | }); 119 | flightTable.grantWriteData(syncFlights); 120 | 121 | const syncFlightsRule = new cdk.aws_events.Rule(this, 'syncFlightsRule', { 122 | schedule: cdk.aws_events.Schedule.rate(cdk.Duration.days(1)), 123 | }); 124 | syncFlightsRule.addTarget(new cdk.aws_events_targets.LambdaFunction(syncFlights)); 125 | } 126 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Code example for my series "Learn serverless on AWS step-by-step" 2 | 3 | ## TL;DR 4 | 5 | This repository contains the code examples for my series "Learn serverless on AWS step-by-step". 6 | It is written using Typescript and the AWS CDK. To each article corresponds a new CDK stack. 7 | 8 | ## How to use 9 | 10 | ```bash 11 | npm i 12 | npm run cdk bootstrap 13 | npm run deploy # deploy all stacks 14 | # or 15 | npm run cdk deploy # deploy a specific stack 16 | ``` 17 | 18 | ## Organization 19 | 20 | All the stacks can be **deployed independently, except for**: 21 | 22 | - `07-EventBridge` 23 | - `08-SQS` 24 | - `11-DynamoDBStreams` 25 | 26 | These stacks need stack `06-SES` to be deployed first (because they use the email identity created in stack `06-SES`). 27 | _You can work around this by deploying the SES identity directly from each stack, but be careful to not deploy it twice (it will fail)_ 28 | 29 | ## AWS Billing 30 | 31 | 🚨 Some resources deployed in this repository are not covered by the AWS Free Tier (but still cheap): 32 | 33 | - 1 Secret in AWS Secrets Manager **(~0.50$/month)** 34 | - 1 Hosted Zone in Route53 **(~0.50$/month)** 35 | - 1 Aurora Serverless DB cluster **(~0$/month with autoPause)** 36 | 37 | _For comparison, on my personal account, I pay **~1$/month** to keep all the resources deployed in this repository._ 38 | 39 | ## Missing articles 40 | 41 | 🚨 Some articles (basically those that need a frontend) are in a dedicated repository. I linked the corresponding repository in each affected folder. 42 | 43 | ## Articles 44 | 45 | ### Part 1 - Lambda functions 46 | 47 | - 🗞 [Article](https://dev.to/slsbytheodo/dont-miss-on-the-cloud-revolution-learn-serverless-on-aws-the-right-way-1kac) 48 | - 💻 [Code](./lib/01-Lambda/stack.ts) 49 | 50 | ### Part 2 - DynamoDB 51 | 52 | - 🗞 [Article](https://dev.to/slsbytheodo/learn-serverless-on-aws-step-by-step-databases-kkg) 53 | - 💻 [Code](./lib/02-DynamoDB/stack.ts) 54 | 55 | ### Part 3 - S3 56 | 57 | - 🗞 [Article](https://dev.to/slsbytheodo/learn-serverless-on-aws-step-by-step-file-storage-10f7) 58 | - 💻 [Code](./lib/03-S3/stack.ts) 59 | 60 | ### Part 4 - Cognito 61 | 62 | - 🗞 [Article](https://dev.to/slsbytheodo/learn-serverless-on-aws-authentication-with-cognito-19bo) 63 | - 💻 [Code](./lib/04-Cognito/stack.ts) 64 | 65 | ### Part 5 - Step Functions 66 | 67 | - 🗞 [Article](https://dev.to/slsbytheodo/learn-serverless-on-aws-step-by-step-step-functions-4m7c) 68 | - 💻 [Code](./lib/05-StepFunctions/stack.ts) 69 | 70 | ### Part 6 - SES 71 | 72 | - 🗞 [Article](https://dev.to/slsbytheodo/learn-serverless-on-aws-step-by-step-emails-49hp) 73 | - 💻 [Code](./lib/06-SES/stack.ts) 74 | 75 | ### Part 7 - EventBridge 76 | 77 | - 🗞 [Article](https://dev.to/slsbytheodo/learn-serverless-on-aws-step-by-step-eventbridge-27aa) 78 | - 💻 [Code](./lib/07-EventBridge/stack.ts) 79 | 80 | ### Part 8 - SQS 81 | 82 | - 🗞 [Article](https://dev.to/slsbytheodo/learn-serverless-on-aws-step-by-step-sqs-26c8) 83 | - 💻 [Code](./lib/08-SQS/stack.ts) 84 | 85 | ### Part 9 - Aurora Serverless 86 | 87 | - 🗞 [Article](https://dev.to/slsbytheodo/learn-serverless-on-aws-step-by-step-sql-with-aurora-5hn1) 88 | - 💻 [Code](./lib/09-Aurora/stack.ts) 89 | 90 | ### Part 10 - SNS 91 | 92 | - 🗞 [Article](https://dev.to/slsbytheodo/learn-serverless-on-aws-step-by-step-sns-2b46) 93 | - 💻 [Code](./lib/10-SNS/stack.ts) 94 | 95 | ### Part 11 - DynamoDB Streams 96 | 97 | - 🗞 [Article](https://dev.to/slsbytheodo/learn-serverless-on-aws-step-by-step-dynamodb-streams-21g5) 98 | - 💻 [Code](./lib/11-DynamoDBStreams/stack.ts) 99 | 100 | ### Part 12 - Deploying a frontend 101 | 102 | - 🗞 [Article](https://dev.to/slsbytheodo/learn-serverless-on-aws-step-by-step-deploy-a-frontend-31a6) 103 | - 💻 [Code](./lib/12-Frontend/README.md) 104 | 105 | ### Part 13 - Strongly typed Lambda functions 106 | 107 | - 🗞 [Article](https://dev.to/slsbytheodo/learn-serverless-on-aws-step-by-step-strong-types-213i) 108 | - 💻 [Code](./lib/13-LambdaTypes/README.md) 109 | 110 | ### Part 14 - Master DynamoDB 111 | 112 | - 🗞 [Article](https://dev.to/slsbytheodo/learn-serverless-on-aws-step-by-step-master-dynamodb-3cki) 113 | - 💻 [Code](./lib/14-MasterDynamoDB/stack.ts) 114 | 115 | ### Part 15 - Upload files on S3 116 | 117 | - 🗞 [Article](https://dev.to/slsbytheodo/learn-serverless-on-aws-step-by-step-upload-files-on-s3-50d4) 118 | - 💻 [Code](./lib/15-UploadS3/README.md) 119 | 120 | ### Part 16 - Lambda Destinations 121 | 122 | - 🗞 [Article](https://dev.to/slsbytheodo/learn-serverless-on-aws-step-by-step-lambda-destinations-f5b) 123 | - 💻 [Code](./lib/16-LambdaDestinations/stack.ts) 124 | 125 | ### Part 17 - EventBridge Scheduler 126 | 127 | - 🗞 [Article](https://dev.to/slsbytheodo/learn-serverless-on-aws-step-by-step-schedule-tasks-with-eventbridge-scheduler-4cbh) 128 | - 💻 [Code](./lib/17-EventBridgeScheduler/stack.ts) 129 | -------------------------------------------------------------------------------- /lib/09-Aurora/stack.ts: -------------------------------------------------------------------------------- 1 | import { Stack, StackProps } from "aws-cdk-lib"; 2 | import { Construct } from "constructs"; 3 | 4 | import * as cdk from 'aws-cdk-lib'; 5 | import { join } from "path"; 6 | 7 | 8 | export class Part09AuroraStack extends Stack { 9 | constructor(scope: Construct, id: string, props?: StackProps) { 10 | super(scope, id, props); 11 | 12 | const api = new cdk.aws_apigateway.RestApi(this, 'api', { 13 | restApiName: 'Part09Service', 14 | }); 15 | 16 | const dbSecret = new cdk.aws_rds.DatabaseSecret(this, 'AuroraSecret', { 17 | username: 'admin', 18 | }); 19 | 20 | const cluster = new cdk.aws_rds.ServerlessCluster(this, 'AuroraCluster', { 21 | engine: cdk.aws_rds.DatabaseClusterEngine.AURORA_MYSQL, 22 | credentials: cdk.aws_rds.Credentials.fromSecret(dbSecret), 23 | defaultDatabaseName: 'my_database', 24 | enableDataApi: true, 25 | scaling: { 26 | autoPause: cdk.Duration.minutes(10), 27 | minCapacity: 2, 28 | maxCapacity: 16, 29 | } 30 | }); 31 | 32 | const migrationsTable = new cdk.aws_dynamodb.Table(this, 'migrationsTable', { 33 | partitionKey: { 34 | name: "PK", 35 | type: cdk.aws_dynamodb.AttributeType.STRING, 36 | }, 37 | sortKey: { 38 | name: "SK", 39 | type: cdk.aws_dynamodb.AttributeType.STRING, 40 | }, 41 | billingMode: cdk.aws_dynamodb.BillingMode.PAY_PER_REQUEST, 42 | }); 43 | 44 | const runMigrations = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'runMigrations', { 45 | entry: join(__dirname, 'runMigrations', 'handler.ts'), 46 | handler: 'handler', 47 | runtime: cdk.aws_lambda.Runtime.NODEJS_18_X, 48 | bundling: { 49 | externalModules: ['@aws-sdk'], 50 | }, 51 | environment: { 52 | DYNAMODB_TABLE_NAME: migrationsTable.tableName, 53 | CLUSTER_ARN: cluster.clusterArn, 54 | SECRET_ARN: cluster.secret?.secretArn ?? '', 55 | }, 56 | timeout: cdk.Duration.seconds(180), 57 | }); 58 | 59 | migrationsTable.grantReadWriteData(runMigrations); 60 | runMigrations.addToRolePolicy(new cdk.aws_iam.PolicyStatement({ 61 | actions: [ 62 | 'rds-data:BatchExecuteStatement', 63 | 'rds-data:BeginTransaction', 64 | 'rds-data:CommitTransaction', 65 | 'rds-data:ExecuteStatement', 66 | 'rds-data:RollbackTransaction', 67 | ], 68 | resources: [cluster.clusterArn], 69 | })); 70 | runMigrations.addToRolePolicy(new cdk.aws_iam.PolicyStatement({ 71 | actions: [ 72 | 'secretsmanager:GetSecretValue', 73 | 'secretsmanager:DescribeSecret' 74 | ], 75 | resources: [dbSecret.secretArn], 76 | })); 77 | 78 | const addUser = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'addUser', { 79 | entry: join(__dirname, 'addUser', 'handler.ts'), 80 | handler: 'handler', 81 | runtime: cdk.aws_lambda.Runtime.NODEJS_18_X, 82 | bundling: { 83 | externalModules: ['@aws-sdk'], 84 | }, 85 | environment: { 86 | CLUSTER_ARN: cluster.clusterArn, 87 | SECRET_ARN: cluster.secret?.secretArn ?? '', 88 | }, 89 | timeout: cdk.Duration.seconds(30), 90 | }); 91 | 92 | addUser.addToRolePolicy(new cdk.aws_iam.PolicyStatement({ 93 | actions: [ 94 | 'rds-data:BatchExecuteStatement', 95 | 'rds-data:BeginTransaction', 96 | 'rds-data:CommitTransaction', 97 | 'rds-data:ExecuteStatement', 98 | 'rds-data:RollbackTransaction', 99 | ], 100 | resources: [cluster.clusterArn], 101 | })); 102 | addUser.addToRolePolicy(new cdk.aws_iam.PolicyStatement({ 103 | actions: [ 104 | 'secretsmanager:GetSecretValue', 105 | 'secretsmanager:DescribeSecret' 106 | ], 107 | resources: [dbSecret.secretArn], 108 | })); 109 | 110 | const getUsers = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'getUsers', { 111 | entry: join(__dirname, 'getUsers', 'handler.ts'), 112 | handler: 'handler', 113 | runtime: cdk.aws_lambda.Runtime.NODEJS_18_X, 114 | bundling: { 115 | externalModules: ['@aws-sdk'], 116 | }, 117 | environment: { 118 | CLUSTER_ARN: cluster.clusterArn, 119 | SECRET_ARN: cluster.secret?.secretArn ?? '', 120 | }, 121 | timeout: cdk.Duration.seconds(30), 122 | }); 123 | 124 | getUsers.addToRolePolicy(new cdk.aws_iam.PolicyStatement({ 125 | actions: [ 126 | 'rds-data:BatchExecuteStatement', 127 | 'rds-data:BeginTransaction', 128 | 'rds-data:CommitTransaction', 129 | 'rds-data:ExecuteStatement', 130 | 'rds-data:RollbackTransaction', 131 | ], 132 | resources: [cluster.clusterArn], 133 | })); 134 | getUsers.addToRolePolicy(new cdk.aws_iam.PolicyStatement({ 135 | actions: [ 136 | 'secretsmanager:GetSecretValue', 137 | 'secretsmanager:DescribeSecret' 138 | ], 139 | resources: [dbSecret.secretArn], 140 | })); 141 | 142 | api.root.addResource('add-user').addMethod('POST', new cdk.aws_apigateway.LambdaIntegration(addUser)); 143 | api.root.addResource('get-users').addMethod('GET', new cdk.aws_apigateway.LambdaIntegration(getUsers)); 144 | 145 | } 146 | } -------------------------------------------------------------------------------- /lib/05-StepFunctions/stack.ts: -------------------------------------------------------------------------------- 1 | import { Stack, StackProps } from "aws-cdk-lib"; 2 | import { Construct } from "constructs"; 3 | 4 | import * as cdk from 'aws-cdk-lib'; 5 | import { join } from "path"; 6 | 7 | export class Part05StepFunctionsStack extends Stack { 8 | constructor(scope: Construct, id: string, props?: StackProps) { 9 | super(scope, id, props); 10 | 11 | const api = new cdk.aws_apigateway.RestApi(this, 'api', { 12 | restApiName: 'Part05Service', 13 | }); 14 | 15 | const storeDB = new cdk.aws_dynamodb.Table(this, 'storeDB', { 16 | partitionKey: { 17 | name: 'PK', 18 | type: cdk.aws_dynamodb.AttributeType.STRING, 19 | }, 20 | sortKey: { 21 | name: 'SK', 22 | type: cdk.aws_dynamodb.AttributeType.STRING, 23 | }, 24 | billingMode: cdk.aws_dynamodb.BillingMode.PAY_PER_REQUEST, 25 | }); 26 | 27 | const isItemInStock = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'isItemInStock', { 28 | entry: join(__dirname, 'isItemInStock', 'handler.ts'), 29 | handler: 'handler', 30 | runtime: cdk.aws_lambda.Runtime.NODEJS_18_X, 31 | bundling: { 32 | externalModules: ['@aws-sdk'], 33 | }, 34 | environment: { 35 | TABLE_NAME: storeDB.tableName, 36 | }, 37 | }); 38 | storeDB.grantReadData(isItemInStock); 39 | 40 | const updateItemStock = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'updateItemStock', { 41 | entry: join(__dirname, 'updateItemStock', 'handler.ts'), 42 | handler: 'handler', 43 | runtime: cdk.aws_lambda.Runtime.NODEJS_18_X, 44 | bundling: { 45 | externalModules: ['@aws-sdk'], 46 | }, 47 | environment: { 48 | TABLE_NAME: storeDB.tableName, 49 | }, 50 | }); 51 | storeDB.grantWriteData(updateItemStock); 52 | 53 | const createOrder = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'createOrder', { 54 | entry: join(__dirname, 'createOrder', 'handler.ts'), 55 | handler: 'handler', 56 | runtime: cdk.aws_lambda.Runtime.NODEJS_18_X, 57 | bundling: { 58 | externalModules: ['@aws-sdk'], 59 | }, 60 | environment: { 61 | TABLE_NAME: storeDB.tableName, 62 | }, 63 | }); 64 | 65 | storeDB.grantWriteData(createOrder); 66 | 67 | const isItemInStockMappedTask = new cdk.aws_stepfunctions.Map(this, 'isItemInStockMappedTask', { 68 | itemsPath: '$.order', 69 | resultPath: cdk.aws_stepfunctions.JsonPath.DISCARD, 70 | parameters: { 71 | 'item.$': '$$.Map.Item.Value', 72 | }, 73 | }).iterator(new cdk.aws_stepfunctions_tasks.LambdaInvoke(this, 'isItemInStockTask', { 74 | lambdaFunction: isItemInStock, 75 | })); 76 | 77 | const updateItemStockMappedTask = new cdk.aws_stepfunctions.Map(this, 'updateItemStockMappedTask', { 78 | itemsPath: '$.order', 79 | parameters: { 80 | 'item.$': '$$.Map.Item.Value', 81 | }, 82 | }).iterator(new cdk.aws_stepfunctions_tasks.LambdaInvoke(this, 'updateItemStockTask', { 83 | lambdaFunction: updateItemStock, 84 | })); 85 | 86 | const createOrderTask = new cdk.aws_stepfunctions_tasks.LambdaInvoke(this, 'createOrderTask', { 87 | lambdaFunction: createOrder, 88 | }); 89 | 90 | const parallelState = new cdk.aws_stepfunctions.Parallel(this, 'parallelState', {}); 91 | 92 | parallelState.branch(updateItemStockMappedTask, createOrderTask); 93 | 94 | const definition = isItemInStockMappedTask.next( 95 | parallelState 96 | ); 97 | 98 | const myFirstStateMachine = new cdk.aws_stepfunctions.StateMachine(this, 'myFirstStateMachine', { 99 | definition, 100 | }); 101 | 102 | const invokeStateMachineRole = new cdk.aws_iam.Role(this, 'invokeStateMachineRole', { 103 | assumedBy: new cdk.aws_iam.ServicePrincipal('apigateway.amazonaws.com'), 104 | }); 105 | 106 | invokeStateMachineRole.addToPolicy( 107 | new cdk.aws_iam.PolicyStatement({ 108 | actions: ['states:StartExecution'], 109 | resources: [myFirstStateMachine.stateMachineArn], 110 | }) 111 | ); 112 | 113 | const createOrderResource = api.root.addResource('create-order'); 114 | 115 | createOrderResource.addMethod('POST', new cdk.aws_apigateway.Integration({ 116 | type: cdk.aws_apigateway.IntegrationType.AWS, 117 | integrationHttpMethod: 'POST', 118 | uri: `arn:aws:apigateway:${cdk.Aws.REGION}:states:action/StartExecution`, 119 | options: { 120 | credentialsRole: invokeStateMachineRole, 121 | requestTemplates: { 122 | 'application/json': `{ 123 | "input": "{\\"order\\": $util.escapeJavaScript($input.json('$'))}", 124 | "stateMachineArn": "${myFirstStateMachine.stateMachineArn}" 125 | }`, 126 | }, 127 | integrationResponses: [ 128 | { 129 | statusCode: '200', 130 | responseTemplates: { 131 | 'application/json': `{ 132 | "statusCode": 200, 133 | "body": { "message": "OK!" }" 134 | }`, 135 | }, 136 | }, 137 | ], 138 | }, 139 | }), 140 | { 141 | methodResponses: [ 142 | { 143 | statusCode: '200', 144 | }, 145 | ], 146 | }); 147 | 148 | const createStoreItem = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'createStoreItem', { 149 | entry: join(__dirname, 'createStoreItem', 'handler.ts'), 150 | handler: 'handler', 151 | runtime: cdk.aws_lambda.Runtime.NODEJS_18_X, 152 | bundling: { 153 | externalModules: ['@aws-sdk'], 154 | }, 155 | environment: { 156 | TABLE_NAME: storeDB.tableName, 157 | }, 158 | }); 159 | 160 | storeDB.grantReadWriteData(createStoreItem); 161 | 162 | const createStoreItemResource = api.root.addResource('create-store-item'); 163 | createStoreItemResource.addMethod('POST', new cdk.aws_apigateway.LambdaIntegration(createStoreItem)); 164 | } 165 | } -------------------------------------------------------------------------------- /lib/11-DynamoDBStreams/stack.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from "constructs"; 2 | import * as cdk from "aws-cdk-lib"; 3 | 4 | import path from "path"; 5 | 6 | import { restaurantBookedTemplateHtml } from './onRestaurantBooked/template'; 7 | import { reservationConfirmedTemplateHtml } from './onReservationConfirmed/template'; 8 | import { Stack } from "aws-cdk-lib"; 9 | 10 | export class Part11DynamoDBStreamsStack extends Stack { 11 | constructor(scope: Construct, id: string, props?: cdk.StackProps) { 12 | super(scope, id, props); 13 | 14 | const api = new cdk.aws_apigateway.RestApi(this, 'api', { 15 | restApiName: 'Part11Service', 16 | }); 17 | 18 | // Use your own domain name 19 | const DOMAIN_NAME = 'pchol.fr'; 20 | // I already created the SES identity in part 06 21 | const identity = cdk.aws_ses.EmailIdentity.fromEmailIdentityName(this, 'sesIdentity', DOMAIN_NAME); 22 | 23 | // Table to store reservations 24 | const table = new cdk.aws_dynamodb.Table(this, 'ReservationsTable', { 25 | partitionKey: { name: 'SK', type: cdk.aws_dynamodb.AttributeType.STRING }, 26 | sortKey: { name: 'PK', type: cdk.aws_dynamodb.AttributeType.STRING }, 27 | billingMode: cdk.aws_dynamodb.BillingMode.PAY_PER_REQUEST, 28 | stream: cdk.aws_dynamodb.StreamViewType.NEW_IMAGE, 29 | }); 30 | 31 | // Event bus to dispatch events 32 | const bus = new cdk.aws_events.EventBus(this, 'EventBus'); 33 | 34 | // Email template when a restaurant is booked 35 | const restaurantBookedTemplate = new cdk.aws_ses.CfnTemplate(this, 'RestaurantBookedTemplate', { 36 | template: { 37 | templateName: 'restaurantBookedTemplate', 38 | subjectPart: 'Restaurant booked', 39 | htmlPart: restaurantBookedTemplateHtml, 40 | } 41 | }); 42 | 43 | // Email template when a reservation is confirmed 44 | const reservationConfirmedTemplate = new cdk.aws_ses.CfnTemplate(this, 'ReservationConfirmedTemplate', { 45 | template: { 46 | templateName: 'reservationConfirmedTemplate', 47 | subjectPart: 'Reservation confirmed', 48 | htmlPart: reservationConfirmedTemplateHtml, 49 | } 50 | }); 51 | 52 | const bookRestaurant = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'BookRestaurant', { 53 | entry: path.join(__dirname, 'bookRestaurant', 'handler.ts'), 54 | handler: 'handler', 55 | runtime: cdk.aws_lambda.Runtime.NODEJS_18_X, 56 | bundling: { 57 | externalModules: ['@aws-sdk'], 58 | }, 59 | environment: { 60 | TABLE_NAME: table.tableName, 61 | } 62 | }); 63 | 64 | table.grantWriteData(bookRestaurant); 65 | api.root.addResource('bookRestaurant').addMethod('POST', new cdk.aws_apigateway.LambdaIntegration(bookRestaurant)); 66 | 67 | const confirmReservation = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'ConfirmReservation', { 68 | entry: path.join(__dirname, 'confirmReservation', 'handler.ts'), 69 | handler: 'handler', 70 | runtime: cdk.aws_lambda.Runtime.NODEJS_18_X, 71 | bundling: { 72 | externalModules: ['@aws-sdk'], 73 | }, 74 | environment: { 75 | TABLE_NAME: table.tableName, 76 | } 77 | }); 78 | 79 | table.grantWriteData(confirmReservation); 80 | api.root.addResource('confirmReservation').addResource('{reservationId}').addMethod('GET', new cdk.aws_apigateway.LambdaIntegration(confirmReservation)); 81 | 82 | const streamTarget = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'StreamTarget', { 83 | entry: path.join(__dirname, 'streamTarget', 'handler.ts'), 84 | handler: 'handler', 85 | runtime: cdk.aws_lambda.Runtime.NODEJS_18_X, 86 | bundling: { 87 | externalModules: ['@aws-sdk'], 88 | }, 89 | environment: { 90 | TABLE_NAME: table.tableName, 91 | EVENT_BUS_NAME: bus.eventBusName, 92 | } 93 | }); 94 | 95 | table.grantStreamRead(streamTarget); 96 | streamTarget.addEventSourceMapping('StreamSource', { 97 | eventSourceArn: table.tableStreamArn, 98 | startingPosition: cdk.aws_lambda.StartingPosition.LATEST, 99 | batchSize: 1, 100 | }); 101 | streamTarget.addToRolePolicy(new cdk.aws_iam.PolicyStatement({ 102 | actions: ['events:PutEvents'], 103 | resources: [bus.eventBusArn], 104 | })); 105 | 106 | const onRestaurantBookedRule = new cdk.aws_events.Rule(this, 'OnRestaurantBookedRule', { 107 | eventBus: bus, 108 | eventPattern: { 109 | source: ['StreamTarget'], 110 | detailType: ['OnRestaurantBooked'], 111 | }, 112 | }); 113 | 114 | const onRestaurantBooked = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'OnRestaurantBookedLambda', { 115 | entry: path.join(__dirname, 'onRestaurantBooked', 'handler.ts'), 116 | handler: 'handler', 117 | runtime: cdk.aws_lambda.Runtime.NODEJS_18_X, 118 | bundling: { 119 | externalModules: ['@aws-sdk'], 120 | }, 121 | environment: { 122 | FROM_EMAIL_ADDRESS: `notifications@${identity.emailIdentityName}`, 123 | API_URL: api.url, 124 | TEMPLATE_NAME: restaurantBookedTemplate.ref, 125 | } 126 | }); 127 | 128 | onRestaurantBookedRule.addTarget(new cdk.aws_events_targets.LambdaFunction(onRestaurantBooked)); 129 | onRestaurantBooked.addToRolePolicy(new cdk.aws_iam.PolicyStatement({ 130 | actions: ['ses:SendTemplatedEmail'], 131 | resources: ['*'], 132 | })); 133 | 134 | const onReservationConfirmedRule = new cdk.aws_events.Rule(this, 'OnReservationConfirmedRule', { 135 | eventBus: bus, 136 | eventPattern: { 137 | source: ['StreamTarget'], 138 | detailType: ['OnReservationConfirmed'], 139 | }, 140 | }); 141 | 142 | const onReservationConfirmed = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'OnReservationConfirmedLambda', { 143 | entry: path.join(__dirname, 'onReservationConfirmed', 'handler.ts'), 144 | handler: 'handler', 145 | runtime: cdk.aws_lambda.Runtime.NODEJS_18_X, 146 | bundling: { 147 | externalModules: ['@aws-sdk'], 148 | }, 149 | environment: { 150 | FROM_EMAIL_ADDRESS: `notifications@${identity.emailIdentityName}`, 151 | TEMPLATE_NAME: reservationConfirmedTemplate.ref, 152 | } 153 | }); 154 | 155 | onReservationConfirmedRule.addTarget(new cdk.aws_events_targets.LambdaFunction(onReservationConfirmed)); 156 | onReservationConfirmed.addToRolePolicy(new cdk.aws_iam.PolicyStatement({ 157 | actions: ['ses:SendTemplatedEmail'], 158 | resources: ['*'], 159 | })); 160 | } 161 | } 162 | --------------------------------------------------------------------------------