# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: 2010-09-09 Transform: - AWS::Serverless-2016-10-31 Description: > Backend API with Lambda and DynamoDB, Lambda Authorizer Globals: Function: Runtime: nodejs14.x MemorySize: 128 Timeout: 100 Tracing: Active Parameters: CognitoStackName: Description: An environment name for Cognito stack Type: String Default: apigw-samples-cognito-shared Resources: LocationsFunction: Type: AWS::Serverless::Function Properties: Handler: src/api/locations.handler Description: Handler for all locations related operations Environment: Variables: LOCATIONS_TABLE: !Ref LocationsTable AWS_EMF_NAMESPACE: !Sub ${AWS::StackName} Policies: - DynamoDBCrudPolicy: TableName: !Ref LocationsTable Events: GetLocations: Type: HttpApi Properties: ApiId: !Ref HttpApi Path: /locations Method: GET PutLocation: Type: HttpApi Properties: ApiId: !Ref HttpApi Path: /locations Method: PUT GetLocation: Type: HttpApi Properties: ApiId: !Ref HttpApi Path: /locations/{locationid} Method: GET DeleteLocation: Type: HttpApi Properties: ApiId: !Ref HttpApi Path: /locations/{locationid} Method: DELETE Tags: Stack: !Sub "${AWS::StackName}" LocationsFunctionLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub "/aws/lambda/${LocationsFunction}" RetentionInDays: 7 ResourcesFunction: Type: AWS::Serverless::Function Properties: Handler: src/api/resources.handler Description: Handler for all resources related operations Environment: Variables: RESOURCES_TABLE: !Ref ResourcesTable AWS_EMF_NAMESPACE: !Sub ${AWS::StackName} Policies: - DynamoDBCrudPolicy: TableName: !Ref ResourcesTable Events: GetResources: Type: HttpApi Properties: ApiId: !Ref HttpApi Path: /locations/{locationid}/resources Method: GET PutResource: Type: HttpApi Properties: ApiId: !Ref HttpApi Path: /locations/{locationid}/resources Method: PUT GetResource: Type: HttpApi Properties: ApiId: !Ref HttpApi Path: /locations/{locationid}/resources/{resourceid} Method: GET DeleteResource: Type: HttpApi Properties: ApiId: !Ref HttpApi Path: /locations/{locationid}/resources/{resourceid} Method: DELETE Tags: Stack: !Sub "${AWS::StackName}" ResourcesFunctionLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub "/aws/lambda/${ResourcesFunction}" RetentionInDays: 7 BookingsFunction: Type: AWS::Serverless::Function Properties: Handler: src/api/bookings.handler Description: Handler for all bookings related operations Environment: Variables: BOOKINGS_TABLE: !Ref BookingsTable AWS_EMF_NAMESPACE: !Sub ${AWS::StackName} Policies: - DynamoDBCrudPolicy: TableName: !Ref BookingsTable Events: GetBookingsForResource: Type: HttpApi Properties: ApiId: !Ref HttpApi Path: /locations/{locationid}/resources/{resourceid}/bookings Method: GET GetBookingsForUser: Type: HttpApi Properties: ApiId: !Ref HttpApi Path: /users/{userid}/bookings Method: GET PutBooking: Type: HttpApi Properties: ApiId: !Ref HttpApi Path: /users/{userid}/bookings Method: PUT GetBooking: Type: HttpApi Properties: ApiId: !Ref HttpApi Path: /users/{userid}/bookings/{bookingid} Method: GET DeleteBooking: Type: HttpApi Properties: ApiId: !Ref HttpApi Path: /users/{userid}/bookings/{bookingid} Method: DELETE Tags: Stack: !Sub "${AWS::StackName}" BookingsFunctionLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub "/aws/lambda/${BookingsFunction}" RetentionInDays: 7 AuthorizerFunction: Type: AWS::Serverless::Function Properties: Handler: src/api/authorizer.handler Description: Handler for Lambda authorizer Environment: Variables: USER_POOL_ID: Fn::ImportValue: !Sub "${CognitoStackName}-UserPool" ADMIN_GROUP_NAME: Fn::ImportValue: !Sub "${CognitoStackName}-UserPoolAdminGroupName" Tags: Stack: !Sub "${AWS::StackName}" AuthorizerFunctionLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub "/aws/lambda/${AuthorizerFunction}" RetentionInDays: 7 AuthorizerFunctionExecutionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Principal: Service: - apigateway.amazonaws.com Action: - sts:AssumeRole Path: "/" Tags: - Key: "Stack" Value: !Sub "${AWS::StackName}" AuthorizerFunctionExecutionRolePolicy: Type: AWS::IAM::Policy Properties: PolicyName: !Sub ${AWS::StackName}-Authorizer-Policy PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: lambda:InvokeFunction Resource: !GetAtt AuthorizerFunction.Arn Roles: - Ref: AuthorizerFunctionExecutionRole HttpApi: Type: AWS::Serverless::HttpApi Properties: FailOnWarnings: True CorsConfiguration: AllowMethods: - PUT - GET - DELETE - OPTIONS AllowHeaders: - Content-Type - Authorization - X-Forwarded-For - X-Api-Key - X-Amz-Date - X-Amz-Security-Token AllowOrigins: - "*" Auth: Authorizers: LambdaAuthorizer: AuthorizerPayloadFormatVersion: 1.0 FunctionArn: !GetAtt AuthorizerFunction.Arn FunctionInvokeRole: !GetAtt AuthorizerFunctionExecutionRole.Arn Identity: Headers: - Authorization DefaultAuthorizer: LambdaAuthorizer AccessLogSettings: DestinationArn: !GetAtt AccessLogs.Arn Format: '{ "requestId":"$context.requestId", "ip": "$context.identity.sourceIp", "requestTime":"$context.requestTime", "httpMethod":"$context.httpMethod","routeKey":"$context.routeKey", "status":"$context.status","protocol":"$context.protocol", "integrationStatus": $context.integrationStatus, "integrationLatency": $context.integrationLatency, "responseLength":"$context.responseLength" }' Tags: Name: !Sub "${AWS::StackName}-API" Stack: !Sub "${AWS::StackName}" AccessLogs: Type: AWS::Logs::LogGroup Properties: RetentionInDays: 30 LogGroupName: !Sub "/${AWS::StackName}/APIAccessLogs" LocationsTable: Type: AWS::Serverless::SimpleTable Properties: PrimaryKey: Name: locationid Type: String ProvisionedThroughput: ReadCapacityUnits: 2 WriteCapacityUnits: 2 Tags: Stack: !Sub "${AWS::StackName}" ResourcesTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: resourceid AttributeType: S - AttributeName: locationid AttributeType: S KeySchema: - AttributeName: resourceid KeyType: HASH ProvisionedThroughput: ReadCapacityUnits: 2 WriteCapacityUnits: 2 GlobalSecondaryIndexes: - IndexName: locationidGSI KeySchema: - AttributeName: locationid KeyType: HASH Projection: ProjectionType: ALL ProvisionedThroughput: ReadCapacityUnits: 2 WriteCapacityUnits: 2 Tags: - Key: "Stack" Value: !Sub "${AWS::StackName}" BookingsTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: bookingid AttributeType: S - AttributeName: userid AttributeType: S - AttributeName: resourceid AttributeType: S - AttributeName: starttimeepochtime AttributeType: N KeySchema: - AttributeName: bookingid KeyType: HASH ProvisionedThroughput: ReadCapacityUnits: 2 WriteCapacityUnits: 2 GlobalSecondaryIndexes: - IndexName: useridGSI KeySchema: - AttributeName: userid KeyType: HASH Projection: ProjectionType: ALL ProvisionedThroughput: ReadCapacityUnits: 2 WriteCapacityUnits: 2 - IndexName: bookingsByUserByTimeGSI KeySchema: - AttributeName: userid KeyType: HASH - AttributeName: starttimeepochtime KeyType: RANGE Projection: ProjectionType: ALL ProvisionedThroughput: ReadCapacityUnits: 2 WriteCapacityUnits: 2 - IndexName: bookingsByResourceByTimeGSI KeySchema: - AttributeName: resourceid KeyType: HASH - AttributeName: starttimeepochtime KeyType: RANGE Projection: ProjectionType: ALL ProvisionedThroughput: ReadCapacityUnits: 2 WriteCapacityUnits: 2 Tags: - Key: "Stack" Value: !Sub "${AWS::StackName}" AlarmsKMSKey: Type: AWS::KMS::Key Properties: Description: CMK for SNS alarms topic Enabled: true EnableKeyRotation: True KeyPolicy: Version: "2012-10-17" Statement: - Effect: Allow Principal: Service: - "cloudwatch.amazonaws.com" - "sns.amazonaws.com" Action: - "kms:GenerateDataKey*" - "kms:Decrypt" Resource: "*" - Effect: Allow Principal: AWS: - !Sub "arn:aws:iam::${AWS::AccountId}:root" Action: - "kms:*" Resource: "*" KeySpec: SYMMETRIC_DEFAULT KeyUsage: ENCRYPT_DECRYPT PendingWindowInDays: 30 Tags: - Key: "Stack" Value: !Sub "${AWS::StackName}" AlarmsTopic: Type: AWS::SNS::Topic Properties: KmsMasterKeyId: !Ref AlarmsKMSKey Tags: - Key: "Stack" Value: !Sub "${AWS::StackName}" HttpApiErrorsAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmActions: - !Ref AlarmsTopic ComparisonOperator: GreaterThanOrEqualToThreshold Dimensions: - Name: ApiName Value: !Ref HttpApi EvaluationPeriods: 1 MetricName: 5XXError Namespace: AWS/ApiGateway Period: 60 Statistic: Sum Threshold: 1.0 AuthorizerFunctionErrorsAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmActions: - !Ref AlarmsTopic ComparisonOperator: GreaterThanOrEqualToThreshold Dimensions: - Name: FunctionName Value: !Ref AuthorizerFunction EvaluationPeriods: 1 MetricName: Errors Namespace: AWS/Lambda Period: 60 Statistic: Sum Threshold: 1.0 LocationsFunctionErrorsAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmActions: - !Ref AlarmsTopic ComparisonOperator: GreaterThanOrEqualToThreshold Dimensions: - Name: FunctionName Value: !Ref LocationsFunction EvaluationPeriods: 1 MetricName: Errors Namespace: AWS/Lambda Period: 60 Statistic: Sum Threshold: 1.0 ResourcesFunctionErrorsAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmActions: - !Ref AlarmsTopic ComparisonOperator: GreaterThanOrEqualToThreshold Dimensions: - Name: FunctionName Value: !Ref ResourcesFunction EvaluationPeriods: 1 MetricName: Errors Namespace: AWS/Lambda Period: 60 Statistic: Sum Threshold: 1.0 BookingsFunctionErrorsAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmActions: - !Ref AlarmsTopic ComparisonOperator: GreaterThanOrEqualToThreshold Dimensions: - Name: FunctionName Value: !Ref BookingsFunction EvaluationPeriods: 1 MetricName: Errors Namespace: AWS/Lambda Period: 60 Statistic: Sum Threshold: 1.0 LocationsFunctionThrottlingAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmActions: - !Ref AlarmsTopic ComparisonOperator: GreaterThanOrEqualToThreshold Dimensions: - Name: FunctionName Value: !Ref LocationsFunction EvaluationPeriods: 1 MetricName: Throttles Namespace: AWS/Lambda Period: 60 Statistic: Sum Threshold: 1.0 ResourcesFunctionThrottlingAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmActions: - !Ref AlarmsTopic ComparisonOperator: GreaterThanOrEqualToThreshold Dimensions: - Name: FunctionName Value: !Ref ResourcesFunction EvaluationPeriods: 1 MetricName: Throttles Namespace: AWS/Lambda Period: 60 Statistic: Sum Threshold: 1.0 BookingsFunctionThrottlingAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmActions: - !Ref AlarmsTopic ComparisonOperator: GreaterThanOrEqualToThreshold Dimensions: - Name: FunctionName Value: !Ref BookingsFunction EvaluationPeriods: 1 MetricName: Throttles Namespace: AWS/Lambda Period: 60 Statistic: Sum Threshold: 1.0 AuthorizerFunctionThrottlingAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmActions: - !Ref AlarmsTopic ComparisonOperator: GreaterThanOrEqualToThreshold Dimensions: - Name: FunctionName Value: !Ref AuthorizerFunction EvaluationPeriods: 1 MetricName: Throttles Namespace: AWS/Lambda Period: 60 Statistic: Sum Threshold: 1.0 LocationsDynamoDBThrottlingAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmActions: - !Ref AlarmsTopic ComparisonOperator: GreaterThanOrEqualToThreshold Dimensions: - Name: TableName Value: !Ref LocationsTable EvaluationPeriods: 1 MetricName: ThrottledRequests Namespace: AWS/DynamoDB Period: 60 Statistic: Sum Threshold: 1.0 ResourcesDynamoDBThrottlingAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmActions: - !Ref AlarmsTopic ComparisonOperator: GreaterThanOrEqualToThreshold Dimensions: - Name: TableName Value: !Ref ResourcesTable EvaluationPeriods: 1 MetricName: ThrottledRequests Namespace: AWS/DynamoDB Period: 60 Statistic: Sum Threshold: 1.0 BookingsDynamoDBThrottlingAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmActions: - !Ref AlarmsTopic ComparisonOperator: GreaterThanOrEqualToThreshold Dimensions: - Name: TableName Value: !Ref BookingsTable EvaluationPeriods: 1 MetricName: ThrottledRequests Namespace: AWS/DynamoDB Period: 60 Statistic: Sum Threshold: 1.0 ApplicationDashboard: Type: AWS::CloudWatch::Dashboard Properties: DashboardName: !Sub "${AWS::StackName}-dashboard" DashboardBody: Fn::Sub: > { "widgets": [ { "height": 6, "width": 6, "y": 12, "x": 0, "type": "metric", "properties": { "metrics": [ [ "AWS/Lambda", "Invocations", "FunctionName", "${LocationsFunction}" ], [ ".", "Errors", ".", "." ], [ ".", "Throttles", ".", "." ], [ ".", "Duration", ".", ".", { "stat": "Average" } ], [ ".", "ConcurrentExecutions", ".", ".", { "stat": "Maximum" } ] ], "view": "timeSeries", "region": "${AWS::Region}", "stacked": false, "title": "Locations Lambda", "period": 60, "stat": "Sum" } }, { "height": 6, "width": 6, "y": 18, "x": 0, "type": "metric", "properties": { "metrics": [ [ "AWS/Lambda", "Invocations", "FunctionName", "${ResourcesFunction}" ], [ ".", "Errors", ".", "." ], [ ".", "Throttles", ".", "." ], [ ".", "Duration", ".", ".", { "stat": "Average" } ], [ ".", "ConcurrentExecutions", ".", ".", { "stat": "Maximum" } ] ], "view": "timeSeries", "region": "${AWS::Region}", "stacked": false, "title": "Resources Lambda", "period": 60, "stat": "Sum" } }, { "height": 6, "width": 6, "y": 24, "x": 0, "type": "metric", "properties": { "metrics": [ [ "AWS/Lambda", "Invocations", "FunctionName", "${BookingsFunction}" ], [ ".", "Errors", ".", "." ], [ ".", "Throttles", ".", "." ], [ ".", "Duration", ".", ".", { "stat": "Average" } ], [ ".", "ConcurrentExecutions", ".", ".", { "stat": "Maximum" } ] ], "view": "timeSeries", "region": "${AWS::Region}", "stacked": false, "title": "Bookings Lambda", "period": 60, "stat": "Sum" } }, { "height": 6, "width": 6, "y": 6, "x": 6, "type": "metric", "properties": { "metrics": [ [ "AWS/Lambda", "Invocations", "FunctionName", "${AuthorizerFunction}" ], [ ".", "Errors", ".", "." ], [ ".", "Throttles", ".", "." ], [ ".", "Duration", ".", ".", { "stat": "Average" } ], [ ".", "ConcurrentExecutions", ".", ".", { "stat": "Maximum" } ] ], "view": "timeSeries", "region": "${AWS::Region}", "stacked": false, "title": "Authorizer Lambda", "period": 60, "stat": "Sum" } }, { "height": 6, "width": 6, "y": 12, "x": 6, "type": "metric", "properties": { "metrics": [ [ "AWS/DynamoDB", "ConsumedReadCapacityUnits", "TableName", "${LocationsTable}", { "stat": "Maximum" } ], [ ".", "ConsumedWriteCapacityUnits", ".", ".", { "stat": "Maximum" } ], [ ".", "ProvisionedReadCapacityUnits", ".", ".", { "period": 300 } ], [ ".", "ProvisionedWriteCapacityUnits", ".", ".", { "period": 300 } ] ], "view": "timeSeries", "stacked": false, "region": "${AWS::Region}", "title": "DynamoDB - Locations", "period": 60, "stat": "Average" } }, { "height": 6, "width": 6, "y": 18, "x": 6, "type": "metric", "properties": { "metrics": [ [ "AWS/DynamoDB", "ConsumedReadCapacityUnits", "TableName", "${ResourcesTable}", { "stat": "Maximum" } ], [ ".", "ConsumedWriteCapacityUnits", ".", ".", { "stat": "Maximum" } ], [ ".", "ProvisionedReadCapacityUnits", ".", ".", { "period": 300 } ], [ ".", "ProvisionedWriteCapacityUnits", ".", ".", { "period": 300 } ] ], "view": "timeSeries", "stacked": false, "region": "${AWS::Region}", "title": "DynamoDB - Resources", "period": 60, "stat": "Average" } }, { "height": 6, "width": 6, "y": 24, "x": 6, "type": "metric", "properties": { "metrics": [ [ "AWS/DynamoDB", "ConsumedReadCapacityUnits", "TableName", "${BookingsTable}", { "period": 60, "stat": "Maximum" } ], [ ".", "ConsumedWriteCapacityUnits", ".", ".", { "period": 60, "stat": "Maximum" } ], [ ".", "ProvisionedReadCapacityUnits", ".", "." ], [ ".", "ProvisionedWriteCapacityUnits", ".", "." ] ], "view": "timeSeries", "stacked": false, "region": "${AWS::Region}", "title": "DynamoDB - Bookings", "period": 300, "stat": "Average" } }, { "height": 6, "width": 6, "y": 6, "x": 0, "type": "metric", "properties": { "metrics": [ [ "AWS/ApiGateway", "4xx", "ApiId", "${HttpApi}", { "yAxis": "right" } ], [ ".", "5xx", ".", ".", { "yAxis": "right" } ], [ ".", "DataProcessed", ".", ".", { "yAxis": "left" } ], [ ".", "Count", ".", ".", { "label": "Count", "yAxis": "right" } ], [ ".", "IntegrationLatency", ".", ".", { "stat": "Average" } ], [ ".", "Latency", ".", ".", { "stat": "Average" } ] ], "view": "timeSeries", "stacked": false, "region": "${AWS::Region}", "period": 60, "stat": "Sum", "title": "API Gateway" } }, { "height": 6, "width": 12, "y": 0, "x": 0, "type": "metric", "properties": { "metrics": [ [ "${AWS::StackName}", "ProcessedBookings", "ServiceName", "${BookingsFunction}", "LogGroup", "${BookingsFunction}", "ServiceType", "AWS::Lambda::Function", "Service", "Bookings", { "label": "Processed Bookings"} ], [ ".", "ProcessedLocations", ".", "${LocationsFunction}", ".", "${LocationsFunction}", ".", ".", ".", "Locations", { "label": "Processed Locations"} ], [ ".", "ProcessedResources", ".", "${ResourcesFunction}", ".", "${ResourcesFunction}", ".", ".", ".", "Resources", { "label": "Processed Resources"} ] ], "view": "timeSeries", "stacked": false, "title": "Business Metrics", "region": "${AWS::Region}", "period": 60, "stat": "Sum" } } ] } Outputs: APIEndpoint: Description: "API Gateway endpoint URL" Value: !Sub "https://${HttpApi}.execute-api.${AWS::Region}.amazonaws.com/" DashboardURL: Description: "Dashboard URL" Value: !Sub "https://console.aws.amazon.com/cloudwatch/home?region=${AWS::Region}#dashboards:name=${ApplicationDashboard}" AlarmsTopic: Description: "SNS Topic to be used for the alarms subscriptions" Value: !Ref AlarmsTopic AccessLogs: Description: "CloudWatch Logs group for API Gateway access logs" Value: !Ref AccessLogs LocationsTable: Description: "DynamoDB Locations table" Value: !Ref LocationsTable ResourcesTable: Description: "DynamoDB Resources table" Value: !Ref ResourcesTable BookingsTable: Description: "DynamoDB Bookings table" Value: !Ref BookingsTable