mirror of
https://github.com/brettstack/civ6-play-by-cloud-turn-notifier.git
synced 2026-04-03 03:00:02 -04:00
786 lines
27 KiB
YAML
786 lines
27 KiB
YAML
service: civ6-pbc${{env:SERVERLESS_SERVICE_SUFFIX, ''}}
|
|
provider:
|
|
name: aws
|
|
stackName: ${{self:service}}-${{self:provider.stage}}
|
|
runtime: nodejs12.x
|
|
memorySize: 128
|
|
timeout: 20
|
|
logRetentionInDays: ${{self:custom.stageConfig.logRetentionInDays}}
|
|
stage: ${{opt:stage, env:NODE_ENV, 'development'}}
|
|
profile: ${{self:custom.stageConfig.profile}}
|
|
region: us-west-2
|
|
# Access serverless variables with ${{self:custom.example}} so it doesn't conflict with AWS ${AWS::Example}
|
|
variableSyntax: "\\${{([ ~:a-zA-Z0-9._@\\'\",\\-\\/\\(\\)]+?)}}"
|
|
logs:
|
|
restApi:
|
|
format: '{ "requestId":"$context.requestId", "ip": "$context.identity.sourceIp", "caller":"$context.identity.caller", "user":"$context.identity.user", "requestTime":"$context.requestTime", "httpMethod":"$context.httpMethod", "resourcePath":"$context.resourcePath", "status":"$context.status", "protocol":"$context.protocol", "responseLength":"$context.responseLength" }'
|
|
level: INFO # TODO: add custom field for setting this; default to ERROR for prod
|
|
environment:
|
|
USER_TABLE: !Ref UserTable
|
|
GAME_TABLE: !Ref GameTable
|
|
MAIN_TABLE: !Ref MainTable
|
|
|
|
# Enable connection reuse for AWS SDK for instant performance boost
|
|
# https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/node-reusing-connections.html
|
|
AWS_NODEJS_CONNECTION_REUSE_ENABLED: 1
|
|
cors: true
|
|
|
|
package:
|
|
individually: true
|
|
|
|
plugins:
|
|
- serverless-dotenv-plugin
|
|
- serverless-domain-manager
|
|
- serverless-prune-plugin
|
|
- serverless-plugin-tracing
|
|
- serverless-iam-roles-per-function
|
|
- serverless-webpack
|
|
- serverless-apigateway-service-proxy
|
|
- serverless-stack-output
|
|
- serverless-cloudside-plugin
|
|
- serverless-plugin-aws-alerts
|
|
- serverless-plugin-metric
|
|
- '@brettstack/serverless-amplify-plugin'
|
|
- serverless-offline
|
|
- serverless-stack-termination-protection
|
|
|
|
custom:
|
|
stages:
|
|
development:
|
|
profile: civ6-pbc_dev
|
|
logRetentionInDays: 1
|
|
amplify:
|
|
api:
|
|
domainEnabled: false
|
|
alarms:
|
|
notificationEmail: ${{env:ALARMS_NOTIFICATION_EMAIL}}
|
|
staging:
|
|
profile: civ6-pbc_staging
|
|
logRetentionInDays: 3
|
|
isDomainRoute53: false
|
|
api:
|
|
domainEnabled: true
|
|
domainName: staging.api.civ.halfstack.software
|
|
# validationDomain: halfstack.software
|
|
amplify:
|
|
domainName: staging.civ.halfstack.software
|
|
alarms:
|
|
notificationEmail: alerts@halfstack.software
|
|
production:
|
|
profile: civ6-pbc_prod
|
|
logRetentionInDays: 14
|
|
# retainDataResources: true
|
|
pointInTimeRecoveryEnabled: true
|
|
isDomainRoute53: false
|
|
api:
|
|
domainEnabled: true
|
|
domainName: api.civ.halfstack.software
|
|
# validationDomain: halfstack.software
|
|
amplify:
|
|
domainName: civ.halfstack.software
|
|
alarms:
|
|
notificationEmail: alerts@halfstack.software
|
|
stageConfig: ${{self:custom.stages.${{self:provider.stage}}}}
|
|
prune:
|
|
automatic: true
|
|
number: 10
|
|
customDomain:
|
|
domainName: ${{self:custom.stageConfig.api.domainName, ''}}
|
|
certificateName: ${{self:custom.stageConfig.api.domainName, ''}}
|
|
# enabled: false
|
|
enabled: ${{self:custom.stageConfig.api.domainEnabled, false}}
|
|
autoDomain: true #${{self:custom.stageConfig.api.domainEnabled, false}}
|
|
createRoute53Record: ${{self:custom.stageConfig.api.isDomainRoute53, false}}
|
|
serverless-offline:
|
|
httpPort: 4911
|
|
noPrependStageInUrl: true
|
|
amplify:
|
|
isManual: true
|
|
domainName: ${{self:custom.stageConfig.amplify.domainName, ''}}
|
|
buildSpecValues:
|
|
artifactBaseDirectory: packages/ui/build
|
|
preBuildWorkingDirectory: packages/ui
|
|
buildCommandEnvVars:
|
|
prefix: 'REACT_APP_'
|
|
allow:
|
|
- ApiEndpoint
|
|
- CognitoIdentityPoolId
|
|
- CognitoUserPoolId
|
|
- CognitoUserPoolClientId
|
|
# dotenv:
|
|
# path: ../../.env
|
|
# amplify:
|
|
# accessTokenSecretName: AmplifyGithub3
|
|
# repository: https://github.com/brettstack/civ6-play-by-cloud-turn-notifier
|
|
# branch: ${{self:custom.stageConfig.branch}}
|
|
# domainName: ${{self:custom.stageConfig.domainName, ''}}
|
|
# buildSpecValues:
|
|
# artifactBaseDirectory: packages/ui/build
|
|
# preBuildWorkingDirectory: packages/ui
|
|
# # TODO: update amplify-plugin to accept custom buildSpecValues.preBuild command so we don't have to set the entire buildSpec
|
|
# buildSpec: |-
|
|
# version: 0.1
|
|
# frontend:
|
|
# phases:
|
|
# preBuild:
|
|
# commands:
|
|
# - npm ci
|
|
# - cd packages/ui
|
|
# - npm ci
|
|
# build:
|
|
# commands:
|
|
# - npm run build
|
|
# artifacts:
|
|
# baseDirectory: packages/ui/build
|
|
# files:
|
|
# - '**/*'
|
|
# cache:
|
|
# paths:
|
|
# - node_modules/**/*
|
|
webpack:
|
|
webpackConfig: ./functions.webpack.config.js
|
|
excludeFiles: packages/**/*.test.[t|j]s
|
|
|
|
output:
|
|
file: ./stack-outputs.${{self:provider.stage}}.json
|
|
alerts:
|
|
dashboards: false
|
|
nameTemplate: $[functionName]-$[metricName]-Alarm
|
|
topics:
|
|
alarm:
|
|
topic: ${{self:service}}-${{self:provider.stage}}-alarm
|
|
notifications:
|
|
- protocol: email
|
|
endpoint: ${{self:custom.stageConfig.alarms.notificationEmail}}
|
|
# TODO: Add short and long alarms for each
|
|
alarms:
|
|
# - functionThrottles
|
|
- functionErrors
|
|
- functionInvocations
|
|
- functionDuration
|
|
definitions:
|
|
functionDuration:
|
|
threshold: 2000
|
|
evaluationPeriods: 5
|
|
datapointsToAlarm: 5
|
|
statistic: p95
|
|
functionErrors:
|
|
threshold: 2
|
|
period: 300
|
|
|
|
metrics:
|
|
- name: SKIP_INACTIVE
|
|
pattern: '{ $.message = "PROCESS_MESSAGE:SKIP_INACTIVE" }'
|
|
functions: [webhook]
|
|
namespace: ${{self:service}}
|
|
- name: GAME_MARKED_INACTIVE
|
|
pattern: '{ $.message = "GAME_CONTROLLER:GAME_MARKED_INACTIVE" }'
|
|
functions: [webhook]
|
|
namespace: ${{self:service}}
|
|
- name: NOTIFICATION_SENT
|
|
pattern: '{ $.message = "PROCESS_MESSAGE:NOTIFICATION_SENT" }'
|
|
functions: [webhook]
|
|
namespace: ${{self:service}}
|
|
- name: GAME_CREATED
|
|
pattern: '{ $.message = "GAME_CONTROLLER:GAME_CREATED" }'
|
|
functions: [express]
|
|
namespace: ${{self:service}}
|
|
|
|
serverlessTerminationProtection:
|
|
stages:
|
|
- staging
|
|
- production
|
|
|
|
apiGatewayServiceProxies:
|
|
- sqs:
|
|
path: /webhook
|
|
method: post
|
|
queueName: !GetAtt WebhookSqsQueue.QueueName
|
|
requestParameters:
|
|
integration.request.querystring.MessageAttribute.1.Name: "'gameId'"
|
|
integration.request.querystring.MessageAttribute.1.Value.StringValue: method.request.querystring.gameId
|
|
integration.request.querystring.MessageAttribute.1.Value.DataType: "'String'"
|
|
functions:
|
|
webhook:
|
|
# NOTE: Lambda generally uses 5 pollers at low load and it's recommended to set reservedConcurrency to at least 5
|
|
# https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html#events-sqs-scaling
|
|
# https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html#events-sqs-eventsource
|
|
reservedConcurrency: 5
|
|
memorySize: 512
|
|
handler: packages/api/functions/webhook/lambda.webhookHandlerMiddy
|
|
layers:
|
|
- !Sub "arn:aws:lambda:${AWS::Region}:580247275435:layer:LambdaInsightsExtension:12"
|
|
events:
|
|
- sqs:
|
|
arn: !GetAtt WebhookSqsQueue.Arn
|
|
batchSize: 100
|
|
maximumBatchingWindow: 20
|
|
iamRoleStatements:
|
|
- Effect: Allow
|
|
Action:
|
|
- sqs:SendMessage
|
|
Resource: !GetAtt WebhookSqsQueueDlq.Arn
|
|
- Effect: "Allow"
|
|
Action:
|
|
- dynamodb:GetItem
|
|
- dynamodb:UpdateItem
|
|
Resource:
|
|
- !GetAtt GameTable.Arn
|
|
- !GetAtt UserTable.Arn
|
|
|
|
express:
|
|
handler: packages/api/functions/express/lambda.handler
|
|
memorySize: 1024
|
|
layers:
|
|
- !Sub "arn:aws:lambda:${AWS::Region}:580247275435:layer:LambdaInsightsExtension:12"
|
|
events:
|
|
- http:
|
|
method: ANY
|
|
path: /
|
|
authorizer:
|
|
type: COGNITO_USER_POOLS
|
|
authorizerId: !Ref ApiGatewayAuthorizer
|
|
- http:
|
|
method: ANY
|
|
path: '{proxy+}'
|
|
authorizer:
|
|
type: COGNITO_USER_POOLS
|
|
authorizerId: !Ref ApiGatewayAuthorizer
|
|
iamRoleStatements:
|
|
- Effect: "Allow"
|
|
Action:
|
|
- "xray:PutTraceSegments"
|
|
- "xray:PutTelemetryRecords"
|
|
Resource:
|
|
- "*"
|
|
- Effect: "Allow"
|
|
Action:
|
|
- dynamodb:BatchGetItem
|
|
- dynamodb:BatchWriteItem
|
|
- dynamodb:DeleteItem
|
|
- dynamodb:GetItem
|
|
- dynamodb:PutItem
|
|
- dynamodb:Query
|
|
- dynamodb:Scan
|
|
- dynamodb:UpdateItem
|
|
Resource:
|
|
- !GetAtt GameTable.Arn
|
|
- !GetAtt UserTable.Arn
|
|
|
|
autoConfirmUser:
|
|
handler: packages/api/functions/cognito/auto-confirm-user.handler
|
|
|
|
postAuthN:
|
|
handler: packages/api/functions/cognito/post-authentication.handler
|
|
iamRoleStatements:
|
|
- Effect: "Allow"
|
|
Action:
|
|
- dynamodb:GetItem
|
|
- dynamodb:PutItem
|
|
Resource:
|
|
- !GetAtt UserTable.Arn
|
|
|
|
resources:
|
|
Conditions:
|
|
IsApiCustomDomainEnabled:
|
|
!Equals
|
|
- ${{self:custom.stageConfig.api.domainEnabled, false}}
|
|
- true
|
|
# RetainDataResources:
|
|
# !Equals
|
|
# - ${{self:custom.stageConfig.retainDataResources, false}}
|
|
# - true
|
|
|
|
Resources:
|
|
WebhookSqsQueue:
|
|
Type: AWS::SQS::Queue
|
|
Properties:
|
|
# NOTE: Following guidance here to reduce the chance of Lambda throttling
|
|
# https://medium.com/@zaccharles/lambda-concurrency-limits-and-sqs-triggers-dont-mix-well-sometimes-eb23d90122e0
|
|
# https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html#events-sqs-queueconfig
|
|
# https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html#events-sqs-eventsource
|
|
# functions.webhook.timeout * 6 + functions.webhook.events.sqs.maximumBatchingWindow
|
|
# 20 * 6 + 20
|
|
VisibilityTimeout: 140
|
|
RedrivePolicy:
|
|
deadLetterTargetArn: !GetAtt WebhookSqsQueueDlq.Arn
|
|
maxReceiveCount: 5
|
|
|
|
WebhookSqsQueueDlq:
|
|
Type: AWS::SQS::Queue
|
|
|
|
ApiGatewayMethodWebhookPost:
|
|
Type: AWS::ApiGateway::Method
|
|
Properties:
|
|
RequestParameters:
|
|
method.request.querystring.gameId: true
|
|
Integration:
|
|
IntegrationResponses:
|
|
- StatusCode: 200
|
|
ResponseTemplates:
|
|
application/json: '{}'
|
|
|
|
# NOTE: This only works when deploying to us-east-1 since ACM certs for EDGE APIs
|
|
# must exist in us-east-1 due to being associated with a CloudFront Distribution.
|
|
# Manually create cert in us-east-1 instead.
|
|
# AcmCertificate:
|
|
# Type: AWS::CertificateManager::Certificate
|
|
# Condition: IsApiCustomDomainEnabled
|
|
# Properties:
|
|
# DomainName: ${{self:custom.stageConfig.api.domainName}}
|
|
# DomainValidationOptions:
|
|
# - DomainName: ${{self:custom.stageConfig.api.domainName}}
|
|
# ValidationDomain: ${{self:custom.stageConfig.api.validationDomain, ''}}
|
|
|
|
MainTable:
|
|
Type: AWS::DynamoDB::Table
|
|
# DeletionPolicy: !If [RetainDataResources, 'Retain', 'Delete']
|
|
Properties:
|
|
BillingMode: PAY_PER_REQUEST
|
|
PointInTimeRecoverySpecification:
|
|
PointInTimeRecoveryEnabled: ${{self:custom.stageConfig.pointInTimeRecoveryEnabled, false}}
|
|
KeySchema:
|
|
- KeyType: HASH
|
|
AttributeName: pk
|
|
- KeyType: RANGE
|
|
AttributeName: sk
|
|
AttributeDefinitions:
|
|
- AttributeName: pk
|
|
AttributeType: S
|
|
- AttributeName: sk
|
|
AttributeType: S
|
|
- AttributeName: data
|
|
AttributeType: S
|
|
GlobalSecondaryIndexes:
|
|
- IndexName: GSI1
|
|
KeySchema:
|
|
- AttributeName: sk
|
|
KeyType: HASH
|
|
- AttributeName: data
|
|
KeyType: RANGE
|
|
Projection:
|
|
ProjectionType: ALL
|
|
|
|
GameTable:
|
|
Type: AWS::DynamoDB::Table
|
|
# DeletionPolicy: !If [RetainDataResources, 'Retain', 'Delete']
|
|
Properties:
|
|
BillingMode: PAY_PER_REQUEST
|
|
PointInTimeRecoverySpecification:
|
|
PointInTimeRecoveryEnabled: ${{self:custom.stageConfig.pointInTimeRecoveryEnabled, false}}
|
|
KeySchema:
|
|
- KeyType: HASH
|
|
AttributeName: id
|
|
AttributeDefinitions:
|
|
- AttributeName: id
|
|
AttributeType: S
|
|
|
|
UserTable:
|
|
Type: AWS::DynamoDB::Table
|
|
# DeletionPolicy: !If [RetainDataResources, 'Retain', 'Delete']
|
|
Properties:
|
|
BillingMode: PAY_PER_REQUEST
|
|
PointInTimeRecoverySpecification:
|
|
PointInTimeRecoveryEnabled: false
|
|
KeySchema:
|
|
- KeyType: HASH
|
|
AttributeName: id
|
|
AttributeDefinitions:
|
|
- AttributeName: id
|
|
AttributeType: S
|
|
|
|
CognitoUserPool:
|
|
Type: AWS::Cognito::UserPool
|
|
# DeletionPolicy: !If [RetainDataResources, 'Retain', 'Delete']
|
|
Properties:
|
|
Policies:
|
|
PasswordPolicy:
|
|
MinimumLength: 6
|
|
Schema:
|
|
- AttributeDataType: String
|
|
Name: email
|
|
Required: true
|
|
AutoVerifiedAttributes:
|
|
- email
|
|
# EmailConfiguration:
|
|
# EmailSendingAccount: DEVELOPER
|
|
# ReplyToEmailAddress: no-reply@halfstack.software
|
|
# SourceArn: arn:aws:ses:us-west-2:xxxx:identity/no-reply@halfstack.software
|
|
LambdaConfig:
|
|
PreSignUp: !GetAtt AutoConfirmUserLambdaFunction.Arn
|
|
PostAuthentication: !GetAtt PostAuthNLambdaFunction.Arn
|
|
|
|
CognitoUserPoolClient:
|
|
Type: AWS::Cognito::UserPoolClient
|
|
# DeletionPolicy: !If [RetainDataResources, 'Retain', 'Delete']
|
|
Properties:
|
|
UserPoolId: !Ref CognitoUserPool
|
|
ClientName: CognitoIdentityPool
|
|
GenerateSecret: false
|
|
RefreshTokenValidity: 30
|
|
|
|
CognitoIdentityPool:
|
|
Type: AWS::Cognito::IdentityPool
|
|
# DeletionPolicy: !If [RetainDataResources, 'Retain', 'Delete']
|
|
Properties:
|
|
AllowUnauthenticatedIdentities: false
|
|
# SupportedLoginProviders:
|
|
# graph.facebook.com: 'xxxxx'
|
|
# accounts.google.com: 'xxxxx-v02jjpd5r9ig0pdacbhpill2asuqtvnf.apps.googleusercontent.com'
|
|
# api.twitter.com:
|
|
CognitoIdentityProviders:
|
|
- ClientId: !Ref CognitoUserPoolClient
|
|
ProviderName: !GetAtt CognitoUserPool.ProviderName
|
|
|
|
# Allow Cognito to invoke the cognitoAutoConfirm and cognitoPostAuthN functions
|
|
AutoConfirmUserLambdaCognitoPermission:
|
|
Type: AWS::Lambda::Permission
|
|
Properties:
|
|
Action: lambda:InvokeFunction
|
|
FunctionName: !GetAtt AutoConfirmUserLambdaFunction.Arn
|
|
Principal: cognito-idp.amazonaws.com
|
|
SourceArn: !GetAtt CognitoUserPool.Arn
|
|
|
|
PostAuthNLambdaCognitoPermission:
|
|
Type: AWS::Lambda::Permission
|
|
Properties:
|
|
Action: lambda:InvokeFunction
|
|
FunctionName: !GetAtt PostAuthNLambdaFunction.Arn
|
|
Principal: cognito-idp.amazonaws.com
|
|
SourceArn: !GetAtt CognitoUserPool.Arn
|
|
|
|
CognitoUserRole:
|
|
Type: AWS::IAM::Role
|
|
Properties:
|
|
AssumeRolePolicyDocument:
|
|
Version: '2012-10-17'
|
|
Statement:
|
|
# Allow authenticated users to assume this role
|
|
- Effect: Allow
|
|
Principal:
|
|
Federated: cognito-identity.amazonaws.com
|
|
Action: sts:AssumeRoleWithWebIdentity
|
|
Condition:
|
|
StringEquals:
|
|
'cognito-identity.amazonaws.com:aud': !Ref CognitoIdentityPool
|
|
'ForAnyValue:StringLike':
|
|
'cognito-identity.amazonaws.com:amr': authenticated
|
|
# Authenticated users are allowed to invoke the API
|
|
Policies:
|
|
- PolicyName: InvokeApi
|
|
PolicyDocument:
|
|
Version: '2012-10-17'
|
|
Statement:
|
|
- Effect: Allow
|
|
Action:
|
|
- execute-api:Invoke
|
|
Resource: !Sub 'arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ApiGatewayRestApi}/${{self:provider.stage}}/*/*'
|
|
Path: '/'
|
|
|
|
CognitoIdentityPoolRoles:
|
|
Type: AWS::Cognito::IdentityPoolRoleAttachment
|
|
# DeletionPolicy: !If [RetainDataResources, 'Retain', 'Delete']
|
|
Properties:
|
|
IdentityPoolId: !Ref CognitoIdentityPool
|
|
Roles:
|
|
authenticated: !GetAtt CognitoUserRole.Arn
|
|
|
|
# Due to a Serverless Framework bug, we need to create our own Authorizer, instead of
|
|
# simply specifying `authorizer.arn: !GetAtt CognitoUserPool.Arn` in the function.
|
|
# https://github.com/serverless/serverless/issues/3212#issuecomment-450574093
|
|
ApiGatewayAuthorizer:
|
|
DependsOn:
|
|
- ApiGatewayRestApi
|
|
Type: AWS::ApiGateway::Authorizer
|
|
Properties:
|
|
Name: CognitoAuthorizer
|
|
IdentitySource: method.request.header.Authorization
|
|
RestApiId:
|
|
Ref: ApiGatewayRestApi
|
|
Type: COGNITO_USER_POOLS
|
|
ProviderARNs:
|
|
- !GetAtt CognitoUserPool.Arn
|
|
|
|
WebhookIamRoleLambdaExecution:
|
|
Properties:
|
|
ManagedPolicyArns:
|
|
- 'arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy'
|
|
|
|
ExpressIamRoleLambdaExecution:
|
|
Properties:
|
|
ManagedPolicyArns:
|
|
- 'arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy'
|
|
|
|
MonthlyBudget:
|
|
Type: AWS::Budgets::Budget
|
|
Properties:
|
|
Budget:
|
|
BudgetName: Monthly
|
|
BudgetType: COST
|
|
TimeUnit: MONTHLY
|
|
BudgetLimit:
|
|
Amount: 5
|
|
Unit: USD
|
|
NotificationsWithSubscribers:
|
|
- Notification:
|
|
NotificationType: ACTUAL
|
|
ComparisonOperator: GREATER_THAN
|
|
Threshold: 10 # 1st alert sent when % of Monthly total is spent.
|
|
Subscribers:
|
|
- SubscriptionType: EMAIL
|
|
Address: ${{self:custom.stageConfig.alarms.notificationEmail}}
|
|
- Notification:
|
|
NotificationType: ACTUAL
|
|
ComparisonOperator: GREATER_THAN
|
|
Threshold: 95 # 2nd alert sent when % of Monthly total is spent.
|
|
Subscribers:
|
|
- SubscriptionType: EMAIL
|
|
Address: ${{self:custom.stageConfig.alarms.notificationEmail}}
|
|
|
|
NoNotificationsSentAlarm:
|
|
Type: AWS::CloudWatch::Alarm
|
|
Properties:
|
|
Namespace: civ6-pbc
|
|
MetricName: webhook-NOTIFICATION_SENT
|
|
AlarmDescription: No notifications have been sent in the past hour. There may be
|
|
something wrong with the Webhook service, or there may simply have been no turns
|
|
taken. Review the Webhook Lambda Function's CloudWatch Logs and Metrics.
|
|
Threshold: 1
|
|
Period: 3600
|
|
EvaluationPeriods: 1
|
|
ComparisonOperator: LessThanThreshold
|
|
AlarmActions:
|
|
- !Ref AwsAlertsAlarm
|
|
- !Sub arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:opsitem:2#CATEGORY=Availability
|
|
TreatMissingData: missing
|
|
Statistic: Sum
|
|
|
|
NoGamesCreatedAlarm:
|
|
Type: AWS::CloudWatch::Alarm
|
|
Properties:
|
|
Namespace: civ6-pbc
|
|
MetricName: express-GAME_CREATED
|
|
AlarmDescription: No games have been created in the past day. There may be
|
|
something wrong with the game create flow, or there may simply have been no games
|
|
created. Review the Express Lambda Function's CloudWatch Logs and Metrics.
|
|
Threshold: 1
|
|
Period: 86400
|
|
EvaluationPeriods: 1
|
|
ComparisonOperator: LessThanThreshold
|
|
AlarmActions:
|
|
- !Ref AwsAlertsAlarm
|
|
- !Sub arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:opsitem:2#CATEGORY=Availability
|
|
TreatMissingData: missing
|
|
Statistic: Sum
|
|
|
|
Dashboard:
|
|
Type: AWS::CloudWatch::Dashboard
|
|
Properties:
|
|
DashboardBody: !Sub |
|
|
{
|
|
"start": "-P10D",
|
|
"end": "P0D",
|
|
"widgets": [
|
|
{
|
|
"type": "metric",
|
|
"x": 0,
|
|
"y": 9,
|
|
"width": 6,
|
|
"height": 6,
|
|
"properties": {
|
|
"metrics": [
|
|
[ "civ6-pbc", "webhook-GAME_MARKED_INACTIVE", { "label": "Games marked inactive" } ],
|
|
[ ".", "webhook-SKIP_INACTIVE", { "label": "Inactive games skipped" } ],
|
|
[ "AWS/Lambda", "Errors", "FunctionName", "${WebhookLambdaFunction}", "Resource", "${WebhookLambdaFunction}" ]
|
|
],
|
|
"view": "timeSeries",
|
|
"stacked": false,
|
|
"region": "${AWS::Region}",
|
|
"stat": "Sum",
|
|
"period": 3600,
|
|
"title": "Webhook"
|
|
}
|
|
},
|
|
{
|
|
"type": "metric",
|
|
"x": 6,
|
|
"y": 9,
|
|
"width": 6,
|
|
"height": 6,
|
|
"properties": {
|
|
"annotations": {
|
|
"alarms": [
|
|
"${WebhookFunctionDurationAlarm.Arn}"
|
|
]
|
|
},
|
|
"yAxis": {
|
|
"left": {
|
|
"min": 0
|
|
}
|
|
},
|
|
"view": "timeSeries",
|
|
"stacked": false,
|
|
"region": "${AWS::Region}",
|
|
"stat": "p90",
|
|
"title": "Duration"
|
|
}
|
|
},
|
|
{
|
|
"type": "metric",
|
|
"x": 12,
|
|
"y": 9,
|
|
"width": 3,
|
|
"height": 6,
|
|
"properties": {
|
|
"metrics": [
|
|
[ "AWS/SQS", "ApproximateNumberOfMessagesVisible", "QueueName", "${WebhookSqsQueueDlq.QueueName}", { "label": "Number of messages" } ]
|
|
],
|
|
"view": "timeSeries",
|
|
"stacked": false,
|
|
"region": "${AWS::Region}",
|
|
"stat": "Maximum",
|
|
"period": 3600,
|
|
"title": "Webhook DLQ"
|
|
}
|
|
},
|
|
{
|
|
"type": "alarm",
|
|
"x": 18,
|
|
"y": 9,
|
|
"width": 6,
|
|
"height": 3,
|
|
"properties": {
|
|
"title": "Alarms",
|
|
"region": "${AWS::Region}",
|
|
"alarms": [
|
|
"${WebhookFunctionErrorsAlarm.Arn}",
|
|
"${WebhookFunctionDurationAlarm.Arn}",
|
|
"${ExpressFunctionErrorsAlarm.Arn}",
|
|
"${ExpressFunctionDurationAlarm.Arn}"
|
|
]
|
|
}
|
|
},
|
|
{
|
|
"type": "metric",
|
|
"x": 0,
|
|
"y": 0,
|
|
"width": 7,
|
|
"height": 6,
|
|
"properties": {
|
|
"annotations": {
|
|
"alarms": [
|
|
"${NoNotificationsSentAlarm.Arn}"
|
|
]
|
|
},
|
|
"yAxis": {
|
|
"left": {
|
|
"min": 0
|
|
}
|
|
},
|
|
"view": "timeSeries",
|
|
"stacked": false,
|
|
"region": "${AWS::Region}",
|
|
"stat": "Sum",
|
|
"title": "Notifications sent"
|
|
}
|
|
},
|
|
{
|
|
"type": "metric",
|
|
"x": 7,
|
|
"y": 0,
|
|
"width": 3,
|
|
"height": 6,
|
|
"properties": {
|
|
"annotations": {
|
|
"alarms": [
|
|
"${NoGamesCreatedAlarm.Arn}"
|
|
]
|
|
},
|
|
"yAxis": {
|
|
"left": {
|
|
"min": 0
|
|
}
|
|
},
|
|
"view": "timeSeries",
|
|
"stacked": false,
|
|
"region": "${AWS::Region}",
|
|
"stat": "Sum",
|
|
"title": "Games created"
|
|
}
|
|
},
|
|
{
|
|
"type": "metric",
|
|
"x": 15,
|
|
"y": 0,
|
|
"width": 9,
|
|
"height": 3,
|
|
"properties": {
|
|
"metrics": [
|
|
[ "civ6-pbc", "webhook-NOTIFICATION_SENT", { "label": "Today", "period": 86400 } ],
|
|
[ "...", { "label": "This week", "period": 604800 } ],
|
|
[ "...", { "label": "This month", "yAxis": "right", "period": 2678400 } ]
|
|
],
|
|
"view": "singleValue",
|
|
"stacked": false,
|
|
"region": "${AWS::Region}",
|
|
"stat": "Sum",
|
|
"title": "Notifications sent",
|
|
"start": "-P4W",
|
|
"end": "P0D",
|
|
"period": 2678400
|
|
}
|
|
},
|
|
{
|
|
"type": "log",
|
|
"x": 15,
|
|
"y": 3,
|
|
"width": 3,
|
|
"height": 3,
|
|
"properties": {
|
|
"query": "SOURCE '/aws/lambda/civ6-pbc-production-webhook' | FIELDS gameId\n| STATS count_distinct(gameId) as activeGames",
|
|
"region": "${AWS::Region}",
|
|
"title": "Active games",
|
|
"view": "singleValue"
|
|
}
|
|
},
|
|
{
|
|
"type": "log",
|
|
"x": 0,
|
|
"y": 12,
|
|
"width": 24,
|
|
"height": 6,
|
|
"properties": {
|
|
"query": "SOURCE '/aws/lambda/${WebhookLambdaFunction}' | SOURCE '/aws/lambda/${ExpressLambdaFunction}' | FIELDS @timestamp, @message\n| SORT @timestamp desc\n| FILTER @message LIKE /Invoke Error/ OR errorType = 'Error' OR level = 'error'\n| DISPLAY @ingestionTime, errorMessage, message",
|
|
"region": "${AWS::Region}",
|
|
"stacked": false,
|
|
"view": "table"
|
|
}
|
|
}
|
|
]
|
|
}
|
|
|
|
Outputs:
|
|
CognitoUserPoolId:
|
|
Description: ID of the Cognito User Pool
|
|
Value: !Ref CognitoUserPool
|
|
|
|
CognitoUserPoolClientId:
|
|
Description: 'Client ID of the Cognito User Pool App: Identity Pool'
|
|
Value: !Ref CognitoUserPoolClient
|
|
|
|
CognitoIdentityPoolId:
|
|
Description: ID of the Cognito Identity Pool
|
|
Value: !Ref CognitoIdentityPool
|
|
|
|
UserTableName:
|
|
Value: !Ref UserTable
|
|
|
|
GameTableName:
|
|
Value: !Ref GameTable
|
|
|
|
ApiEndpoint:
|
|
Value:
|
|
Fn::If:
|
|
- IsApiCustomDomainEnabled
|
|
- https://${{self:custom.stageConfig.api.domainName, ''}}
|
|
- !Sub https://${ApiGatewayRestApi}.execute-api.${AWS::Region}.amazonaws.com/${{self:provider.stage}} |