# 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 AppSync API Parameters: CognitoStackName: Description: An environment name for Cognito stack Type: String Default: serverless-api-cognito UserPoolAdminGroupName: Description: 'User pool group name for API administrators, leave default value unless you used different one in the shared Cognito stack. NOTE: If you need to change this parameter value, update ./src/mapping_templates/delete_booking_request.vtl accordingly' Type: String Default: apiAdmins Resources: GraphQLApi: Type: "AWS::AppSync::GraphQLApi" Properties: Name: !Sub "GraphQLBackendAPI-${AWS::StackName}" AuthenticationType: "AMAZON_COGNITO_USER_POOLS" UserPoolConfig: UserPoolId: Fn::ImportValue: !Sub "${CognitoStackName}-UserPool" AwsRegion: !Ref "AWS::Region" DefaultAction: "ALLOW" XrayEnabled: true LogConfig: ExcludeVerboseContent: false FieldLogLevel: "ALL" CloudWatchLogsRoleArn: !GetAtt PushToCloudWatchLogsRole.Arn Tags: - Key: "Stack" Value: !Sub "${AWS::StackName}" AppSyncLogsKMSKey: Type: AWS::KMS::Key Properties: Description: CMK for AppSync logs Enabled: true EnableKeyRotation: True KeyPolicy: Version: "2012-10-17" Statement: - Effect: Allow Principal: AWS: - !Sub "arn:aws:iam::${AWS::AccountId}:root" Action: - "kms:*" Resource: "*" - Effect: Allow Principal: Service: - !Sub logs.${AWS::Region}.amazonaws.com Action: - "kms:Encrypt*" - "kms:Decrypt*" - "kms:ReEncrypt*" - "kms:GenerateDataKey*" - "kms:Describe*" Resource: "*" Condition: ArnEquals: kms:EncryptionContext:aws:logs:arn: !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/appsync/apis/${GraphQLApi.ApiId}" KeySpec: SYMMETRIC_DEFAULT KeyUsage: ENCRYPT_DECRYPT Tags: - Key: "Stack" Value: !Sub "${AWS::StackName}" GraphQLApiLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub "/aws/appsync/apis/${GraphQLApi.ApiId}" RetentionInDays: 7 KmsKeyId: !GetAtt AppSyncLogsKMSKey.Arn Tags: - Key: "Stack" Value: !Sub "${AWS::StackName}" PushToCloudWatchLogsRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Principal: Service: - appsync.amazonaws.com Action: - sts:AssumeRole Path: "/" Tags: - Key: "Stack" Value: !Sub "${AWS::StackName}" PushToCloudWatchLogsRolePolicy: Type: AWS::IAM::Policy Properties: PolicyName: !Sub ${AWS::StackName}-PushToCloudWatchLogs-Policy PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Resource: !GetAtt GraphQLApiLogGroup.Arn Roles: - Ref: PushToCloudWatchLogsRole GrapgQLSchema: Type: "AWS::AppSync::GraphQLSchema" Properties: ApiId: !GetAtt GraphQLApi.ApiId Definition: !Sub - | schema { query: Query mutation: Mutation subscription: Subscription } type Location { locationid: ID! name: String! description: String imageUrl: String resources: [Resource] timestamp: String } type Resource { resourceid: ID! locationid: ID! name: String! description: String type: String bookings: [Booking] timestamp: String } type Booking { bookingid: ID! resourceid: ID! userid: ID! starttimeepochtime: Float timestamp: String } type Mutation { createLocation(locationid: ID, name: String!, description: String, imageUrl: String): Location @aws_auth(cognito_groups: ["${AdminGroupName}"]) deleteLocation(locationid: ID!): ID @aws_auth(cognito_groups: ["${AdminGroupName}"]) createResource(resourceid: ID, locationid: ID!, name: String!, description: String, type: String): Resource @aws_auth(cognito_groups: ["${AdminGroupName}"]) deleteResource(resourceid: ID!): ID @aws_auth(cognito_groups: ["${AdminGroupName}"]) createBooking(bookingid: ID, resourceid: ID!, starttimeepochtime: Float): Booking deleteBooking(bookingid: ID!): ID } type Query { getAllLocations: [Location] getLocation(locationid: ID!): Location getResource(resourceid: ID!): Resource getBooking(bookingid: ID!): Booking getMyBookings: [Booking] } type Subscription { locationCreated: Location @aws_subscribe(mutations: ["createLocation"]) resourceCreated(resourceid: ID): Resource @aws_subscribe(mutations: ["createResource"]) bookingCreated(resourceid: ID): Booking @aws_subscribe(mutations: ["createBooking"]) } - AdminGroupName: !Sub "${UserPoolAdminGroupName}" CreateLocationMutationResolver: Type: "AWS::AppSync::Resolver" Properties: ApiId: !GetAtt GraphQLApi.ApiId TypeName: "Mutation" FieldName: "createLocation" DataSourceName: !GetAtt LocationsDataSource.Name RequestMappingTemplateS3Location: ./src/mapping_templates/create_location_request.vtl ResponseMappingTemplateS3Location: ./src/mapping_templates/create_location_response.vtl DeleteLocationMutationResolver: Type: "AWS::AppSync::Resolver" Properties: ApiId: !GetAtt GraphQLApi.ApiId TypeName: "Mutation" FieldName: "deleteLocation" DataSourceName: !GetAtt LocationsDataSource.Name RequestMappingTemplateS3Location: ./src/mapping_templates/delete_location_request.vtl ResponseMappingTemplateS3Location: ./src/mapping_templates/delete_location_response.vtl SingleLocationResolver: Type: "AWS::AppSync::Resolver" Properties: ApiId: !GetAtt GraphQLApi.ApiId TypeName: "Query" FieldName: "getLocation" DataSourceName: !GetAtt LocationsDataSource.Name RequestMappingTemplateS3Location: ./src/mapping_templates/get_location_request.vtl ResponseMappingTemplateS3Location: ./src/mapping_templates/get_location_response.vtl AllLocationsResolver: Type: "AWS::AppSync::Resolver" Properties: ApiId: !GetAtt GraphQLApi.ApiId TypeName: "Query" FieldName: "getAllLocations" DataSourceName: !GetAtt LocationsDataSource.Name RequestMappingTemplateS3Location: ./src/mapping_templates/get_locations_request.vtl ResponseMappingTemplateS3Location: ./src/mapping_templates/get_locations_response.vtl CreateResourceMutationResolver: Type: "AWS::AppSync::Resolver" Properties: ApiId: !GetAtt GraphQLApi.ApiId TypeName: "Mutation" FieldName: "createResource" DataSourceName: !GetAtt ResourcesDataSource.Name RequestMappingTemplateS3Location: ./src/mapping_templates/create_resource_request.vtl ResponseMappingTemplateS3Location: ./src/mapping_templates/create_resource_response.vtl DeleteResourceMutationResolver: Type: "AWS::AppSync::Resolver" Properties: ApiId: !GetAtt GraphQLApi.ApiId TypeName: "Mutation" FieldName: "deleteResource" DataSourceName: !GetAtt ResourcesDataSource.Name RequestMappingTemplateS3Location: ./src/mapping_templates/delete_resource_request.vtl ResponseMappingTemplateS3Location: ./src/mapping_templates/delete_resource_response.vtl SingleResourceResolver: Type: "AWS::AppSync::Resolver" Properties: ApiId: !GetAtt GraphQLApi.ApiId TypeName: "Query" FieldName: "getResource" DataSourceName: !GetAtt ResourcesDataSource.Name RequestMappingTemplateS3Location: ./src/mapping_templates/get_resource_request.vtl ResponseMappingTemplateS3Location: ./src/mapping_templates/get_resource_response.vtl ResourcesForLocationResolver: Type: "AWS::AppSync::Resolver" Properties: ApiId: !GetAtt GraphQLApi.ApiId TypeName: "Location" FieldName: "resources" DataSourceName: !GetAtt ResourcesDataSource.Name RequestMappingTemplateS3Location: ./src/mapping_templates/get_resources_for_location_request.vtl ResponseMappingTemplateS3Location: ./src/mapping_templates/get_resources_for_location_response.vtl CreateBookingMutationResolver: Type: "AWS::AppSync::Resolver" Properties: ApiId: !GetAtt GraphQLApi.ApiId TypeName: "Mutation" FieldName: "createBooking" DataSourceName: !GetAtt BookingsDataSource.Name RequestMappingTemplateS3Location: ./src/mapping_templates/create_booking_request.vtl ResponseMappingTemplateS3Location: ./src/mapping_templates/create_booking_response.vtl DeleteBookingMutationResolver: Type: "AWS::AppSync::Resolver" Properties: ApiId: !GetAtt GraphQLApi.ApiId TypeName: "Mutation" FieldName: "deleteBooking" DataSourceName: !GetAtt BookingsDataSource.Name RequestMappingTemplateS3Location: ./src/mapping_templates/delete_booking_request.vtl ResponseMappingTemplateS3Location: ./src/mapping_templates/delete_booking_response.vtl SingleBookingResolver: Type: "AWS::AppSync::Resolver" Properties: ApiId: !GetAtt GraphQLApi.ApiId TypeName: "Query" FieldName: "getBooking" DataSourceName: !GetAtt BookingsDataSource.Name RequestMappingTemplateS3Location: ./src/mapping_templates/get_booking_request.vtl ResponseMappingTemplateS3Location: ./src/mapping_templates/get_booking_response.vtl BookingsForResourceResolver: Type: "AWS::AppSync::Resolver" Properties: ApiId: !GetAtt GraphQLApi.ApiId TypeName: "Resource" FieldName: "bookings" DataSourceName: !GetAtt BookingsDataSource.Name RequestMappingTemplateS3Location: ./src/mapping_templates/get_bookings_for_resource_request.vtl ResponseMappingTemplateS3Location: ./src/mapping_templates/get_bookings_for_resource_response.vtl BookingsForUserResolver: Type: "AWS::AppSync::Resolver" Properties: ApiId: !GetAtt GraphQLApi.ApiId TypeName: "Query" FieldName: "getMyBookings" DataSourceName: !GetAtt BookingsDataSource.Name RequestMappingTemplateS3Location: ./src/mapping_templates/get_bookings_for_user_request.vtl ResponseMappingTemplateS3Location: ./src/mapping_templates/get_bookings_for_user_response.vtl AppSyncServiceRole: Type: "AWS::IAM::Role" Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Principal: Service: - "appsync.amazonaws.com" Action: - "sts:AssumeRole" Path: "/" Tags: - Key: "Stack" Value: !Sub "${AWS::StackName}" LocationsDataSource: Type: "AWS::AppSync::DataSource" Properties: Type: "AMAZON_DYNAMODB" ApiId: !GetAtt GraphQLApi.ApiId Name: "LocationsDataSource" ServiceRoleArn: !GetAtt AppSyncServiceRole.Arn DynamoDBConfig: TableName: !Ref LocationsTable AwsRegion: !Ref "AWS::Region" ResourcesDataSource: Type: "AWS::AppSync::DataSource" Properties: Type: "AMAZON_DYNAMODB" ApiId: !GetAtt GraphQLApi.ApiId Name: "ResourcesDataSource" ServiceRoleArn: !GetAtt AppSyncServiceRole.Arn DynamoDBConfig: TableName: !Ref ResourcesTable AwsRegion: !Ref "AWS::Region" BookingsDataSource: Type: "AWS::AppSync::DataSource" Properties: Type: "AMAZON_DYNAMODB" ApiId: !GetAtt GraphQLApi.ApiId Name: "BookingsDataSource" ServiceRoleArn: !GetAtt AppSyncServiceRole.Arn DynamoDBConfig: TableName: !Ref BookingsTable AwsRegion: !Ref "AWS::Region" DynamoDBAccessPolicy: Type: "AWS::IAM::Policy" Properties: PolicyName: "dynamodb-access" PolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Action: - "dynamodb:BatchGet*" - "dynamodb:DescribeStream" - "dynamodb:DescribeTable" - "dynamodb:Get*" - "dynamodb:Query" - "dynamodb:Scan" - "dynamodb:BatchWrite*" - "dynamodb:CreateTable" - "dynamodb:Delete*" - "dynamodb:Update*" - "dynamodb:PutItem" - "dynamodb:ConditionCheckItem" Resource: - !GetAtt LocationsTable.Arn - !GetAtt ResourcesTable.Arn - !GetAtt BookingsTable.Arn - !Sub "${ResourcesTable.Arn}/index/locationidGSI" - !Sub "${BookingsTable.Arn}/index/useridGSI" - !Sub "${BookingsTable.Arn}/index/bookingsByUserByTimeGSI" - !Sub "${BookingsTable.Arn}/index/bookingsByResourceByTimeGSI" Roles: - Ref: "AppSyncServiceRole" 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: BillingMode: PAY_PER_REQUEST PointInTimeRecoverySpecification: PointInTimeRecoveryEnabled: True SSESpecification: KMSMasterKeyId: alias/aws/dynamodb SSEEnabled: True SSEType: KMS AttributeDefinitions: - AttributeName: resourceid AttributeType: S - AttributeName: locationid AttributeType: S KeySchema: - AttributeName: resourceid KeyType: HASH GlobalSecondaryIndexes: - IndexName: locationidGSI KeySchema: - AttributeName: locationid KeyType: HASH Projection: ProjectionType: ALL Tags: - Key: "Stack" Value: !Sub "${AWS::StackName}" BookingsTable: Type: AWS::DynamoDB::Table Properties: BillingMode: PAY_PER_REQUEST PointInTimeRecoverySpecification: PointInTimeRecoveryEnabled: True SSESpecification: KMSMasterKeyId: alias/aws/dynamodb SSEEnabled: True SSEType: KMS AttributeDefinitions: - AttributeName: bookingid AttributeType: S - AttributeName: userid AttributeType: S - AttributeName: resourceid AttributeType: S - AttributeName: starttimeepochtime AttributeType: N KeySchema: - AttributeName: bookingid KeyType: HASH GlobalSecondaryIndexes: - IndexName: useridGSI KeySchema: - AttributeName: userid KeyType: HASH Projection: ProjectionType: ALL - IndexName: bookingsByUserByTimeGSI KeySchema: - AttributeName: userid KeyType: HASH - AttributeName: starttimeepochtime KeyType: RANGE Projection: ProjectionType: ALL - IndexName: bookingsByResourceByTimeGSI KeySchema: - AttributeName: resourceid KeyType: HASH - AttributeName: starttimeepochtime KeyType: RANGE Projection: ProjectionType: ALL 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}" ApiErrorsAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmActions: - !Ref AlarmsTopic ComparisonOperator: GreaterThanOrEqualToThreshold Dimensions: - Name: GraphQLAPIId Value: !GetAtt GraphQLApi.ApiId EvaluationPeriods: 1 MetricName: 5XXError Namespace: AWS/AppSync 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": 18, "y": 0, "x": 0, "type": "metric", "properties": { "metrics": [ [ "AWS/AppSync", "4xx", "GraphQLAPIId", "${GraphQLApi.ApiId}", { "yAxis": "right" } ], [ ".", "5xx", ".", ".", { "yAxis": "right" } ], [ ".", "Latency", ".", ".", { "stat": "Average" } ] ], "view": "timeSeries", "stacked": false, "region": "${AWS::Region}", "period": 60, "stat": "Sum", "title": "AppSync" } }, { "height": 6, "width": 6, "y": 6, "x": 0, "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": 6, "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": 6, "x": 12, "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" } } ] } Outputs: APIEndpoint: Description: "GraphQL API endpoint URL" Value: !GetAtt GraphQLApi.GraphQLUrl 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 AppSyncLogs: Description: "CloudWatch Logs group for AppSync logs" Value: !Ref GraphQLApiLogGroup LocationsTable: Description: "DynamoDB Locations table" Value: !Ref LocationsTable ResourcesTable: Description: "DynamoDB Resources table" Value: !Ref ResourcesTable BookingsTable: Description: "DynamoDB Bookings table" Value: !Ref BookingsTable