Lesson 35 - Get Compute Auth Token Working

This commit is contained in:
Norman Lansing
2026-02-28 12:32:28 -05:00
parent 1d477ee42a
commit 4fde462bce
7743 changed files with 1397833 additions and 18 deletions

View File

@@ -0,0 +1,2 @@
VC_redist.x64.exe /q
Engine\Extras\Redist\en-us\UEPrereqSetup_x64.exe /q

View File

@@ -0,0 +1,527 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
AWSTemplateFormatVersion: "2010-09-09"
Description: >
This CloudFormation template sets up a game backend service with a single Amazon GameLift fleet. After player
authenticates and start a game via POST /start_game, a lambda handler searches for an existing viable game session
with open player slot on the fleet, and if not found, creates a new game session. The game client is then expected
to poll POST /get_game_connection to receive a viable game session.
Parameters:
ApiGatewayStageNameParameter:
Type: String
Default: v1
Description: Name of the Api Gateway stage
BuildNameParameter:
Type: String
Default: Sample GameLift Build
Description: Name of the build
BuildOperatingSystemParameter:
Type: String
Default: WINDOWS_2016
Description: Operating system of the build
BuildServerSdkVersionParameter:
Type: String
Description: GameLift Server SDK version used in the server build
BuildS3BucketParameter:
Type: String
Description: Bucket that stores the server build
BuildS3KeyParameter:
Type: String
Description: Key of the server build in the S3 bucket
BuildVersionParameter:
Type: String
Description: Version number of the build
FleetDescriptionParameter:
Type: String
Default: Deployed by the Amazon GameLift Plug-in for Unreal.
Description: Description of the fleet
FleetNameParameter:
Type: String
Default: Sample GameLift Fleet
Description: Name of the fleet
FleetTcpFromPortParameter:
Type: Number
Default: 33430
Description: Starting port number for TCP ports to be opened
FleetTcpToPortParameter:
Type: Number
Default: 33440
Description: Ending port number for TCP ports to be opened
FleetUdpFromPortParameter:
Type: Number
Default: 33430
Description: Starting port number for UDP ports to be opened
FleetUdpToPortParameter:
Type: Number
Default: 33440
Description: Ending port number for UDP ports to be opened
GameNameParameter:
Type: String
Default: MyGame
Description: Game name to prepend before resource names
MaxLength: 30
LambdaZipS3BucketParameter:
Type: String
Description: S3 bucket that stores the lambda function zip
LambdaZipS3KeyParameter:
Type: String
Description: S3 key that stores the lambda function zip
LaunchParametersParameter:
Type: String
Description: Parameters used to launch the game server process
LaunchPathParameter:
Type: String
Description: Location of the game server executable in the build
MaxPlayersPerGameParameter:
Type: Number
Default: 10
Description: Maximum number of players per game session
MaxTransactionsPerFiveMinutesPerIpParameter:
Type: Number
Default: 100
MaxValue: 20000000
MinValue: 100
UnrealEngineVersionParameter:
Type: String
Description: "Unreal Engine Version being used by the plugin"
EnableMetricsParameter:
Type: String
Default: "false"
AllowedValues: ["true", "false"]
Description: "Enable telemetry metrics collection using OTEL collector"
Conditions:
ShouldCreateMetricsResources: !Equals [!Ref EnableMetricsParameter, "true"]
Resources:
ApiGatewayCloudWatchRole:
Type: "AWS::IAM::Role"
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- apigateway.amazonaws.com
Action: "sts:AssumeRole"
ManagedPolicyArns:
- "arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs"
Account:
Type: "AWS::ApiGateway::Account"
Properties:
CloudWatchRoleArn: !GetAtt ApiGatewayCloudWatchRole.Arn
GameRequestLambdaFunctionExecutionRole:
Type: "AWS::IAM::Role"
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- "sts:AssumeRole"
ManagedPolicyArns:
- "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
Policies:
- PolicyName: !Sub ${GameNameParameter}GameRequestLambdaFunctionGameLiftPolicies
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- "gamelift:CreateGameSession"
- "gamelift:CreatePlayerSession"
- "gamelift:SearchGameSessions"
Resource: "*"
RestApi:
Type: "AWS::ApiGateway::RestApi"
Properties:
Name: !Sub ${GameNameParameter}RestApi
ResultsRequestLambdaFunctionExecutionRole:
Type: "AWS::IAM::Role"
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- "sts:AssumeRole"
ManagedPolicyArns:
- "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
Policies:
- PolicyName: !Sub ${GameNameParameter}ResultsRequestLambdaFunctionGameLiftPolicies
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- "gamelift:CreateGameSession"
- "gamelift:CreatePlayerSession"
- "gamelift:SearchGameSessions"
Resource: "*"
MetricsInstanceRole:
Type: "AWS::IAM::Role"
Condition: ShouldCreateMetricsResources
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: gamelift.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: AMPRemoteWriteAccess
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- aps:RemoteWrite
Resource: !Sub 'arn:aws:aps:*:${AWS::AccountId}:workspace/*'
- PolicyName: OTelCollectorEMFExporter
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- logs:PutLogEvents
- logs:CreateLogStream
- logs:CreateLogGroup
- logs:PutRetentionPolicy
Resource: !Sub 'arn:aws:logs:*:${AWS::AccountId}:log-group:*:log-stream:*'
UserPool:
Type: "AWS::Cognito::UserPool"
Properties:
AdminCreateUserConfig:
AllowAdminCreateUserOnly: false
AutoVerifiedAttributes:
- email
EmailConfiguration:
EmailSendingAccount: COGNITO_DEFAULT
EmailVerificationMessage: "Please verify your email to complete account registration for the GameLift Plugin Single-fleet deployment scenario. Confirmation Code {####}."
EmailVerificationSubject: GameLift Plugin - Deployment Scenario Account Verification
Policies:
PasswordPolicy:
MinimumLength: 8
RequireLowercase: true
RequireNumbers: true
RequireSymbols: true
RequireUppercase: true
Schema:
- Name: email
AttributeDataType: String
Mutable: false
Required: true
UserPoolName: !Sub ${GameNameParameter}UserPool
UsernameAttributes:
- email
BuildAccessRole:
Type: "AWS::IAM::Role"
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- cloudformation.amazonaws.com
- gamelift.amazonaws.com
Action: "sts:AssumeRole"
Policies:
- PolicyName: !Sub ${GameNameParameter}BuildS3AccessPolicy
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- "s3:GetObject"
- "s3:GetObjectVersion"
Resource:
- "Fn::Sub": "arn:aws:s3:::${BuildS3BucketParameter}/${BuildS3KeyParameter}"
RoleName: !Sub ${GameNameParameter}BuildIAMRole
GameRequestApiResource:
Type: "AWS::ApiGateway::Resource"
Properties:
ParentId: !GetAtt RestApi.RootResourceId
PathPart: start_game
RestApiId: !Ref RestApi
ResultsRequestApiResource:
Type: "AWS::ApiGateway::Resource"
Properties:
ParentId: !GetAtt RestApi.RootResourceId
PathPart: get_game_connection
RestApiId: !Ref RestApi
UserPoolClient:
Type: "AWS::Cognito::UserPoolClient"
Properties:
AccessTokenValidity: 1
ClientName: !Sub ${GameNameParameter}UserPoolClient
ExplicitAuthFlows:
- ALLOW_USER_PASSWORD_AUTH
- ALLOW_REFRESH_TOKEN_AUTH
GenerateSecret: false
IdTokenValidity: 1
PreventUserExistenceErrors: ENABLED
ReadAttributes:
- email
- preferred_username
RefreshTokenValidity: 30
SupportedIdentityProviders:
- COGNITO
UserPoolId: !Ref UserPool
WebACL:
Type: "AWS::WAFv2::WebACL"
DependsOn:
- ApiDeployment
Properties:
DefaultAction:
Allow:
{}
Description: !Sub "WebACL for game: ${GameNameParameter}"
Name: !Sub ${GameNameParameter}WebACL
Rules:
- Name: !Sub ${GameNameParameter}WebACLPerIpThrottleRule
Action:
Block:
{}
Priority: 0
Statement:
RateBasedStatement:
AggregateKeyType: IP
Limit: !Ref MaxTransactionsPerFiveMinutesPerIpParameter
VisibilityConfig:
CloudWatchMetricsEnabled: true
MetricName: !Sub ${GameNameParameter}WebACLPerIpThrottleRuleMetrics
SampledRequestsEnabled: true
Scope: REGIONAL
VisibilityConfig:
CloudWatchMetricsEnabled: true
MetricName: !Sub ${GameNameParameter}WebACLMetrics
SampledRequestsEnabled: true
ApiDeployment:
Type: "AWS::ApiGateway::Deployment"
DependsOn:
- GameRequestApiMethod
- ResultsRequestApiMethod
Properties:
RestApiId: !Ref RestApi
StageDescription:
DataTraceEnabled: true
LoggingLevel: INFO
MetricsEnabled: true
StageName: !Ref ApiGatewayStageNameParameter
Authorizer:
Type: "AWS::ApiGateway::Authorizer"
Properties:
IdentitySource: method.request.header.Auth
Name: CognitoAuthorizer
ProviderARNs:
- "Fn::GetAtt":
- UserPool
- Arn
RestApiId: !Ref RestApi
Type: COGNITO_USER_POOLS
GameRequestApiMethod:
Type: "AWS::ApiGateway::Method"
Properties:
AuthorizationType: COGNITO_USER_POOLS
AuthorizerId: !Ref Authorizer
HttpMethod: POST
Integration:
Type: AWS_PROXY
IntegrationHttpMethod: POST
Uri: !Sub "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GameRequestLambdaFunction.Arn}/invocations"
OperationName: GameRequest
ResourceId: !Ref GameRequestApiResource
RestApiId: !Ref RestApi
ResultsRequestApiMethod:
Type: "AWS::ApiGateway::Method"
Properties:
AuthorizationType: COGNITO_USER_POOLS
AuthorizerId: !Ref Authorizer
HttpMethod: POST
Integration:
Type: AWS_PROXY
IntegrationHttpMethod: POST
Uri: !Sub "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ResultsRequestLambdaFunction.Arn}/invocations"
OperationName: ResultsRequest
ResourceId: !Ref ResultsRequestApiResource
RestApiId: !Ref RestApi
WebACLAssociation:
Type: "AWS::WAFv2::WebACLAssociation"
DependsOn:
- ApiDeployment
- WebACL
Properties:
ResourceArn: !Sub
- "arn:aws:apigateway:${REGION}::/restapis/${REST_API_ID}/stages/${STAGE_NAME}"
- REGION: !Ref "AWS::Region"
REST_API_ID: !Ref RestApi
STAGE_NAME: !Ref ApiGatewayStageNameParameter
WebACLArn: !GetAtt WebACL.Arn
ServerBuild:
Type: "AWS::GameLift::Build"
Properties:
Name: !Ref BuildNameParameter
OperatingSystem: !Ref BuildOperatingSystemParameter
ServerSdkVersion: !Ref BuildServerSdkVersionParameter
StorageLocation:
Bucket: !Ref BuildS3BucketParameter
Key: !Ref BuildS3KeyParameter
RoleArn: !GetAtt BuildAccessRole.Arn
Version: !Ref BuildVersionParameter
FleetResource:
Type: "AWS::GameLift::Fleet"
Properties:
BuildId: !Ref ServerBuild
CertificateConfiguration:
CertificateType: GENERATED
Description: !Sub
- "${FleetDescriptionParameter} Using Unreal Engine Version ${UnrealEngineVersionParameter}${MetricsText}"
- MetricsText: !If [ShouldCreateMetricsResources, " with telemetry metrics enabled", ""]
DesiredEC2Instances: 1
EC2InboundPermissions:
- FromPort: !Ref FleetTcpFromPortParameter
IpRange: "0.0.0.0/0"
Protocol: TCP
ToPort: !Ref FleetTcpToPortParameter
- FromPort: !Ref FleetUdpFromPortParameter
IpRange: "0.0.0.0/0"
Protocol: UDP
ToPort: !Ref FleetUdpToPortParameter
EC2InstanceType: c5.large
FleetType: ON_DEMAND
InstanceRoleARN: !If [ShouldCreateMetricsResources, !GetAtt MetricsInstanceRole.Arn, !Ref "AWS::NoValue"]
InstanceRoleCredentialsProvider: !If [ShouldCreateMetricsResources, "SHARED_CREDENTIAL_FILE", !Ref "AWS::NoValue"]
Name: !Ref FleetNameParameter
NewGameSessionProtectionPolicy: FullProtection
ResourceCreationLimitPolicy:
NewGameSessionsPerCreator: 5
PolicyPeriodInMinutes: 2
RuntimeConfiguration:
GameSessionActivationTimeoutSeconds: 300
MaxConcurrentGameSessionActivations: 1
ServerProcesses:
- ConcurrentExecutions: 1
LaunchPath: !Ref LaunchPathParameter
Parameters: !Ref LaunchParametersParameter
AliasResource:
Type: "AWS::GameLift::Alias"
Properties:
Description: !Sub Alias to access ${GameNameParameter} fleet
Name: !Sub ${GameNameParameter}FleetAlias
RoutingStrategy:
Type: SIMPLE
FleetId: !Ref FleetResource
ResultsRequestLambdaFunction:
Type: "AWS::Lambda::Function"
Properties:
Code:
S3Bucket: !Ref LambdaZipS3BucketParameter
S3Key: !Ref LambdaZipS3KeyParameter
Description: Lambda function to handle game requests
Environment:
Variables:
FleetAlias: !Ref AliasResource
FunctionName: !Sub ${GameNameParameter}ResultsRequestLambda
Handler: results_request.handler
MemorySize: 128
Role: !GetAtt ResultsRequestLambdaFunctionExecutionRole.Arn
Runtime: python3.14
GameRequestLambdaFunction:
Type: "AWS::Lambda::Function"
Properties:
Code:
S3Bucket: !Ref LambdaZipS3BucketParameter
S3Key: !Ref LambdaZipS3KeyParameter
Description: Lambda function to handle game requests
Environment:
Variables:
FleetAlias: !Ref AliasResource
MaxPlayersPerGame: !Ref MaxPlayersPerGameParameter
FunctionName: !Sub ${GameNameParameter}GameRequestLambda
Handler: game_request.handler
MemorySize: 128
Role: !GetAtt GameRequestLambdaFunctionExecutionRole.Arn
Runtime: python3.14
ResultsRequestLambdaFunctionApiGatewayPermission:
Type: "AWS::Lambda::Permission"
Properties:
Action: "lambda:InvokeFunction"
FunctionName: !GetAtt ResultsRequestLambdaFunction.Arn
Principal: apigateway.amazonaws.com
SourceArn: !Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${RestApi}/*/*/*"
GameRequestLambdaFunctionApiGatewayPermission:
Type: "AWS::Lambda::Permission"
Properties:
Action: "lambda:InvokeFunction"
FunctionName: !GetAtt GameRequestLambdaFunction.Arn
Principal: apigateway.amazonaws.com
SourceArn: !Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${RestApi}/*/*/*"
Outputs:
ApiGatewayEndpoint:
Description: Url of ApiGateway Endpoint
Value: !Sub "https://${RestApi}.execute-api.${AWS::Region}.amazonaws.com/${ApiGatewayStageNameParameter}/"
UserPoolClientId:
Description: Id of UserPoolClient
Value: !Ref UserPoolClient
IdentityRegion:
Description: Region name
Value: !Ref "AWS::Region"

View File

@@ -0,0 +1,44 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
---
# THIS IS A SAMPLE CLOUDFORMATION PARAMETERS FILE
GameNameParameter:
value: "{{AWSGAMELIFT::SYS::GAMENAME}}"
LambdaZipS3BucketParameter:
value: "{{AWSGAMELIFT::VARS::LambdaZipS3BucketParameter}}"
LambdaZipS3KeyParameter:
value: "{{AWSGAMELIFT::VARS::LambdaZipS3KeyParameter}}"
ApiGatewayStageNameParameter:
value: "{{AWSGAMELIFT::VARS::ApiGatewayStageNameParameter}}"
BuildS3BucketParameter:
value: "{{AWSGAMELIFT::VARS::BuildS3BucketParameter}}"
BuildS3KeyParameter:
value: "{{AWSGAMELIFT::VARS::BuildS3KeyParameter}}"
LaunchPathParameter:
value: "{{AWSGAMELIFT::VARS::LaunchPathParameter}}"
LaunchParametersParameter:
value: "-port=7777 -UNATTENDED LOG=server.log"
BuildNameParameter:
value: "Sample GameLift Build for {{AWSGAMELIFT::SYS::GAMENAME}}"
BuildVersionParameter:
value: "1"
BuildOperatingSystemParameter:
value: "{{AWSGAMELIFT::VARS::BuildOperatingSystemParameter}}"
BuildServerSdkVersionParameter:
value: "5.4.0"
FleetTcpFromPortParameter:
value: "7770"
FleetTcpToPortParameter:
value: "7780"
FleetUdpFromPortParameter:
value: "7770"
FleetUdpToPortParameter:
value: "7780"
FleetNameParameter:
value: "Single-Region Fleet"
UnrealEngineVersionParameter:
value: "{{AWSGAMELIFT::VARS::UnrealEngineVersionParameter}}"
EnableMetricsParameter:
value: "{{AWSGAMELIFT::VARS::EnableMetricsParameter}}"

View File

@@ -0,0 +1,11 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
---
# Key/value pairs to be added to the game's awsGameLiftClientConfig.yml file
# These values will be replaced at the end of create/update of
# the feature's CloudFormation stack.
user_pool_client_id: "{{AWSGAMELIFT::CFNOUTPUT::UserPoolClientId}}"
identity_api_gateway_base_url: "{{AWSGAMELIFT::CFNOUTPUT::ApiGatewayEndpoint}}"
identity_region: "{{AWSGAMELIFT::CFNOUTPUT::IdentityRegion}}"

View File

@@ -0,0 +1,79 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
import boto3
import os
import json
def handler(event, context):
"""
Handles requests to start games from the game client.
This function first looks for any game session
:param event: lambda event, contains the region to player latency mapping in `regionToLatencyMapping` key, as well
as the player information from the Cognito id tokens.
:param context: lambda context, not used by this function
:return:
- 202 (Accepted) if the matchmaking request is accepted and is now being processed
- 409 (Conflict) if the another matchmaking request is in progress
- 500 (Internal Error) if error occurred when processing the matchmaking request
"""
gamelift = boto3.client('gamelift')
fleet_alias = os.environ['FleetAlias']
max_players_per_game = int(os.environ['MaxPlayersPerGame'])
player_id = event["requestContext"]["authorizer"]["claims"]["sub"]
print(f'Handling start game request. PlayerId: {player_id}')
# NOTE: latency mapping is not used in this deployment scenario
region_to_latency_mapping = get_region_to_latency_mapping(event)
if region_to_latency_mapping:
print(f"Region to latency mapping: {region_to_latency_mapping}")
else:
print("No regionToLatencyMapping mapping provided")
if not has_viable_game_sessions(gamelift, fleet_alias):
create_game_session(gamelift, fleet_alias, max_players_per_game)
return {
'headers': {
'Content-Type': 'text/plain'
},
'statusCode': 202
}
def has_viable_game_sessions(gamelift, fleet_alias):
print(f"Checking for viable game sessions: {fleet_alias}")
# NOTE: SortExpression="creationTimeMillis ASC" is not needed because we are looking for any viable game sessions,
# hence the order does not matter.
search_game_sessions_response = gamelift.search_game_sessions(
AliasId=fleet_alias,
FilterExpression="hasAvailablePlayerSessions=true",
)
return len(search_game_sessions_response['GameSessions']) != 0
def create_game_session(gamelift, fleet_alias, max_players_per_game):
print(f"Creating game session: {fleet_alias}")
gamelift.create_game_session(
AliasId=fleet_alias,
MaximumPlayerSessionCount=max_players_per_game,
)
def get_region_to_latency_mapping(event):
request_body = event.get("body")
if not request_body:
return None
try:
request_body_json = json.loads(request_body)
except ValueError:
print(f"Error parsing request body: {request_body}")
return None
if request_body_json and request_body_json.get('regionToLatencyMapping'):
return request_body_json.get('regionToLatencyMapping')

View File

@@ -0,0 +1,73 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
import boto3
import os
import json
def handler(event, context):
"""
Handles requests to describe the game session connection information after a StartGame request.
This function will look up the MatchmakingRequest table to find a pending matchmaking request by
the player, and if it is QUEUED, look up the GameSessionPlacement table to find the game's
connection information.
:param event: lambda event, contains the region to player latency mapping in `regionToLatencyMapping` key, as well
as the player information from the Cognito id tokens. Cognito provides a `sub` user attribute that we use as a
Player ID. Unlike `username` value `sub` is UUID for a user which is never reassigned to another user.
:param context: lambda context, not used by this function
:return:
- 200 (OK) if the game connection is ready, along with server info: "IpAddress", "Port", "DnsName", "PlayerSessionId", "PlayerId"
- 204 (No Content) if the requested game is still in progress of matchmaking
- 404 (Not Found) if no game has been started by the player, or if all started game were expired
- 500 (Internal Error) if errors occurred during matchmaking or placement
"""
gamelift = boto3.client('gamelift')
fleet_alias = os.environ['FleetAlias']
player_id = event["requestContext"]["authorizer"]["claims"]["sub"]
print(f'Handling request result request. PlayerId: {player_id}')
oldest_viable_game_session = get_oldest_viable_game_session(gamelift, fleet_alias)
if oldest_viable_game_session:
player_session = create_player_session(gamelift, oldest_viable_game_session['GameSessionId'], player_id)
game_session_connection_info = dict((k, player_session[k]) for k in ('IpAddress', 'Port', 'DnsName', 'PlayerSessionId', 'PlayerId'))
game_session_connection_info['GameSessionArn'] = player_session['GameSessionId']
print(f"Connection info: {game_session_connection_info}")
return {
'body': json.dumps(game_session_connection_info),
'headers': {
'Content-Type': 'text/plain'
},
'statusCode': 200
}
else:
return {
'headers': {
'Content-Type': 'text/plain'
},
'statusCode': 404
}
def get_oldest_viable_game_session(gamelift, fleet_alias):
print("Checking for viable game sessions:", fleet_alias)
search_game_sessions_response = gamelift.search_game_sessions(
AliasId=fleet_alias,
FilterExpression="hasAvailablePlayerSessions=true",
SortExpression="creationTimeMillis ASC",
)
print(f"Received search game session response: {search_game_sessions_response}")
return next(iter(search_game_sessions_response['GameSessions']), None)
def create_player_session(gamelift, game_session_id, player_id):
print(f"Creating PlayerSession session on GameSession: {game_session_id}, PlayerId: {player_id}")
create_player_session_response = gamelift.create_player_session(
GameSessionId=game_session_id,
PlayerId=player_id
)
print(f"Received create player session response: {create_player_session_response}")
return create_player_session_response['PlayerSession']

View File

@@ -0,0 +1,43 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
#
# GameLift Region Mappings
# Keep synchronized with: https://docs.aws.amazon.com/general/latest/gr/gamelift.html
#
# ----------------------------------------------------------------------------------------
# Region Name Region Endpoint Protocol
# ----------------------------------------------------------------------------------------
# US East (Ohio) us-east-2 gamelift.us-east-2.amazonaws.com HTTPS
# US East (N. Virginia) us-east-1 gamelift.us-east-1.amazonaws.com HTTPS
# US West (N. California) us-west-1 gamelift.us-west-1.amazonaws.com HTTPS
# US West (Oregon) us-west-2 gamelift.us-west-2.amazonaws.com HTTPS
# Asia Pacific (Mumbai) ap-south-1 gamelift.ap-south-1.amazonaws.com HTTPS
# Asia Pacific (Seoul) ap-northeast-2 gamelift.ap-northeast-2.amazonaws.com HTTPS
# Asia Pacific (Singapore) ap-southeast-1 gamelift.ap-southeast-1.amazonaws.com HTTPS
# Asia Pacific (Sydney) ap-southeast-2 gamelift.ap-southeast-2.amazonaws.com HTTPS
# Asia Pacific (Tokyo) ap-northeast-1 gamelift.ap-northeast-1.amazonaws.com HTTPS
# Canada (Central) ca-central-1 gamelift.ca-central-1.amazonaws.com HTTPS
# Europe (Frankfurt) eu-central-1 gamelift.eu-central-1.amazonaws.com HTTPS
# Europe (Ireland) eu-west-1 gamelift.eu-west-1.amazonaws.com HTTPS
# Europe (London) eu-west-2 gamelift.eu-west-2.amazonaws.com HTTPS
# South America (São Paulo) sa-east-1 gamelift.sa-east-1.amazonaws.com HTTPS
# ----------------------------------------------------------------------------------------
{
five_letter_region_codes: {
us-east-2 : usea2,
us-east-1 : usea1,
us-west-1 : uswe1,
us-west-2 : uswe2,
ap-south-1 : apso1,
ap-northeast-2 : apne2,
ap-southeast-1 : apse1,
ap-southeast-2 : apse2,
ap-northeast-1 : apne1,
ca-central-1 : cace1,
eu-central-1 : euce1,
eu-west-1 : euwe1,
eu-west-2 : euwe2,
sa-east-1 : saea1
}
}

View File

@@ -0,0 +1,161 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
import boto3
import requests
import json
import time
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("-g", "--game", help="game name", type=str, required=True)
parser.add_argument("-r", "--region", help="region name, e.g. eu-west-1", type=str, required=True)
parser.add_argument("-p", "--profile", help="profile name in the AWS shared credentials file ~/.aws/credentials", type=str, required=True)
args = parser.parse_args()
GAME_NAME = args.game.lower() # e.g. 'GameLiftSampleGame2ue4'
REGION = args.region # e.g. 'eu-west-1'
PROFILE_NAME = args.profile
USER_POOL_NAME = GAME_NAME + 'UserPool'
USER_POOL_CLIENT_NAME = GAME_NAME + 'UserPoolClient'
USERNAME = 'testuser@example.com'
PASSWORD = 'TestPassw0rd.'
REST_API_NAME = GAME_NAME + 'RestApi'
REST_API_STAGE = 'dev'
GAME_REQUEST_PATH = 'start_game'
RESULTS_REQUEST_PATH = 'get_game_connection'
session = boto3.Session(profile_name=PROFILE_NAME)
cognito_idp = session.client('cognito-idp', region_name=REGION)
apig = session.client('apigateway', region_name=REGION)
REGION_TO_LATENCY_MAPPING = {
"regionToLatencyMapping": {
"us-west-2": 50,
"us-east-1": 100,
"eu-west-1": 150,
"ap-northeast-1": 300
}
}
GAME_REQUEST_PAYLOAD = json.dumps(REGION_TO_LATENCY_MAPPING)
def main():
user_pool = find_user_pool(USER_POOL_NAME)
user_pool_id = user_pool['Id']
print("User Pool Id:", user_pool_id)
user_pool_client = find_user_pool_client(user_pool_id, USER_POOL_CLIENT_NAME)
user_pool_client_id = user_pool_client['ClientId']
print("User Pool Client Id:", user_pool_client_id)
try:
cognito_idp.sign_up(
ClientId=user_pool_client_id,
Username=USERNAME,
Password=PASSWORD,
)
print("Created user:", USERNAME)
cognito_idp.admin_confirm_sign_up(
UserPoolId=user_pool_id,
Username=USERNAME,
)
init_auth_result = cognito_idp.initiate_auth(
AuthFlow='USER_PASSWORD_AUTH',
AuthParameters={
'USERNAME': USERNAME,
'PASSWORD': PASSWORD,
},
ClientId=user_pool_client_id
)
assert init_auth_result['ResponseMetadata']['HTTPStatusCode'] == 200, "Unsuccessful init_auth"
print("Authenticated via username and password")
id_token = init_auth_result['AuthenticationResult']['IdToken']
headers = {
'Auth': id_token
}
results_request_url = get_rest_api_endpoint(REST_API_NAME, REGION, REST_API_STAGE, RESULTS_REQUEST_PATH)
game_request_url = get_rest_api_endpoint(REST_API_NAME, REGION, REST_API_STAGE, GAME_REQUEST_PATH)
print ("results_request_url: " + results_request_url)
print ("game_request_url: " + game_request_url)
results_request_response = requests.post(url=results_request_url, headers=headers)
assert results_request_response.status_code == 204 or results_request_response.status_code == 200, \
"Expect 'POST /get_game_info' status code to be 200 (Success) or 204 (No Content). Actual: " \
f"{str(results_request_response.status_code)}"
print("Verified mock ResultsRequest response", results_request_response)
game_request_response = requests.post(url=game_request_url, headers=headers, data=GAME_REQUEST_PAYLOAD)
assert game_request_response.status_code == 202, "Expect 'POST /start_game' status code to be 202 (Accepted)/ Actual: " \
f"{str(results_request_response.status_code)}"
print("Verified lambda GameRequest response", game_request_response)
#game_request_info = json.loads(game_request_response.content)
print(f"Received start game info: {game_request_response}")
print("Waiting for game session to be created...")
time.sleep(10) # 10 seconds
results_request_response = requests.post(url=results_request_url, headers=headers)
assert results_request_response.status_code == 200, "Expect 'POST /get_game_info' status code to be 200 (Success). Actual: " \
f"{str(results_request_response.status_code)}"
print("Verified lambda ResultsRequest response", results_request_response.content)
game_connection_info = json.loads(results_request_response.content)
print(f"Received game connection info: {game_connection_info}")
assert game_connection_info['IpAddress'] != ''
assert game_connection_info['Port'] > 0
assert REGION in game_connection_info['DnsName'], \
f"Expect {game_connection_info['DnsName']} to contain '{REGION}'"
assert "psess-" in game_connection_info['PlayerSessionId'], \
f"Expect {game_connection_info['PlayerSessionId']} to contain 'psess-'"
assert REGION in game_connection_info['GameSessionArn'], \
f"Expect {game_connection_info['GameSessionArn']} to contain '{REGION}'"
print("Verified game connection info:", game_connection_info)
finally:
cognito_idp.admin_delete_user(
UserPoolId=user_pool_id,
Username=USERNAME,
)
print("Deleted user:", USERNAME)
print("Test Succeeded!")
def find_user_pool(user_pool_name):
print("Finding user pool:", user_pool_name)
result = cognito_idp.list_user_pools(MaxResults=50)
pools = result['UserPools']
return next(x for x in pools if x['Name'] == user_pool_name)
def find_user_pool_client(user_pool_id, user_pool_client_name):
print("Finding user pool client:", user_pool_client_name)
results = cognito_idp.list_user_pool_clients(UserPoolId=user_pool_id)
clients = results['UserPoolClients']
return next(x for x in clients if x['ClientName'] == user_pool_client_name)
def find_rest_api(rest_api_name):
print("Finding rest api:", rest_api_name)
results = apig.get_rest_apis()
rest_apis = results['items']
return next(x for x in rest_apis if x['name'] == rest_api_name)
def get_rest_api_endpoint(rest_api_name, region, stage, path):
print("Getting rest api endpoint", rest_api_name)
rest_api = find_rest_api(rest_api_name)
rest_api_id = rest_api['id']
return f'https://{rest_api_id}.execute-api.{region}.amazonaws.com/{stage}/{path}'
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,481 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
AWSTemplateFormatVersion: "2010-09-09"
Description: >
This CloudFormation template sets up a game backend service with a single Amazon GameLift fleet. After player
authenticates and start a game via POST /start_game, a lambda handler searches for an existing viable game session
with open player slot on the fleet, and if not found, creates a new game session. The game client is then expected
to poll POST /get_game_connection to receive a viable game session.
Parameters:
ApiGatewayStageNameParameter:
Type: String
Default: v1
Description: Name of the Api Gateway stage
ContainerGroupDefinitionNameParameter:
Type: String
Default: SampleCGDName
Description: Name of the Api Gateway stage
ContainerImageNameParameter:
Type: String
Default: SampleContainerImageName
Description: Name for the Container Image
ContainerImageUriParameter:
Type: String
Description: URI pointing to a Container Image in ECR
FleetDescriptionParameter:
Type: String
Default: Deployed by the Amazon GameLift Plug-in for Unreal.
Description: Description of the fleet
TotalMemoryLimitParameter:
Type: Number
Default: 4000
Description: The maximum amount of memory (in MiB) to allocate to the container group
TotalVcpuLimitParameter:
Type: Number
Default: 2
Description: The maximum amount of CPU units to allocate to the container group
FleetUdpFromPortParameter:
Type: Number
Default: 33430
Description: Starting port number for UDP ports to be opened
FleetUdpToPortParameter:
Type: Number
Default: 33440
Description: Ending port number for UDP ports to be opened
GameNameParameter:
Type: String
Default: MyGame
Description: Game name to prepend before resource names
MaxLength: 30
LambdaZipS3BucketParameter:
Type: String
Description: S3 bucket that stores the lambda function zip
LambdaZipS3KeyParameter:
Type: String
Description: S3 key that stores the lambda function zip
LaunchPathParameter:
Type: String
Description: Location of the game server executable in the build
MaxPlayersPerGameParameter:
Type: Number
Default: 10
Description: Maximum number of players per game session
MaxTransactionsPerFiveMinutesPerIpParameter:
Type: Number
Default: 100
MaxValue: 20000000
MinValue: 100
UnrealEngineVersionParameter:
Type: String
Description: "Unreal Engine Version being used by the plugin"
EnableMetricsParameter:
Type: String
Default: "false"
AllowedValues: ["true", "false"]
Description: "Enable telemetry metrics collection using OTEL collector"
Conditions:
ShouldCreateMetricsResources: !Equals [!Ref EnableMetricsParameter, "true"]
Resources:
ApiGatewayCloudWatchRole:
Type: "AWS::IAM::Role"
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- apigateway.amazonaws.com
Action: "sts:AssumeRole"
ManagedPolicyArns:
- "arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs"
Account:
Type: "AWS::ApiGateway::Account"
Properties:
CloudWatchRoleArn: !GetAtt ApiGatewayCloudWatchRole.Arn
GameRequestLambdaFunctionExecutionRole:
Type: "AWS::IAM::Role"
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- "sts:AssumeRole"
ManagedPolicyArns:
- "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
Policies:
- PolicyName: !Sub ${GameNameParameter}GameRequestLambdaFunctionGameLiftPolicies
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- "gamelift:CreateGameSession"
- "gamelift:CreatePlayerSession"
- "gamelift:SearchGameSessions"
Resource: "*"
RestApi:
Type: "AWS::ApiGateway::RestApi"
Properties:
Name: !Sub ${GameNameParameter}RestApi
ResultsRequestLambdaFunctionExecutionRole:
Type: "AWS::IAM::Role"
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- "sts:AssumeRole"
ManagedPolicyArns:
- "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
Policies:
- PolicyName: !Sub ${GameNameParameter}ResultsRequestLambdaFunctionGameLiftPolicies
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- "gamelift:CreateGameSession"
- "gamelift:CreatePlayerSession"
- "gamelift:SearchGameSessions"
Resource: "*"
UserPool:
Type: "AWS::Cognito::UserPool"
Properties:
AdminCreateUserConfig:
AllowAdminCreateUserOnly: false
AutoVerifiedAttributes:
- email
EmailConfiguration:
EmailSendingAccount: COGNITO_DEFAULT
EmailVerificationMessage: "Please verify your email to complete account registration for the GameLift Plugin Single-fleet deployment scenario. Confirmation Code {####}."
EmailVerificationSubject: GameLift Plugin - Deployment Scenario Account Verification
Policies:
PasswordPolicy:
MinimumLength: 8
RequireLowercase: true
RequireNumbers: true
RequireSymbols: true
RequireUppercase: true
Schema:
- Name: email
AttributeDataType: String
Mutable: false
Required: true
UserPoolName: !Sub ${GameNameParameter}UserPool
UsernameAttributes:
- email
GameRequestApiResource:
Type: "AWS::ApiGateway::Resource"
Properties:
ParentId: !GetAtt RestApi.RootResourceId
PathPart: start_game
RestApiId: !Ref RestApi
ResultsRequestApiResource:
Type: "AWS::ApiGateway::Resource"
Properties:
ParentId: !GetAtt RestApi.RootResourceId
PathPart: get_game_connection
RestApiId: !Ref RestApi
UserPoolClient:
Type: "AWS::Cognito::UserPoolClient"
Properties:
AccessTokenValidity: 1
ClientName: !Sub ${GameNameParameter}UserPoolClient
ExplicitAuthFlows:
- ALLOW_USER_PASSWORD_AUTH
- ALLOW_REFRESH_TOKEN_AUTH
GenerateSecret: false
IdTokenValidity: 1
PreventUserExistenceErrors: ENABLED
ReadAttributes:
- email
- preferred_username
RefreshTokenValidity: 30
SupportedIdentityProviders:
- COGNITO
UserPoolId: !Ref UserPool
WebACL:
Type: "AWS::WAFv2::WebACL"
DependsOn:
- ApiDeployment
Properties:
DefaultAction:
Allow:
{}
Description: !Sub "WebACL for game: ${GameNameParameter}"
Name: !Sub ${GameNameParameter}WebACL
Rules:
- Name: !Sub ${GameNameParameter}WebACLPerIpThrottleRule
Action:
Block:
{}
Priority: 0
Statement:
RateBasedStatement:
AggregateKeyType: IP
Limit: !Ref MaxTransactionsPerFiveMinutesPerIpParameter
VisibilityConfig:
CloudWatchMetricsEnabled: true
MetricName: !Sub ${GameNameParameter}WebACLPerIpThrottleRuleMetrics
SampledRequestsEnabled: true
Scope: REGIONAL
VisibilityConfig:
CloudWatchMetricsEnabled: true
MetricName: !Sub ${GameNameParameter}WebACLMetrics
SampledRequestsEnabled: true
ApiDeployment:
Type: "AWS::ApiGateway::Deployment"
DependsOn:
- GameRequestApiMethod
- ResultsRequestApiMethod
Properties:
RestApiId: !Ref RestApi
StageDescription:
DataTraceEnabled: true
LoggingLevel: INFO
MetricsEnabled: true
StageName: !Ref ApiGatewayStageNameParameter
Authorizer:
Type: "AWS::ApiGateway::Authorizer"
Properties:
IdentitySource: method.request.header.Auth
Name: CognitoAuthorizer
ProviderARNs:
- "Fn::GetAtt":
- UserPool
- Arn
RestApiId: !Ref RestApi
Type: COGNITO_USER_POOLS
GameRequestApiMethod:
Type: "AWS::ApiGateway::Method"
Properties:
AuthorizationType: COGNITO_USER_POOLS
AuthorizerId: !Ref Authorizer
HttpMethod: POST
Integration:
Type: AWS_PROXY
IntegrationHttpMethod: POST
Uri: !Sub "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GameRequestLambdaFunction.Arn}/invocations"
OperationName: GameRequest
ResourceId: !Ref GameRequestApiResource
RestApiId: !Ref RestApi
ResultsRequestApiMethod:
Type: "AWS::ApiGateway::Method"
Properties:
AuthorizationType: COGNITO_USER_POOLS
AuthorizerId: !Ref Authorizer
HttpMethod: POST
Integration:
Type: AWS_PROXY
IntegrationHttpMethod: POST
Uri: !Sub "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ResultsRequestLambdaFunction.Arn}/invocations"
OperationName: ResultsRequest
ResourceId: !Ref ResultsRequestApiResource
RestApiId: !Ref RestApi
WebACLAssociation:
Type: "AWS::WAFv2::WebACLAssociation"
DependsOn:
- ApiDeployment
- WebACL
Properties:
ResourceArn: !Sub
- "arn:aws:apigateway:${REGION}::/restapis/${REST_API_ID}/stages/${STAGE_NAME}"
- REGION: !Ref "AWS::Region"
REST_API_ID: !Ref RestApi
STAGE_NAME: !Ref ApiGatewayStageNameParameter
WebACLArn: !GetAtt WebACL.Arn
ContainerFleetRole:
Type: "AWS::IAM::Role"
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- cloudformation.amazonaws.com
- gamelift.amazonaws.com
Action:
- "sts:AssumeRole"
ManagedPolicyArns:
- "arn:aws:iam::aws:policy/GameLiftContainerFleetPolicy"
Policies: !If
- ShouldCreateMetricsResources
- - PolicyName: AMPRemoteWriteAccess
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- aps:RemoteWrite
Resource: !Sub 'arn:aws:aps:*:${AWS::AccountId}:workspace/*'
- PolicyName: OTelCollectorEMFExporter
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- logs:PutLogEvents
- logs:CreateLogStream
- logs:CreateLogGroup
- logs:PutRetentionPolicy
Resource: !Sub 'arn:aws:logs:*:${AWS::AccountId}:log-group:*:log-stream:*'
- []
ContainerGroupResource:
Type: "AWS::GameLift::ContainerGroupDefinition"
Properties:
GameServerContainerDefinition:
ContainerName: !Ref ContainerImageNameParameter
ImageUri: !Ref ContainerImageUriParameter
ServerSdkVersion: "5.4.0"
PortConfiguration:
ContainerPortRanges:
- FromPort: !Ref FleetUdpFromPortParameter
Protocol: "UDP"
ToPort: !Ref FleetUdpToPortParameter
Name: !Ref ContainerGroupDefinitionNameParameter
OperatingSystem: "AMAZON_LINUX_2023"
TotalVcpuLimit: !Ref TotalVcpuLimitParameter
TotalMemoryLimitMebibytes: !Ref TotalMemoryLimitParameter
ContainerFleetResource:
DependsOn:
- ContainerGroupResource
Type: "AWS::GameLift::ContainerFleet"
Properties:
GameServerContainerGroupDefinitionName: !Ref ContainerGroupDefinitionNameParameter
InstanceConnectionPortRange:
FromPort: !Ref FleetUdpFromPortParameter
ToPort: !Ref FleetUdpToPortParameter
Description: !Sub
- "${FleetDescriptionParameter} Using Unreal Engine Version ${UnrealEngineVersionParameter}${MetricsText}"
- MetricsText: !If [ShouldCreateMetricsResources, " with telemetry metrics enabled", ""]
InstanceInboundPermissions:
- FromPort: !Ref FleetUdpFromPortParameter
IpRange: "0.0.0.0/0"
Protocol: UDP
ToPort: !Ref FleetUdpToPortParameter
InstanceType: c4.xlarge
BillingType: ON_DEMAND
FleetRoleArn: !GetAtt ContainerFleetRole.Arn
NewGameSessionProtectionPolicy: FullProtection
GameSessionCreationLimitPolicy:
NewGameSessionsPerCreator: 5
PolicyPeriodInMinutes: 2
AliasResource:
Type: "AWS::GameLift::Alias"
Properties:
Description: !Sub Alias to access ${GameNameParameter} fleet
Name: !Sub ${GameNameParameter}FleetAlias
RoutingStrategy:
Type: SIMPLE
FleetId: !Ref ContainerFleetResource
ResultsRequestLambdaFunction:
Type: "AWS::Lambda::Function"
Properties:
Code:
S3Bucket: !Ref LambdaZipS3BucketParameter
S3Key: !Ref LambdaZipS3KeyParameter
Description: Lambda function to handle game requests
Environment:
Variables:
FleetAlias: !Ref AliasResource
FunctionName: !Sub ${GameNameParameter}ResultsRequestLambda
Handler: results_request.handler
MemorySize: 128
Role: !GetAtt ResultsRequestLambdaFunctionExecutionRole.Arn
Runtime: python3.14
GameRequestLambdaFunction:
Type: "AWS::Lambda::Function"
Properties:
Code:
S3Bucket: !Ref LambdaZipS3BucketParameter
S3Key: !Ref LambdaZipS3KeyParameter
Description: Lambda function to handle game requests
Environment:
Variables:
FleetAlias: !Ref AliasResource
MaxPlayersPerGame: !Ref MaxPlayersPerGameParameter
FunctionName: !Sub ${GameNameParameter}GameRequestLambda
Handler: game_request.handler
MemorySize: 128
Role: !GetAtt GameRequestLambdaFunctionExecutionRole.Arn
Runtime: python3.14
ResultsRequestLambdaFunctionApiGatewayPermission:
Type: "AWS::Lambda::Permission"
Properties:
Action: "lambda:InvokeFunction"
FunctionName: !GetAtt ResultsRequestLambdaFunction.Arn
Principal: apigateway.amazonaws.com
SourceArn: !Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${RestApi}/*/*/*"
GameRequestLambdaFunctionApiGatewayPermission:
Type: "AWS::Lambda::Permission"
Properties:
Action: "lambda:InvokeFunction"
FunctionName: !GetAtt GameRequestLambdaFunction.Arn
Principal: apigateway.amazonaws.com
SourceArn: !Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${RestApi}/*/*/*"
Outputs:
ApiGatewayEndpoint:
Description: Url of ApiGateway Endpoint
Value: !Sub "https://${RestApi}.execute-api.${AWS::Region}.amazonaws.com/${ApiGatewayStageNameParameter}/"
UserPoolClientId:
Description: Id of UserPoolClient
Value: !Ref UserPoolClient
IdentityRegion:
Description: Region name
Value: !Ref "AWS::Region"

View File

@@ -0,0 +1,33 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
---
# THIS IS A SAMPLE CLOUDFORMATION PARAMETERS FILE
GameNameParameter:
value: "{{AWSGAMELIFT::SYS::GAMENAME}}"
LambdaZipS3BucketParameter:
value: "{{AWSGAMELIFT::VARS::LambdaZipS3BucketParameter}}"
LambdaZipS3KeyParameter:
value: "{{AWSGAMELIFT::VARS::LambdaZipS3KeyParameter}}"
ApiGatewayStageNameParameter:
value: "{{AWSGAMELIFT::VARS::ApiGatewayStageNameParameter}}"
ContainerGroupDefinitionNameParameter:
value: "{{AWSGAMELIFT::VARS::ContainerGroupDefinitionNameParameter}}"
ContainerImageNameParameter:
value: "{{AWSGAMELIFT::VARS::ContainerImageNameParameter}}"
ContainerImageUriParameter:
value: "{{AWSGAMELIFT::VARS::ContainerImageUriParameter}}"
LaunchPathParameter:
value: "{{AWSGAMELIFT::VARS::LaunchPathParameter}}"
TotalVcpuLimitParameter:
value: "{{AWSGAMELIFT::VARS::TotalVcpuLimitParameter}}"
TotalMemoryLimitParameter:
value: "{{AWSGAMELIFT::VARS::TotalMemoryLimitParameter}}"
FleetUdpFromPortParameter:
value: "{{AWSGAMELIFT::VARS::FleetUdpFromPortParameter}}"
FleetUdpToPortParameter:
value: "{{AWSGAMELIFT::VARS::FleetUdpToPortParameter}}"
UnrealEngineVersionParameter:
value: "{{AWSGAMELIFT::VARS::UnrealEngineVersionParameter}}"
EnableMetricsParameter:
value: "{{AWSGAMELIFT::VARS::EnableMetricsParameter}}"

View File

@@ -0,0 +1,11 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
---
# Key/value pairs to be added to the game's awsGameLiftClientConfig.yml file
# These values will be replaced at the end of create/update of
# the feature's CloudFormation stack.
user_pool_client_id: "{{AWSGAMELIFT::CFNOUTPUT::UserPoolClientId}}"
identity_api_gateway_base_url: "{{AWSGAMELIFT::CFNOUTPUT::ApiGatewayEndpoint}}"
identity_region: "{{AWSGAMELIFT::CFNOUTPUT::IdentityRegion}}"

View File

@@ -0,0 +1,79 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
import boto3
import os
import json
def handler(event, context):
"""
Handles requests to start games from the game client.
This function first looks for any game session
:param event: lambda event, contains the region to player latency mapping in `regionToLatencyMapping` key, as well
as the player information from the Cognito id tokens.
:param context: lambda context, not used by this function
:return:
- 202 (Accepted) if the matchmaking request is accepted and is now being processed
- 409 (Conflict) if the another matchmaking request is in progress
- 500 (Internal Error) if error occurred when processing the matchmaking request
"""
gamelift = boto3.client('gamelift')
fleet_alias = os.environ['FleetAlias']
max_players_per_game = int(os.environ['MaxPlayersPerGame'])
player_id = event["requestContext"]["authorizer"]["claims"]["sub"]
print(f'Handling start game request. PlayerId: {player_id}')
# NOTE: latency mapping is not used in this deployment scenario
region_to_latency_mapping = get_region_to_latency_mapping(event)
if region_to_latency_mapping:
print(f"Region to latency mapping: {region_to_latency_mapping}")
else:
print("No regionToLatencyMapping mapping provided")
if not has_viable_game_sessions(gamelift, fleet_alias):
create_game_session(gamelift, fleet_alias, max_players_per_game)
return {
'headers': {
'Content-Type': 'text/plain'
},
'statusCode': 202
}
def has_viable_game_sessions(gamelift, fleet_alias):
print(f"Checking for viable game sessions: {fleet_alias}")
# NOTE: SortExpression="creationTimeMillis ASC" is not needed because we are looking for any viable game sessions,
# hence the order does not matter.
search_game_sessions_response = gamelift.search_game_sessions(
AliasId=fleet_alias,
FilterExpression="hasAvailablePlayerSessions=true",
)
return len(search_game_sessions_response['GameSessions']) != 0
def create_game_session(gamelift, fleet_alias, max_players_per_game):
print(f"Creating game session: {fleet_alias}")
gamelift.create_game_session(
AliasId=fleet_alias,
MaximumPlayerSessionCount=max_players_per_game,
)
def get_region_to_latency_mapping(event):
request_body = event.get("body")
if not request_body:
return None
try:
request_body_json = json.loads(request_body)
except ValueError:
print(f"Error parsing request body: {request_body}")
return None
if request_body_json and request_body_json.get('regionToLatencyMapping'):
return request_body_json.get('regionToLatencyMapping')

View File

@@ -0,0 +1,73 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
import boto3
import os
import json
def handler(event, context):
"""
Handles requests to describe the game session connection information after a StartGame request.
This function will look up the MatchmakingRequest table to find a pending matchmaking request by
the player, and if it is QUEUED, look up the GameSessionPlacement table to find the game's
connection information.
:param event: lambda event, contains the region to player latency mapping in `regionToLatencyMapping` key, as well
as the player information from the Cognito id tokens. Cognito provides a `sub` user attribute that we use as a
Player ID. Unlike `username` value `sub` is UUID for a user which is never reassigned to another user.
:param context: lambda context, not used by this function
:return:
- 200 (OK) if the game connection is ready, along with server info: "IpAddress", "Port", "DnsName", "PlayerSessionId", "PlayerId"
- 204 (No Content) if the requested game is still in progress of matchmaking
- 404 (Not Found) if no game has been started by the player, or if all started game were expired
- 500 (Internal Error) if errors occurred during matchmaking or placement
"""
gamelift = boto3.client('gamelift')
fleet_alias = os.environ['FleetAlias']
player_id = event["requestContext"]["authorizer"]["claims"]["sub"]
print(f'Handling request result request. PlayerId: {player_id}')
oldest_viable_game_session = get_oldest_viable_game_session(gamelift, fleet_alias)
if oldest_viable_game_session:
player_session = create_player_session(gamelift, oldest_viable_game_session['GameSessionId'], player_id)
game_session_connection_info = dict((k, player_session[k]) for k in ('IpAddress', 'Port', 'DnsName', 'PlayerSessionId', 'PlayerId'))
game_session_connection_info['GameSessionArn'] = player_session['GameSessionId']
print(f"Connection info: {game_session_connection_info}")
return {
'body': json.dumps(game_session_connection_info),
'headers': {
'Content-Type': 'text/plain'
},
'statusCode': 200
}
else:
return {
'headers': {
'Content-Type': 'text/plain'
},
'statusCode': 404
}
def get_oldest_viable_game_session(gamelift, fleet_alias):
print("Checking for viable game sessions:", fleet_alias)
search_game_sessions_response = gamelift.search_game_sessions(
AliasId=fleet_alias,
FilterExpression="hasAvailablePlayerSessions=true",
SortExpression="creationTimeMillis ASC",
)
print(f"Received search game session response: {search_game_sessions_response}")
return next(iter(search_game_sessions_response['GameSessions']), None)
def create_player_session(gamelift, game_session_id, player_id):
print(f"Creating PlayerSession session on GameSession: {game_session_id}, PlayerId: {player_id}")
create_player_session_response = gamelift.create_player_session(
GameSessionId=game_session_id,
PlayerId=player_id
)
print(f"Received create player session response: {create_player_session_response}")
return create_player_session_response['PlayerSession']

View File

@@ -0,0 +1,43 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
#
# GameLift Region Mappings
# Keep synchronized with: https://docs.aws.amazon.com/general/latest/gr/gamelift.html
#
# ----------------------------------------------------------------------------------------
# Region Name Region Endpoint Protocol
# ----------------------------------------------------------------------------------------
# US East (Ohio) us-east-2 gamelift.us-east-2.amazonaws.com HTTPS
# US East (N. Virginia) us-east-1 gamelift.us-east-1.amazonaws.com HTTPS
# US West (N. California) us-west-1 gamelift.us-west-1.amazonaws.com HTTPS
# US West (Oregon) us-west-2 gamelift.us-west-2.amazonaws.com HTTPS
# Asia Pacific (Mumbai) ap-south-1 gamelift.ap-south-1.amazonaws.com HTTPS
# Asia Pacific (Seoul) ap-northeast-2 gamelift.ap-northeast-2.amazonaws.com HTTPS
# Asia Pacific (Singapore) ap-southeast-1 gamelift.ap-southeast-1.amazonaws.com HTTPS
# Asia Pacific (Sydney) ap-southeast-2 gamelift.ap-southeast-2.amazonaws.com HTTPS
# Asia Pacific (Tokyo) ap-northeast-1 gamelift.ap-northeast-1.amazonaws.com HTTPS
# Canada (Central) ca-central-1 gamelift.ca-central-1.amazonaws.com HTTPS
# Europe (Frankfurt) eu-central-1 gamelift.eu-central-1.amazonaws.com HTTPS
# Europe (Ireland) eu-west-1 gamelift.eu-west-1.amazonaws.com HTTPS
# Europe (London) eu-west-2 gamelift.eu-west-2.amazonaws.com HTTPS
# South America (São Paulo) sa-east-1 gamelift.sa-east-1.amazonaws.com HTTPS
# ----------------------------------------------------------------------------------------
{
five_letter_region_codes: {
us-east-2 : usea2,
us-east-1 : usea1,
us-west-1 : uswe1,
us-west-2 : uswe2,
ap-south-1 : apso1,
ap-northeast-2 : apne2,
ap-southeast-1 : apse1,
ap-southeast-2 : apse2,
ap-northeast-1 : apne1,
ca-central-1 : cace1,
eu-central-1 : euce1,
eu-west-1 : euwe1,
eu-west-2 : euwe2,
sa-east-1 : saea1
}
}

View File

@@ -0,0 +1,161 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
import boto3
import requests
import json
import time
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("-g", "--game", help="game name", type=str, required=True)
parser.add_argument("-r", "--region", help="region name, e.g. eu-west-1", type=str, required=True)
parser.add_argument("-p", "--profile", help="profile name in the AWS shared credentials file ~/.aws/credentials", type=str, required=True)
args = parser.parse_args()
GAME_NAME = args.game.lower() # e.g. 'GameLiftSampleGame2ue4'
REGION = args.region # e.g. 'eu-west-1'
PROFILE_NAME = args.profile
USER_POOL_NAME = GAME_NAME + 'UserPool'
USER_POOL_CLIENT_NAME = GAME_NAME + 'UserPoolClient'
USERNAME = 'testuser@example.com'
PASSWORD = 'TestPassw0rd.'
REST_API_NAME = GAME_NAME + 'RestApi'
REST_API_STAGE = 'dev'
GAME_REQUEST_PATH = 'start_game'
RESULTS_REQUEST_PATH = 'get_game_connection'
session = boto3.Session(profile_name=PROFILE_NAME)
cognito_idp = session.client('cognito-idp', region_name=REGION)
apig = session.client('apigateway', region_name=REGION)
REGION_TO_LATENCY_MAPPING = {
"regionToLatencyMapping": {
"us-west-2": 50,
"us-east-1": 100,
"eu-west-1": 150,
"ap-northeast-1": 300
}
}
GAME_REQUEST_PAYLOAD = json.dumps(REGION_TO_LATENCY_MAPPING)
def main():
user_pool = find_user_pool(USER_POOL_NAME)
user_pool_id = user_pool['Id']
print("User Pool Id:", user_pool_id)
user_pool_client = find_user_pool_client(user_pool_id, USER_POOL_CLIENT_NAME)
user_pool_client_id = user_pool_client['ClientId']
print("User Pool Client Id:", user_pool_client_id)
try:
cognito_idp.sign_up(
ClientId=user_pool_client_id,
Username=USERNAME,
Password=PASSWORD,
)
print("Created user:", USERNAME)
cognito_idp.admin_confirm_sign_up(
UserPoolId=user_pool_id,
Username=USERNAME,
)
init_auth_result = cognito_idp.initiate_auth(
AuthFlow='USER_PASSWORD_AUTH',
AuthParameters={
'USERNAME': USERNAME,
'PASSWORD': PASSWORD,
},
ClientId=user_pool_client_id
)
assert init_auth_result['ResponseMetadata']['HTTPStatusCode'] == 200, "Unsuccessful init_auth"
print("Authenticated via username and password")
id_token = init_auth_result['AuthenticationResult']['IdToken']
headers = {
'Auth': id_token
}
results_request_url = get_rest_api_endpoint(REST_API_NAME, REGION, REST_API_STAGE, RESULTS_REQUEST_PATH)
game_request_url = get_rest_api_endpoint(REST_API_NAME, REGION, REST_API_STAGE, GAME_REQUEST_PATH)
print ("results_request_url: " + results_request_url)
print ("game_request_url: " + game_request_url)
results_request_response = requests.post(url=results_request_url, headers=headers)
assert results_request_response.status_code == 204 or results_request_response.status_code == 200, \
"Expect 'POST /get_game_info' status code to be 200 (Success) or 204 (No Content). Actual: " \
f"{str(results_request_response.status_code)}"
print("Verified mock ResultsRequest response", results_request_response)
game_request_response = requests.post(url=game_request_url, headers=headers, data=GAME_REQUEST_PAYLOAD)
assert game_request_response.status_code == 202, "Expect 'POST /start_game' status code to be 202 (Accepted)/ Actual: " \
f"{str(results_request_response.status_code)}"
print("Verified lambda GameRequest response", game_request_response)
#game_request_info = json.loads(game_request_response.content)
print(f"Received start game info: {game_request_response}")
print("Waiting for game session to be created...")
time.sleep(10) # 10 seconds
results_request_response = requests.post(url=results_request_url, headers=headers)
assert results_request_response.status_code == 200, "Expect 'POST /get_game_info' status code to be 200 (Success). Actual: " \
f"{str(results_request_response.status_code)}"
print("Verified lambda ResultsRequest response", results_request_response.content)
game_connection_info = json.loads(results_request_response.content)
print(f"Received game connection info: {game_connection_info}")
assert game_connection_info['IpAddress'] != ''
assert game_connection_info['Port'] > 0
assert REGION in game_connection_info['DnsName'], \
f"Expect {game_connection_info['DnsName']} to contain '{REGION}'"
assert "psess-" in game_connection_info['PlayerSessionId'], \
f"Expect {game_connection_info['PlayerSessionId']} to contain 'psess-'"
assert REGION in game_connection_info['GameSessionArn'], \
f"Expect {game_connection_info['GameSessionArn']} to contain '{REGION}'"
print("Verified game connection info:", game_connection_info)
finally:
cognito_idp.admin_delete_user(
UserPoolId=user_pool_id,
Username=USERNAME,
)
print("Deleted user:", USERNAME)
print("Test Succeeded!")
def find_user_pool(user_pool_name):
print("Finding user pool:", user_pool_name)
result = cognito_idp.list_user_pools(MaxResults=50)
pools = result['UserPools']
return next(x for x in pools if x['Name'] == user_pool_name)
def find_user_pool_client(user_pool_id, user_pool_client_name):
print("Finding user pool client:", user_pool_client_name)
results = cognito_idp.list_user_pool_clients(UserPoolId=user_pool_id)
clients = results['UserPoolClients']
return next(x for x in clients if x['ClientName'] == user_pool_client_name)
def find_rest_api(rest_api_name):
print("Finding rest api:", rest_api_name)
results = apig.get_rest_apis()
rest_apis = results['items']
return next(x for x in rest_apis if x['name'] == rest_api_name)
def get_rest_api_endpoint(rest_api_name, region, stage, path):
print("Getting rest api endpoint", rest_api_name)
rest_api = find_rest_api(rest_api_name)
rest_api_id = rest_api['id']
return f'https://{rest_api_id}.execute-api.{region}.amazonaws.com/{stage}/{path}'
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,864 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: MIT-0
AWSTemplateFormatVersion: "2010-09-09"
Description: >
This CloudFormation template sets up a scenario to use FlexMatch -- a managed matchmaking service provided by
GameLift. The template demonstrates best practices in acquiring the matchmaking ticket status, by listening to
FlexMatch events in conjunction with a low frequency poller to ensure incomplete tickets are periodically pinged
and therefore are not discarded by GameLift.
Parameters:
ApiGatewayStageNameParameter:
Type: String
Default: v1
Description: Name of the Api Gateway stage
BuildNameParameter:
Type: String
Default: Sample GameLift Build
Description: Name of the build
BuildOperatingSystemParameter:
Type: String
Default: "WINDOWS_2016"
Description: Operating system of the build
BuildServerSdkVersionParameter:
Type: String
Description: GameLift Server SDK version used in the server build
BuildS3BucketParameter:
Type: String
Description: Bucket that stores the server build
BuildS3KeyParameter:
Type: String
Description: Key of the server build in the S3 bucket
BuildVersionParameter:
Type: String
Description: Version number of the build
FleetDescriptionParameter:
Type: String
Default: Deployed by the Amazon GameLift Plug-in for Unreal.
Description: Description of the fleet
FleetNameParameter:
Type: String
Default: Sample GameLift Fleet
Description: Name of the fleet
FleetTcpFromPortParameter:
Type: Number
Default: 33430
Description: Starting port number for TCP ports to be opened
FleetTcpToPortParameter:
Type: Number
Default: 33440
Description: Ending port number for TCP ports to be opened
FleetUdpFromPortParameter:
Type: Number
Default: 33430
Description: Starting port number for UDP ports to be opened
FleetUdpToPortParameter:
Type: Number
Default: 33440
Description: Ending port number for UDP ports to be opened
GameNameParameter:
Type: String
Default: MyGame
Description: Game name to prepend before resource names
MaxLength: 30
LambdaZipS3BucketParameter:
Type: String
Description: S3 bucket that stores the lambda function zip
LambdaZipS3KeyParameter:
Type: String
Description: S3 key that stores the lambda function zip
LaunchParametersParameter:
Type: String
Description: Parameters used to launch the game server process
LaunchPathParameter:
Type: String
Description: Location of the game server executable in the build
MatchmakerTimeoutInSecondsParameter:
Type: Number
Default: 60
Description: Time in seconds before matchmaker times out to place players on a server
MatchmakingTimeoutInSecondsParameter:
Type: Number
Default: 60
Description: Time in seconds before matchmaker times out to wait for enough players to create game session placement
MaxTransactionsPerFiveMinutesPerIpParameter:
Type: Number
Default: 100
MaxValue: 20000000
MinValue: 100
NumPlayersPerGameParameter:
Type: Number
Default: 2
Description: Number of players per game session
QueueTimeoutInSecondsParameter:
Type: Number
Default: 60
Description: Time in seconds before game session placement times out to place players on a server
TeamNameParameter:
Type: String
Default: MySampleTeam
Description: Team name used in matchmaking ruleset and StartMatchmaking API requests
TicketIdIndexNameParameter:
Type: String
Default: ticket-id-index
Description: Name of the global secondary index on MatchmakingRequest table with partition key TicketId
UnrealEngineVersionParameter:
Type: String
Description: "Unreal Engine Version being used by the plugin"
EnableMetricsParameter:
Type: String
Default: "false"
AllowedValues: ["true", "false"]
Description: "Enable telemetry metrics collection using OTEL collector"
Conditions:
ShouldCreateMetricsResources: !Equals [!Ref EnableMetricsParameter, "true"]
Resources:
ApiGatewayCloudWatchRole:
Type: "AWS::IAM::Role"
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- apigateway.amazonaws.com
Action: "sts:AssumeRole"
ManagedPolicyArns:
- "arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs"
Account:
Type: "AWS::ApiGateway::Account"
Properties:
CloudWatchRoleArn: !GetAtt ApiGatewayCloudWatchRole.Arn
FlexMatchStatusPollerLambdaFunctionExecutionRole:
Type: "AWS::IAM::Role"
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- "sts:AssumeRole"
ManagedPolicyArns:
- "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
Policies:
- PolicyName: !Sub ${GameNameParameter}FlexMatchStatusPollerLambdaFunctionPolicies
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- "dynamodb:Scan"
- "dynamodb:UpdateItem"
- "gamelift:DescribeMatchmaking"
Resource: "*"
GameRequestLambdaFunctionExecutionRole:
Type: "AWS::IAM::Role"
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- "sts:AssumeRole"
ManagedPolicyArns:
- "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
Policies:
- PolicyName: !Sub ${GameNameParameter}GameRequestLambdaFunctionPolicies
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- "dynamodb:PutItem"
- "dynamodb:UpdateItem"
- "dynamodb:GetItem"
- "dynamodb:Query"
- "gamelift:StartMatchmaking"
Resource: "*"
MatchmakerEventHandlerLambdaFunctionExecutionRole:
Type: "AWS::IAM::Role"
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- "sts:AssumeRole"
ManagedPolicyArns:
- "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
Policies:
- PolicyName: !Sub ${GameNameParameter}MatchmakerEventHandlerLambdaFunctionPolicies
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- "dynamodb:Query"
- "dynamodb:UpdateItem"
Resource: "*"
RestApi:
Type: "AWS::ApiGateway::RestApi"
Properties:
Name: !Sub ${GameNameParameter}RestApi
ResultsRequestLambdaFunctionExecutionRole:
Type: "AWS::IAM::Role"
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- "sts:AssumeRole"
ManagedPolicyArns:
- "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
Policies:
- PolicyName: !Sub ${GameNameParameter}ResultsRequestLambdaFunctionPolicies
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- "dynamodb:Query"
Resource: "*"
MetricsInstanceRole:
Type: "AWS::IAM::Role"
Condition: ShouldCreateMetricsResources
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: gamelift.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: AMPRemoteWriteAccess
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- aps:RemoteWrite
Resource: !Sub 'arn:aws:aps:*:${AWS::AccountId}:workspace/*'
- PolicyName: OTelCollectorEMFExporter
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- logs:PutLogEvents
- logs:CreateLogStream
- logs:CreateLogGroup
- logs:PutRetentionPolicy
Resource: !Sub 'arn:aws:logs:*:${AWS::AccountId}:log-group:*:log-stream:*'
UserPool:
Type: "AWS::Cognito::UserPool"
Properties:
AdminCreateUserConfig:
AllowAdminCreateUserOnly: false
AutoVerifiedAttributes:
- email
EmailConfiguration:
EmailSendingAccount: COGNITO_DEFAULT
EmailVerificationMessage: "Please verify your email to complete account registration for the GameLift Plugin FlexMatch fleet deployment scenario. Confirmation Code {####}."
EmailVerificationSubject: GameLift Plugin - Deployment Scenario Account Verification
Policies:
PasswordPolicy:
MinimumLength: 8
RequireLowercase: true
RequireNumbers: true
RequireSymbols: true
RequireUppercase: true
Schema:
- Name: email
AttributeDataType: String
Mutable: false
Required: true
UserPoolName: !Sub ${GameNameParameter}UserPool
UsernameAttributes:
- email
BuildAccessRole:
Type: "AWS::IAM::Role"
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- cloudformation.amazonaws.com
- gamelift.amazonaws.com
Action: "sts:AssumeRole"
Policies:
- PolicyName: !Sub ${GameNameParameter}BuildS3AccessPolicy
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- "s3:GetObject"
- "s3:GetObjectVersion"
Resource:
- "Fn::Sub": "arn:aws:s3:::${BuildS3BucketParameter}/${BuildS3KeyParameter}"
RoleName: !Sub ${GameNameParameter}BuildIAMRole
GameRequestApiResource:
Type: "AWS::ApiGateway::Resource"
Properties:
ParentId: !GetAtt RestApi.RootResourceId
PathPart: start_game
RestApiId: !Ref RestApi
MatchmakingRequestTable:
Type: "AWS::DynamoDB::Table"
Properties:
AttributeDefinitions:
- AttributeName: PlayerId
AttributeType: S
- AttributeName: TicketId
AttributeType: S
- AttributeName: StartTime
AttributeType: "N"
GlobalSecondaryIndexes:
- IndexName: !Ref TicketIdIndexNameParameter
KeySchema:
- AttributeName: TicketId
KeyType: HASH
Projection:
ProjectionType: ALL
ProvisionedThroughput:
ReadCapacityUnits: 5
WriteCapacityUnits: 5
KeySchema:
- AttributeName: PlayerId
KeyType: HASH
- AttributeName: StartTime
KeyType: RANGE
ProvisionedThroughput:
ReadCapacityUnits: 5
WriteCapacityUnits: 5
TableName: !Sub ${GameNameParameter}MatchmakingRequestTable
TimeToLiveSpecification:
AttributeName: ExpirationTime
Enabled: true
ResultsRequestApiResource:
Type: "AWS::ApiGateway::Resource"
Properties:
ParentId: !GetAtt RestApi.RootResourceId
PathPart: get_game_connection
RestApiId: !Ref RestApi
UserPoolClient:
Type: "AWS::Cognito::UserPoolClient"
Properties:
AccessTokenValidity: 1
ClientName: !Sub ${GameNameParameter}UserPoolClient
ExplicitAuthFlows:
- ALLOW_USER_PASSWORD_AUTH
- ALLOW_REFRESH_TOKEN_AUTH
GenerateSecret: false
IdTokenValidity: 1
PreventUserExistenceErrors: ENABLED
ReadAttributes:
- email
- preferred_username
RefreshTokenValidity: 30
SupportedIdentityProviders:
- COGNITO
UserPoolId: !Ref UserPool
WebACL:
Type: "AWS::WAFv2::WebACL"
DependsOn:
- ApiDeployment
Properties:
DefaultAction:
Allow:
{}
Description: !Sub "WebACL for game: ${GameNameParameter}"
Name: !Sub ${GameNameParameter}WebACL
Rules:
- Name: !Sub ${GameNameParameter}WebACLPerIpThrottleRule
Action:
Block:
{}
Priority: 0
Statement:
RateBasedStatement:
AggregateKeyType: IP
Limit: !Ref MaxTransactionsPerFiveMinutesPerIpParameter
VisibilityConfig:
CloudWatchMetricsEnabled: true
MetricName: !Sub ${GameNameParameter}WebACLPerIpThrottleRuleMetrics
SampledRequestsEnabled: true
Scope: REGIONAL
VisibilityConfig:
CloudWatchMetricsEnabled: true
MetricName: !Sub ${GameNameParameter}WebACLMetrics
SampledRequestsEnabled: true
ApiDeployment:
Type: "AWS::ApiGateway::Deployment"
DependsOn:
- GameRequestApiMethod
- ResultsRequestApiMethod
Properties:
RestApiId: !Ref RestApi
StageDescription:
DataTraceEnabled: true
LoggingLevel: INFO
MetricsEnabled: true
StageName: !Ref ApiGatewayStageNameParameter
Authorizer:
Type: "AWS::ApiGateway::Authorizer"
Properties:
IdentitySource: method.request.header.Auth
Name: CognitoAuthorizer
ProviderARNs:
- "Fn::GetAtt":
- UserPool
- Arn
RestApiId: !Ref RestApi
Type: COGNITO_USER_POOLS
GameSessionQueue:
Type: "AWS::GameLift::GameSessionQueue"
Properties:
Destinations:
- DestinationArn: !Sub "arn:aws:gamelift:${AWS::Region}:${AWS::AccountId}:fleet/${OnDemandFleetResource}"
- DestinationArn: !Sub "arn:aws:gamelift:${AWS::Region}:${AWS::AccountId}:fleet/${C5LargeSpotFleetResource}"
- DestinationArn: !Sub "arn:aws:gamelift:${AWS::Region}:${AWS::AccountId}:fleet/${C4LargeSpotFleetResource}"
Name: !Sub ${GameNameParameter}GameSessionQueue
TimeoutInSeconds: !Ref QueueTimeoutInSecondsParameter
MatchmakingRuleSet:
Type: "AWS::GameLift::MatchmakingRuleSet"
Properties:
Name: !Sub ${GameNameParameter}MatchmakingRuleSet
RuleSetBody: !Sub
- |-
{
"name": "MyMatchmakingRuleSet",
"ruleLanguageVersion": "1.0",
"teams": [{
"name": "${teamName}",
"minPlayers": ${minPlayers},
"maxPlayers": ${maxPlayers}
}]
}
- maxPlayers: !Ref NumPlayersPerGameParameter
minPlayers: !Ref NumPlayersPerGameParameter
teamName: !Ref TeamNameParameter
FlexMatchStatusPollerLambdaFunction:
Type: "AWS::Lambda::Function"
Properties:
Code:
S3Bucket: !Ref LambdaZipS3BucketParameter
S3Key: !Ref LambdaZipS3KeyParameter
Description: Lambda function to handle game requests
Environment:
Variables:
MatchmakingRequestTableName: !Ref MatchmakingRequestTable
FunctionName: !Sub ${GameNameParameter}FlexMatchStatusPollerLambda
Handler: flexmatch_status_poller.handler
MemorySize: 128
Role: !GetAtt FlexMatchStatusPollerLambdaFunctionExecutionRole.Arn
Runtime: python3.14
GameRequestApiMethod:
Type: "AWS::ApiGateway::Method"
Properties:
AuthorizationType: COGNITO_USER_POOLS
AuthorizerId: !Ref Authorizer
HttpMethod: POST
Integration:
Type: AWS_PROXY
IntegrationHttpMethod: POST
Uri: !Sub "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GameRequestLambdaFunction.Arn}/invocations"
OperationName: GameRequest
ResourceId: !Ref GameRequestApiResource
RestApiId: !Ref RestApi
MatchmakerEventHandlerLambdaFunction:
Type: "AWS::Lambda::Function"
Properties:
Code:
S3Bucket: !Ref LambdaZipS3BucketParameter
S3Key: !Ref LambdaZipS3KeyParameter
Description: Lambda function to handle game requests
Environment:
Variables:
MatchmakingRequestTableName: !Ref MatchmakingRequestTable
TicketIdIndexName: !Ref TicketIdIndexNameParameter
FunctionName: !Sub ${GameNameParameter}MatchmakerEventHandlerLambda
Handler: matchmaker_event_handler.handler
MemorySize: 128
Role: !GetAtt MatchmakerEventHandlerLambdaFunctionExecutionRole.Arn
Runtime: python3.14
ResultsRequestApiMethod:
Type: "AWS::ApiGateway::Method"
Properties:
AuthorizationType: COGNITO_USER_POOLS
AuthorizerId: !Ref Authorizer
HttpMethod: POST
Integration:
Type: AWS_PROXY
IntegrationHttpMethod: POST
Uri: !Sub "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ResultsRequestLambdaFunction.Arn}/invocations"
OperationName: ResultsRequest
ResourceId: !Ref ResultsRequestApiResource
RestApiId: !Ref RestApi
ResultsRequestLambdaFunction:
Type: "AWS::Lambda::Function"
Properties:
Code:
S3Bucket: !Ref LambdaZipS3BucketParameter
S3Key: !Ref LambdaZipS3KeyParameter
Description: Lambda function to handle game requests
Environment:
Variables:
MatchmakingRequestTableName: !Ref MatchmakingRequestTable
FunctionName: !Sub ${GameNameParameter}ResultsRequestLambda
Handler: results_request.handler
MemorySize: 128
Role: !GetAtt ResultsRequestLambdaFunctionExecutionRole.Arn
Runtime: python3.14
WebACLAssociation:
Type: "AWS::WAFv2::WebACLAssociation"
DependsOn:
- ApiDeployment
- WebACL
Properties:
ResourceArn: !Sub
- "arn:aws:apigateway:${REGION}::/restapis/${REST_API_ID}/stages/${STAGE_NAME}"
- REGION: !Ref "AWS::Region"
REST_API_ID: !Ref RestApi
STAGE_NAME: !Ref ApiGatewayStageNameParameter
WebACLArn: !GetAtt WebACL.Arn
FlexMatchStatusPollerScheduledRule:
Type: "AWS::Events::Rule"
Properties:
Description: !Sub ${GameNameParameter}FlexMatchStatusPollerScheduledRule
ScheduleExpression: rate(1 minute)
State: ENABLED
Targets:
- Arn: !GetAtt FlexMatchStatusPollerLambdaFunction.Arn
Id: !Sub ${GameNameParameter}FlexMatchStatusPollerScheduledRule
MatchmakerEventTopicKey:
Type: "AWS::KMS::Key"
Properties:
Description: KMS key for FlexMatch SNS notifications
KeyPolicy:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
AWS: !Sub "arn:aws:iam::${AWS::AccountId}:root"
Action: "kms:*"
Resource: "*"
- Effect: Allow
Principal:
Service: gamelift.amazonaws.com
Action:
- "kms:Decrypt"
- "kms:GenerateDataKey"
Resource: "*"
MatchmakerEventTopic:
Type: "AWS::SNS::Topic"
Properties:
KmsMasterKeyId: !Ref MatchmakerEventTopicKey
Subscription:
- Endpoint: !GetAtt MatchmakerEventHandlerLambdaFunction.Arn
Protocol: lambda
TopicName: !Sub ${GameNameParameter}MatchmakerEventTopic
ServerBuild:
Type: "AWS::GameLift::Build"
Properties:
Name: !Ref BuildNameParameter
OperatingSystem: !Ref BuildOperatingSystemParameter
ServerSdkVersion: !Ref BuildServerSdkVersionParameter
StorageLocation:
Bucket: !Ref BuildS3BucketParameter
Key: !Ref BuildS3KeyParameter
RoleArn: !GetAtt BuildAccessRole.Arn
Version: !Ref BuildVersionParameter
FlexMatchStatusPollerLambdaPermission:
Type: "AWS::Lambda::Permission"
Properties:
Action: "lambda:InvokeFunction"
FunctionName: !Ref FlexMatchStatusPollerLambdaFunction
Principal: events.amazonaws.com
SourceArn: !GetAtt FlexMatchStatusPollerScheduledRule.Arn
MatchmakerEventHandlerLambdaPermission:
Type: "AWS::Lambda::Permission"
Properties:
Action: "lambda:InvokeFunction"
FunctionName: !Ref MatchmakerEventHandlerLambdaFunction
Principal: sns.amazonaws.com
SourceArn: !Ref MatchmakerEventTopic
MatchmakerEventTopicPolicy:
Type: "AWS::SNS::TopicPolicy"
DependsOn: MatchmakerEventTopic
Properties:
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service: gamelift.amazonaws.com
Action:
- "sns:Publish"
Resource: !Ref MatchmakerEventTopic
Topics:
- Ref: MatchmakerEventTopic
ResultsRequestLambdaFunctionApiGatewayPermission:
Type: "AWS::Lambda::Permission"
Properties:
Action: "lambda:InvokeFunction"
FunctionName: !GetAtt ResultsRequestLambdaFunction.Arn
Principal: apigateway.amazonaws.com
SourceArn: !Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${RestApi}/*/*/*"
MatchmakingConfiguration:
Type: "AWS::GameLift::MatchmakingConfiguration"
DependsOn:
- GameSessionQueue
- MatchmakingRuleSet
Properties:
AcceptanceRequired: false
BackfillMode: MANUAL
Description: Matchmaking configuration for sample GameLift game
FlexMatchMode: WITH_QUEUE
GameSessionQueueArns:
- "Fn::GetAtt":
- GameSessionQueue
- Arn
Name: !Sub ${GameNameParameter}MatchmakingConfiguration
NotificationTarget: !Ref MatchmakerEventTopic
RequestTimeoutSeconds: !Ref MatchmakerTimeoutInSecondsParameter
RuleSetName: !Ref MatchmakingRuleSet
C4LargeSpotFleetResource:
Type: "AWS::GameLift::Fleet"
Properties:
BuildId: !Ref ServerBuild
CertificateConfiguration:
CertificateType: GENERATED
Description: !Sub
- "${FleetDescriptionParameter} Using Unreal Engine Version ${UnrealEngineVersionParameter}${MetricsText}"
- MetricsText: !If [ShouldCreateMetricsResources, " with telemetry metrics enabled", ""]
DesiredEC2Instances: 1
EC2InboundPermissions:
- FromPort: !Ref FleetTcpFromPortParameter
IpRange: "0.0.0.0/0"
Protocol: TCP
ToPort: !Ref FleetTcpToPortParameter
- FromPort: !Ref FleetUdpFromPortParameter
IpRange: "0.0.0.0/0"
Protocol: UDP
ToPort: !Ref FleetUdpToPortParameter
EC2InstanceType: c4.large
FleetType: SPOT
InstanceRoleARN: !If [ShouldCreateMetricsResources, !GetAtt MetricsInstanceRole.Arn, !Ref "AWS::NoValue"]
InstanceRoleCredentialsProvider: !If [ShouldCreateMetricsResources, "SHARED_CREDENTIAL_FILE", !Ref "AWS::NoValue"]
Locations:
- Location: us-west-2
- Location: us-east-1
- Location: eu-west-1
Name: !Ref FleetNameParameter
NewGameSessionProtectionPolicy: FullProtection
ResourceCreationLimitPolicy:
NewGameSessionsPerCreator: 5
PolicyPeriodInMinutes: 2
RuntimeConfiguration:
GameSessionActivationTimeoutSeconds: 300
MaxConcurrentGameSessionActivations: 1
ServerProcesses:
- ConcurrentExecutions: 1
LaunchPath: !Ref LaunchPathParameter
Parameters: !Ref LaunchParametersParameter
C5LargeSpotFleetResource:
Type: "AWS::GameLift::Fleet"
Properties:
BuildId: !Ref ServerBuild
CertificateConfiguration:
CertificateType: GENERATED
Description: !Sub
- "${FleetDescriptionParameter} Using Unreal Engine Version ${UnrealEngineVersionParameter}${MetricsText}"
- MetricsText: !If [ShouldCreateMetricsResources, " with telemetry metrics enabled", ""]
DesiredEC2Instances: 1
EC2InboundPermissions:
- FromPort: !Ref FleetTcpFromPortParameter
IpRange: "0.0.0.0/0"
Protocol: TCP
ToPort: !Ref FleetTcpToPortParameter
- FromPort: !Ref FleetUdpFromPortParameter
IpRange: "0.0.0.0/0"
Protocol: UDP
ToPort: !Ref FleetUdpToPortParameter
EC2InstanceType: c5.large
FleetType: SPOT
InstanceRoleARN: !If [ShouldCreateMetricsResources, !GetAtt MetricsInstanceRole.Arn, !Ref "AWS::NoValue"]
InstanceRoleCredentialsProvider: !If [ShouldCreateMetricsResources, "SHARED_CREDENTIAL_FILE", !Ref "AWS::NoValue"]
Locations:
- Location: us-west-2
- Location: us-east-1
- Location: eu-west-1
Name: !Ref FleetNameParameter
NewGameSessionProtectionPolicy: FullProtection
ResourceCreationLimitPolicy:
NewGameSessionsPerCreator: 5
PolicyPeriodInMinutes: 2
RuntimeConfiguration:
GameSessionActivationTimeoutSeconds: 300
MaxConcurrentGameSessionActivations: 1
ServerProcesses:
- ConcurrentExecutions: 1
LaunchPath: !Ref LaunchPathParameter
Parameters: !Ref LaunchParametersParameter
OnDemandFleetResource:
Type: "AWS::GameLift::Fleet"
Properties:
BuildId: !Ref ServerBuild
CertificateConfiguration:
CertificateType: GENERATED
Description: !Sub
- "${FleetDescriptionParameter} Using Unreal Engine Version ${UnrealEngineVersionParameter}${MetricsText}"
- MetricsText: !If [ShouldCreateMetricsResources, " with telemetry metrics enabled", ""]
DesiredEC2Instances: 1
EC2InboundPermissions:
- FromPort: !Ref FleetTcpFromPortParameter
IpRange: "0.0.0.0/0"
Protocol: TCP
ToPort: !Ref FleetTcpToPortParameter
- FromPort: !Ref FleetUdpFromPortParameter
IpRange: "0.0.0.0/0"
Protocol: UDP
ToPort: !Ref FleetUdpToPortParameter
EC2InstanceType: c5.large
FleetType: ON_DEMAND
InstanceRoleARN: !If [ShouldCreateMetricsResources, !GetAtt MetricsInstanceRole.Arn, !Ref "AWS::NoValue"]
InstanceRoleCredentialsProvider: !If [ShouldCreateMetricsResources, "SHARED_CREDENTIAL_FILE", !Ref "AWS::NoValue"]
Locations:
- Location: us-west-2
- Location: us-east-1
- Location: eu-west-1
Name: !Ref FleetNameParameter
NewGameSessionProtectionPolicy: FullProtection
ResourceCreationLimitPolicy:
NewGameSessionsPerCreator: 5
PolicyPeriodInMinutes: 2
RuntimeConfiguration:
GameSessionActivationTimeoutSeconds: 300
MaxConcurrentGameSessionActivations: 1
ServerProcesses:
- ConcurrentExecutions: 1
LaunchPath: !Ref LaunchPathParameter
Parameters: !Ref LaunchParametersParameter
GameRequestLambdaFunction:
Type: "AWS::Lambda::Function"
Properties:
Code:
S3Bucket: !Ref LambdaZipS3BucketParameter
S3Key: !Ref LambdaZipS3KeyParameter
Description: Lambda function to handle game requests
Environment:
Variables:
MatchmakingConfigurationName: !GetAtt MatchmakingConfiguration.Name
MatchmakingRequestTableName: !Ref MatchmakingRequestTable
TeamName: !Ref TeamNameParameter
FunctionName: !Sub ${GameNameParameter}GameRequestLambda
Handler: game_request.handler
MemorySize: 128
Role: !GetAtt GameRequestLambdaFunctionExecutionRole.Arn
Runtime: python3.14
GameRequestLambdaFunctionApiGatewayPermission:
Type: "AWS::Lambda::Permission"
Properties:
Action: "lambda:InvokeFunction"
FunctionName: !GetAtt GameRequestLambdaFunction.Arn
Principal: apigateway.amazonaws.com
SourceArn: !Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${RestApi}/*/*/*"
Outputs:
ApiGatewayEndpoint:
Description: Url of ApiGateway Endpoint
Value: !Sub "https://${RestApi}.execute-api.${AWS::Region}.amazonaws.com/${ApiGatewayStageNameParameter}/"
UserPoolClientId:
Description: Id of UserPoolClient
Value: !Ref UserPoolClient
IdentityRegion:
Description: Region name
Value: !Ref "AWS::Region"

View File

@@ -0,0 +1,44 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
---
# THIS IS A SAMPLE CLOUDFORMATION PARAMETERS FILE
GameNameParameter:
value: "{{AWSGAMELIFT::SYS::GAMENAME}}"
LambdaZipS3BucketParameter:
value: "{{AWSGAMELIFT::VARS::LambdaZipS3BucketParameter}}"
LambdaZipS3KeyParameter:
value: "{{AWSGAMELIFT::VARS::LambdaZipS3KeyParameter}}"
ApiGatewayStageNameParameter:
value: "{{AWSGAMELIFT::VARS::ApiGatewayStageNameParameter}}"
BuildS3BucketParameter:
value: "{{AWSGAMELIFT::VARS::BuildS3BucketParameter}}"
BuildS3KeyParameter:
value: "{{AWSGAMELIFT::VARS::BuildS3KeyParameter}}"
LaunchPathParameter:
value: "{{AWSGAMELIFT::VARS::LaunchPathParameter}}"
LaunchParametersParameter:
value: "-port=7777 -UNATTENDED LOG=server.log"
BuildNameParameter:
value: "Sample GameLift Build for {{AWSGAMELIFT::SYS::GAMENAME}}"
BuildVersionParameter:
value: "1"
BuildOperatingSystemParameter:
value: "{{AWSGAMELIFT::VARS::BuildOperatingSystemParameter}}"
BuildServerSdkVersionParameter:
value: "5.4.0"
FleetTcpFromPortParameter:
value: "7770"
FleetTcpToPortParameter:
value: "7780"
FleetUdpFromPortParameter:
value: "7770"
FleetUdpToPortParameter:
value: "7780"
FleetNameParameter:
value: "FlexMatch Fleet"
UnrealEngineVersionParameter:
value: "{{AWSGAMELIFT::VARS::UnrealEngineVersionParameter}}"
EnableMetricsParameter:
value: "{{AWSGAMELIFT::VARS::EnableMetricsParameter}}"

View File

@@ -0,0 +1,11 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
---
# Key/value pairs to be added to the game's awsGameLiftClientConfig.yml file
# These values will be replaced at the end of create/update of
# the feature's CloudFormation stack.
user_pool_client_id: "{{AWSGAMELIFT::CFNOUTPUT::UserPoolClientId}}"
identity_api_gateway_base_url: "{{AWSGAMELIFT::CFNOUTPUT::ApiGatewayEndpoint}}"
identity_region: "{{AWSGAMELIFT::CFNOUTPUT::IdentityRegion}}"

View File

@@ -0,0 +1,145 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
import boto3
from boto3.dynamodb.conditions import Attr, And
from botocore.exceptions import ClientError
import os
import time
NON_TERMINAL_REQUEST_QUERY_LIMIT = 50
MATCHMAKING_STARTED_STATUS = 'MatchmakingStarted'
MATCHMAKING_SUCCEEDED_STATUS = 'MatchmakingSucceeded'
MATCHMAKING_TIMED_OUT_STATUS = 'MatchmakingTimedOut'
MATCHMAKING_CANCELLED_STATUS = 'MatchmakingCancelled'
MATCHMAKING_FAILED_STATUS = 'MatchmakingFailed'
MAX_PARTITION_SIZE = 10
MIN_TIME_ELAPSED_BEFORE_UPDATE_IN_SECONDS = 30
def handler(event, context):
"""
Finds all non-terminal matchmaking requests and poll for their status. It is recommended by GameLift
to regularly poll for ticket status at a low rate.
See: https://docs.aws.amazon.com/gamelift/latest/flexmatchguide/match-client.html#match-client-track
:param event: lambda event, not used by this function
:param context: lambda context, not used by this function
:return: None
"""
lambda_start_time = round(time.time())
print(f"Polling non-terminal matchmaking tickets. Lambda start time: {lambda_start_time}")
matchmaking_request_table_name = os.environ['MatchmakingRequestTableName']
dynamodb = boto3.resource('dynamodb')
matchmaking_request_table = dynamodb.Table(matchmaking_request_table_name)
gamelift = boto3.client('gamelift')
matchmaking_requests = matchmaking_request_table.scan(
Limit=NON_TERMINAL_REQUEST_QUERY_LIMIT,
FilterExpression=And(Attr('TicketStatus').eq(MATCHMAKING_STARTED_STATUS),
Attr('LastUpdatedTime').lt(lambda_start_time - MIN_TIME_ELAPSED_BEFORE_UPDATE_IN_SECONDS))
)
if matchmaking_requests['Count'] <= 0:
print("No non-terminal matchmaking requests found")
for matchmaking_requests in partition(matchmaking_requests['Items'], MAX_PARTITION_SIZE):
ticket_id_to_request_mapping = {request['TicketId']: request for request in matchmaking_requests}
describe_matchmaking_result = gamelift.describe_matchmaking(
TicketIds=list(ticket_id_to_request_mapping.keys())
)
ticket_list = describe_matchmaking_result['TicketList']
if len(ticket_list) != len(matchmaking_requests):
print(f"Resulting TicketList length: {len(ticket_list)} from DescribeMatchmaking "
f"does not match the request size: {len(matchmaking_requests)}")
for ticket in ticket_list:
ticket_id = ticket['TicketId']
ticket_status = ticket['Status']
matchmaking_request_status = to_matchmaking_request_status(ticket_status)
matchmaking_request = ticket_id_to_request_mapping[ticket_id]
player_id = matchmaking_request['PlayerId']
start_time = matchmaking_request['StartTime']
last_updated_time = matchmaking_request['LastUpdatedTime']
try:
attribute_updates = {
'LastUpdatedTime': {
'Value': lambda_start_time
}
}
if ticket_status in ['COMPLETED', 'FAILED', 'TIMED_OUT', 'CANCELLED']:
print(f'Ticket: {ticket_id} status was updated to {ticket_status}')
attribute_updates.update({
'TicketStatus': {
'Value': matchmaking_request_status
}
})
if ticket_status == 'COMPLETED':
# parse the playerSessionId
matched_player_sessions = ticket.get('GameSessionConnectionInfo', {}).get('MatchedPlayerSessions')
player_session_id = None
if matched_player_sessions is not None and len(matched_player_sessions) == 1:
player_session_id = matched_player_sessions[0].get('PlayerSessionId')
attribute_updates.update({
'IpAddress': {
'Value': ticket.get('GameSessionConnectionInfo', {}).get('IpAddress')
},
'DnsName': {
'Value': ticket.get('GameSessionConnectionInfo', {}).get('DnsName')
},
'Port': {
'Value': str(ticket.get('GameSessionConnectionInfo', {}).get('Port'))
},
'GameSessionArn': {
'Value': str(ticket.get('GameSessionConnectionInfo', {}).get('GameSessionArn'))
},
'PlayerSessionId': {
'Value' : str(player_session_id)
}
})
else:
print(f'No updates to ticket: {ticket_id} compared to '
f'{lambda_start_time - last_updated_time} seconds ago')
matchmaking_request_table.update_item(
Key={
'PlayerId': player_id,
'StartTime': start_time
},
AttributeUpdates=attribute_updates,
Expected={
'TicketStatus': {
'Value': MATCHMAKING_STARTED_STATUS,
'ComparisonOperator': 'EQ'
}
}
)
except ClientError as e:
error_code = e.response['Error']['Code']
if error_code == 'ConditionCheckFailedException':
print(f"Ticket: {ticket_id} status has been updated (likely by MatchMakerEventHandler). "
f"No change is made")
continue
raise e
def partition(collection, n):
"""Yield successive n-sized partitions from collection."""
for i in range(0, len(collection), n):
yield collection[i:i + n]
def to_matchmaking_request_status(ticket_status):
if ticket_status == 'COMPLETED':
return MATCHMAKING_SUCCEEDED_STATUS
if ticket_status == 'FAILED':
return MATCHMAKING_FAILED_STATUS
if ticket_status == 'TIMED_OUT':
return MATCHMAKING_TIMED_OUT_STATUS
if ticket_status == 'CANCELLED':
return MATCHMAKING_CANCELLED_STATUS

View File

@@ -0,0 +1,133 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
import boto3
from boto3.dynamodb.conditions import Key
import time
import os
import json
DEFAULT_TTL_IN_SECONDS = 10 * 60 # 10 minutes
MATCHMAKING_STARTED_STATUS = 'MatchmakingStarted'
MATCHMAKING_SUCCEEDED_STATUS = 'MatchmakingSucceeded'
MATCHMAKING_TIMED_OUT_STATUS = 'MatchmakingTimedOut'
MATCHMAKING_CANCELLED_STATUS = 'MatchmakingCancelled'
MATCHMAKING_FAILED_STATUS = 'MatchmakingFailed'
def handler(event, context):
"""
Handles requests to start games from the game client.
This function records the game request from the client in the MatchmakingRequest table and calls
GameLift to start matchmaking.
:param event: lambda event, contains the region to player latency mapping in `regionToLatencyMapping` key, as well
as the player information from the Cognito id tokens.
:param context: lambda context, not used by this function
:return:
- 202 (Accepted) if the matchmaking request is accepted and is now being processed
- 409 (Conflict) if the another matchmaking request is in progress
- 500 (Internal Error) if error occurred when calling GameLift to start matchmaking
"""
player_id = event["requestContext"]["authorizer"]["claims"]["sub"]
start_time = round(time.time())
print(f'Handling start game request. PlayerId: {player_id}, StartTime: {start_time}')
region_to_latency_mapping = get_region_to_latency_mapping(event)
if region_to_latency_mapping:
print(f"Region to latency mapping: {region_to_latency_mapping}")
else:
print("No regionToLatencyMapping mapping provided")
matchmaking_request_table_name = os.environ['MatchmakingRequestTableName']
team_name = os.environ['TeamName']
matchmaking_configuration_name = os.environ['MatchmakingConfigurationName']
dynamodb = boto3.resource('dynamodb')
matchmaking_request_table = dynamodb.Table(matchmaking_request_table_name)
gamelift = boto3.client('gamelift')
matchmaking_requests = matchmaking_request_table.query(
KeyConditionExpression=Key('PlayerId').eq(player_id),
ScanIndexForward=False
)
if matchmaking_requests['Count'] > 0 \
and not is_matchmaking_request_terminal(matchmaking_requests['Items'][0]):
# A existing matchmaking request in progress
return {
'headers': {
'Content-Type': 'text/plain'
},
'statusCode': 409 # Conflict
}
try:
player = {
'PlayerId': player_id,
'Team': team_name
}
if region_to_latency_mapping:
player['LatencyInMs'] = region_to_latency_mapping
start_matchmaking_request = {
"ConfigurationName": matchmaking_configuration_name,
"Players": [player]
}
print(f"Starting matchmaking in GameLift. Request: {start_matchmaking_request}")
start_matchmaking_result = gamelift.start_matchmaking(**start_matchmaking_request)
ticket_id = start_matchmaking_result['MatchmakingTicket']['TicketId']
ticket_status = MATCHMAKING_STARTED_STATUS
matchmaking_request_table.put_item(
Item={
'PlayerId': player_id,
'StartTime': start_time,
'LastUpdatedTime': start_time,
'ExpirationTime': start_time + DEFAULT_TTL_IN_SECONDS,
'TicketStatus': ticket_status,
'TicketId': ticket_id
}
)
return {
# Matchmaking request enqueued
'headers': {
'Content-Type': 'text/plain'
},
'statusCode': 202
}
except Exception as ex:
print(f'Error occurred when calling GameLift to start matchmaking. Exception: {ex}')
return {
# Error occurred when enqueuing matchmaking request
'headers': {
'Content-Type': 'text/plain'
},
'statusCode': 500
}
def is_matchmaking_request_terminal(matchmaking_request):
return matchmaking_request['TicketStatus'] in [
MATCHMAKING_SUCCEEDED_STATUS,
MATCHMAKING_TIMED_OUT_STATUS,
MATCHMAKING_CANCELLED_STATUS,
MATCHMAKING_FAILED_STATUS
]
def get_region_to_latency_mapping(event):
request_body = event.get("body")
if not request_body:
return None
try:
request_body_json = json.loads(request_body)
except ValueError:
print(f"Error parsing request body: {request_body}")
return None
if request_body_json and request_body_json.get('regionToLatencyMapping'):
return request_body_json.get('regionToLatencyMapping')

View File

@@ -0,0 +1,107 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
import boto3
from boto3.dynamodb.conditions import Key
import os
import json
import time
MATCHMAKING_STARTED_STATUS = 'MatchmakingStarted'
MATCHMAKING_SUCCEEDED_STATUS = 'MatchmakingSucceeded'
MATCHMAKING_TIMED_OUT_STATUS = 'MatchmakingTimedOut'
MATCHMAKING_CANCELLED_STATUS = 'MatchmakingCancelled'
MATCHMAKING_FAILED_STATUS = 'MatchmakingFailed'
def handler(event, context):
"""
Handles game session event from GameLift FlexMatch. This function parses the event messages and updates all
related requests in the the MatchmakingPlacement DynamoDB table,
which will be looked up to fulfill game client's Results Requests.
:param event: lambda event containing game session event from GameLift queue
:param context: lambda context, not used by this function
:return: None
"""
lambda_start_time = round(time.time())
message = json.loads(event['Records'][0]['Sns']['Message'])
print(f'Handling FlexMatch event. StartTime: {lambda_start_time}. Message: {message}')
status_type = message['detail']['type']
if status_type not in [MATCHMAKING_SUCCEEDED_STATUS, MATCHMAKING_TIMED_OUT_STATUS, MATCHMAKING_CANCELLED_STATUS,
MATCHMAKING_FAILED_STATUS]:
print(f'Received non-terminal status type: {status_type}. Skip processing.')
return
tickets = message['detail']['tickets']
ip_address = message['detail']['gameSessionInfo'].get('ipAddress')
dns_name = message['detail']['gameSessionInfo'].get('dnsName')
port = str(message['detail']['gameSessionInfo'].get('port'))
game_session_arn = str(message['detail']['gameSessionInfo'].get('gameSessionArn'))
players = message['detail']['gameSessionInfo']['players']
players_map = {player.get('playerId'):player.get('playerSessionId') for player in players}
ticket_id_index_name = os.environ['TicketIdIndexName']
matchmaking_request_table_name = os.environ['MatchmakingRequestTableName']
dynamodb = boto3.resource('dynamodb')
matchmaking_request_table = dynamodb.Table(matchmaking_request_table_name)
for ticket in tickets:
ticket_id = ticket['ticketId']
matchmaking_requests = matchmaking_request_table.query(
IndexName=ticket_id_index_name,
KeyConditionExpression=Key('TicketId').eq(ticket_id)
)
if matchmaking_requests['Count'] <= 0:
print(f"Cannot find matchmaking request with ticket id: {ticket_id}")
continue
matchmaking_request_status = matchmaking_requests['Items'][0]['TicketStatus']
player_id = matchmaking_requests['Items'][0]['PlayerId']
player_session_id = players_map.get(player_id)
print(f'Processing Ticket: {ticket_id}, PlayerId: {player_id}, PlayerSessionId: {player_session_id}')
matchmaking_request_start_time = matchmaking_requests['Items'][0]['StartTime']
if matchmaking_request_status != MATCHMAKING_STARTED_STATUS:
print(f"Unexpected TicketStatus on matchmaking request. Expected: 'MatchmakingStarted'. "
f"Found: {matchmaking_request_status}")
continue
attribute_updates = {
'TicketStatus': {
'Value': status_type
},
'LastUpdatedTime': {
'Value': lambda_start_time
}
}
if status_type == MATCHMAKING_SUCCEEDED_STATUS:
attribute_updates.update({
'IpAddress': {
'Value': ip_address
},
'DnsName': {
'Value': dns_name
},
'Port': {
'Value': port
},
'GameSessionArn': {
'Value': game_session_arn
},
'PlayerSessionId': {
'Value': player_session_id
}
})
matchmaking_request_table.update_item(
Key={
'PlayerId': player_id,
'StartTime': matchmaking_request_start_time
},
AttributeUpdates=attribute_updates
)

View File

@@ -0,0 +1,84 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
import boto3
import os
import json
from boto3.dynamodb.conditions import Key
MATCHMAKING_STARTED_STATUS = 'MatchmakingStarted'
MATCHMAKING_SUCCEEDED_STATUS = 'MatchmakingSucceeded'
def handler(event, context):
"""
Handles requests to describe the game session connection information after a StartGame request.
This function will look up the MatchmakingRequest table to find the latest matchmaking request
by the player, and return its game connection information if any.
:param event: lambda event, contains the region to player latency mapping in `regionToLatencyMapping` key, as well
as the player information from the Cognito id tokens. Cognito provides a `sub` user attribute that we use as a
Player ID. Unlike `username` value `sub` is UUID for a user which is never reassigned to another user.
:param context: lambda context, not used by this function
:return:
- 200 (OK) if the game connection is ready, along with server info: "IpAddress", "Port", "DnsName", "PlayerSessionId", "PlayerId", "PlayerSessionId", "PlayerId"
- 204 (No Content) if the requested game is still in progress of matchmaking
- 404 (Not Found) if no game has been started by the player, or if all started game were expired
- 500 (Internal Error) if errors occurred during matchmaking or placement
"""
player_id = event["requestContext"]["authorizer"]["claims"]["sub"]
print(f'Handling request result request. PlayerId: {player_id}')
matchmaking_request_table_name = os.environ['MatchmakingRequestTableName']
dynamodb = boto3.resource('dynamodb')
matchmaking_request_table = dynamodb.Table(matchmaking_request_table_name)
matchmaking_requests = matchmaking_request_table.query(
KeyConditionExpression=Key('PlayerId').eq(player_id),
ScanIndexForward=False
)
if matchmaking_requests['Count'] <= 0:
return {
'headers': {
'Content-Type': 'text/plain'
},
'statusCode': 404
}
latest_matchmaking_request = matchmaking_requests['Items'][0]
print(f'Current Matchmaking Request: {latest_matchmaking_request}')
matchmaking_request_status = latest_matchmaking_request['TicketStatus']
if matchmaking_request_status == MATCHMAKING_STARTED_STATUS:
# still waiting for ticket to be processed
return {
'headers': {
'Content-Type': 'text/plain'
},
'statusCode': 204
}
elif matchmaking_request_status == MATCHMAKING_SUCCEEDED_STATUS:
game_session_connection_info = \
dict((k, latest_matchmaking_request[k]) for k in ('IpAddress', 'Port', 'DnsName', 'PlayerSessionId', 'PlayerId', 'GameSessionArn'))
print(f"Connection info: {game_session_connection_info}")
return {
'body': json.dumps(game_session_connection_info),
'headers': {
'Content-Type': 'text/plain'
},
'statusCode': 200
}
else:
# We count MatchmakingCancelled as internal error also because cancelling placement requests is not
# in the current implementation, so it should never happen.
print(f'Received non-successful terminal status {matchmaking_request_status}, responding with 500 error.')
return {
'headers': {
'Content-Type': 'text/plain'
},
'statusCode': 500
}

View File

@@ -0,0 +1,43 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
#
# GameLift Region Mappings
# Keep synchronized with: https://docs.aws.amazon.com/general/latest/gr/gamelift.html
#
# ----------------------------------------------------------------------------------------
# Region Name Region Endpoint Protocol
# ----------------------------------------------------------------------------------------
# US East (Ohio) us-east-2 gamelift.us-east-2.amazonaws.com HTTPS
# US East (N. Virginia) us-east-1 gamelift.us-east-1.amazonaws.com HTTPS
# US West (N. California) us-west-1 gamelift.us-west-1.amazonaws.com HTTPS
# US West (Oregon) us-west-2 gamelift.us-west-2.amazonaws.com HTTPS
# Asia Pacific (Mumbai) ap-south-1 gamelift.ap-south-1.amazonaws.com HTTPS
# Asia Pacific (Seoul) ap-northeast-2 gamelift.ap-northeast-2.amazonaws.com HTTPS
# Asia Pacific (Singapore) ap-southeast-1 gamelift.ap-southeast-1.amazonaws.com HTTPS
# Asia Pacific (Sydney) ap-southeast-2 gamelift.ap-southeast-2.amazonaws.com HTTPS
# Asia Pacific (Tokyo) ap-northeast-1 gamelift.ap-northeast-1.amazonaws.com HTTPS
# Canada (Central) ca-central-1 gamelift.ca-central-1.amazonaws.com HTTPS
# Europe (Frankfurt) eu-central-1 gamelift.eu-central-1.amazonaws.com HTTPS
# Europe (Ireland) eu-west-1 gamelift.eu-west-1.amazonaws.com HTTPS
# Europe (London) eu-west-2 gamelift.eu-west-2.amazonaws.com HTTPS
# South America (São Paulo) sa-east-1 gamelift.sa-east-1.amazonaws.com HTTPS
# ----------------------------------------------------------------------------------------
{
five_letter_region_codes: {
us-east-2 : usea2,
us-east-1 : usea1,
us-west-1 : uswe1,
us-west-2 : uswe2,
ap-south-1 : apso1,
ap-northeast-2 : apne2,
ap-southeast-1 : apse1,
ap-southeast-2 : apse2,
ap-northeast-1 : apne1,
ca-central-1 : cace1,
eu-central-1 : euce1,
eu-west-1 : euwe1,
eu-west-2 : euwe2,
sa-east-1 : saea1
}
}

View File

@@ -0,0 +1,217 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
import string
import boto3
import requests
import json
import time
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("-g", "--game", help="game name", type=str, required=True)
parser.add_argument("-r", "--region", help="region name, e.g. eu-west-1", type=str, required=True)
parser.add_argument("-p", "--profile", help="profile name in the AWS shared credentials file ~/.aws/credentials", type=str, required=True)
args = parser.parse_args()
GAME_NAME = args.game.lower() # e.g. 'GameLiftSampleGame5ue4'
REGION = args.region # e.g. 'eu-west-1'
PROFILE_NAME = args.profile
USER_POOL_NAME = GAME_NAME + 'UserPool'
USER_POOL_CLIENT_NAME = GAME_NAME + 'UserPoolClient'
USERNAME1 = 'testuser1@example.com'
USERNAME2 = 'testuser2@example.com'
USERNAMES = [USERNAME1, USERNAME2]
PASSWORD = 'TestPassw0rd.'
REST_API_NAME = GAME_NAME + 'RestApi'
REST_API_STAGE = 'dev'
GAME_REQUEST_PATH = 'start_game'
RESULTS_REQUEST_PATH = 'get_game_connection'
session = boto3.Session(profile_name=PROFILE_NAME)
cognito_idp = session.client('cognito-idp', region_name=REGION)
apig = session.client('apigateway', region_name=REGION)
REGION_US_WEST_2 = 'us-west-2'
REGION_EU_WEST_1 = 'eu-west-1'
REGION_US_EAST_1 = 'us-east-1'
NO_LATENCY = 'no-latency'
REGIONS_TO_TEST = [REGION_US_WEST_2, REGION_EU_WEST_1, NO_LATENCY]
US_WEST_2_FIRST_REGION_TO_LATENCY_MAPPING = {
"regionToLatencyMapping": {
"us-west-2": 50,
"us-east-1": 100,
"eu-west-1": 150,
"ap-northeast-1": 300
}
}
EU_WEST_1_FIRST_REGION_TO_LATENCY_MAPPING = {
"regionToLatencyMapping": {
"us-west-2": 50,
"us-east-1": 100,
"eu-west-1": 10,
"ap-northeast-1": 300
}
}
REGION_TO_GAME_REQUEST_PAYLOAD_MAPPING = {
REGION_US_WEST_2: json.dumps(US_WEST_2_FIRST_REGION_TO_LATENCY_MAPPING),
REGION_EU_WEST_1: json.dumps(EU_WEST_1_FIRST_REGION_TO_LATENCY_MAPPING),
NO_LATENCY: None
}
REGION_TO_GAME_SESSION_ARN_EXPECTED_LOCATION = {
REGION_US_WEST_2: REGION_US_WEST_2,
REGION_EU_WEST_1: REGION_EU_WEST_1,
NO_LATENCY: REGION_US_EAST_1
}
def main():
user_pool = find_user_pool(USER_POOL_NAME)
user_pool_id = user_pool['Id']
print("User Pool Id:", user_pool_id)
user_pool_client = find_user_pool_client(user_pool_id, USER_POOL_CLIENT_NAME)
user_pool_client_id = user_pool_client['ClientId']
print("User Pool Client Id:", user_pool_client_id)
try:
for region in REGIONS_TO_TEST:
game_request_payload = REGION_TO_GAME_REQUEST_PAYLOAD_MAPPING.get(region)
expected_game_session_region = REGION_TO_GAME_SESSION_ARN_EXPECTED_LOCATION.get(region)
headers_list = []
for username in USERNAMES:
regional_username = get_regional_user_name(username, region)
cognito_idp.sign_up(
ClientId=user_pool_client_id,
Username=regional_username,
Password=PASSWORD,
)
print(f"Created user: {regional_username}")
cognito_idp.admin_confirm_sign_up(
UserPoolId=user_pool_id,
Username=regional_username,
)
init_auth_result = cognito_idp.initiate_auth(
AuthFlow='USER_PASSWORD_AUTH',
AuthParameters={
'USERNAME': regional_username,
'PASSWORD': PASSWORD,
},
ClientId=user_pool_client_id
)
assert init_auth_result['ResponseMetadata']['HTTPStatusCode'] == 200, "Unsuccessful init_auth"
print(f"Authenticated via username and password for {regional_username}")
id_token = init_auth_result['AuthenticationResult']['IdToken']
headers = {
'Auth': id_token
}
headers_list.append(headers)
results_request_url = get_rest_api_endpoint(REST_API_NAME, REGION, REST_API_STAGE, RESULTS_REQUEST_PATH)
game_request_url = get_rest_api_endpoint(REST_API_NAME, REGION, REST_API_STAGE, GAME_REQUEST_PATH)
print(f"results_request_url: {results_request_url}")
print(f"game_request_url: {game_request_url}")
for headers in headers_list:
results_request_response = requests.post(url=results_request_url, headers=headers)
assert results_request_response.status_code == 404, \
f"Expect 'POST /get_game_connection' status code to be 404 (Not Found). Actual: " \
f"{str(results_request_response.status_code)}"
print("Verified lambda ResultsRequest response", results_request_response)
game_request_response = requests.post(url=game_request_url, headers=headers, data=game_request_payload)
print(f"Game request response '{game_request_response}'")
assert game_request_response.status_code == 202, \
f"Expect 'POST /start_game' status code to be 202 (Accepted), actual: " \
f"{str(game_request_response.status_code)}"
print("Verified lambda GameRequest response", game_request_response)
print("Waiting for matchmaking request to be processed...")
verified_players = 0
while verified_players != len(headers_list):
verified_players = 0
time.sleep(10) # 10 seconds
for headers in headers_list:
results_request_response = requests.post(url=results_request_url, headers=headers)
if results_request_response.status_code == 204:
print("Match is not ready yet")
continue
assert results_request_response.status_code == 200, \
f"Expect 'POST /get_game_connection' status code to be 200 (Success), actual: " \
f"{str(results_request_response.status_code)}"
print("Verified lambda ResultsRequest response", results_request_response)
game_connection_info = json.loads(results_request_response.content)
print(f"Game connection info '{game_connection_info}'")
assert game_connection_info['IpAddress'] != ''
assert int(game_connection_info['Port']) > 0
assert REGION in game_connection_info['DnsName'], \
f"Expect {game_connection_info['DnsName']} to contain '{REGION}'"
assert expected_game_session_region in game_connection_info['GameSessionArn'], \
f"Expect {game_connection_info['GameSessionArn']} to contain '{expected_game_session_region}'"
assert "psess-" in game_connection_info['PlayerSessionId'], \
f"Expect {game_connection_info['PlayerSessionId']} to contain 'psess-'"
print("Verified game connection info:", game_connection_info)
verified_players += 1
print(f"{verified_players} players' game sessions verified")
finally:
for region in REGIONS_TO_TEST:
for username in USERNAMES:
regional_username = get_regional_user_name(username, region)
cognito_idp.admin_delete_user(
UserPoolId=user_pool_id,
Username=regional_username,
)
print("Deleted user:", regional_username)
print("Test Succeeded!")
def get_regional_user_name(username, region):
return f"{region}_{username}"
def find_user_pool(user_pool_name):
print("Finding user pool:", user_pool_name)
result = cognito_idp.list_user_pools(MaxResults=50)
pools = result['UserPools']
return next(x for x in pools if x['Name'] == user_pool_name)
def find_user_pool_client(user_pool_id, user_pool_client_name):
print("Finding user pool client:", user_pool_client_name)
results = cognito_idp.list_user_pool_clients(UserPoolId=user_pool_id)
clients = results['UserPoolClients']
return next(x for x in clients if x['ClientName'] == user_pool_client_name)
def find_rest_api(rest_api_name):
print("Finding rest api:", rest_api_name)
results = apig.get_rest_apis()
rest_apis = results['items']
return next(x for x in rest_apis if x['name'] == rest_api_name)
def get_rest_api_endpoint(rest_api_name, region, stage, path):
print("Getting rest api endpoint", rest_api_name)
rest_api = find_rest_api(rest_api_name)
rest_api_id = rest_api['id']
return f'https://{rest_api_id}.execute-api.{region}.amazonaws.com/{stage}/{path}'
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,736 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: MIT-0
AWSTemplateFormatVersion: "2010-09-09"
Description: >
This CloudFormation template sets up a scenario to use FlexMatch -- a managed matchmaking service provided by
GameLift. The template demonstrates best practices in acquiring the matchmaking ticket status, by listening to
FlexMatch events in conjunction with a low frequency poller to ensure incomplete tickets are periodically pinged
and therefore are not discarded by GameLift.
Parameters:
ApiGatewayStageNameParameter:
Type: String
Default: v1
Description: Name of the Api Gateway stage
ContainerGroupDefinitionNameParameter:
Type: String
Default: SampleCGDName
Description: Name of the Api Gateway stage
ContainerImageNameParameter:
Type: String
Default: SampleContainerImageName
Description: Name for the Container Image
ContainerImageUriParameter:
Type: String
Description: URI pointing to a Container Image in ECR
FleetDescriptionParameter:
Type: String
Default: Deployed by the Amazon GameLift Plug-in for Unreal.
Description: Description of the fleet
TotalMemoryLimitParameter:
Type: Number
Default: 4
Description: The maximum amount of memory (in MiB) to allocate to the container group
TotalVcpuLimitParameter:
Type: Number
Default: 2
Description: The maximum amount of CPU units to allocate to the container group
FleetUdpFromPortParameter:
Type: Number
Default: 33430
Description: Starting port number for UDP ports to be opened
FleetUdpToPortParameter:
Type: Number
Default: 33440
Description: Ending port number for UDP ports to be opened
GameNameParameter:
Type: String
Default: MyGame
Description: Game name to prepend before resource names
MaxLength: 30
LambdaZipS3BucketParameter:
Type: String
Description: S3 bucket that stores the lambda function zip
LambdaZipS3KeyParameter:
Type: String
Description: S3 key that stores the lambda function zip
LaunchPathParameter:
Type: String
Description: Location of the game server executable in the build
MatchmakerTimeoutInSecondsParameter:
Type: Number
Default: 60
Description: Time in seconds before matchmaker times out to place players on a server
MatchmakingTimeoutInSecondsParameter:
Type: Number
Default: 60
Description: Time in seconds before matchmaker times out to wait for enough players to create game session placement
MaxTransactionsPerFiveMinutesPerIpParameter:
Type: Number
Default: 100
MaxValue: 20000000
MinValue: 100
NumPlayersPerGameParameter:
Type: Number
Default: 2
Description: Number of players per game session
QueueTimeoutInSecondsParameter:
Type: Number
Default: 60
Description: Time in seconds before game session placement times out to place players on a server
TeamNameParameter:
Type: String
Default: MySampleTeam
Description: Team name used in matchmaking ruleset and StartMatchmaking API requests
TicketIdIndexNameParameter:
Type: String
Default: ticket-id-index
Description: Name of the global secondary index on MatchmakingRequest table with partition key TicketId
UnrealEngineVersionParameter:
Type: String
Description: "Unreal Engine Version being used by the plugin"
EnableMetricsParameter:
Type: String
Default: "false"
AllowedValues: ["true", "false"]
Description: "Enable telemetry metrics collection using OTEL collector"
Conditions:
ShouldCreateMetricsResources: !Equals [!Ref EnableMetricsParameter, "true"]
Resources:
ApiGatewayCloudWatchRole:
Type: "AWS::IAM::Role"
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- apigateway.amazonaws.com
Action: "sts:AssumeRole"
ManagedPolicyArns:
- "arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs"
Account:
Type: "AWS::ApiGateway::Account"
Properties:
CloudWatchRoleArn: !GetAtt ApiGatewayCloudWatchRole.Arn
FlexMatchStatusPollerLambdaFunctionExecutionRole:
Type: "AWS::IAM::Role"
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- "sts:AssumeRole"
ManagedPolicyArns:
- "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
Policies:
- PolicyName: !Sub ${GameNameParameter}FlexMatchStatusPollerLambdaFunctionPolicies
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- "dynamodb:Scan"
- "dynamodb:UpdateItem"
- "gamelift:DescribeMatchmaking"
Resource: "*"
GameRequestLambdaFunctionExecutionRole:
Type: "AWS::IAM::Role"
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- "sts:AssumeRole"
ManagedPolicyArns:
- "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
Policies:
- PolicyName: !Sub ${GameNameParameter}GameRequestLambdaFunctionPolicies
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- "dynamodb:PutItem"
- "dynamodb:UpdateItem"
- "dynamodb:GetItem"
- "dynamodb:Query"
- "gamelift:StartMatchmaking"
Resource: "*"
MatchmakerEventHandlerLambdaFunctionExecutionRole:
Type: "AWS::IAM::Role"
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- "sts:AssumeRole"
ManagedPolicyArns:
- "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
Policies:
- PolicyName: !Sub ${GameNameParameter}MatchmakerEventHandlerLambdaFunctionPolicies
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- "dynamodb:Query"
- "dynamodb:UpdateItem"
Resource: "*"
RestApi:
Type: "AWS::ApiGateway::RestApi"
Properties:
Name: !Sub ${GameNameParameter}RestApi
ResultsRequestLambdaFunctionExecutionRole:
Type: "AWS::IAM::Role"
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- "sts:AssumeRole"
ManagedPolicyArns:
- "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
Policies:
- PolicyName: !Sub ${GameNameParameter}ResultsRequestLambdaFunctionPolicies
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- "dynamodb:Query"
Resource: "*"
UserPool:
Type: "AWS::Cognito::UserPool"
Properties:
AdminCreateUserConfig:
AllowAdminCreateUserOnly: false
AutoVerifiedAttributes:
- email
EmailConfiguration:
EmailSendingAccount: COGNITO_DEFAULT
EmailVerificationMessage: "Please verify your email to complete account registration for the GameLift Plugin FlexMatch fleet deployment scenario. Confirmation Code {####}."
EmailVerificationSubject: GameLift Plugin - Deployment Scenario Account Verification
Policies:
PasswordPolicy:
MinimumLength: 8
RequireLowercase: true
RequireNumbers: true
RequireSymbols: true
RequireUppercase: true
Schema:
- Name: email
AttributeDataType: String
Mutable: false
Required: true
UserPoolName: !Sub ${GameNameParameter}UserPool
UsernameAttributes:
- email
GameRequestApiResource:
Type: "AWS::ApiGateway::Resource"
Properties:
ParentId: !GetAtt RestApi.RootResourceId
PathPart: start_game
RestApiId: !Ref RestApi
MatchmakingRequestTable:
Type: "AWS::DynamoDB::Table"
Properties:
AttributeDefinitions:
- AttributeName: PlayerId
AttributeType: S
- AttributeName: TicketId
AttributeType: S
- AttributeName: StartTime
AttributeType: "N"
GlobalSecondaryIndexes:
- IndexName: !Ref TicketIdIndexNameParameter
KeySchema:
- AttributeName: TicketId
KeyType: HASH
Projection:
ProjectionType: ALL
ProvisionedThroughput:
ReadCapacityUnits: 5
WriteCapacityUnits: 5
KeySchema:
- AttributeName: PlayerId
KeyType: HASH
- AttributeName: StartTime
KeyType: RANGE
ProvisionedThroughput:
ReadCapacityUnits: 5
WriteCapacityUnits: 5
TableName: !Sub ${GameNameParameter}MatchmakingRequestTable
TimeToLiveSpecification:
AttributeName: ExpirationTime
Enabled: true
ResultsRequestApiResource:
Type: "AWS::ApiGateway::Resource"
Properties:
ParentId: !GetAtt RestApi.RootResourceId
PathPart: get_game_connection
RestApiId: !Ref RestApi
UserPoolClient:
Type: "AWS::Cognito::UserPoolClient"
Properties:
AccessTokenValidity: 1
ClientName: !Sub ${GameNameParameter}UserPoolClient
ExplicitAuthFlows:
- ALLOW_USER_PASSWORD_AUTH
- ALLOW_REFRESH_TOKEN_AUTH
GenerateSecret: false
IdTokenValidity: 1
PreventUserExistenceErrors: ENABLED
ReadAttributes:
- email
- preferred_username
RefreshTokenValidity: 30
SupportedIdentityProviders:
- COGNITO
UserPoolId: !Ref UserPool
WebACL:
Type: "AWS::WAFv2::WebACL"
DependsOn:
- ApiDeployment
Properties:
DefaultAction:
Allow:
{}
Description: !Sub "WebACL for game: ${GameNameParameter}"
Name: !Sub ${GameNameParameter}WebACL
Rules:
- Name: !Sub ${GameNameParameter}WebACLPerIpThrottleRule
Action:
Block:
{}
Priority: 0
Statement:
RateBasedStatement:
AggregateKeyType: IP
Limit: !Ref MaxTransactionsPerFiveMinutesPerIpParameter
VisibilityConfig:
CloudWatchMetricsEnabled: true
MetricName: !Sub ${GameNameParameter}WebACLPerIpThrottleRuleMetrics
SampledRequestsEnabled: true
Scope: REGIONAL
VisibilityConfig:
CloudWatchMetricsEnabled: true
MetricName: !Sub ${GameNameParameter}WebACLMetrics
SampledRequestsEnabled: true
ApiDeployment:
Type: "AWS::ApiGateway::Deployment"
DependsOn:
- GameRequestApiMethod
- ResultsRequestApiMethod
Properties:
RestApiId: !Ref RestApi
StageDescription:
DataTraceEnabled: true
LoggingLevel: INFO
MetricsEnabled: true
StageName: !Ref ApiGatewayStageNameParameter
Authorizer:
Type: "AWS::ApiGateway::Authorizer"
Properties:
IdentitySource: method.request.header.Auth
Name: CognitoAuthorizer
ProviderARNs:
- "Fn::GetAtt":
- UserPool
- Arn
RestApiId: !Ref RestApi
Type: COGNITO_USER_POOLS
GameSessionQueue:
Type: "AWS::GameLift::GameSessionQueue"
Properties:
Destinations:
- DestinationArn: !Sub "arn:aws:gamelift:${AWS::Region}:${AWS::AccountId}:containerfleet/${ContainerFleetResource}"
Name: !Sub ${GameNameParameter}GameSessionQueue
TimeoutInSeconds: !Ref QueueTimeoutInSecondsParameter
MatchmakingRuleSet:
Type: "AWS::GameLift::MatchmakingRuleSet"
Properties:
Name: !Sub ${GameNameParameter}MatchmakingRuleSet
RuleSetBody: !Sub
- |-
{
"name": "MyMatchmakingRuleSet",
"ruleLanguageVersion": "1.0",
"teams": [{
"name": "${teamName}",
"minPlayers": ${minPlayers},
"maxPlayers": ${maxPlayers}
}]
}
- maxPlayers: !Ref NumPlayersPerGameParameter
minPlayers: !Ref NumPlayersPerGameParameter
teamName: !Ref TeamNameParameter
FlexMatchStatusPollerLambdaFunction:
Type: "AWS::Lambda::Function"
Properties:
Code:
S3Bucket: !Ref LambdaZipS3BucketParameter
S3Key: !Ref LambdaZipS3KeyParameter
Description: Lambda function to handle game requests
Environment:
Variables:
MatchmakingRequestTableName: !Ref MatchmakingRequestTable
FunctionName: !Sub ${GameNameParameter}FlexMatchStatusPollerLambda
Handler: flexmatch_status_poller.handler
MemorySize: 128
Role: !GetAtt FlexMatchStatusPollerLambdaFunctionExecutionRole.Arn
Runtime: python3.14
GameRequestApiMethod:
Type: "AWS::ApiGateway::Method"
Properties:
AuthorizationType: COGNITO_USER_POOLS
AuthorizerId: !Ref Authorizer
HttpMethod: POST
Integration:
Type: AWS_PROXY
IntegrationHttpMethod: POST
Uri: !Sub "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GameRequestLambdaFunction.Arn}/invocations"
OperationName: GameRequest
ResourceId: !Ref GameRequestApiResource
RestApiId: !Ref RestApi
MatchmakerEventHandlerLambdaFunction:
Type: "AWS::Lambda::Function"
Properties:
Code:
S3Bucket: !Ref LambdaZipS3BucketParameter
S3Key: !Ref LambdaZipS3KeyParameter
Description: Lambda function to handle game requests
Environment:
Variables:
MatchmakingRequestTableName: !Ref MatchmakingRequestTable
TicketIdIndexName: !Ref TicketIdIndexNameParameter
FunctionName: !Sub ${GameNameParameter}MatchmakerEventHandlerLambda
Handler: matchmaker_event_handler.handler
MemorySize: 128
Role: !GetAtt MatchmakerEventHandlerLambdaFunctionExecutionRole.Arn
Runtime: python3.14
ResultsRequestApiMethod:
Type: "AWS::ApiGateway::Method"
Properties:
AuthorizationType: COGNITO_USER_POOLS
AuthorizerId: !Ref Authorizer
HttpMethod: POST
Integration:
Type: AWS_PROXY
IntegrationHttpMethod: POST
Uri: !Sub "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ResultsRequestLambdaFunction.Arn}/invocations"
OperationName: ResultsRequest
ResourceId: !Ref ResultsRequestApiResource
RestApiId: !Ref RestApi
ResultsRequestLambdaFunction:
Type: "AWS::Lambda::Function"
Properties:
Code:
S3Bucket: !Ref LambdaZipS3BucketParameter
S3Key: !Ref LambdaZipS3KeyParameter
Description: Lambda function to handle game requests
Environment:
Variables:
MatchmakingRequestTableName: !Ref MatchmakingRequestTable
FunctionName: !Sub ${GameNameParameter}ResultsRequestLambda
Handler: results_request.handler
MemorySize: 128
Role: !GetAtt ResultsRequestLambdaFunctionExecutionRole.Arn
Runtime: python3.14
WebACLAssociation:
Type: "AWS::WAFv2::WebACLAssociation"
DependsOn:
- ApiDeployment
- WebACL
Properties:
ResourceArn: !Sub
- "arn:aws:apigateway:${REGION}::/restapis/${REST_API_ID}/stages/${STAGE_NAME}"
- REGION: !Ref "AWS::Region"
REST_API_ID: !Ref RestApi
STAGE_NAME: !Ref ApiGatewayStageNameParameter
WebACLArn: !GetAtt WebACL.Arn
FlexMatchStatusPollerScheduledRule:
Type: "AWS::Events::Rule"
Properties:
Description: !Sub ${GameNameParameter}FlexMatchStatusPollerScheduledRule
ScheduleExpression: rate(1 minute)
State: ENABLED
Targets:
- Arn: !GetAtt FlexMatchStatusPollerLambdaFunction.Arn
Id: !Sub ${GameNameParameter}FlexMatchStatusPollerScheduledRule
MatchmakerEventTopicKey:
Type: "AWS::KMS::Key"
Properties:
Description: KMS key for FlexMatch SNS notifications
KeyPolicy:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
AWS: !Sub "arn:aws:iam::${AWS::AccountId}:root"
Action: "kms:*"
Resource: "*"
- Effect: Allow
Principal:
Service: gamelift.amazonaws.com
Action:
- "kms:Decrypt"
- "kms:GenerateDataKey"
Resource: "*"
MatchmakerEventTopic:
Type: "AWS::SNS::Topic"
Properties:
KmsMasterKeyId: !Ref MatchmakerEventTopicKey
Subscription:
- Endpoint: !GetAtt MatchmakerEventHandlerLambdaFunction.Arn
Protocol: lambda
TopicName: !Sub ${GameNameParameter}MatchmakerEventTopic
ContainerFleetRole:
Type: "AWS::IAM::Role"
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- cloudformation.amazonaws.com
- gamelift.amazonaws.com
Action:
- "sts:AssumeRole"
ManagedPolicyArns:
- "arn:aws:iam::aws:policy/GameLiftContainerFleetPolicy"
Policies: !If
- ShouldCreateMetricsResources
- - PolicyName: AMPRemoteWriteAccess
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- aps:RemoteWrite
Resource: !Sub 'arn:aws:aps:*:${AWS::AccountId}:workspace/*'
- PolicyName: OTelCollectorEMFExporter
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- logs:PutLogEvents
- logs:CreateLogStream
- logs:CreateLogGroup
- logs:PutRetentionPolicy
Resource: !Sub 'arn:aws:logs:*:${AWS::AccountId}:log-group:*:log-stream:*'
- []
ContainerGroupResource:
Type: "AWS::GameLift::ContainerGroupDefinition"
Properties:
GameServerContainerDefinition:
ContainerName: !Ref ContainerImageNameParameter
ImageUri: !Ref ContainerImageUriParameter
ServerSdkVersion: "5.4.0"
PortConfiguration:
ContainerPortRanges:
- FromPort: !Ref FleetUdpFromPortParameter
Protocol: "UDP"
ToPort: !Ref FleetUdpToPortParameter
Name: !Ref ContainerGroupDefinitionNameParameter
OperatingSystem: "AMAZON_LINUX_2023"
TotalVcpuLimit: !Ref TotalVcpuLimitParameter
TotalMemoryLimitMebibytes: !Ref TotalMemoryLimitParameter
ContainerFleetResource:
DependsOn:
- ContainerGroupResource
Type: "AWS::GameLift::ContainerFleet"
Properties:
GameServerContainerGroupDefinitionName: !Ref ContainerGroupDefinitionNameParameter
InstanceConnectionPortRange:
FromPort: !Ref FleetUdpFromPortParameter
ToPort: !Ref FleetUdpToPortParameter
Description: !Sub
- "${FleetDescriptionParameter} Using Unreal Engine Version ${UnrealEngineVersionParameter}${MetricsText}"
- MetricsText: !If [ShouldCreateMetricsResources, " with telemetry metrics enabled", ""]
InstanceInboundPermissions:
- FromPort: !Ref FleetUdpFromPortParameter
IpRange: "0.0.0.0/0"
Protocol: UDP
ToPort: !Ref FleetUdpToPortParameter
Locations:
- Location: us-west-2
- Location: us-east-1
- Location: eu-west-1
InstanceType: c4.xlarge
BillingType: ON_DEMAND
FleetRoleArn: !GetAtt ContainerFleetRole.Arn
NewGameSessionProtectionPolicy: FullProtection
GameSessionCreationLimitPolicy:
NewGameSessionsPerCreator: 5
PolicyPeriodInMinutes: 2
FlexMatchStatusPollerLambdaPermission:
Type: "AWS::Lambda::Permission"
Properties:
Action: "lambda:InvokeFunction"
FunctionName: !Ref FlexMatchStatusPollerLambdaFunction
Principal: events.amazonaws.com
SourceArn: !GetAtt FlexMatchStatusPollerScheduledRule.Arn
MatchmakerEventHandlerLambdaPermission:
Type: "AWS::Lambda::Permission"
Properties:
Action: "lambda:InvokeFunction"
FunctionName: !Ref MatchmakerEventHandlerLambdaFunction
Principal: sns.amazonaws.com
SourceArn: !Ref MatchmakerEventTopic
MatchmakerEventTopicPolicy:
Type: "AWS::SNS::TopicPolicy"
DependsOn: MatchmakerEventTopic
Properties:
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service: gamelift.amazonaws.com
Action:
- "sns:Publish"
Resource: !Ref MatchmakerEventTopic
Topics:
- Ref: MatchmakerEventTopic
ResultsRequestLambdaFunctionApiGatewayPermission:
Type: "AWS::Lambda::Permission"
Properties:
Action: "lambda:InvokeFunction"
FunctionName: !GetAtt ResultsRequestLambdaFunction.Arn
Principal: apigateway.amazonaws.com
SourceArn: !Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${RestApi}/*/*/*"
MatchmakingConfiguration:
Type: "AWS::GameLift::MatchmakingConfiguration"
DependsOn:
- GameSessionQueue
- MatchmakingRuleSet
Properties:
AcceptanceRequired: false
BackfillMode: MANUAL
Description: Matchmaking configuration for sample GameLift game
FlexMatchMode: WITH_QUEUE
GameSessionQueueArns:
- "Fn::GetAtt":
- GameSessionQueue
- Arn
Name: !Sub ${GameNameParameter}MatchmakingConfiguration
NotificationTarget: !Ref MatchmakerEventTopic
RequestTimeoutSeconds: !Ref MatchmakerTimeoutInSecondsParameter
RuleSetName: !Ref MatchmakingRuleSet
GameRequestLambdaFunction:
Type: "AWS::Lambda::Function"
Properties:
Code:
S3Bucket: !Ref LambdaZipS3BucketParameter
S3Key: !Ref LambdaZipS3KeyParameter
Description: Lambda function to handle game requests
Environment:
Variables:
MatchmakingConfigurationName: !GetAtt MatchmakingConfiguration.Name
MatchmakingRequestTableName: !Ref MatchmakingRequestTable
TeamName: !Ref TeamNameParameter
FunctionName: !Sub ${GameNameParameter}GameRequestLambda
Handler: game_request.handler
MemorySize: 128
Role: !GetAtt GameRequestLambdaFunctionExecutionRole.Arn
Runtime: python3.14
GameRequestLambdaFunctionApiGatewayPermission:
Type: "AWS::Lambda::Permission"
Properties:
Action: "lambda:InvokeFunction"
FunctionName: !GetAtt GameRequestLambdaFunction.Arn
Principal: apigateway.amazonaws.com
SourceArn: !Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${RestApi}/*/*/*"
Outputs:
ApiGatewayEndpoint:
Description: Url of ApiGateway Endpoint
Value: !Sub "https://${RestApi}.execute-api.${AWS::Region}.amazonaws.com/${ApiGatewayStageNameParameter}/"
UserPoolClientId:
Description: Id of UserPoolClient
Value: !Ref UserPoolClient
IdentityRegion:
Description: Region name
Value: !Ref "AWS::Region"

View File

@@ -0,0 +1,33 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
---
# THIS IS A SAMPLE CLOUDFORMATION PARAMETERS FILE
GameNameParameter:
value: "{{AWSGAMELIFT::SYS::GAMENAME}}"
LambdaZipS3BucketParameter:
value: "{{AWSGAMELIFT::VARS::LambdaZipS3BucketParameter}}"
LambdaZipS3KeyParameter:
value: "{{AWSGAMELIFT::VARS::LambdaZipS3KeyParameter}}"
ApiGatewayStageNameParameter:
value: "{{AWSGAMELIFT::VARS::ApiGatewayStageNameParameter}}"
ContainerGroupDefinitionNameParameter:
value: "{{AWSGAMELIFT::VARS::ContainerGroupDefinitionNameParameter}}"
ContainerImageNameParameter:
value: "{{AWSGAMELIFT::VARS::ContainerImageNameParameter}}"
ContainerImageUriParameter:
value: "{{AWSGAMELIFT::VARS::ContainerImageUriParameter}}"
LaunchPathParameter:
value: "{{AWSGAMELIFT::VARS::LaunchPathParameter}}"
TotalVcpuLimitParameter:
value: "{{AWSGAMELIFT::VARS::TotalVcpuLimitParameter}}"
TotalMemoryLimitParameter:
value: "{{AWSGAMELIFT::VARS::TotalMemoryLimitParameter}}"
FleetUdpFromPortParameter:
value: "{{AWSGAMELIFT::VARS::FleetUdpFromPortParameter}}"
FleetUdpToPortParameter:
value: "{{AWSGAMELIFT::VARS::FleetUdpToPortParameter}}"
UnrealEngineVersionParameter:
value: "{{AWSGAMELIFT::VARS::UnrealEngineVersionParameter}}"
EnableMetricsParameter:
value: "{{AWSGAMELIFT::VARS::EnableMetricsParameter}}"

View File

@@ -0,0 +1,11 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
---
# Key/value pairs to be added to the game's awsGameLiftClientConfig.yml file
# These values will be replaced at the end of create/update of
# the feature's CloudFormation stack.
user_pool_client_id: "{{AWSGAMELIFT::CFNOUTPUT::UserPoolClientId}}"
identity_api_gateway_base_url: "{{AWSGAMELIFT::CFNOUTPUT::ApiGatewayEndpoint}}"
identity_region: "{{AWSGAMELIFT::CFNOUTPUT::IdentityRegion}}"

View File

@@ -0,0 +1,145 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
import boto3
from boto3.dynamodb.conditions import Attr, And
from botocore.exceptions import ClientError
import os
import time
NON_TERMINAL_REQUEST_QUERY_LIMIT = 50
MATCHMAKING_STARTED_STATUS = 'MatchmakingStarted'
MATCHMAKING_SUCCEEDED_STATUS = 'MatchmakingSucceeded'
MATCHMAKING_TIMED_OUT_STATUS = 'MatchmakingTimedOut'
MATCHMAKING_CANCELLED_STATUS = 'MatchmakingCancelled'
MATCHMAKING_FAILED_STATUS = 'MatchmakingFailed'
MAX_PARTITION_SIZE = 10
MIN_TIME_ELAPSED_BEFORE_UPDATE_IN_SECONDS = 30
def handler(event, context):
"""
Finds all non-terminal matchmaking requests and poll for their status. It is recommended by GameLift
to regularly poll for ticket status at a low rate.
See: https://docs.aws.amazon.com/gamelift/latest/flexmatchguide/match-client.html#match-client-track
:param event: lambda event, not used by this function
:param context: lambda context, not used by this function
:return: None
"""
lambda_start_time = round(time.time())
print(f"Polling non-terminal matchmaking tickets. Lambda start time: {lambda_start_time}")
matchmaking_request_table_name = os.environ['MatchmakingRequestTableName']
dynamodb = boto3.resource('dynamodb')
matchmaking_request_table = dynamodb.Table(matchmaking_request_table_name)
gamelift = boto3.client('gamelift')
matchmaking_requests = matchmaking_request_table.scan(
Limit=NON_TERMINAL_REQUEST_QUERY_LIMIT,
FilterExpression=And(Attr('TicketStatus').eq(MATCHMAKING_STARTED_STATUS),
Attr('LastUpdatedTime').lt(lambda_start_time - MIN_TIME_ELAPSED_BEFORE_UPDATE_IN_SECONDS))
)
if matchmaking_requests['Count'] <= 0:
print("No non-terminal matchmaking requests found")
for matchmaking_requests in partition(matchmaking_requests['Items'], MAX_PARTITION_SIZE):
ticket_id_to_request_mapping = {request['TicketId']: request for request in matchmaking_requests}
describe_matchmaking_result = gamelift.describe_matchmaking(
TicketIds=list(ticket_id_to_request_mapping.keys())
)
ticket_list = describe_matchmaking_result['TicketList']
if len(ticket_list) != len(matchmaking_requests):
print(f"Resulting TicketList length: {len(ticket_list)} from DescribeMatchmaking "
f"does not match the request size: {len(matchmaking_requests)}")
for ticket in ticket_list:
ticket_id = ticket['TicketId']
ticket_status = ticket['Status']
matchmaking_request_status = to_matchmaking_request_status(ticket_status)
matchmaking_request = ticket_id_to_request_mapping[ticket_id]
player_id = matchmaking_request['PlayerId']
start_time = matchmaking_request['StartTime']
last_updated_time = matchmaking_request['LastUpdatedTime']
try:
attribute_updates = {
'LastUpdatedTime': {
'Value': lambda_start_time
}
}
if ticket_status in ['COMPLETED', 'FAILED', 'TIMED_OUT', 'CANCELLED']:
print(f'Ticket: {ticket_id} status was updated to {ticket_status}')
attribute_updates.update({
'TicketStatus': {
'Value': matchmaking_request_status
}
})
if ticket_status == 'COMPLETED':
# parse the playerSessionId
matched_player_sessions = ticket.get('GameSessionConnectionInfo', {}).get('MatchedPlayerSessions')
player_session_id = None
if matched_player_sessions is not None and len(matched_player_sessions) == 1:
player_session_id = matched_player_sessions[0].get('PlayerSessionId')
attribute_updates.update({
'IpAddress': {
'Value': ticket.get('GameSessionConnectionInfo', {}).get('IpAddress')
},
'DnsName': {
'Value': ticket.get('GameSessionConnectionInfo', {}).get('DnsName')
},
'Port': {
'Value': str(ticket.get('GameSessionConnectionInfo', {}).get('Port'))
},
'GameSessionArn': {
'Value': str(ticket.get('GameSessionConnectionInfo', {}).get('GameSessionArn'))
},
'PlayerSessionId': {
'Value' : str(player_session_id)
}
})
else:
print(f'No updates to ticket: {ticket_id} compared to '
f'{lambda_start_time - last_updated_time} seconds ago')
matchmaking_request_table.update_item(
Key={
'PlayerId': player_id,
'StartTime': start_time
},
AttributeUpdates=attribute_updates,
Expected={
'TicketStatus': {
'Value': MATCHMAKING_STARTED_STATUS,
'ComparisonOperator': 'EQ'
}
}
)
except ClientError as e:
error_code = e.response['Error']['Code']
if error_code == 'ConditionCheckFailedException':
print(f"Ticket: {ticket_id} status has been updated (likely by MatchMakerEventHandler). "
f"No change is made")
continue
raise e
def partition(collection, n):
"""Yield successive n-sized partitions from collection."""
for i in range(0, len(collection), n):
yield collection[i:i + n]
def to_matchmaking_request_status(ticket_status):
if ticket_status == 'COMPLETED':
return MATCHMAKING_SUCCEEDED_STATUS
if ticket_status == 'FAILED':
return MATCHMAKING_FAILED_STATUS
if ticket_status == 'TIMED_OUT':
return MATCHMAKING_TIMED_OUT_STATUS
if ticket_status == 'CANCELLED':
return MATCHMAKING_CANCELLED_STATUS

View File

@@ -0,0 +1,133 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
import boto3
from boto3.dynamodb.conditions import Key
import time
import os
import json
DEFAULT_TTL_IN_SECONDS = 10 * 60 # 10 minutes
MATCHMAKING_STARTED_STATUS = 'MatchmakingStarted'
MATCHMAKING_SUCCEEDED_STATUS = 'MatchmakingSucceeded'
MATCHMAKING_TIMED_OUT_STATUS = 'MatchmakingTimedOut'
MATCHMAKING_CANCELLED_STATUS = 'MatchmakingCancelled'
MATCHMAKING_FAILED_STATUS = 'MatchmakingFailed'
def handler(event, context):
"""
Handles requests to start games from the game client.
This function records the game request from the client in the MatchmakingRequest table and calls
GameLift to start matchmaking.
:param event: lambda event, contains the region to player latency mapping in `regionToLatencyMapping` key, as well
as the player information from the Cognito id tokens.
:param context: lambda context, not used by this function
:return:
- 202 (Accepted) if the matchmaking request is accepted and is now being processed
- 409 (Conflict) if the another matchmaking request is in progress
- 500 (Internal Error) if error occurred when calling GameLift to start matchmaking
"""
player_id = event["requestContext"]["authorizer"]["claims"]["sub"]
start_time = round(time.time())
print(f'Handling start game request. PlayerId: {player_id}, StartTime: {start_time}')
region_to_latency_mapping = get_region_to_latency_mapping(event)
if region_to_latency_mapping:
print(f"Region to latency mapping: {region_to_latency_mapping}")
else:
print("No regionToLatencyMapping mapping provided")
matchmaking_request_table_name = os.environ['MatchmakingRequestTableName']
team_name = os.environ['TeamName']
matchmaking_configuration_name = os.environ['MatchmakingConfigurationName']
dynamodb = boto3.resource('dynamodb')
matchmaking_request_table = dynamodb.Table(matchmaking_request_table_name)
gamelift = boto3.client('gamelift')
matchmaking_requests = matchmaking_request_table.query(
KeyConditionExpression=Key('PlayerId').eq(player_id),
ScanIndexForward=False
)
if matchmaking_requests['Count'] > 0 \
and not is_matchmaking_request_terminal(matchmaking_requests['Items'][0]):
# A existing matchmaking request in progress
return {
'headers': {
'Content-Type': 'text/plain'
},
'statusCode': 409 # Conflict
}
try:
player = {
'PlayerId': player_id,
'Team': team_name
}
if region_to_latency_mapping:
player['LatencyInMs'] = region_to_latency_mapping
start_matchmaking_request = {
"ConfigurationName": matchmaking_configuration_name,
"Players": [player]
}
print(f"Starting matchmaking in GameLift. Request: {start_matchmaking_request}")
start_matchmaking_result = gamelift.start_matchmaking(**start_matchmaking_request)
ticket_id = start_matchmaking_result['MatchmakingTicket']['TicketId']
ticket_status = MATCHMAKING_STARTED_STATUS
matchmaking_request_table.put_item(
Item={
'PlayerId': player_id,
'StartTime': start_time,
'LastUpdatedTime': start_time,
'ExpirationTime': start_time + DEFAULT_TTL_IN_SECONDS,
'TicketStatus': ticket_status,
'TicketId': ticket_id
}
)
return {
# Matchmaking request enqueued
'headers': {
'Content-Type': 'text/plain'
},
'statusCode': 202
}
except Exception as ex:
print(f'Error occurred when calling GameLift to start matchmaking. Exception: {ex}')
return {
# Error occurred when enqueuing matchmaking request
'headers': {
'Content-Type': 'text/plain'
},
'statusCode': 500
}
def is_matchmaking_request_terminal(matchmaking_request):
return matchmaking_request['TicketStatus'] in [
MATCHMAKING_SUCCEEDED_STATUS,
MATCHMAKING_TIMED_OUT_STATUS,
MATCHMAKING_CANCELLED_STATUS,
MATCHMAKING_FAILED_STATUS
]
def get_region_to_latency_mapping(event):
request_body = event.get("body")
if not request_body:
return None
try:
request_body_json = json.loads(request_body)
except ValueError:
print(f"Error parsing request body: {request_body}")
return None
if request_body_json and request_body_json.get('regionToLatencyMapping'):
return request_body_json.get('regionToLatencyMapping')

View File

@@ -0,0 +1,107 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
import boto3
from boto3.dynamodb.conditions import Key
import os
import json
import time
MATCHMAKING_STARTED_STATUS = 'MatchmakingStarted'
MATCHMAKING_SUCCEEDED_STATUS = 'MatchmakingSucceeded'
MATCHMAKING_TIMED_OUT_STATUS = 'MatchmakingTimedOut'
MATCHMAKING_CANCELLED_STATUS = 'MatchmakingCancelled'
MATCHMAKING_FAILED_STATUS = 'MatchmakingFailed'
def handler(event, context):
"""
Handles game session event from GameLift FlexMatch. This function parses the event messages and updates all
related requests in the the MatchmakingPlacement DynamoDB table,
which will be looked up to fulfill game client's Results Requests.
:param event: lambda event containing game session event from GameLift queue
:param context: lambda context, not used by this function
:return: None
"""
lambda_start_time = round(time.time())
message = json.loads(event['Records'][0]['Sns']['Message'])
print(f'Handling FlexMatch event. StartTime: {lambda_start_time}. Message: {message}')
status_type = message['detail']['type']
if status_type not in [MATCHMAKING_SUCCEEDED_STATUS, MATCHMAKING_TIMED_OUT_STATUS, MATCHMAKING_CANCELLED_STATUS,
MATCHMAKING_FAILED_STATUS]:
print(f'Received non-terminal status type: {status_type}. Skip processing.')
return
tickets = message['detail']['tickets']
ip_address = message['detail']['gameSessionInfo'].get('ipAddress')
dns_name = message['detail']['gameSessionInfo'].get('dnsName')
port = str(message['detail']['gameSessionInfo'].get('port'))
game_session_arn = str(message['detail']['gameSessionInfo'].get('gameSessionArn'))
players = message['detail']['gameSessionInfo']['players']
players_map = {player.get('playerId'):player.get('playerSessionId') for player in players}
ticket_id_index_name = os.environ['TicketIdIndexName']
matchmaking_request_table_name = os.environ['MatchmakingRequestTableName']
dynamodb = boto3.resource('dynamodb')
matchmaking_request_table = dynamodb.Table(matchmaking_request_table_name)
for ticket in tickets:
ticket_id = ticket['ticketId']
matchmaking_requests = matchmaking_request_table.query(
IndexName=ticket_id_index_name,
KeyConditionExpression=Key('TicketId').eq(ticket_id)
)
if matchmaking_requests['Count'] <= 0:
print(f"Cannot find matchmaking request with ticket id: {ticket_id}")
continue
matchmaking_request_status = matchmaking_requests['Items'][0]['TicketStatus']
player_id = matchmaking_requests['Items'][0]['PlayerId']
player_session_id = players_map.get(player_id)
print(f'Processing Ticket: {ticket_id}, PlayerId: {player_id}, PlayerSessionId: {player_session_id}')
matchmaking_request_start_time = matchmaking_requests['Items'][0]['StartTime']
if matchmaking_request_status != MATCHMAKING_STARTED_STATUS:
print(f"Unexpected TicketStatus on matchmaking request. Expected: 'MatchmakingStarted'. "
f"Found: {matchmaking_request_status}")
continue
attribute_updates = {
'TicketStatus': {
'Value': status_type
},
'LastUpdatedTime': {
'Value': lambda_start_time
}
}
if status_type == MATCHMAKING_SUCCEEDED_STATUS:
attribute_updates.update({
'IpAddress': {
'Value': ip_address
},
'DnsName': {
'Value': dns_name
},
'Port': {
'Value': port
},
'GameSessionArn': {
'Value': game_session_arn
},
'PlayerSessionId': {
'Value': player_session_id
}
})
matchmaking_request_table.update_item(
Key={
'PlayerId': player_id,
'StartTime': matchmaking_request_start_time
},
AttributeUpdates=attribute_updates
)

View File

@@ -0,0 +1,84 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
import boto3
import os
import json
from boto3.dynamodb.conditions import Key
MATCHMAKING_STARTED_STATUS = 'MatchmakingStarted'
MATCHMAKING_SUCCEEDED_STATUS = 'MatchmakingSucceeded'
def handler(event, context):
"""
Handles requests to describe the game session connection information after a StartGame request.
This function will look up the MatchmakingRequest table to find the latest matchmaking request
by the player, and return its game connection information if any.
:param event: lambda event, contains the region to player latency mapping in `regionToLatencyMapping` key, as well
as the player information from the Cognito id tokens. Cognito provides a `sub` user attribute that we use as a
Player ID. Unlike `username` value `sub` is UUID for a user which is never reassigned to another user.
:param context: lambda context, not used by this function
:return:
- 200 (OK) if the game connection is ready, along with server info: "IpAddress", "Port", "DnsName", "PlayerSessionId", "PlayerId", "PlayerSessionId", "PlayerId"
- 204 (No Content) if the requested game is still in progress of matchmaking
- 404 (Not Found) if no game has been started by the player, or if all started game were expired
- 500 (Internal Error) if errors occurred during matchmaking or placement
"""
player_id = event["requestContext"]["authorizer"]["claims"]["sub"]
print(f'Handling request result request. PlayerId: {player_id}')
matchmaking_request_table_name = os.environ['MatchmakingRequestTableName']
dynamodb = boto3.resource('dynamodb')
matchmaking_request_table = dynamodb.Table(matchmaking_request_table_name)
matchmaking_requests = matchmaking_request_table.query(
KeyConditionExpression=Key('PlayerId').eq(player_id),
ScanIndexForward=False
)
if matchmaking_requests['Count'] <= 0:
return {
'headers': {
'Content-Type': 'text/plain'
},
'statusCode': 404
}
latest_matchmaking_request = matchmaking_requests['Items'][0]
print(f'Current Matchmaking Request: {latest_matchmaking_request}')
matchmaking_request_status = latest_matchmaking_request['TicketStatus']
if matchmaking_request_status == MATCHMAKING_STARTED_STATUS:
# still waiting for ticket to be processed
return {
'headers': {
'Content-Type': 'text/plain'
},
'statusCode': 204
}
elif matchmaking_request_status == MATCHMAKING_SUCCEEDED_STATUS:
game_session_connection_info = \
dict((k, latest_matchmaking_request[k]) for k in ('IpAddress', 'Port', 'DnsName', 'PlayerSessionId', 'PlayerId', 'GameSessionArn'))
print(f"Connection info: {game_session_connection_info}")
return {
'body': json.dumps(game_session_connection_info),
'headers': {
'Content-Type': 'text/plain'
},
'statusCode': 200
}
else:
# We count MatchmakingCancelled as internal error also because cancelling placement requests is not
# in the current implementation, so it should never happen.
print(f'Received non-successful terminal status {matchmaking_request_status}, responding with 500 error.')
return {
'headers': {
'Content-Type': 'text/plain'
},
'statusCode': 500
}

View File

@@ -0,0 +1,43 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
#
# GameLift Region Mappings
# Keep synchronized with: https://docs.aws.amazon.com/general/latest/gr/gamelift.html
#
# ----------------------------------------------------------------------------------------
# Region Name Region Endpoint Protocol
# ----------------------------------------------------------------------------------------
# US East (Ohio) us-east-2 gamelift.us-east-2.amazonaws.com HTTPS
# US East (N. Virginia) us-east-1 gamelift.us-east-1.amazonaws.com HTTPS
# US West (N. California) us-west-1 gamelift.us-west-1.amazonaws.com HTTPS
# US West (Oregon) us-west-2 gamelift.us-west-2.amazonaws.com HTTPS
# Asia Pacific (Mumbai) ap-south-1 gamelift.ap-south-1.amazonaws.com HTTPS
# Asia Pacific (Seoul) ap-northeast-2 gamelift.ap-northeast-2.amazonaws.com HTTPS
# Asia Pacific (Singapore) ap-southeast-1 gamelift.ap-southeast-1.amazonaws.com HTTPS
# Asia Pacific (Sydney) ap-southeast-2 gamelift.ap-southeast-2.amazonaws.com HTTPS
# Asia Pacific (Tokyo) ap-northeast-1 gamelift.ap-northeast-1.amazonaws.com HTTPS
# Canada (Central) ca-central-1 gamelift.ca-central-1.amazonaws.com HTTPS
# Europe (Frankfurt) eu-central-1 gamelift.eu-central-1.amazonaws.com HTTPS
# Europe (Ireland) eu-west-1 gamelift.eu-west-1.amazonaws.com HTTPS
# Europe (London) eu-west-2 gamelift.eu-west-2.amazonaws.com HTTPS
# South America (São Paulo) sa-east-1 gamelift.sa-east-1.amazonaws.com HTTPS
# ----------------------------------------------------------------------------------------
{
five_letter_region_codes: {
us-east-2 : usea2,
us-east-1 : usea1,
us-west-1 : uswe1,
us-west-2 : uswe2,
ap-south-1 : apso1,
ap-northeast-2 : apne2,
ap-southeast-1 : apse1,
ap-southeast-2 : apse2,
ap-northeast-1 : apne1,
ca-central-1 : cace1,
eu-central-1 : euce1,
eu-west-1 : euwe1,
eu-west-2 : euwe2,
sa-east-1 : saea1
}
}

View File

@@ -0,0 +1,217 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
import string
import boto3
import requests
import json
import time
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("-g", "--game", help="game name", type=str, required=True)
parser.add_argument("-r", "--region", help="region name, e.g. eu-west-1", type=str, required=True)
parser.add_argument("-p", "--profile", help="profile name in the AWS shared credentials file ~/.aws/credentials", type=str, required=True)
args = parser.parse_args()
GAME_NAME = args.game.lower() # e.g. 'GameLiftSampleGame5ue4'
REGION = args.region # e.g. 'eu-west-1'
PROFILE_NAME = args.profile
USER_POOL_NAME = GAME_NAME + 'UserPool'
USER_POOL_CLIENT_NAME = GAME_NAME + 'UserPoolClient'
USERNAME1 = 'testuser1@example.com'
USERNAME2 = 'testuser2@example.com'
USERNAMES = [USERNAME1, USERNAME2]
PASSWORD = 'TestPassw0rd.'
REST_API_NAME = GAME_NAME + 'RestApi'
REST_API_STAGE = 'dev'
GAME_REQUEST_PATH = 'start_game'
RESULTS_REQUEST_PATH = 'get_game_connection'
session = boto3.Session(profile_name=PROFILE_NAME)
cognito_idp = session.client('cognito-idp', region_name=REGION)
apig = session.client('apigateway', region_name=REGION)
REGION_US_WEST_2 = 'us-west-2'
REGION_EU_WEST_1 = 'eu-west-1'
REGION_US_EAST_1 = 'us-east-1'
NO_LATENCY = 'no-latency'
REGIONS_TO_TEST = [REGION_US_WEST_2, REGION_EU_WEST_1, NO_LATENCY]
US_WEST_2_FIRST_REGION_TO_LATENCY_MAPPING = {
"regionToLatencyMapping": {
"us-west-2": 50,
"us-east-1": 100,
"eu-west-1": 150,
"ap-northeast-1": 300
}
}
EU_WEST_1_FIRST_REGION_TO_LATENCY_MAPPING = {
"regionToLatencyMapping": {
"us-west-2": 50,
"us-east-1": 100,
"eu-west-1": 10,
"ap-northeast-1": 300
}
}
REGION_TO_GAME_REQUEST_PAYLOAD_MAPPING = {
REGION_US_WEST_2: json.dumps(US_WEST_2_FIRST_REGION_TO_LATENCY_MAPPING),
REGION_EU_WEST_1: json.dumps(EU_WEST_1_FIRST_REGION_TO_LATENCY_MAPPING),
NO_LATENCY: None
}
REGION_TO_GAME_SESSION_ARN_EXPECTED_LOCATION = {
REGION_US_WEST_2: REGION_US_WEST_2,
REGION_EU_WEST_1: REGION_EU_WEST_1,
NO_LATENCY: REGION_US_EAST_1
}
def main():
user_pool = find_user_pool(USER_POOL_NAME)
user_pool_id = user_pool['Id']
print("User Pool Id:", user_pool_id)
user_pool_client = find_user_pool_client(user_pool_id, USER_POOL_CLIENT_NAME)
user_pool_client_id = user_pool_client['ClientId']
print("User Pool Client Id:", user_pool_client_id)
try:
for region in REGIONS_TO_TEST:
game_request_payload = REGION_TO_GAME_REQUEST_PAYLOAD_MAPPING.get(region)
expected_game_session_region = REGION_TO_GAME_SESSION_ARN_EXPECTED_LOCATION.get(region)
headers_list = []
for username in USERNAMES:
regional_username = get_regional_user_name(username, region)
cognito_idp.sign_up(
ClientId=user_pool_client_id,
Username=regional_username,
Password=PASSWORD,
)
print(f"Created user: {regional_username}")
cognito_idp.admin_confirm_sign_up(
UserPoolId=user_pool_id,
Username=regional_username,
)
init_auth_result = cognito_idp.initiate_auth(
AuthFlow='USER_PASSWORD_AUTH',
AuthParameters={
'USERNAME': regional_username,
'PASSWORD': PASSWORD,
},
ClientId=user_pool_client_id
)
assert init_auth_result['ResponseMetadata']['HTTPStatusCode'] == 200, "Unsuccessful init_auth"
print(f"Authenticated via username and password for {regional_username}")
id_token = init_auth_result['AuthenticationResult']['IdToken']
headers = {
'Auth': id_token
}
headers_list.append(headers)
results_request_url = get_rest_api_endpoint(REST_API_NAME, REGION, REST_API_STAGE, RESULTS_REQUEST_PATH)
game_request_url = get_rest_api_endpoint(REST_API_NAME, REGION, REST_API_STAGE, GAME_REQUEST_PATH)
print(f"results_request_url: {results_request_url}")
print(f"game_request_url: {game_request_url}")
for headers in headers_list:
results_request_response = requests.post(url=results_request_url, headers=headers)
assert results_request_response.status_code == 404, \
f"Expect 'POST /get_game_connection' status code to be 404 (Not Found). Actual: " \
f"{str(results_request_response.status_code)}"
print("Verified lambda ResultsRequest response", results_request_response)
game_request_response = requests.post(url=game_request_url, headers=headers, data=game_request_payload)
print(f"Game request response '{game_request_response}'")
assert game_request_response.status_code == 202, \
f"Expect 'POST /start_game' status code to be 202 (Accepted), actual: " \
f"{str(game_request_response.status_code)}"
print("Verified lambda GameRequest response", game_request_response)
print("Waiting for matchmaking request to be processed...")
verified_players = 0
while verified_players != len(headers_list):
verified_players = 0
time.sleep(10) # 10 seconds
for headers in headers_list:
results_request_response = requests.post(url=results_request_url, headers=headers)
if results_request_response.status_code == 204:
print("Match is not ready yet")
continue
assert results_request_response.status_code == 200, \
f"Expect 'POST /get_game_connection' status code to be 200 (Success), actual: " \
f"{str(results_request_response.status_code)}"
print("Verified lambda ResultsRequest response", results_request_response)
game_connection_info = json.loads(results_request_response.content)
print(f"Game connection info '{game_connection_info}'")
assert game_connection_info['IpAddress'] != ''
assert int(game_connection_info['Port']) > 0
assert REGION in game_connection_info['DnsName'], \
f"Expect {game_connection_info['DnsName']} to contain '{REGION}'"
assert expected_game_session_region in game_connection_info['GameSessionArn'], \
f"Expect {game_connection_info['GameSessionArn']} to contain '{expected_game_session_region}'"
assert "psess-" in game_connection_info['PlayerSessionId'], \
f"Expect {game_connection_info['PlayerSessionId']} to contain 'psess-'"
print("Verified game connection info:", game_connection_info)
verified_players += 1
print(f"{verified_players} players' game sessions verified")
finally:
for region in REGIONS_TO_TEST:
for username in USERNAMES:
regional_username = get_regional_user_name(username, region)
cognito_idp.admin_delete_user(
UserPoolId=user_pool_id,
Username=regional_username,
)
print("Deleted user:", regional_username)
print("Test Succeeded!")
def get_regional_user_name(username, region):
return f"{region}_{username}"
def find_user_pool(user_pool_name):
print("Finding user pool:", user_pool_name)
result = cognito_idp.list_user_pools(MaxResults=50)
pools = result['UserPools']
return next(x for x in pools if x['Name'] == user_pool_name)
def find_user_pool_client(user_pool_id, user_pool_client_name):
print("Finding user pool client:", user_pool_client_name)
results = cognito_idp.list_user_pool_clients(UserPoolId=user_pool_id)
clients = results['UserPoolClients']
return next(x for x in clients if x['ClientName'] == user_pool_client_name)
def find_rest_api(rest_api_name):
print("Finding rest api:", rest_api_name)
results = apig.get_rest_apis()
rest_apis = results['items']
return next(x for x in rest_apis if x['name'] == rest_api_name)
def get_rest_api_endpoint(rest_api_name, region, stage, path):
print("Getting rest api endpoint", rest_api_name)
rest_api = find_rest_api(rest_api_name)
rest_api_id = rest_api['id']
return f'https://{rest_api_id}.execute-api.{region}.amazonaws.com/{stage}/{path}'
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,43 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
#
# GameLift Region Mappings
# Keep synchronized with: https://docs.aws.amazon.com/general/latest/gr/gamelift.html
#
# ----------------------------------------------------------------------------------------
# Region Name Region Endpoint Protocol
# ----------------------------------------------------------------------------------------
# US East (Ohio) us-east-2 gamelift.us-east-2.amazonaws.com HTTPS
# US East (N. Virginia) us-east-1 gamelift.us-east-1.amazonaws.com HTTPS
# US West (N. California) us-west-1 gamelift.us-west-1.amazonaws.com HTTPS
# US West (Oregon) us-west-2 gamelift.us-west-2.amazonaws.com HTTPS
# Asia Pacific (Mumbai) ap-south-1 gamelift.ap-south-1.amazonaws.com HTTPS
# Asia Pacific (Seoul) ap-northeast-2 gamelift.ap-northeast-2.amazonaws.com HTTPS
# Asia Pacific (Singapore) ap-southeast-1 gamelift.ap-southeast-1.amazonaws.com HTTPS
# Asia Pacific (Sydney) ap-southeast-2 gamelift.ap-southeast-2.amazonaws.com HTTPS
# Asia Pacific (Tokyo) ap-northeast-1 gamelift.ap-northeast-1.amazonaws.com HTTPS
# Canada (Central) ca-central-1 gamelift.ca-central-1.amazonaws.com HTTPS
# Europe (Frankfurt) eu-central-1 gamelift.eu-central-1.amazonaws.com HTTPS
# Europe (Ireland) eu-west-1 gamelift.eu-west-1.amazonaws.com HTTPS
# Europe (London) eu-west-2 gamelift.eu-west-2.amazonaws.com HTTPS
# South America (São Paulo) sa-east-1 gamelift.sa-east-1.amazonaws.com HTTPS
# ----------------------------------------------------------------------------------------
{
five_letter_region_codes: {
us-east-2 : usea2,
us-east-1 : usea1,
us-west-1 : uswe1,
us-west-2 : uswe2,
ap-south-1 : apso1,
ap-northeast-2 : apne2,
ap-southeast-1 : apse1,
ap-southeast-2 : apse2,
ap-northeast-1 : apne1,
ca-central-1 : cace1,
eu-central-1 : euce1,
eu-west-1 : euwe1,
eu-west-2 : euwe2,
sa-east-1 : saea1
}
}

View File

@@ -0,0 +1,24 @@
$ErrorActionPreference="Stop"
echo "Running aws ecr get-login-password --region {{REGION}} --profile {{PROFILE_NAME}} | docker login --username AWS --password-stdin {{ECR_REGISTRY_URL}}"
aws ecr get-login-password --region {{REGION}} --profile {{PROFILE_NAME}} | docker login --username AWS --password-stdin {{ECR_REGISTRY_URL}}
if (!$LastExitCode -eq 0) {
echo "Docker login has failed."
exit $LastExitCode
}
echo "Running docker tag {{IMAGE_ID}} {{ECR_REPO_URI}}:{{IMAGE_TAG}}"
docker tag {{IMAGE_ID}} {{ECR_REPO_URI}}:{{IMAGE_TAG}}
if (!$LastExitCode -eq 0) {
echo "Docker tag has failed."
exit $LastExitCode
}
echo "Running docker push {{ECR_REPO_URI}}:{{IMAGE_TAG}}"
docker push {{ECR_REPO_URI}}:{{IMAGE_TAG}}
if ($LastExitCode -eq 0)
{
echo "ECR Container image setup succeeded."
}
else
{
echo "ECR Container image setup has failed."
}

View File

@@ -0,0 +1,61 @@
# Base image
# ----------
# Add the base image that you want to use over here,
# Make sure to use an image with the same architecture as the
# Instance type you are planning to use on your fleets.
FROM public.ecr.aws/amazonlinux/amazonlinux
# Game build directory
# --------------------
# Add your game build directory in the 'GAME_BUILD_DIRECTORY' env variable below.
#
# Game executable
# ---------------
# Add the relative path to your executable in the 'GAME_EXECUTABLE' env variable below.
# The game build provided over here needs to be integrated with gamelift server sdk.
# This template assumes that the executable path is relative to the game build directory.
#
# Launch parameters
# -----------------
# Add any launch parameters to pass into your executable in the 'LAUNCH_PARAMS' env variable below.
#
# Default directory
# -----------------
# The value provided in 'HOME_DIR' below will be where the game executable and logs exist.
#
ARG GAME_BUILD_DIRECTORY
ARG GAME_EXECUTABLE
ARG LAUNCH_PARAMS
ENV GAME_BUILD_DIRECTORY=$GAME_BUILD_DIRECTORY \
GAME_EXECUTABLE=$GAME_EXECUTABLE \
LAUNCH_PARAMS=$LAUNCH_PARAMS \
HOME_DIR="/local/game"
# install dependencies as necessary
RUN yum install -y shadow-utils
RUN mkdir -p $HOME_DIR
COPY ./gamebuild/$GAME_BUILD_DIRECTORY/ $HOME_DIR
# Change directory to home
WORKDIR $HOME_DIR
RUN useradd -m gamescale && \
echo "gamescale ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers && \
chown -R gamescale:gamescale $HOME_DIR
# Add permissions to game build
RUN chmod +x ./$GAME_EXECUTABLE
USER gamescale
# Check directory before starting the container
RUN ls -lhrt .
# Check path before starting the container
RUN echo $PATH
# Start the game build
ENTRYPOINT ["/bin/sh", "-c", "./$GAME_EXECUTABLE", "$LAUNCH_PARAMS"]

View File

@@ -0,0 +1,39 @@
$ErrorActionPreference="Stop"
echo "Running aws ecr get-login-password --region {{REGION}} --profile {{PROFILE_NAME}} | docker login --username AWS --password-stdin {{ECR_REGISTRY_URL}}"
aws ecr get-login-password --region {{REGION}} --profile {{PROFILE_NAME}} | docker login --username AWS --password-stdin {{ECR_REGISTRY_URL}}
if (!$LastExitCode -eq 0) {
echo "Docker login has failed."
exit $LastExitCode
}
echo "Running docker ps"
docker ps
if (!$LastExitCode -eq 0) {
echo "Docker ps has failed. Please start Docker Desktop and try again."
exit $LastExitCode
}
$docker_build_cmd = 'docker build -t {{REPO_NAME}}:{{IMAGE_TAG}} "{{IMAGE_PATH}}" --progress=plain --build-arg GAME_BUILD_DIRECTORY="{{GAME_BUILD_DIRECTORY}}" --build-arg GAME_EXECUTABLE="{{GAME_EXECUTABLE}}"'
echo "Running: $docker_build_cmd"
Invoke-Expression $docker_build_cmd
if (!$LastExitCode -eq 0) {
echo "Docker build has failed."
exit $LastExitCode
}
echo "Running docker tag {{REPO_NAME}}:{{IMAGE_TAG}} {{ECR_REPO_URI}}:{{IMAGE_TAG}}"
docker tag {{REPO_NAME}}:{{IMAGE_TAG}} {{ECR_REPO_URI}}:{{IMAGE_TAG}}
if (!$LastExitCode -eq 0) {
echo "Docker tag has failed."
exit $LastExitCode
}
echo "Running docker push {{ECR_REPO_URI}}:{{IMAGE_TAG}}"
docker push {{ECR_REPO_URI}}:{{IMAGE_TAG}}
if ($LastExitCode -eq 0)
{
echo "ECR Container image setup succeeded."
}
else
{
echo "ECR Container image setup has failed."
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB