How do I invoke an AWS Step Function using an API Gateway POST request, and the request's JSON payload to the Step Function ?
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.
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:
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' });
};
Related
I am trying to set up a private API with a single lambda function using a sam template, but whatever I do the API gateway logs API_CONFIGURATION_ERROR. I have no idea what I do wrong. The docs isn't very specific about it. According to this example I think my template looks alright.
My code uses axios and aws4, I have also tried fetch instead of axios. I'm also getting the same error when I use Postman so I don't really think it is something wrong with my request. Everything works perfectly fine if I just disable the IAM authorization.
This is my template for the api and the lambda function I try to invoke.
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: >
iam-api-example
Parameters:
SamplePrivateApiStageName:
Type: String
Default: endpoint
SampleTableName:
Type: String
Default: SampleTable
Globals:
Function:
Runtime: nodejs14.x
Timeout: 10
MemorySize: 128
Handler: app.lambdaHandler
Resources:
SamplePrivateApi:
Type: AWS::Serverless::Api
Properties:
Name: SamplePrivateApi
StageName: !Ref SamplePrivateApiStageName
Auth:
DefaultAuthorizer: AWS_IAM
AccessLogSetting:
DestinationArn: !GetAtt SamplePrivateApiAccessLogs.Arn
Format: '{ "requestId":"$context.requestId", "ip": "$context.identity.sourceIp", "requestTime":"$context.requestTime", "httpMethod":"$context.httpMethod","routeKey":"$context.routeKey", "path":"$context.path", "status":"$context.status","protocol":"$context.protocol", "responseLength":"$context.responseLength", "errorMessage":"$context.error.message", "responseType":"$context.error.responseType" }'
SamplePrivateApiAccessLogs:
Type: AWS::Logs::LogGroup
GetQuestionFn:
Type: AWS::Serverless::Function
Properties:
Policies:
- AmazonDynamoDBFullAccess
CodeUri: build/lambdas/private/advertisement/get-question
Environment:
Variables:
TABLE_NAME: !Ref SampleTableName
Events:
GetQuestionFnEvent:
Type: Api
Properties:
Path: /questions
Method: GET
RestApiId: !Ref SamplePrivateApi
#Auth:
#Authorizer: "NONE"
If I stop using the defaultAuthorizer by uncommenting the last two auth-lines (i.e. using Authorizer: "NONE") in the lambda function everything works fine. So it only fails when I leave the defaultAuthorizer in charge.
The error I get in the API log is:
{
"requestId": "12311ec0-732d-4088-a641-71f05e4bd418",
"ip": "x.xx.xxx.xxx",
"requestTime": "16/Jul/2021:12:37:01 +0000",
"httpMethod": "GET",
"routeKey": "-",
"path": "/endpoint/questions",
"status": "500",
"protocol": "HTTP/1.1",
"responseLength": "36",
"errorMessage": "Internal server error",
"responseType": "API_CONFIGURATION_ERROR"
}
In Postman, I use AWS signature authorization and the access key and secret key from an IAM user that has AmazonAPIGatewayInvokeFullAccess policy. The regions should be correct as well.
My axios code I use in another lambda function (which also has AmazonAPIGatewayInvokeFullAccess policy) currently looks like this.
import AWS from 'aws-sdk'
const aws4 = require('aws4')
const fetchQuestion = async () => {
// Host is domain without https://
const urlParts = QUESTION_API_DOMAIN.split('://')
const host = urlParts.length > 1 ? urlParts[1] : ''
if (!host) {
console.error('No host could be extracted.')
return null
}
const request = {
host,
method: 'GET',
url: `${QUESTION_API_DOMAIN}/endpoint/questions`,
path: `/endpoint/questions`,
}
const secretAccessKey = AWS.config.credentials?.secretAccessKey
const accessKeyId = AWS.config.credentials?.accessKeyId
if (!secretAccessKey || !accessKeyId) {
console.error('No credentials could be found.')
return null
}
let signedRequest = aws4.sign(request, {
secretAccessKey: secretAccessKey,
accessKeyId: accessKeyId,
sessionToken: AWS.config.credentials?.sessionToken,
})
// delete signedRequest.headers['Host']
// delete signedRequest.headers['Content-Length']
try {
const response = await axios(signedRequest)
return response.data
} catch (err) {
console.error('Error while retrieving question:', err)
}
}
No idea left what could be wrong.
Your API Gateway needs to have a role assigned to it which allows it to invoke the lambda function (authorizer)
Try using the InvokeRole property in the API Auth section (https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-property-api-apiauth.html).
--- Updated ----
The invoke role arn must point to a role arn and not to a user see example below
Type: AWS::Serverless::Api
Properties:
Name: SamplePrivateApi
StageName: !Ref SamplePrivateApiStageName
Auth:
DefaultAuthorizer: AWS_IAM
InvokeRole: !GetAtt APIGatewayRole.Arn
AccessLogSetting:
DestinationArn: !GetAtt SamplePrivateApiAccessLogs.Arn
Format: '{ "requestId":"$context.requestId", "ip": "$context.identity.sourceIp", "requestTime":"$context.requestTime", "httpMethod":"$context.httpMethod","routeKey":"$context.routeKey", "path":"$context.path", "status":"$context.status","protocol":"$context.protocol", "responseLength":"$context.responseLength", "errorMessage":"$context.error.message", "responseType":"$context.error.responseType" }'
SamplePrivateApiAccessLogs:
Type: AWS::Logs::LogGroup
APIGatewayRole:
Type: "AWS::IAM::Role"
Properties:
Path: "/"
ManagedPolicyArns:
- "arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs"
Policies:
- PolicyName: "authorizerLambdaInvokeAccess"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Action:
- lambda:InvokeAsync
- lambda:InvokeFunction
Resource: !Sub ${AuthorizerLambdaFunction.Arn}
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Sid: "AllowApiGatewayServiceToAssumeRole"
Effect: "Allow"
Action:
- "sts:AssumeRole"
Principal:
Service:
- "apigateway.amazonaws.com" ```
I built an cloudFormation template. It perform a simple workflow: client make a request -> api gateway handle it and send to lambda fucntion via proxy integration. There is a role between api gateway an a lambda.
My template consist of:
root resource (AudienceApi)
nested resource (Segment)
method post (PostMethod) with lambda`s integration
lambda fucntion (lambdaFunction)
lambda invoker (it is a permission for api gateway to invoke the lambda)
lambda role
And during testing the whole workflow I have faced with a problem - api responce with an error 500 Internal Server Error. From log I found message:
Invalid permissions on Lambda function.
I continue testing and find out when I remove nested resource (Segment) and connect method post resource (PostMethod) directly to root resource (AudienceApi) my workflow started to working.
Question: What wrong with my template? why it is not working with nested resource path?
Maybe someone look at my template and find an error?
template:
AWSTemplateFormatVersion: "2010-09-09"
Description: "My API Gateway and Lambda function"
Parameters:
apiGatewayStageName:
Type: "String"
AllowedPattern: "^[a-z0-9]+$"
Default: "call"
lambdaFunctionName:
Type: "String"
AllowedPattern: "^[a-zA-Z0-9]+[a-zA-Z0-9-]+[a-zA-Z0-9]+$"
Default: "my-function"
Resources:
AudienceApi:
Type: "AWS::ApiGateway::RestApi"
Properties:
Name: "my-api"
Description: "My API"
Segment:
Type: 'AWS::ApiGateway::Resource'
Properties:
RestApiId: !Ref AudienceApi
ParentId: !GetAtt
- AudienceApi
- RootResourceId
PathPart: segment
PostMethod:
Type: 'AWS::ApiGateway::Method'
Properties:
HttpMethod: POST
# RequestModels:
# application/json: !Ref SegmentationRequestModel
AuthorizationType: NONE
Integration:
IntegrationHttpMethod: POST
Type: AWS_PROXY
Uri: !Sub
- "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${lambdaArn}/invocations"
- lambdaArn: !GetAtt "lambdaFunction.Arn"
ResourceId: !Ref Segment
RestApiId: !Ref AudienceApi
lambdaFunction:
Type: "AWS::Lambda::Function"
Properties:
Code:
ZipFile: |
def handler(event,context):
return {
'body': 'Hello there {0}'.format(event['requestContext']['identity']['sourceIp']),
'headers': {
'Content-Type': 'text/plain'
},
'statusCode': 200
}
Description: "My function"
FunctionName: !Ref "lambdaFunctionName"
Handler: "index.handler"
MemorySize: 128
Role: !GetAtt "lambdaIAMRole.Arn"
Runtime: "python2.7"
Timeout: 10
lambdaApiGatewayInvoke:
Type: "AWS::Lambda::Permission"
Properties:
Action: "lambda:InvokeFunction"
FunctionName: !GetAtt "lambdaFunction.Arn"
Principal: "apigateway.amazonaws.com"
SourceArn: !Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${AudienceApi}/*/POST/"
lambdaIAMRole:
Type: "AWS::IAM::Role"
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Action:
- "sts:AssumeRole"
Effect: "Allow"
Principal:
Service:
- "lambda.amazonaws.com"
Policies:
- PolicyDocument:
Version: "2012-10-17"
Statement:
- Action:
- "logs:CreateLogGroup"
- "logs:CreateLogStream"
- "logs:PutLogEvents"
Effect: "Allow"
Resource:
- !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${lambdaFunctionName}:*"
PolicyName: "lambda"
lambdaLogGroup:
Type: "AWS::Logs::LogGroup"
Properties:
LogGroupName: !Sub "/aws/lambda/${lambdaFunctionName}"
RetentionInDays: 90
Based on the comments, the solution was to add * to the ARN after the POST/ in SourceArn:
SourceArn: !Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${AudienceApi}/*/POST/*"
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.
is there any difference between starting the api with either of these commands?
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
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 {};
}
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.
I am trying to request parameters using API Gateway in AWS CloudFormation. The parameter that I want to pass down from API gateway to Lambda function is 'action'. I have tried the following code and so far I ran into the error, mention below. Can someone please help me with identifying the issue and a possible resolution?
"Invalid mapping expression specified: Validation Result: warnings : [], errors : [Invalid mapping expression specified: Integration.request.path.action] (Service: AmazonApiGateway; Status Code: 400; Error Code: BadRequestException; Request ID: 037f4753-52b5-4276-979a-131a0f903e63)"
AWSTemplateFormatVersion: "2010-09-09"
Description: "API Gateway and Lambda function"
Resources:
SampleApi:
Type: "AWS::ApiGateway::RestApi"
Properties:
Name: Sample
SampleApiMethod:
Type: "AWS::ApiGateway::Method"
Properties:
AuthorizationType: "NONE"
HttpMethod: "GET"
RequestParameters:
method.request.path.action: true
RequestTemplates:
application/yaml
Integration:
IntegrationHttpMethod: "POST"
Type: "AWS_PROXY"
RequestParameters:
Integration.request.path.action: method.request.path.action
Uri: !Sub
- "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${lambdaArn}/invocations"
- lambdaArn: !GetAtt "SampleLambda.Arn"
CacheKeyParameters:
- method.request.path.action
ResourceId: !GetAtt "SampleApi.RootResourceId"
RestApiId: !Ref "SampleApi"
SampleApiDeployment:
Type: "AWS::ApiGateway::Deployment"
DependsOn: "SampleApiMethod"
Properties:
RestApiId: !Ref "SampleApi"
StageName: test
SampleLambda:
Type: "AWS::Lambda::Function"
Properties:
Code:
ZipFile: |
import yaml
import boto3
cf_client = boto3.client('cloudformation')
cf_client.create_stack(
StackName='your-stack',
TemplateURL='Some URL',
Parameters=[
{
'ParameterKey':'action',
'ParameterValue': 'kms:*'
},
]
)
Handler: "index.handler"
Role: !GetAtt "SampleLambdaRole.Arn"
Runtime: python3.7
LambdaApiGatewayInvoke:
Type: "AWS::Lambda::Permission"
Properties:
Action: "lambda:InvokeFunction"
FunctionName: !GetAtt "SampleLambda.Arn"
Principal: "apigateway.amazonaws.com"
SourceArn: !Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${SampleApi}/*/GET/"
SampleLambdaRole:
Type: "AWS::IAM::Role"
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Action: ["sts:AssumeRole"]
Effect: "Allow"
Principal:
Service: ["lambda.amazonaws.com"]
Policies:
- PolicyDocument:
Version: "2012-10-17"
Statement:
- Action: ["cloudwatch:*", "logs:*"]
Effect: "Allow"
Resource: "*"
PolicyName: "lambdaLogPolicy"
Outputs:
apiGatewayInvokeURL:
Value: !Sub 'https://Sample.execute-api.${AWS::Region}.amazonaws.com/test'
According to the docs, the key for RequestParameters should be like integration.request.<location>.<name>, with a lowercase i for integration. You are using an uppercase I. From the AWS CloudFormation docs:
Specify the destination by using the following pattern integration.request.location.name, where location is query string, path, or header, and name is a valid, unique parameter name.
Also, your template from above contains a RequestTemplates property which is placed in the wrong hierarchy level. It must be placed below Integration as noted in the AWS CloudFormation docs as well. Here is the correct template for AWS::ApiGateway::Method for you:
SampleApiMethod:
Type: "AWS::ApiGateway::Method"
Properties:
AuthorizationType: "NONE"
HttpMethod: "GET"
RequestParameters:
method.request.path.action: true
Integration:
IntegrationHttpMethod: "POST"
Type: "AWS_PROXY"
RequestParameters:
integration.request.path.action: method.request.path.action
RequestTemplates:
"application/yaml": "<define your template here>"
Uri: !Sub
- "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${lambdaArn}/invocations"
- lambdaArn: !GetAtt "SampleLambda.Arn"
CacheKeyParameters:
- method.request.path.action
ResourceId: !GetAtt "SampleApi.RootResourceId"
RestApiId: !Ref "SampleApi"
More information about defining a request template can be found in the developer reference.
I am trying to run a bit modified example:
AWSTemplateFormatVersion: "2010-09-09"
Description: "My API Gateway and Lambda function"
Parameters:
apiGatewayStageName:
Type: "String"
AllowedPattern: "^[a-z0-9]+$"
Default: "call"
lambdaFunctionName:
Type: "String"
AllowedPattern: "^[a-zA-Z0-9]+[a-zA-Z0-9-]+[a-zA-Z0-9]+$"
Default: "my-function"
Resources:
apiGateway:
Type: "AWS::ApiGateway::RestApi"
Properties:
Name: "my-api"
Description: "My API"
apiGatewayRootMethod:
Type: "AWS::ApiGateway::Method"
Properties:
AuthorizationType: "NONE"
HttpMethod: "GET"
Integration:
IntegrationHttpMethod: "POST"
Type: "AWS_PROXY"
Uri: !Sub
- "arn:aws:apigateway:${AWS::Region}:lambda:path/functions/${lambdaArn}/invocations"
- lambdaArn: !GetAtt "lambdaFunction.Arn"
ResourceId: !GetAtt "apiGateway.RootResourceId"
RestApiId: !Ref "apiGateway"
apiGatewayDeployment:
Type: "AWS::ApiGateway::Deployment"
DependsOn:
- "apiGatewayRootMethod"
Properties:
RestApiId: !Ref "apiGateway"
StageName: !Ref "apiGatewayStageName"
lambdaFunction:
Type: "AWS::Lambda::Function"
Properties:
Code:
ZipFile: |
def handler(event,context):
return {
'body': 'Hello there {0}'.format(event['requestContext']['identity']['sourceIp']),
'headers': {
'Content-Type': 'text/plain'
},
'statusCode': 200
}
Description: "My function"
FunctionName: !Ref "lambdaFunctionName"
Handler: "index.handler"
MemorySize: 128
Role: !GetAtt "lambdaIAMRole.Arn"
Runtime: "python2.7"
Timeout: 10
lambdaApiGatewayInvoke:
Type: "AWS::Lambda::Permission"
Properties:
Action: "lambda:InvokeFunction"
FunctionName: !GetAtt "lambdaFunction.Arn"
Principal: "apigateway.amazonaws.com"
SourceArn: !Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${apiGateway}/*/GET/"
lambdaIAMRole:
Type: "AWS::IAM::Role"
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Action:
- "sts:AssumeRole"
Effect: "Allow"
Principal:
Service:
- "lambda.amazonaws.com"
Policies:
- PolicyDocument:
Version: "2012-10-17"
Statement:
- Action:
- "logs:CreateLogGroup"
- "logs:CreateLogStream"
- "logs:PutLogEvents"
Effect: "Allow"
Resource:
- !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${lambdaFunctionName}:*"
PolicyName: "lambda"
lambdaLogGroup:
Type: "AWS::Logs::LogGroup"
Properties:
LogGroupName: !Sub "/aws/lambda/${lambdaFunctionName}"
RetentionInDays: 90
Outputs:
apiGatewayInvokeURL:
Value: !Sub "https://${apiGateway}.execute-api.${AWS::Region}.amazonaws.com/${apiGatewayStageName}"
lambdaArn:
Value: !GetAtt "lambdaFunction.Arn"
Everything works, but when I try to get query string I got null. What necessary config is needed to pass query string paramters to lambda?
How exactly are you trying to get query string parameters?
Since you are using proxy integration, HTTP request is sent as is to your lambda function without any modification at API Gateway, meaning that the query string parameters are available through event object - event['queryStringParameters']
I have tried your example and added print statement to the function
def lambda_handler(event,context):
print('query string params', event['queryStringParameters'])
return {
'body': 'Hello there {0}'.format(event['requestContext']['identity']['sourceIp']),
'headers': {
'Content-Type': 'text/plain'
},
'statusCode': 200
}
And if I hit that API endpoint, while specifying some query string params such as ?a=1&b=2, I see that the function is logging those parameters correctly.
CloudWatch logs:
START RequestId: 18f343c8-55ff-4b26-8d74-ae81ce90e8de Version: $LATEST
query string params {'a': '1', 'b': '2'}
END RequestId: 18f343c8-55ff-4b26-8d74-ae81ce90e8de
REPORT RequestId: 18f343c8-55ff-4b26-8d74-ae81ce90e8de Duration: 20.58 ms Billed Duration: 100 ms Memory Size: 128 MB Max Memory Used: 53 MB
You will find the query string parameters in the queryStringParameters and the multiValueQueryStringParameters subdocuments of the received event, e.g.
const queryStringParams = event.queryStringParameters;
console.log(JSON.stringify(queryStringParams));
The documentation of query string parameters and other data that is available can be found in the Input Format of a Lambda Function for Proxy Integration chapter of the Amazon API Gateway developer guide.
Side note, the data available in a Lambda event differs depending on the event source that triggers the Lambda. Sometimes, it is sufficient to just log the incoming event, e.g. console.log(JSON.stringify(event));.