12

How do I invoke an AWS Step Function using an API Gateway POST request, and the request's JSON payload to the Step Function ?

ElFitz
  • 908
  • 1
  • 8
  • 26

2 Answers2

27

1. Create your step function

Quite obvious. I guess that if you're reading this you know how to do it.

Otherwise, you can go have a look at the documentation here: What is AWS Step Functions?.


2. Create the IAM Role for your API

It can be for either all Step Functions, or only this one. We'll only cover the first case, as explained in an Amazon tutorial: Creating an API Using API Gateway.

To create the IAM role

  • Log in to the AWS Identity and Access Management console.

  • On the Roles page, choose Create New Role.

  • On the Set Role Name page, type APIGatewayToStepFunctions for Role Name, and then choose Next Step.

  • On the Select Role Type page, under Select Role Type, select Amazon API Gateway.

  • On the Attach Policy page, choose Next Step.

  • On the Review page, note the Role ARN, for example:

  • arn:aws:iam::123456789012:role/APIGatewayToStepFunctions

  • Choose Create Role.

To attach a policy to the IAM role

  • On the Roles page, search for your role by name (APIGatewayToStepFunctions) and then choose the role.
  • On the Permissions tab, choose Attach Policy.
  • On the Attach Policy page, search for AWSStepFunctionsFullAccess, choose the policy, and then choose Attach Policy.

3. Setup

3.a If you don't have a JSON payload

As explained by Ka Hou Ieong in How can i call AWS Step Functions by API Gateway?, you can create an AWS Service integration via API Gateway Console, like this:

  • Integration Type: AWS Service
  • AWS Service: Step Functions
  • HTTP method: POST
  • Action Type: Use action name
  • Action: StartExecution
  • Execution role: role to start the execution (the one we just created. Just paste it's ARN)
  • Headers:

    X-Amz-Target -> 'AWSStepFunctions.StartExecution'
    Content-Type -> 'application/x-amz-json-1.0'

  • Body Mapping Templates/Request payload:

    {
        "input": "string" (optional),
        "name": "string" (optional),
        "stateMachineArn": "string"
    }
    

3.b If you do have JSON payload to pass as an input

Everything is the same as in 2.a, except for the body mapping template. You have to do is make it into a string. Using $util.escapeJavascript(), like this for example. It will pass your whole request's body as an input to your Step Function

    #set($data = $util.escapeJavaScript($input.json('$')))
    {
        "input": "$data",
        "name": "string" (optional),
        "stateMachineArn": "string" (required)
    }

Notes

  • stateMachineArn: If you do not want to have to pass the stateMachineArn as part of your requests to API Gateway, you can simply hard-code it inside your Body Mapping Template (see AWS API Gateway with Step Function)
  • name: Omitting the name property will have API Gateway generate a different one for you at each execution.

Now, this is my first "Answer your own question", so maybe this is not how it's done, but I did spend quite a few hours trying to understand what was wrong with my Mapping Template. Hope this will help save other people's hair and time.

ElFitz
  • 908
  • 1
  • 8
  • 26
  • 1
    Note that the max payload size to a StepFunction execution is 32K, so if your body is larger than that, I'd suggest the API calls a Lambda to store the body in S3 and then executes the StepFunction, passing in the S3 bucket/key. Makes the API logic simpler and is less fragile to variable data size. – Geoff Jun 01 '17 at 22:28
  • Absolutely right. And this limit is also enforced within the StepFunction (when a step requires downloading a picture from a source), which is solved in the same way. Should I update my answer ? – ElFitz Jun 09 '17 at 20:49
  • @ElFitz Referring your post helped me a lot. In section 3.b I wanted to form the stateMachineArn with region and account id populating dynamically. As in my case Stepfunction and API Gateway is in same region and account id. I was able to get the account id from `$context.accountId` but not able to find anything that gives the current region name. In my template stateMachineArn looks like this now, `"arn:aws:states:us-east-1:$context.accountId:stateMachine:AbcStateMachine"` Any idea how I can get the current region dynamically. – rockyPeoplesChamp Feb 26 '21 at 21:55
  • 1
    question: since using that `$data` above simply uses the data field of the request body, how would the `name` field make it into the request if we were to pass it thru in a similar fashion, since it would be inside `$data`? i tried using `$data.name` but it did not work. any idea? – ChumiestBucket Jul 09 '21 at 19:59
12

For those ones that are looking a way to directly connect ApiGateway with a Step Functions State Machine using the OpenApi integration and CloudFormation, this is an example of how I managed to make it work:

This is the Visual Workflow I designed (more details in the CloudFormation file) as a proof of concept:

visual workflow

template.yaml

AWSTemplateFormatVersion: 2010-09-09
Transform: 'AWS::Serverless-2016-10-31'
Description: POC Lambda Examples - Step Functions

Parameters:
  CorsOrigin:
    Description: Header Access-Control-Allow-Origin
    Default: "'http://localhost:3000'"
    Type: String
  CorsMethods:
    Description: Header Access-Control-Allow-Headers
    Default: "'*'"
    Type: String
  CorsHeaders:
    Description: Header Access-Control-Allow-Headers
    Default: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key'"
    Type: String
  SwaggerS3File:
    Description: 'S3 "swagger.yaml" file location'
    Default: "./swagger.yaml"
    Type: String

Resources:
  LambdaRoleForRuleExecution:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub ${AWS::StackName}-lambda-role
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Action: 'sts:AssumeRole'
            Principal:
              Service: lambda.amazonaws.com
      Policies:
        - PolicyName: WriteCloudWatchLogs
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - 'logs:CreateLogGroup'
                  - 'logs:CreateLogStream'
                  - 'logs:PutLogEvents'
                Resource: 'arn:aws:logs:*:*:*'

  ApiGatewayStepFunctionsRole:
    Type: AWS::IAM::Role
    Properties:
      Path: !Join ["", ["/", !Ref "AWS::StackName", "/"]]
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Sid: AllowApiGatewayServiceToAssumeRole
            Effect: Allow
            Action:
              - 'sts:AssumeRole'
            Principal:
              Service:
                - apigateway.amazonaws.com
      Policies:
        - PolicyName: CallStepFunctions
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - 'states:StartExecution'
                Resource:
                  - !Ref Workflow

  Start:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: !Sub ${AWS::StackName}-start
      Code: ../dist/src/step-functions
      Handler: step-functions.start
      Role: !GetAtt LambdaRoleForRuleExecution.Arn
      Runtime: nodejs8.10
      Timeout: 1

  Wait3000:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: !Sub ${AWS::StackName}-wait3000
      Code: ../dist/src/step-functions
      Handler: step-functions.wait3000
      Role: !GetAtt LambdaRoleForRuleExecution.Arn
      Runtime: nodejs8.10
      Timeout: 4

  Wait500:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: !Sub ${AWS::StackName}-wait500
      Code: ../dist/src/step-functions
      Handler: step-functions.wait500
      Role: !GetAtt LambdaRoleForRuleExecution.Arn
      Runtime: nodejs8.10
      Timeout: 2

  End:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: !Sub ${AWS::StackName}-end
      Code: ../dist/src/step-functions
      Handler: step-functions.end
      Role: !GetAtt LambdaRoleForRuleExecution.Arn
      Runtime: nodejs8.10
      Timeout: 1

  StateExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - !Sub states.${AWS::Region}.amazonaws.com
            Action:
              - 'sts:AssumeRole'
      Policies:
        - PolicyName: "StatesExecutionPolicy"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: "Allow"
                Action: "lambda:InvokeFunction"
                Resource:
                  - !GetAtt Start.Arn
                  - !GetAtt Wait3000.Arn
                  - !GetAtt Wait500.Arn
                  - !GetAtt End.Arn

  Workflow:
    Type: AWS::StepFunctions::StateMachine
    Properties:
      StateMachineName: !Sub ${AWS::StackName}-state-machine
      RoleArn: !GetAtt StateExecutionRole.Arn
      DefinitionString: !Sub |
        {
          "Comment": "AWS Step Functions Example",
          "StartAt": "Start",
          "Version": "1.0",
          "States": {
            "Start": {
              "Type": "Task",
              "Resource": "${Start.Arn}",
              "Next": "Parallel State"
            },
            "Parallel State": {
              "Type": "Parallel",
              "Next": "End",
              "Branches": [
                {
                  "StartAt": "Wait3000",
                  "States": {
                    "Wait3000": {
                      "Type": "Task",
                      "Resource": "${Wait3000.Arn}",
                      "End": true
                    }
                  }
                },
                {
                  "StartAt": "Wait500",
                  "States": {
                    "Wait500": {
                      "Type": "Task",
                      "Resource": "${Wait500.Arn}",
                      "End": true
                    }
                  }
                }
              ]
            },
            "End": {
              "Type": "Task",
              "Resource": "${End.Arn}",
              "End": true
            }
          }
        }

  RestApi:
    Type: AWS::Serverless::Api
    Properties:
      StageName: !Ref Environment
      Name: !Sub ${AWS::StackName}-api
      DefinitionBody:
        'Fn::Transform':
          Name: AWS::Include
          Parameters:
            # s3 location of the swagger file
            Location: !Ref SwaggerS3File

swagger.yaml

openapi: 3.0.0
info:
  version: '1.0'
  title: "pit-jv-lambda-examples"
  description: POC API
  license:
    name: MIT

x-amazon-apigateway-request-validators:
  Validate body:
    validateRequestParameters: false
    validateRequestBody: true
  params:
    validateRequestParameters: true
    validateRequestBody: false
  Validate body, query string parameters, and headers:
    validateRequestParameters: true
    validateRequestBody: true

paths:
  /execute:
    options:
      x-amazon-apigateway-integration:
        type: mock
        requestTemplates:
          application/json: |
            {
              "statusCode" : 200
            }
        responses:
          "default":
            statusCode: "200"
            responseParameters:
              method.response.header.Access-Control-Allow-Headers:
                Fn::Sub: ${CorsHeaders}
              method.response.header.Access-Control-Allow-Methods:
                Fn::Sub: ${CorsMethods}
              method.response.header.Access-Control-Allow-Origin:
                Fn::Sub: ${CorsOrigin}
            responseTemplates:
              application/json: |
                {}
      responses:
        200:
          $ref: '#/components/responses/200Cors'
    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
            responseParameters:
              method.response.header.Access-Control-Allow-Headers:
                Fn::Sub: ${CorsHeaders}
              method.response.header.Access-Control-Allow-Origin:
                Fn::Sub: ${CorsOrigin}
          ".*CREATION_FAILED.*":
            statusCode: 403
            responseParameters:
              method.response.header.Access-Control-Allow-Headers:
                Fn::Sub: ${CorsHeaders}
              method.response.header.Access-Control-Allow-Origin:
                Fn::Sub: ${CorsOrigin}
            responseTemplates:
              application/json: $input.path('$.errorMessage')
        requestTemplates:
          application/json:
            Fn::Sub: |-
              {
                "input": "$util.escapeJavaScript($input.json('$'))",
                "name": "$context.requestId",
                "stateMachineArn": "${Workflow}"
              }
      summary: Start workflow
      responses:
        200:
          $ref: '#/components/responses/200Empty'
        403:
          $ref: '#/components/responses/Error'

components:
  schemas:
    Error:
      title: Error
      type: object
      properties:
        code:
          type: string
        message:
          type: string

  responses:
    200Empty:
      description: Default OK response

    200Cors:
      description: Default response for CORS method
      headers:
        Access-Control-Allow-Headers:
          schema:
            type: "string"
        Access-Control-Allow-Methods:
          schema:
            type: "string"
        Access-Control-Allow-Origin:
          schema:
            type: "string"

    Error:
      description: Error Response
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
      headers:
        Access-Control-Allow-Headers:
          schema:
            type: "string"
        Access-Control-Allow-Origin:
          schema:
            type: "string" 

step-functions.js

exports.start = (event, context, callback) => {
    console.log('start event', event);
    console.log('start context', context);
    callback(undefined, { function: 'start' });
};
exports.wait3000 = (event, context, callback) => {
    console.log('wait3000 event', event);
    console.log('wait3000 context', context);
    setTimeout(() => {
        callback(undefined, { function: 'wait3000' });
    }, 3000);
};
exports.wait500 = (event, context, callback) => {
    console.log('wait500 event', event);
    console.log('wait500 context', context);
    setTimeout(() => {
        callback(undefined, { function: 'wait500' });
    }, 500);
};
exports.end = (event, context, callback) => {
    console.log('end event', event);
    console.log('end context', context);
    callback(undefined, { function: 'end' });
};
Julio Villane
  • 994
  • 16
  • 28
  • 3
    This is a nightmare come to life. – Nihil Jul 02 '20 at 22:57
  • @Nihil, what part of it? Maybe my example that includes IaC and a direct connection between Api Gateway and Step Functions brings more complications and it isn't simple enough? – Julio Villane Jul 03 '20 at 06:10
  • 1
    Not your example Julio, that's actually very helpful. I meant the whole setup of triggering Step Functions using API Gateway. Many resources needed (Method, integration, response, response method..), strange request ARN/URI (apigateway:{region}:action/StartExecution), request template syntax.. all very complicated. Got it working (with your help, thanks!), and now it seems simple enough, but when getting started - might as well be particle physics (you got an up quark, down quark, strange and beauty - what could be simpler?) – Nihil Jul 05 '20 at 10:03
  • 3
    Yeah, it's true, looks very complicated at start, now i'm used to, and there is an architectural satisfaction on not using a lambda in between, because maybe that solution (using lambdas) make a simpler example. Nice to know it helped you. – Julio Villane Jul 05 '20 at 12:43
  • Nice, is there a way to do it in synchronous way? I mean to wait for the final response from the workflow and then send it back with API Gateway response. – m52509791 Dec 07 '20 at 09:23
  • 1
    @m52509791, there is always the less elegant solution that considers using a Lambda to wait the end of the Step Function instance, but you will be paying for that waiting time and an error if the process takes more than 30 seconds (Api Gateway's requests have a 30 seconds timeout). A better alternative is using websockets through Api Gateway, to let the client know the process has end. – Julio Villane Dec 07 '20 at 18:34
  • I was not able to get it working using SAM and tried multiple options. Followed the instruction mentioned above and it didn't help. The details mentioned in this blog helped me to move forward and fix it - https://towardsserverless.com/articles/how-to-invoke-step-fn-using-aws-apigateway/ – Annette May 24 '22 at 01:33