mirror of
https://github.com/brettstack/civ6-play-by-cloud-turn-notifier.git
synced 2026-02-19 11:55:11 -05:00
522 lines
18 KiB
YAML
522 lines
18 KiB
YAML
service: civ6-pbc${env:SERVERLESS_SERVICE_SUFFIX, ''}
|
|
useDotenv: true
|
|
provider:
|
|
name: aws
|
|
stackName: ${self:service}-${self:provider.stage}
|
|
runtime: nodejs16.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
|
|
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:
|
|
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-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-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
|
|
webpack:
|
|
webpackConfig: ./functions.webpack.config.js
|
|
excludeFiles: packages/**/*.test.[t|j]s
|
|
|
|
output:
|
|
file: ./stack-outputs.${self:provider.stage}.json
|
|
|
|
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
|
|
|
|
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: /
|
|
- http:
|
|
method: ANY
|
|
path: '{proxy+}'
|
|
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
|
|
|
|
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
|
|
|
|
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: MonthlyBudget
|
|
BudgetType: COST
|
|
TimeUnit: MONTHLY
|
|
BudgetLimit:
|
|
Amount: 5
|
|
Unit: USD
|
|
NotificationsWithSubscribers:
|
|
- Notification:
|
|
NotificationType: ACTUAL
|
|
ComparisonOperator: GREATER_THAN
|
|
Threshold: 50 # 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:
|
|
- !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:
|
|
- !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": 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": "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:
|
|
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}
|