Create a simple custom service using AWS Lambda
Overview
One way to extend AccelByte Gaming Services (AGS) is to use the AccelByte Extend SDK to invoke AGS endpoints from a custom built service.
The AccelByte Extend SDK is available in multiple programming languages. In this guide, we will show you how to create a simple custom service using the AWS Lambda Function URL written in Go using the Go Extend SDK. This simple custom service will provide endpoints to add a stat to a user, get stats on a user, and delete a stat from a user.
Goals
In this guide you will:
- Create An AWS Lambda Project using the AWS SAM CLI
- Create AWS Lambda Handlers
- Configure the SAM Template YAML
- Test the AWS Lambda locally
- Deploy the Lambda to AWS
Prerequisites
In order to start on this guide, you should have:
- Installed AWS SAM CLI
- Installed Docker
- Installed Go 1.16
- Installed Postman
- Been granted access to the AGS demo environment:
- Create a Game Namespace if you don't have one yet. Keep the
Namespace ID
. - Create an OAuth Client with the
confidential
client type. Make sure to add the following permissions, and record theClient ID
andClient Secret
: ADMIN:NAMESPACE:{namespace}:USER:*:STATITEM - CREATE, READ - Create a statistic configuration, and record the
Stat Code
. - One test user account. Record the
User ID
.
- Create a Game Namespace if you don't have one yet. Keep the
- An
AWS_ACCESS_KEY_ID
andAWS_SECRET_ACCESS_KEY
for deploying AWS Lambda
Before starting you should be familiar with:
- Developing an AWS Lambda Function URL in Go
- Using the Go Extend SDK in a Go project
- Deploying an AWS Lambda Function URL
Create an AWS Lambda project using AWS SAM CLI
Run the following
sam init
command to create a new AWS Lambda project with a go1.x runtime. Use thehello-world
app template for this project.sam init --name aws-lambda-example --runtime go1.x --app-template hello-world --no-tracing --no-application-insights
If the
sam init
command is successful, a new folder namedaws-lambda-example
will be created.
Create AWS Lambda handlers
In the aws-lambda-example
folder, copy the existing hello-world
folder to three folders named CreateUserStats
, GetUserStats
, and DeleteUserStats
. In each of these three *UserStats
folders, implement the necessary handlers in the main.go
file. These handlers are for adding a stat to a user, getting stats of a user, and deleting a stat from a user, respectively, as shown below. Run go mod tidy
as required for each of them. Finally, delete the hello-world
folder as it is no longer required.
Implement the handlers in CreateUserStats/main.go.
package main
import (
"encoding/json"
"fmt"
"strings"
"time"
"github.com/AccelByte/accelbyte-go-sdk/services-api/pkg/factory"
"github.com/AccelByte/accelbyte-go-sdk/services-api/pkg/service/iam"
"github.com/AccelByte/accelbyte-go-sdk/services-api/pkg/service/social"
"github.com/AccelByte/accelbyte-go-sdk/services-api/pkg/utils/auth"
"github.com/AccelByte/accelbyte-go-sdk/services-api/pkg/utils/auth/validator"
"github.com/AccelByte/accelbyte-go-sdk/social-sdk/pkg/socialclient/user_statistic"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
"github.com/sirupsen/logrus"
)
var (
// use the default config and token implementation
configRepo = *auth.DefaultConfigRepositoryImpl()
tokenRepo = *auth.DefaultTokenRepositoryImpl()
oAuth20Service = iam.OAuth20Service{
Client: factory.NewIamClient(&configRepo),
ConfigRepository: &configRepo,
TokenRepository: &tokenRepo,
}
userStatisticService = &social.UserStatisticService{
Client: factory.NewSocialClient(&configRepo),
TokenRepository: &tokenRepo,
}
)
type Request struct {
Namespace string `json:"namespace"`
UserID string `json:"userId"`
StatCode string `json:"statCode"`
}
func main() {
lambda.Start(Handler)
}
func Handler(evt events.LambdaFunctionURLRequest) (events.LambdaFunctionURLResponse, error) {
// parse the events
request := Request{}
err := json.Unmarshal([]byte(evt.Body), &request)
if err != nil {
errString := fmt.Errorf("failed to parse the request. %s", err.Error())
logrus.Error(errString)
return events.LambdaFunctionURLResponse{}, errString
}
// parse the access token
reqToken := evt.Headers["authorization"]
splitToken := strings.Split(reqToken, "Bearer ")
if len(splitToken) == 1 || len(splitToken) > 2 {
errString := fmt.Errorf("invalid token. Token split \"Bearer\" and token authorization")
logrus.Print(errString)
return events.LambdaFunctionURLResponse{}, errString
}
// login client
clientId := oAuth20Service.ConfigRepository.GetClientId()
clientSecret := oAuth20Service.ConfigRepository.GetClientSecret()
errLogin := oAuth20Service.LoginClient(&clientId, &clientSecret)
if errLogin != nil {
errString := fmt.Errorf("failed to login client. %s", errLogin.Error())
logrus.Error(errString)
return events.LambdaFunctionURLResponse{}, errString
}
// start token validation
errValidateToken := validateToken(splitToken[1], request.Namespace, request.UserID)
if errValidateToken != nil {
errString := fmt.Errorf("failed to validate token. %s", errValidateToken.Error())
logrus.Error(errString)
return events.LambdaFunctionURLResponse{}, errString
}
// create user stat item
inputUserStatItem := &user_statistic.CreateUserStatItemParams{
Namespace: request.Namespace,
StatCode: request.StatCode,
UserID: request.UserID,
}
errUserStatItem := userStatisticService.CreateUserStatItemShort(inputUserStatItem)
if errUserStatItem != nil {
errString := fmt.Errorf("failed to create user stat item. %s", errUserStatItem.Error())
logrus.Error(errString)
return events.LambdaFunctionURLResponse{}, errString
}
return events.LambdaFunctionURLResponse{
StatusCode: 200,
Body: "User stat code added successfully",
}, nil
}
func validateToken(accessToken, namespace, userId string) error {
// initialize token validator
tokenValidator := validator.NewTokenValidator(oAuth20Service, time.Hour)
tokenValidator.Initialize()
// validate stat item
requiredPermissionStatItem := validator.Permission{
Action: 1, // create
Resource: fmt.Sprintf("ADMIN:NAMESPACE:%s:USER:%s:STATITEM", namespace, userId),
}
errValidateStatItem := tokenValidator.Validate(accessToken, &requiredPermissionStatItem, &namespace, nil)
if errValidateStatItem != nil {
return errValidateStatItem
}
return nil
}Implement the handlers in GetUserStats/main.go.
package main
import (
"encoding/json"
"fmt"
"strings"
"time"
"github.com/AccelByte/accelbyte-go-sdk/services-api/pkg/factory"
"github.com/AccelByte/accelbyte-go-sdk/services-api/pkg/service/iam"
"github.com/AccelByte/accelbyte-go-sdk/services-api/pkg/service/social"
"github.com/AccelByte/accelbyte-go-sdk/services-api/pkg/utils/auth"
"github.com/AccelByte/accelbyte-go-sdk/services-api/pkg/utils/auth/validator"
"github.com/AccelByte/accelbyte-go-sdk/social-sdk/pkg/socialclient/user_statistic"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
"github.com/sirupsen/logrus"
)
var (
// use the default config and token implementation
configRepo = *auth.DefaultConfigRepositoryImpl()
tokenRepo = *auth.DefaultTokenRepositoryImpl()
oAuth20Service = iam.OAuth20Service{
Client: factory.NewIamClient(&configRepo),
ConfigRepository: &configRepo,
TokenRepository: &tokenRepo,
}
userStatisticService = &social.UserStatisticService{
Client: factory.NewSocialClient(&configRepo),
TokenRepository: &tokenRepo,
}
)
type Request struct {
Namespace string `json:"namespace"`
UserID string `json:"userId"`
StatCode string `json:"statCode"`
Limit int `json:"limit"`
Offset int `json:"offset"`
Tags string `json:"tags"`
}
func main() {
lambda.Start(Handler)
}
func Handler(evt events.LambdaFunctionURLRequest) (events.LambdaFunctionURLResponse, error) {
// parse the events body
request := Request{}
err := json.Unmarshal([]byte(evt.Body), &request)
if err != nil {
errString := fmt.Errorf("failed to parse the request. %s", err.Error())
logrus.Error(errString)
return events.LambdaFunctionURLResponse{}, errString
}
// parse the access token
reqToken := evt.Headers["authorization"]
splitToken := strings.Split(reqToken, "Bearer ")
if len(splitToken) == 1 || len(splitToken) > 2 {
errString := fmt.Errorf("invalid token. Token split \"Bearer\" and token authorization")
logrus.Print(errString)
return events.LambdaFunctionURLResponse{}, errString
}
// login client
clientId := oAuth20Service.ConfigRepository.GetClientId()
clientSecret := oAuth20Service.ConfigRepository.GetClientSecret()
errLogin := oAuth20Service.LoginClient(&clientId, &clientSecret)
if errLogin != nil {
errString := fmt.Errorf("failed to login client. %s", errLogin.Error())
logrus.Error(errString)
return events.LambdaFunctionURLResponse{}, errString
}
// start token validation
errValidateToken := validateToken(splitToken[1], request.Namespace, request.UserID)
if errValidateToken != nil {
errString := fmt.Errorf("failed to validate token. %s", errValidateToken.Error())
logrus.Error(errString)
return events.LambdaFunctionURLResponse{}, errString
}
// get user stat item
limit := int32(request.Limit)
offset := int32(request.Offset)
inputUserStatItem := &user_statistic.GetUserStatItemsParams{
Limit: &limit,
Namespace: request.Namespace,
Offset: &offset,
StatCodes: &request.StatCode,
Tags: &request.Tags,
UserID: request.UserID,
}
getUserStatItem, errUserStatItem := userStatisticService.GetUserStatItemsShort(inputUserStatItem)
if errUserStatItem != nil {
errString := fmt.Errorf("failed to create user stat item. %s", errUserStatItem.Error())
logrus.Error(errString)
return events.LambdaFunctionURLResponse{}, errString
}
var js []byte
js, err = json.Marshal(getUserStatItem)
if err != nil {
errString := fmt.Errorf("failed to marshal the response. %s", err.Error())
logrus.Error(errString)
return events.LambdaFunctionURLResponse{}, errString
}
return events.LambdaFunctionURLResponse{
StatusCode: 200,
Body: string(js),
}, nil
}
func validateToken(accessToken, namespace, userId string) error {
// initialize token validator
tokenValidator := validator.NewTokenValidator(oAuth20Service, time.Hour)
tokenValidator.Initialize()
// validate stat item
requiredPermissionStatItem := validator.Permission{
Action: 2, // read
Resource: fmt.Sprintf("ADMIN:NAMESPACE:%s:USER:%s:STATITEM", namespace, userId),
}
errValidateStatItem := tokenValidator.Validate(accessToken, &requiredPermissionStatItem, &namespace, nil)
if errValidateStatItem != nil {
return errValidateStatItem
}
return nil
}Implement the handlers in DeleteUserStats/main.go.
package main
import (
"encoding/json"
"fmt"
"strings"
"time"
"github.com/AccelByte/accelbyte-go-sdk/services-api/pkg/factory"
"github.com/AccelByte/accelbyte-go-sdk/services-api/pkg/service/iam"
"github.com/AccelByte/accelbyte-go-sdk/services-api/pkg/service/social"
"github.com/AccelByte/accelbyte-go-sdk/services-api/pkg/utils/auth"
"github.com/AccelByte/accelbyte-go-sdk/services-api/pkg/utils/auth/validator"
"github.com/AccelByte/accelbyte-go-sdk/social-sdk/pkg/socialclient/user_statistic"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
"github.com/sirupsen/logrus"
)
var (
// use the default config and token implementation
configRepo = *auth.DefaultConfigRepositoryImpl()
tokenRepo = *auth.DefaultTokenRepositoryImpl()
oAuth20Service = iam.OAuth20Service{
Client: factory.NewIamClient(&configRepo),
ConfigRepository: &configRepo,
TokenRepository: &tokenRepo,
}
userStatisticService = &social.UserStatisticService{
Client: factory.NewSocialClient(&configRepo),
TokenRepository: &tokenRepo,
}
)
type Request struct {
Namespace string `json:"namespace"`
UserID string `json:"userId"`
StatCode string `json:"statCode"`
}
func main() {
lambda.Start(Handler)
}
func Handler(evt events.LambdaFunctionURLRequest) (events.LambdaFunctionURLResponse, error) {
// parse the events
request := Request{}
err := json.Unmarshal([]byte(evt.Body), &request)
if err != nil {
errString := fmt.Errorf("failed to parse the request. %s", err.Error())
logrus.Error(errString)
return events.LambdaFunctionURLResponse{}, errString
}
// parse the access token
reqToken := evt.Headers["authorization"]
splitToken := strings.Split(reqToken, "Bearer ")
if len(splitToken) == 1 || len(splitToken) > 2 {
errString := fmt.Errorf("invalid token. Token split \"Bearer\" and token authorization")
logrus.Print(errString)
return events.LambdaFunctionURLResponse{}, errString
}
// login client
clientId := oAuth20Service.ConfigRepository.GetClientId()
clientSecret := oAuth20Service.ConfigRepository.GetClientSecret()
errLogin := oAuth20Service.LoginClient(&clientId, &clientSecret)
if errLogin != nil {
errString := fmt.Errorf("failed to login client. %s", errLogin.Error())
logrus.Error(errString)
return events.LambdaFunctionURLResponse{}, errString
}
// start token validation
errValidateToken := validateToken(splitToken[1], request.Namespace, request.UserID)
if errValidateToken != nil {
errString := fmt.Errorf("failed to validate token. %s", errValidateToken.Error())
logrus.Error(errString)
return events.LambdaFunctionURLResponse{}, errString
}
// delete user stat item
inputDeleteUserStatItem := &user_statistic.DeleteUserStatItemsParams{
Namespace: request.Namespace,
StatCode: request.StatCode,
UserID: request.UserID,
}
errDeleteUserStatItems := userStatisticService.DeleteUserStatItems(inputDeleteUserStatItem)
if errDeleteUserStatItems != nil {
errString := fmt.Errorf("failed to delete user stat code. %s", errDeleteUserStatItems.Error())
logrus.Error(errString)
return events.LambdaFunctionURLResponse{}, errString
}
return events.LambdaFunctionURLResponse{
StatusCode: 200,
Body: "User stat code deleted successfully",
}, nil
}
func validateToken(accessToken, namespace, userId string) error {
// initialize token validator
tokenValidator := validator.NewTokenValidator(oAuth20Service, time.Hour)
tokenValidator.Initialize()
// validate stat item
requiredPermissionStatItem := validator.Permission{
Action: 8, // delete
Resource: fmt.Sprintf("ADMIN:NAMESPACE:%s:USER:%s:STATITEM", namespace, userId),
}
errValidateStatItem := tokenValidator.Validate(accessToken, &requiredPermissionStatItem, &namespace, nil)
if errValidateStatItem != nil {
errString := fmt.Errorf("failed to validate permission for stat item. %s", errValidateStatItem)
logrus.Error(errString)
return errString
}
return nil
}
Configure the SAM template YAML
In this step, we are doing two things in AWS SAM template.yaml
.
Set the required environment variables:
AB_BASE_URL
,AB_CLIENT_ID
, andAB_CLIENT_SECRET
.Hook up the three
*UserStats
handlers, which have been created as AWS Lambda Function URLs:template.yml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
aws-lambda-example
AccelByte Go SDK Lambda Example
Globals:
Function:
Timeout: 15
Environment:
Variables:
AB_BASE_URL: https://prod.gamingservices.accelbyte.io
AB_CLIENT_ID: <Put your AccelByte Client ID here>
AB_CLIENT_SECRET: <Put your AccelByte Client Secret here>
Resources:
CreateUserStatsFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: CreateUserStats
Handler: main
Runtime: go1.x
CreateUserStatsFunctionUrl:
Type: AWS::Lambda::Url
Properties:
AuthType: NONE
TargetFunctionArn:
Ref: CreateUserStatsFunction
DeleteUserStatsFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: DeleteUserStats
Handler: main
Runtime: go1.x
DeleteUserStatsFunctionUrl:
Type: AWS::Lambda::Url
Properties:
AuthType: NONE
TargetFunctionArn:
Ref: DeleteUserStatsFunction
GetUserStatsFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: GetUserStats
Handler: main
Runtime: go1.x
GetUserStatsFunctionUrl:
Type: AWS::Lambda::Url
Properties:
AuthType: NONE
TargetFunctionArn:
Ref: GetUserStatsFunction
Outputs:
CreateUserStatsFunction:
Description: "Create User Stats Function ARN"
Value: !GetAtt CreateUserStatsFunction.Arn
CreateUserStatsFunctionUrlEndpoint:
Description: "Access CreateUserStats function with this URL"
Value: !GetAtt CreateUserStatsFunctionUrl.FunctionUrl
CreateUserStatsFunctionIamRole:
Description: "Implicit IAM Role created for Create User Stats Function"
Value: !GetAtt CreateUserStatsFunctionRole.Arn
DeleteUserStatsFunction:
Description: "Delete User Stats Function ARN"
Value: !GetAtt DeleteUserStatsFunction.Arn
DeleteUserStatsFunctionUrlEndpoint:
Description: "Access DeleteUserStats function with this URL"
Value: !GetAtt DeleteUserStatsFunctionUrl.FunctionUrl
DeleteUserStatsFunctionIamRole:
Description: "Implicit IAM Role created for Delete User Stats Function"
Value: !GetAtt DeleteUserStatsFunctionRole.Arn
GetUserStatsFunction:
Description: "Get User Stats Function ARN"
Value: !GetAtt GetUserStatsFunction.Arn
GetUserStatsFunctionUrlEndpoint:
Description: "Access GetUserStats function with this URL"
Value: !GetAtt GetUserStatsFunctionUrl.FunctionUrl
GetUserStatsFunctionIamRole:
Description: "Implicit IAM Role created for Get User Stats Function"
Value: !GetAtt GetUserStatsFunctionRole.Arn
Test the AWS Lambda locally
Build and serve our AWS Lambda locally for testing.
sam build
sam local start-lambdaTry to add a stat to a user, get stats of a user, and delete a stat from a user using the following commands.
# Set the required environment variables
AB_BASE_URL=https://prod.gamingservices.accelbyte.io
AB_CLIENT_ID='xxxxxxxxxx' # Your Client ID
AB_CLIENT_SECRET='xxxxxxxxxx' # Your Client Secret
AB_NAMESPACE='xxxxxxxxxx' # Your Namespace ID
TEST_USER_ID='xxxxxxxxx' # Your test User ID
TEST_STAT_CODE='xxxxxxxxxx' # Your test Stat Code
# Login client
ACCESS_TOKEN="$(curl -s ${AB_BASE_URL}/iam/v3/oauth/token -H 'Content-Type: application/x-www-form-urlencoded' -u "$AB_CLIENT_ID:$AB_CLIENT_SECRET" -d "grant_type=client_credentials" | jq --raw-output .access_token)"
# Add a stat to a user
curl -X POST "http://127.0.0.1:3001/2015-03-31/functions/CreateUserStatsFunction/invocations" -d "{\"headers\":{\"authorization\":\"Bearer $ACCESS_TOKEN\"},\"body\":\"{\\\"namespace\\\":\\\"$AB_NAMESPACE\\\",\\\"userId\\\":\\\"$TEST_USER_ID\\\",\\\"statCode\\\":\\\"$TEST_STAT_CODE\\\"}\"}"
# Get stats of a user
curl -X POST "http://127.0.0.1:3001/2015-03-31/functions/GetUserStatsFunction/invocations" -d "{\"headers\":{\"authorization\":\"Bearer $ACCESS_TOKEN\"},\"body\":\"{\\\"namespace\\\":\\\"$AB_NAMESPACE\\\",\\\"userId\\\":\\\"$TEST_USER_ID\\\",\\\"statCode\\\":\\\"$TEST_STAT_CODE\\\"}\"}"
# Delete a stat from a user
curl -X POST "http://127.0.0.1:3001/2015-03-31/functions/DeleteUserStatsFunction/invocations" -d "{\"headers\":{\"authorization\":\"Bearer $ACCESS_TOKEN\"},\"body\":\"{\\\"namespace\\\":\\\"$AB_NAMESPACE\\\",\\\"userId\\\":\\\"$TEST_USER_ID\\\",\\\"statCode\\\":\\\"$TEST_STAT_CODE\\\"}\"}"
Deploy to AWS
Build and deploy our AWS Lambda Function URLs. Take note of the deployed URLs (e.g., https://xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.lambda-url.us-west-2.on.aws/). There will be three URLs for
CreateUserStats
,GetUserStats
, andDeleteUserStats
respectively.sam build
sam deploy --guidedTry to add a stat to a user, get stats of a user, and delete a stat from a user using Postman, by importing this Postman collection.
Set these variables in the Postman collection you imported:
base_url='https://prod.gamingservices.accelbyte.io'
client_id='xxxxxxxxxx' # Your Client ID
client_secret='xxxxxxxxxx' # Your Client Secret
create_user_stats_url='https://xxxxxxxxxx.lambda-url.us-west-2.on.aws/' # Your create user stats URL
get_user_stats_url='https://xxxxxxxxxx.lambda-url.us-west-2.on.aws/' # Your get user stats URL
delete_user_stats_url='https://xxxxxxxxxx.lambda-url.us-west-2.on.aws/' # Your delete user stats URLRun
00 GetAccessToken
and record theaccess token
for subsequent requests.Open
01 CreateUserStats
, set theaccess token
inAuthorization
Bearer Token
, modify the request body as shown below, and run it to add a stat to a user.{
"namespace":"Put Your Namespace ID",
"userId":"Put Your Test User Id",
"statCode":"Put Your Test Stat Code"
}Open
02 GetUserStats
, set theaccess token
inAuthorization
Bearer Token
, modify the request body as shown below, and run it to get stats of a user. You should see the stat you added listed.{
"namespace":"Put Your Namespace ID",
"userId":"Put Your Test User Id",
"statCode":"Put Your Test Stat Code"
}Open
03 DeleteUserStats
, set theaccess token
inAuthorization
Bearer Token
, modify the request body as shown below, and run it to delete the stat from the user. If you try02 GetUserStats
again after this, you should see that the stat is not listed anymore.{
"namespace":"Put Your Namespace ID",
"userId":"Put Your Test User Id",
"statCode":"Put Your Test Stat Code"
}
Next steps
- Consider trying to create another custom service which invokes other AGS endpoints.
Resources
- See the full source code described above in the AWS Lambda Example Update.
- For examples on how to achieve some common use cases using the Go Extend SDK, see the common use cases docs.
- If you know which AGS endpoints you need to invoke, and you want to invoke them using the Go Extend SDK, see the operations docs.