StepFunctions: URL de función Lambda

stepfuncitons

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.

  1. La función Lambda se llama desde StepFunctions y envía un correo electrónico SNS al usuario.
  2. El usuario elige uno de los enlaces URL para aprobar o rechazar.
  3. 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.
Arquitectura usando API Gateway (originalmente en el repositorio awsdoc)

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.

Architecture not using API gateway by using Lambda Function URLs

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 en NONE (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.

StepFunctions

StepFunctions

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ó.

StepFunctions

Puede ver ManualApprovalchoicestate continuar con ApprovedPassStateEnd.

StepFunctions


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:

Recent Post