Search code examples
amazon-web-servicesserverlessaws-samaws-sam-cli

AWS SAM, how to run a state machine from an api gateway call?


I'm trying to set up a state machine for a workflow, but for the life of me I cannot seem to get it working, here is my SAM template:

AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: >
  strest

  Sample SAM Template for strest

Globals:
  Function:
    Timeout: 3

Resources:
  PublicApi:
    Type: AWS::Serverless::Api
    Properties:
      StageName: Prod
      # TracingEnabled: true
      DefinitionBody:
        swagger: "2.0"
        info:
          version: "1.1"
          title: "StrestApi"
        schemes:
          - "http"
        paths:
          /start: # api gateway invokes lambda synchronously, which in turn invokes the stepfunction and waits for its final result
            get:
              produces:
                - "application/json"
              responses:
                "200":
                  description: "200 response"
                  schema:
                    $ref: "#/definitions/Empty"
                  headers:
                    Access-Control-Allow-Headers:
                    type: "string"

              security: []
              x-amazon-apigateway-integration:
                responses:
                  default:
                    statusCode: "200"
                    headers:
                      Access-Control-Allow-Headers:
                      type: "'*'"
                httpMethod: GET
                type: aws_proxy
                uri:
                  Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${StartFunction.Arn}/invocations

        definitions:
          Empty:
            type: "object"
            title: "Empty Schema"

  # Role which allows step functions to invoke lambda functions
  StatesExecutionRole:
    Type: "AWS::IAM::Role"
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Principal:
              Service:
                - !Sub states.${AWS::Region}.amazonaws.com
            Action: "sts:AssumeRole"
      Path: "/"
      Policies:
        - PolicyName: StatesExecutionPolicy
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - "lambda:InvokeFunction"
                Resource: "*"

  # LAMBDAS
  StartFunction:
    Type: AWS::Serverless::Function
    Properties:
      Description: Starts the state machine
      CodeUri: dist/
      Handler: start/start.handler
      Runtime: nodejs12.x
      Environment:
        Variables:
          STEP_FUNCTION_ARN: !Ref StepFunctionsStateMachine
      Policies:
        - Version: "2012-10-17"
          Statement:
            - Effect: "Allow" # step function permissions open for now
              Action:
                - states:*
              Resource: "*"
      Events:
        ExecSFNResource:
          Type: Api
          Properties:
            RestApiId: !Ref PublicApi
            Path: /start
            Method: GET

  ExecutorFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: dist/
      Handler: executor/executor.handler
      Runtime: nodejs12.x
      # Events:
      #   HelloWorld:
      #     Type: Api
      #     Properties:
      #       Path: /execute
      #       Method: get

  # State machine
  StepFunctionsStateMachine:
    Type: AWS::StepFunctions::StateMachine
    Properties:
      RoleArn: !GetAtt [StatesExecutionRole, Arn]
      DefinitionString: !Sub |-
        {
          "StartAt": "execute",
          "Comment": "State machine for executing the strest main loop",
          "States": {
            "execute": {
              "Type": "Task",
              "Resource": "${ExecutorFunction.Arn}",
              "Comment": "Run the Executor Lambda function",
              "End": true
            }
          }
        }

I either start the service by doing sam local start-api or sam local start-lambda.

  1. is there any difference between starting the api with either of these commands?
  2. In the template I pasted, I'm using !Ref to get the state machine ARN, however this is not working, the same string is returned, it works if I change it to !GetAtt StepFunctionsStateMachine.Arn
  3. After changing 2. then I query the /start endpoint, the start lambda function starts running, I get the arn of the state machine, but when I try to start it I get a Service not valid in this context: lambda error (after the marker 2), here is the code for the start function:
import AWS from "aws-sdk";

export async function handler(event: any, context: any) {
  let stepFunctionArn = process.env.STEP_FUNCTION_ARN;

  console.log("marker0 stepFunctionArn", stepFunctionArn);

  let params = {
    stateMachineArn: stepFunctionArn!,
    name: "Execution lambda " + new Date().toString()
  };
  console.log("marker 1");

  let sf_client = new AWS.StepFunctions();
  console.log("marker 2");

  let res = await sf_client.startExecution(params).promise();

  console.log("marker 3", res);

  return {};
}

Solution

  • It's viable to start a step function from a Lambda function, but i think in your case it's a better solution to start it directly from Api Gateway by using the DefinitionBody of API Gateway like this:

      /workflow:
        post:
          x-amazon-apigateway-integration:
            credentials:
              Fn::GetAtt: [ ApiGatewayStepFunctionsRole, Arn ]
            uri:
              Fn::Sub: arn:aws:apigateway:${AWS::Region}:states:action/StartExecution
            httpMethod: POST
            type: aws
            responses:
              default:
                statusCode: 200
                responseTemplates:
                  application/json: |
                    '{ "executionId": "$input.json('executionArn').split(':').get(7) }'
            requestTemplates:
              application/json:
                Fn::Sub: |-
                  {
                    "input": "$util.escapeJavaScript($input.json('$'))",
                    "name": "$context.requestId",
                    "stateMachineArn": "${Workflow}"
                  }
          summary: Start workflow instance
          responses:
            200:
              $ref: '#/components/responses/200Execution'
            403:
              $ref: '#/components/responses/Error'
    

    I have a working example commited in github in https://github.com/jvillane/aws-sam-step-functions-lambda/blob/master/openapi.yaml with an additional method for checking the execution state.