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
AuthType
está 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 ApprovedPassState
y 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: