Create a simple custom service using AWS Lambda
Overview
One way to extend AccelByte Gaming Services (AGS) is to use the AccelByte Modular 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
This guide will show you how to:
- 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
To perform the tasks in this guide, you must have:
- Installed AWS SAM CLI
- Installed Docker
- Installed Go 1.16
- Installed Postman
- Been granted access to the AGS demo environment:
- Base URL:
https://prod.gamingservices.accelbyte.io
. - Create a Game Namespace if you don't have one yet. Record the
Namespace ID
. - Create an OAuth Client with the
confidential
client type. Make sure to add the following permissions, and record the Client ID and Client 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
.
- Base URL:
- An
AWS_ACCESS_KEY_ID
andAWS_SECRET_ACCESS_KEY
for deploying AWS Lambda.
You should also 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 ago1.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 stats to a user, getting stats of a user, and deleting stats 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"
iam "github.com/AccelByte/accelbyte-go-modular-sdk/iam-sdk/pkg"
"github.com/AccelByte/accelbyte-go-modular-sdk/services-api/pkg/utils/auth"
social "github.com/AccelByte/accelbyte-go-modular-sdk/social-sdk/pkg"
"github.com/AccelByte/accelbyte-go-modular-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: iam.NewIamClient(&configRepo),
ConfigRepository: &configRepo,
TokenRepository: &tokenRepo,
}
userStatisticService = &social.UserStatisticService{
Client: social.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 := iam.NewTokenValidator(oAuth20Service, time.Hour)
tokenValidator.Initialize()
// validate stat item
requiredPermissionStatItem := iam.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"
iam "github.com/AccelByte/accelbyte-go-modular-sdk/iam-sdk/pkg"
"github.com/AccelByte/accelbyte-go-modular-sdk/services-api/pkg/utils/auth"
social "github.com/AccelByte/accelbyte-go-modular-sdk/social-sdk/pkg"
"github.com/AccelByte/accelbyte-go-modular-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: iam.NewIamClient(&configRepo),
ConfigRepository: &configRepo,
TokenRepository: &tokenRepo,
}
userStatisticService = &social.UserStatisticService{
Client: social.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 := iam.NewTokenValidator(oAuth20Service, time.Hour)
tokenValidator.Initialize()
// validate stat item
requiredPermissionStatItem := iam.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"
iam "github.com/AccelByte/accelbyte-go-modular-sdk/iam-sdk/pkg"
"github.com/AccelByte/accelbyte-go-modular-sdk/services-api/pkg/utils/auth"
social "github.com/AccelByte/accelbyte-go-modular-sdk/social-sdk/pkg"
"github.com/AccelByte/accelbyte-go-modular-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: iam.NewIamClient(&configRepo),
ConfigRepository: &configRepo,
TokenRepository: &tokenRepo,
}
userStatisticService = &social.UserStatisticService{
Client: social.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 := iam.NewTokenValidator(oAuth20Service, time.Hour)
tokenValidator.Initialize()
// validate stat item
requiredPermissionStatItem := iam.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 and hook up the three UserStats
.
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 forCreateUserStats
,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
and set theaccess token
inAuthorization
Bearer Token
. Then, 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 the 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
and set theaccess token
inAuthorization
Bearer Token
. Then, modify the request body, as shown below, and run it to delete the stat of a 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
As a next step, you can try to create another custom service which invokes other AGS endpoints.
Resources
- See the full source code in the AWS Lambda Example Update.
- For examples on how to achieve some common use cases using the Go Extend SDK, refer to the Common use cases list.
- If you know which AGS endpoints you need to invoke and you want to invoke them using the Go Modular Extend SDK, refer to the Operations docs.