Startup Clix: First Steps with AWS Step Functions

K - May 9 '18 - - Dev Community

Yesterday I finally got presence channel working with Pusher, which I need to check who's in what game and to distribute game updates to them.

Today was about getting myself educated in the art of state-machines with AWS Step Functions.

Step Functions

AWS Step Functions is a serverless (autoscale, pay per use, etc.) service, that lets you define and execute state-machines in the AWS cloud.

It can control Lambda functions and most importantly it can be controlled by Lambda functions.

I created a test implementation to see how this could work.

First I created a state-machine definition and integrated it into my CloudFormation template.yaml. I also created a IAM role that now will be used by Lambda and Step Functions to do their thing.

The definition of a state-machine is generally straight forward, but kinda ugly to integrate into CloudFormation, because it needs to be done as string and the definition is written in JSON.

Anyway, this is the definition inside the template:

  GameStateMachine:
    Type: "AWS::StepFunctions::StateMachine"
    Properties:
      RoleArn: !GetAtt [ ExecutionRole, Arn ]
      DefinitionString:
        Fn::Sub:
          - |-
            {
              "StartAt": "WaitingForPlayers",
              "States": {
                "WaitingForPlayers": {
                  "Type": "Task",
                  "Resource": "${joinGameActivityArn}",
                  "TimeoutSeconds": 20,
                  "End": true
                }
              }
            }
          -
            joinGameActivityArn: !Ref JoinGameActivity

  JoinGameActivity:
    Type: "AWS::StepFunctions::Activity"
    Properties:
      Name: JoinGame
Enter fullscreen mode Exit fullscreen mode

As one can see, some string substitution is taking place to get the ARN the JoinGameActivity into the definition. The activity is also defined in the template.

It goes like this:

1) startGame Lambda function is called via API-Gateway

module.exports = async (event, context) => {
  const executionParams = {
    // The GameStateMachine ARN is available via env-var
    // it's passed here by CloudFormation
    stateMachineArn: process.env.GAME_STATE_MACHINE_ARN,

    // some input data that is used as start input for the state-machine
    input: JSON.stringify({ gameId: GAME_ID })
  };

  // starting a new execution via the AWS-SDK
  await stepFunctions
    .startExecution(executionParams)
    .promise();
};
Enter fullscreen mode Exit fullscreen mode

2) GameStateMachine execution goes into the WaitingForPlayers state until some kind of worker sends a success via the AWS-SDK or if the timeout hits.

3) joinGame Lambda function is called via API-Gateway

module.exports = async (event, context) => {
  let task;
  {
    const getTaskParams = {
      // The JoinGame activity ARN is available via env-var
      // it's passed here by CloudFormation
      activityArn: process.env.JOIN_GAME_ACTIVITY_ARN
    };

    // If a task for this activity is available it will be polled here
    task = await stepFunctions.getActivityTask(getTaskParams).promise();
  }

  // some game logic happening, haha
  const input = JSON.parse(task.input);
  {
    // this token is need to send a success or fail state later
    const { taskToken } = task;

    const taskSuccessParams = {
      taskToken,
      output: JSON.stringify(input)
    };

    // the success is send to the activity
    // so the GameStateMachine can transition to the next state
    await stepFunctions.sendTaskSuccess(taskSuccessParams).promise();
  }
};
Enter fullscreen mode Exit fullscreen mode

4) The GameStateMachine transitions to its end state and succeeds execution.

Next

The problem is, there are multiple executions, one for every game, but is still only one JoinGame activity that can be polled. Now, when players for 2 games poll this activity they should only get the activity tasks for their game, which is not possible at the moment.

Well, maybe tomorrow :)

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .