Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Serverless Office Hours | Serverless for Startups

Serverless Office Hours | Serverless for Startups

Slobodan Stojanović

June 21, 2022
Tweet

More Decks by Slobodan Stojanović

Other Decks in Programming

Transcript

  1. In 2016 we decided to build a leave tracking system

    to build a leave tracking system
  2. In 2016 we decided to build a leave tracking system

    To solve our own leave tracking problem
  3. In 2016 we decided to build a leave tracking system

    2017 To solve our own leave tracking problem
  4. In 2016 we decided to build a leave tracking system

    2017 2018 To solve our own leave tracking problem
  5. The idea was simple: - track leave requests and number

    of remaining days - We do not want to remember more passwords - use SSO - connect to Slack to show requests and info in the chat - connect to the calendar so we can subscribe to events
  6. - startups - sma! and medium companies - schools -

    non-profits - teams from enterprises - government organizations - churches
  7. s e r v e r l e s s

    low xpensive endor lock-in esponse latency
  8. s e r v e r l e s s

    low xpensive endor lock-in esponse latency
  9. Serverless because: - Sma! team without devops experience - AUTO

    SCALABLE - Cheap - Fast to build a prototype - Secure
  10. - Quick and independent deployments - Easy to understand and

    maintain - Easy to onboard new people - Cheap Benefits:
  11. - Everything is in IaaC CloudFormation - Replaced Node.js server

    with serverless services - Added TypeScript - Replaced MongoDB with DynamoDB Things we changed:
  12. - Storing state not events - Wasting time on less

    important things - Hard to onboard new developers - Many new services - Developers don't like YAML :) Downsides:
  13. - Ana created a location and moved John and Mike

    - Ana assigned Mike as an approver - Ana assigned a leave policy (20 PTO days per year) - John requested a leave, Ana approved - Ro!over, some unused days were transferred to the next year - Ana changed John's working w"k - Mike added some past leaves for John - Alex transferred John to another location with different policy - …
  14. 1. The client sends an API POST request or GraphQL

    mutation 2. The event is stored in the Events table (append-only, no edits)
  15. 1. The client sends an API POST request or GraphQL

    mutation 2. The event is stored in the Events table (append-only, no edits) 3. The DynamoDB streams the event to the Lambda function that sends it to the EventBridge event bus
  16. 1. The client sends an API POST request or GraphQL

    mutation 2. The event is stored in the Events table (append-only, no edits) 3. The DynamoDB streams the event to the Lambda function that sends it to the EventBridge event bus 4. EventBridge triggers the specific business logic Lambda function
  17. 1. The client sends an API POST request or GraphQL

    mutation 2. The event is stored in the Events table (append-only, no edits) 3. The DynamoDB streams the event to the Lambda function that sends it to the EventBridge event bus 4. EventBridge triggers the specific business logic Lambda function 5. The business logic Lambda stores the "cached" data to one of the read-only DynamoDB tables
  18. 1. The client sends an API POST request or GraphQL

    mutation 2. The event is stored in the Events table (append-only, no edits) 3. The DynamoDB streams the event to the Lambda function that sends it to the EventBridge event bus 4. EventBridge triggers the specific business logic Lambda function 5. The business logic Lambda stores the "cached" data to one of the read-only DynamoDB tables 6. And then triggers the mutation that sends a "fake" mutation
  19. 1. The client sends an API POST request or GraphQL

    mutation 2. The event is stored in the Events table (append-only, no edits) 3. The DynamoDB streams the event to the Lambda function that sends it to the EventBridge event bus 4. EventBridge triggers the specific business logic Lambda function 5. The business logic Lambda stores the "cached" data to one of the read-only DynamoDB tables 6. And then triggers the mutation that sends a "fake" mutation 7. A "fake" mutation triggers the GraphQl subscription to notify the clients
  20. 1. The client sends an API POST request or GraphQL

    mutation 2. The event is stored in the Events table (append-only, no edits) 3. The DynamoDB streams the event to the Lambda function that sends it to the EventBridge event bus 4. EventBridge triggers the specific business logic Lambda function 5. The business logic Lambda stores the "cached" data to one of the read-only DynamoDB tables 6. And then triggers the mutation that sends a "fake" mutation 7. A "fake" mutation triggers the GraphQl subscription to notify the clients
  21. 1. The client sends an API POST request or GraphQL

    mutation 2. The event is stored in the Events table (append-only, no edits) 3. The DynamoDB streams the event to the Lambda function that sends it to the EventBridge event bus 4. EventBridge triggers the specific business logic Lambda function 5. The business logic Lambda stores the "cached" data to one of the read-only DynamoDB tables 6. And then triggers the mutation that sends a "fake" mutation 7. A "fake" mutation triggers the GraphQl subscription to notify the clients 8. App uses an event bus to "route" the response to the user's platform
  22. - Fu!y Managed GraphQL - Less code - Better control

    - A! benefits from the previous architecture - Monorepo Benefits:
  23. Current team - 3 fu! stack developers + 1 WP

    +1 QA - 1 Product manager - 2 Marketing - 2 Customer support - 2 Founders - Some fr"lance support (marketing + design)
  24. "Use the force, Luke!" a! developers are fu!-stack, but some

    are better with front end and some are better with back end
  25. @slobodan_ export async function getPresignedUrl<T>({ event, bucket, region, userImagesManager, parser,

    }: IGetPresignedUrlParams<T>): Promise<IGetPresignedUrlresponse> { try { const { userId, mimeType } = parser(event) const extension = getFileExtension(mimeType) const fileName = `${userId}-${Date.now()}.${extension}` const uploadUrl = await userImagesManager.getPresignedUrl(fileName) const imageUrl = `https://${bucket}.s3.${region}.amazonaws.com/${fileName}` return { uploadUrl, imageUrl, } } catch (error) { logger.error(error) throw generateError(PresignedUrlFailed) } } main.ts
  26. @slobodan_ export async function getPresignedUrl<T>({ event, bucket, region, userImagesManager, parser,

    }: IGetPresignedUrlParams<T>): Promise<IGetPresignedUrlresponse> { try { const { userId, mimeType } = parser(event) const extension = getFileExtension(mimeType) const fileName = `${userId}-${Date.now()}.${extension}` const uploadUrl = await userImagesManager.getPresignedUrl(fileName) const imageUrl = `https://${bucket}.s3.${region}.amazonaws.com/${fileName}` return { uploadUrl, imageUrl, } } catch (error) { logger.error(error) throw generateError(PresignedUrlFailed) } } main.ts
  27. @slobodan_ export async function handler (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> { try

    { const { imageUrl, uploadUrl } = await getPresignedUrl<APIGatewayProxyEvent>({ event, userImagesManager, parser: parseApiEvent, bucket: process.env.USER_IMAGES_BUCKET, region: process.env.AWS_REGION, }) return httpResponse({ body: { imageUrl, uploadUrl, }}) } catch (error) { return httpResponse({ statusCode: 400, body: error, }) } } lambda.ts
  28. @slobodan_ export async function handler (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> { try

    { const { imageUrl, uploadUrl } = await getPresignedUrl<APIGatewayProxyEvent>({ event, userImagesManager, parser: parseApiEvent, bucket: process.env.USER_IMAGES_BUCKET, region: process.env.AWS_REGION, }) return httpResponse({ body: { imageUrl, uploadUrl, }}) } catch (error) { return httpResponse({ statusCode: 400, body: error, }) } } lambda.ts
  29. @slobodan_ describe('DynamoDB repository', () => { describe('unit', () => {

    ... }) describe('integration', () => { beforeAll(() => { // Create test DB }) afterAll(() => { // Destroy test DB }) // Tests }) })
  30. @slobodan_ beforeAll(async () => { const params = { ...

    } await dynamoDb.createTable(params).promise() await dynamoDb.waitFor('tableExists', { TableName: tableName }).promise() })
  31. @slobodan_ afterAll(async () => { await dynamoDb.deleteTable({ TableName: tableName }).promise()

    await dynamoDb.waitFor('tableNotExists', { TableName: tableName }).promise() })
  32. Following best practices can be expensive! You do not n"d

    to use a! the fancy tools. At least not at the beginning.
  33. » aws-sso-util login --profile vtslobodan Logging in https://notrealurl.awsapps.com/start AWS SSO

    login required. Attempting to open the SSO authorization page in your default browser. If the browser does not open or you wish to use a different device to authorize this request, open the following URL: https://device.sso.eu-central-1.amazonaws.com/ Then enter the code: ABCD-EFGH Login succeeded, valid until 2022-06-22 02:26 CEST
  34. @slobodan_ • Evolve your architecture with your product • Pick

    a good architecture, it helps you to keep your migration and onboarding costs low • Think about onboarding new team members • Hexagonal architecture is a nice fit for serverless apps • CQRS is also A nice fit for serverless apps and excellent fit for Vacation Tracker • Test your integrations (and app in general) • Testing is not enough, you'll need monitoring and error tracking