
URL de función Lambda para acciones de aprobación en StepFunctions
StepFunctions – Mediante el uso de las URL de la función Lambda, podemos eliminar API Gateway de la arquitectura de la acción de aprobación.
Motivación
Cuando usamos StepFunctions, podemos administrar algunas acciones de aprobación para continuar con los siguientes pasos. AWS muestra la plantilla de CloudFormation para realizar acciones de aprobación.
La siguiente imagen muestra la arquitectura.
- La función Lambda se llama desde StepFunctions y envía un correo electrónico SNS al usuario.
- El usuario elige uno de los enlaces URL para aprobar o rechazar.
- De acuerdo con el enlace URL en el que hace clic el usuario, API Gateway envía el parámetro de regreso al “Estado de elección” de StepFunctions.

Utilice la URL de las funciones de Lambda
AWS lanzó las URL de la función Lambda en 2022 y esto permite que la función Lambda llegue directamente al punto final. Eso significa que podemos eliminar API Gateway de la arquitectura.
La mayoría de las partes de la arquitectura son las mismas que las anteriores, excepto que se eliminó API Gateway.
Tenga en cuenta que el uso de URL de funciones de Lambda se limita a patrones relativamente simples.

Código
Aquí está la plantilla YAML de CloudFormation. (Puede encontrar la versión de CDK en mi repositorio).
Crea la pila. Por ejemplo, siga este documento de AWS.
Los puntos modificados de la plantilla de la versión anterior de AWS son:
- Se eliminan las líneas para API Gateway.
- Algunas partes de JSON se cambiaron para usar las URL de la función Lambda
- La descripción de Lambda::Url es así a continuación. Tenga en cuenta que
AuthTypeestá configurado enNONE(sin requisitos para IAM).
LambdaApprovalFunctionFunctionUrl: Type: AWS::Lambda::Url Properties: AuthType: NONE TargetFunctionArn: !GetAtt LambdaApprovalFunction.Arn LambdaApprovalFunctioninvokefunctionurl: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunctionUrl FunctionName: !GetAtt LambdaApprovalFunction.Arn Principal: "*" FunctionUrlAuthType: NONE
AWSTemplateFormatVersion: "2010-09-09"
Description: "AWS Step Functions Human based task example. It sends an email with an HTTP URL for approval. Lambda function URLs is used for this architecture."
Parameters:
Email:
Type: String
AllowedPattern: "^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$"
ConstraintDescription: Must be a valid email address.
Resources:
SNSHumanApprovalEmailTopic:
Type: AWS::SNS::Topic
SNSHumanApprovalEmailTopicyourmail:
Type: AWS::SNS::Subscription
Properties:
Protocol: email
TopicArn:
Ref: SNSHumanApprovalEmailTopic
Endpoint: !Ref Email
LambdaStepFunctionsIAMRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Action: sts:AssumeRole
Effect: Allow
Principal:
Service: lambda.amazonaws.com
Version: "2012-10-17"
Policies:
- PolicyDocument:
Statement:
- Action: logs:*
Effect: Allow
Resource: "*"
Version: "2012-10-17"
PolicyName: CloudWatchLogsPolicy
- PolicyDocument:
Statement:
- Action:
- states:SendTaskFailure
- states:SendTaskSuccess
Effect: Allow
Resource: "*"
Version: "2012-10-17"
PolicyName: StepFunctionsPolicy
LambdaApprovalFunction:
Type: AWS::Lambda::Function
Properties:
Code:
ZipFile:
Fn::Sub: |
const AWS = require('aws-sdk');
var redirectToStepFunctions = function (lambdaArn, statemachineName, executionName, callback) {
const lambdaArnTokens = lambdaArn.split(":");
const partition = lambdaArnTokens[1];
const region = lambdaArnTokens[3];
const accountId = lambdaArnTokens[4];
console.log("partition=" + partition);
console.log("region=" + region);
console.log("accountId=" + accountId);
const executionArn = "arn:" + partition + ":states:" + region + ":" + accountId + ":execution:" + statemachineName + ":" + executionName;
console.log("executionArn=" + executionArn);
const url = "https://console.aws.amazon.com/states/home?region=" + region + "#/executions/details/" + executionArn;
callback(null, {
statusCode: 302,
headers: {
Location: url
}
});
};
exports.handler = (event, context, callback) => {
console.log('Event= ' + JSON.stringify(event));
// const action = event.queryStringParameters;
const action = event.queryStringParameters.action;
const taskToken = event.queryStringParameters.taskToken;
const statemachineName = event.queryStringParameters.sm;
const executionName = event.queryStringParameters.ex;
const stepfunctions = new AWS.StepFunctions();
var message = "";
if (action === "approve") {
message = { "Status": "Approved! Task approved by ${Email}" };
} else if (action === "reject") {
message = { "Status": "Rejected! Task rejected by ${Email}" };
} else {
console.error("Unrecognized action. Expected: approve, reject.");
callback({ "Status": "Failed to process the request. Unrecognized Action." });
}
stepfunctions.sendTaskSuccess({
output: JSON.stringify(message),
taskToken: taskToken
})
.promise()
.then(function (data) {
redirectToStepFunctions(context.invokedFunctionArn, statemachineName, executionName, callback);
}).catch(function (err) {
console.error(err, err.stack);
callback(err);
});
};
Role: !GetAtt LambdaStepFunctionsIAMRole.Arn
Handler: index.handler
Runtime: nodejs16.x
Timeout: 60
DependsOn:
- LambdaStepFunctionsIAMRole
LambdaApprovalFunctionEventInvokeConfig:
Type: AWS::Lambda::EventInvokeConfig
Properties:
FunctionName:
Ref: LambdaApprovalFunction
Qualifier: $LATEST
MaximumRetryAttempts: 0
LambdaApprovalFunctionFunctionUrl:
Type: AWS::Lambda::Url
Properties:
AuthType: NONE
TargetFunctionArn: !GetAtt LambdaApprovalFunction.Arn
LambdaApprovalFunctioninvokefunctionurl:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunctionUrl
FunctionName: !GetAtt LambdaApprovalFunction.Arn
Principal: "*"
FunctionUrlAuthType: NONE
LambdaSendEmailExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Action: sts:AssumeRole
Effect: Allow
Principal:
Service: lambda.amazonaws.com
Version: "2012-10-17"
Policies:
- PolicyDocument:
Statement:
- Action: logs:*
Effect: Allow
Resource: "*"
Version: "2012-10-17"
PolicyName: CloudWatchLogsPolicy
- PolicyDocument:
Statement:
- Action: SNS:Publish
Effect: Allow
Resource: "*"
Version: "2012-10-17"
PolicyName: SNSSendEmailPolicy
LambdaHumanApprovalSendEmailFunction:
Type: AWS::Lambda::Function
Properties:
Code:
ZipFile: |
console.log('Loading function');
const AWS = require('aws-sdk');
exports.handler = (event, context, callback) => {
let SNS_ARN = process.env.SNS_ARN;
console.log('event= ' + JSON.stringify(event));
console.log('context= ' + JSON.stringify(context));
const executionContext = event.ExecutionContext;
console.log('executionContext= ' + executionContext);
const executionName = executionContext.Execution.Name;
console.log('executionName= ' + executionName);
const statemachineName = executionContext.StateMachine.Name;
console.log('statemachineName= ' + statemachineName);
const taskToken = executionContext.Task.Token;
console.log('taskToken= ' + taskToken);
const functionURL = event.LambdaURL;
console.log('functionURL = ' + functionURL);
const approveEndpoint = functionURL + "?action=approve&ex=" + executionName + "&sm=" + statemachineName + "&taskToken=" + encodeURIComponent(taskToken);
console.log('approveEndpoint= ' + approveEndpoint);
const rejectEndpoint = functionURL + "?action=reject&ex=" + executionName + "&sm=" + statemachineName + "&taskToken=" + encodeURIComponent(taskToken);
console.log('rejectEndpoint= ' + rejectEndpoint);
const emailSnsTopic = SNS_ARN;
console.log('emailSnsTopic= ' + emailSnsTopic);
var emailMessage = 'Welcome! \n\n';
emailMessage += 'This is an email requiring an approval for a step functions execution. \n\n';
emailMessage += 'Please check the following information and click "Approve" link if you want to approve. \n\n';
emailMessage += 'Approve ' + approveEndpoint + '\n\n';
emailMessage += 'Reject ' + rejectEndpoint + '\n\n';
emailMessage += 'Thanks for using Step functions!';
const sns = new AWS.SNS();
var params = {
Message: emailMessage,
Subject: "Required approval from AWS Step Functions",
TopicArn: emailSnsTopic
};
sns.publish(params)
.promise()
.then(function (data) {
console.log("MessageID is " + data.MessageId);
callback(null);
}).catch(
function (err) {
console.error(err, err.stack);
callback(err);
});
};
Role: !GetAtt LambdaSendEmailExecutionRole.Arn
Environment:
Variables:
SNS_ARN:
Ref: SNSHumanApprovalEmailTopic
Handler: index.handler
Runtime: nodejs16.x
Timeout: 60
DependsOn:
- LambdaSendEmailExecutionRole
LambdaHumanApprovalSendEmailFunctionEventInvokeConfig:
Type: AWS::Lambda::EventInvokeConfig
Properties:
FunctionName:
Ref: LambdaHumanApprovalSendEmailFunction
Qualifier: $LATEST
MaximumRetryAttempts: 0
LambdaStateMachineExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Action: sts:AssumeRole
Effect: Allow
Principal:
Service:
Fn::Join:
- ""
- - states.
- Ref: AWS::Region
- .amazonaws.com
Version: "2012-10-17"
Policies:
- PolicyDocument:
Statement:
- Action: lambda:InvokeFunction
Effect: Allow
Resource: !GetAtt LambdaHumanApprovalSendEmailFunction.Arn
Version: "2012-10-17"
PolicyName: InvokeCallbackLambdaPolicy
LambdaStateMachineExecutionRoleDefaultPolicy:
Type: AWS::IAM::Policy
Properties:
PolicyDocument:
Statement:
- Action: lambda:InvokeFunction
Effect: Allow
Resource:
- Fn::GetAtt:
- LambdaHumanApprovalSendEmailFunction
- Arn
- Fn::Join:
- ""
- - Fn::GetAtt:
- LambdaHumanApprovalSendEmailFunction
- Arn
- :*
Version: "2012-10-17"
PolicyName: LambdaStateMachineExecutionRoleDefaultPolicy
Roles:
- Ref: LambdaStateMachineExecutionRole
HumanApprovalLambdaStateMachine:
Type: AWS::StepFunctions::StateMachine
Properties:
RoleArn: !GetAtt LambdaStateMachineExecutionRole.Arn
DefinitionString:
Fn::Sub: |
{
"StartAt":"Lambda Callback",
"TimeoutSeconds":300,
"States": {
"Lambda Callback":{
"Type":"Task",
"Resource":"arn:${AWS::Partition}:states:::lambda:invoke.waitForTaskToken",
"Parameters": {
"FunctionName": "${LambdaHumanApprovalSendEmailFunction.Arn}",
"Payload":{
"token.$":"$$.Task.Token",
"ExecutionContext.$": "$$",
"LambdaURL":"${LambdaApprovalFunctionFunctionUrl.FunctionUrl}"
}
},
"Next":"ManualApprovalChoiceState"
},
"ManualApprovalChoiceState":{
"Type":"Choice",
"Choices":[
{
"Variable":"$.Status",
"StringEquals":"Approved! Task approved by ${Email}",
"Next":"ApprovedPassState"
}
],
"Default":"RejectedPassState"
},
"RejectedPassState":{
"Type":"Pass",
"End":true
},
"ApprovedPassState":{
"Type":"Pass",
"End":true
}
}
}
DependsOn:
- LambdaStateMachineExecutionRoleDefaultPolicy
- LambdaStateMachineExecutionRole
Outputs:
FunctionURL:
Value: !GetAtt LambdaApprovalFunctionFunctionUrl.FunctionUrl
Ejecutar la máquina de estado
Después de crear la pila, asegúrese de confirmar la suscripción a SNS. En la consola de StepFunctions, encontrará una máquina de estado con el nombre HumanApprovalLambdaStateMachine.
Simplemente comience la ejecución con la entrada predeterminada.
Mientras el paso Lambda Callback está en progreso, recibirá el correo electrónico.


Cuando se hace clic en el enlace Aprobación, Lambda Callback recibe el mensaje Approved! Task approved by ${Email} de la función Lambda directamente. ${Email} se sustituye por el parámetro CloudFormation, que indica quién aprobó o rechazó.

Puede ver ManualApprovalchoicestate continuar con ApprovedPassStatey End.

Resumen
He demostrado la acción de aprobación mediante el uso de StepFunctions y las URL de funciones de Lambda.
Gracias por llegar hasta aquí, si encuentras esto útil no olvides dejar un👍🏼y suscribirse para recibir más contenido.
Si le interesa, puede echar un vistazo a algunos de los otros artículos que he escrito recientemente sobre AWS y Laravel:




