Skip to main content

Create a simple custom service using AWS Lambda

Last updated on October 24, 2024

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:
  • An AWS_ACCESS_KEY_ID and AWS_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

  1. Run the following sam init command to create a new AWS Lambda project with a go1.x runtime. Use the hello-world app template for this project.

    sam init --name aws-lambda-example --runtime go1.x --app-template hello-world --no-tracing --no-application-insights
  2. If the sam init command is successful, a new folder named aws-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.

  1. 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
    }
  2. 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
    }
  3. 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.

  1. Set the required environment variables: AB_BASE_URL, AB_CLIENT_ID, and AB_CLIENT_SECRET.

  2. 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

  1. Build and serve our AWS Lambda locally for testing.

    sam build
    sam local start-lambda
  2. Try 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

  1. 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, and DeleteUserStats respectively.

    sam build
    sam deploy --guided
  2. Try 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.

  3. 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 URL
  4. Run 00 GetAccessToken and record the access token for subsequent requests.

  5. Open 01 CreateUserStats, set the access token in Authorization 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"
    }
  6. Open 02 GetUserStats, set the access token in Authorization 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"
    }
  7. Open 03 DeleteUserStats, set the access token in Authorization Bearer Token, modify the request body as shown below, and run it to delete the stat from the user. If you try 02 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.