Menerapkan alur kerja yang menunggu persetujuan manusia di Step Functions - AWS Step Functions

Terjemahan disediakan oleh mesin penerjemah. Jika konten terjemahan yang diberikan bertentangan dengan versi bahasa Inggris aslinya, utamakan versi bahasa Inggris.

Menerapkan alur kerja yang menunggu persetujuan manusia di Step Functions

Tutorial ini menunjukkan cara men-deploy proyek persetujuan manusia yang memungkinkan eksekusi AWS Step Functions untuk dijeda selama tugas, dan menunggu pengguna untuk merespons email. Alur kerja berlanjut ke status berikutnya setelah pengguna menyetujui tugas untuk melanjutkan.

Menyebarkan AWS CloudFormation tumpukan yang disertakan dalam tutorial ini akan membuat semua sumber daya yang diperlukan, termasuk:

  • Sumber daya Amazon API Gateway

  • Sebuah AWS Lambda fungsi

  • Mesin AWS Step Functions negara

  • Topik email Amazon Simple Notification Service

  • AWS Identity and Access Management Peran dan izin terkait

catatan

Anda harus memberikan alamat email yang valid yang dapat Anda akses saat membuat AWS CloudFormation tumpukan.

Untuk informasi selengkapnya, lihat Bekerja dengan CloudFormation Template dan AWS::StepFunctions::StateMachine sumber daya di Panduan AWS CloudFormation Pengguna.

Langkah 1: Buat AWS CloudFormation template

  1. Salin kode contoh dari bagian AWS CloudFormation Kode Sumber Template.

  2. Tempelkan sumber AWS CloudFormation template ke dalam file di mesin lokal Anda.

    Untuk contoh ini file disebut human-approval.yaml.

Langkah 2: Buat tumpukan

  1. Masuk ke Konsol AWS CloudFormation.

  2. Pilih Buat Tumpukan, lalu pilih Dengan sumber daya baru (standar).

  3. Pada halaman Buat tumpukan, lakukan hal berikut:

    1. Di bagian Prasyarat - Siapkan templat, pastikan Template sudah siap dipilih.

    2. Di bagian Tentukan templat, pilih Unggah file templat lalu pilih Pilih file untuk mengunggah human-approval.yaml file yang Anda buat sebelumnya yang menyertakan kode sumber templat.

  4. Pilih Berikutnya.

  5. Pada halaman Tentukan detail tumpukan, lakukan hal berikut:

    1. Untuk nama Stack, masukkan nama untuk tumpukan Anda.

    2. Di bawah Parameter, masukkan alamat email yang valid. Anda akan menggunakan alamat email ini untuk berlangganan SNS topik Amazon.

  6. Pilih Berikutnya, lalu pilih Berikutnya lagi.

  7. Pada halaman Tinjauan, pilih Saya mengakui yang AWS CloudFormation mungkin membuat IAM sumber daya dan kemudian pilih Buat.

    AWS CloudFormation mulai membuat tumpukan Anda dan menampilkan status CREATE_IN_ PROGRESS. Ketika proses selesai, AWS CloudFormation menampilkan COMPLETE status CREATE_.

  8. (Opsional) Untuk menampilkan sumber daya di tumpukan Anda, pilih tumpukan dan pilih tab Sumber Daya.

Langkah 3: Menyetujui langganan Amazon SNS

Setelah SNS topik Amazon dibuat, Anda akan menerima email yang meminta Anda mengonfirmasi langganan.

  1. Buka akun email yang Anda berikan saat Anda membuat AWS CloudFormation tumpukan.

  2. Buka pesan Notifikasi AWS - Konfirmasi Langganan dari no-reply@sns.amazonaws.com

    Email tersebut akan mencantumkan Nama Sumber Daya Amazon untuk SNS topik Amazon, dan tautan konfirmasi.

  3. Pilih tautan konfirmasikan langganan.

    Tangkapan layar ilustratif dari email konfirmasi berlangganan.

Langkah 4: Jalankan mesin negara

  1. Pada HumanApprovalLambdaStateMachinehalaman, pilih Mulai eksekusi.

    Kotak dialog Mulai eksekusi ditampilkan.

  2. Dalam kotak dialog Mulai eksekusi, lakukan hal berikut:

    1. (Opsional) Untuk mengidentifikasi eksekusi Anda, Anda dapat menentukan nama, atau menggunakan nama eksekusi default yang dihasilkan.

      catatan

      Step Functions menerima nama untuk mesin negara, eksekusi, aktivitas, dan label yang berisi ASCII non-karakter. Karena nama dengan ASCII non-karakter tidak akan berfungsi dengan Amazon CloudWatch, sebaiknya gunakan hanya ASCII charcter sehingga Anda dapat melacak metrik. CloudWatch

    2. Dalam kotak Input, masukkan JSON input berikut untuk menjalankan alur kerja Anda.

      { "Comment": "Testing the human approval tutorial." }
    3. Pilih Mulai Eksekusi.

      Eksekusi mesin ApprovalTeststatus dimulai, dan berhenti pada tugas Lambda Callback.

    4. Konsol Step Functions mengarahkan Anda ke halaman yang berjudul dengan ID eksekusi Anda. Halaman ini dikenal sebagai halaman Detail Eksekusi. Di halaman ini, Anda dapat meninjau hasil eksekusi saat eksekusi berlangsung atau setelah selesai.

      Untuk meninjau hasil eksekusi, pilih status individual pada tampilan Grafik, lalu pilih tab individual di Detail langkah panel untuk melihat detail setiap status termasuk input, output, dan definisi masing-masing. Untuk detail tentang informasi eksekusi yang dapat Anda lihat di halaman Rincian Eksekusi, lihatHalaman Detail Eksekusi - Ikhtisar antarmuka.

      Eksekusi menunggu callback
  3. Di akun email yang Anda gunakan untuk SNS topik Amazon sebelumnya, buka pesan dengan subjek Persetujuan yang diperlukan dari AWS Step Functions.

    Pesan termasuk terpisah URLs untuk Menyetujui dan Menolak.

  4. Pilih Menyetujui. URL

    Alur kerja berlanjut berdasarkan pilihan Anda.

    Eksekusi menunggu callback

AWS CloudFormation Kode Sumber Template

Gunakan AWS CloudFormation template ini untuk menyebarkan contoh alur kerja proses persetujuan manusia.

AWSTemplateFormatVersion: "2010-09-09" Description: "AWS Step Functions Human based task example. It sends an email with an HTTP URL for approval." 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: # Begin API Gateway Resources ExecutionApi: Type: "AWS::ApiGateway::RestApi" Properties: Name: "Human approval endpoint" Description: "HTTP Endpoint backed by API Gateway and Lambda" FailOnWarnings: true ExecutionResource: Type: 'AWS::ApiGateway::Resource' Properties: RestApiId: !Ref ExecutionApi ParentId: !GetAtt "ExecutionApi.RootResourceId" PathPart: execution ExecutionMethod: Type: "AWS::ApiGateway::Method" Properties: AuthorizationType: NONE HttpMethod: GET Integration: Type: AWS IntegrationHttpMethod: POST Uri: !Sub "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${LambdaApprovalFunction.Arn}/invocations" IntegrationResponses: - StatusCode: 302 ResponseParameters: method.response.header.Location: "integration.response.body.headers.Location" RequestTemplates: application/json: | { "body" : $input.json('$'), "headers": { #foreach($header in $input.params().header.keySet()) "$header": "$util.escapeJavaScript($input.params().header.get($header))" #if($foreach.hasNext),#end #end }, "method": "$context.httpMethod", "params": { #foreach($param in $input.params().path.keySet()) "$param": "$util.escapeJavaScript($input.params().path.get($param))" #if($foreach.hasNext),#end #end }, "query": { #foreach($queryParam in $input.params().querystring.keySet()) "$queryParam": "$util.escapeJavaScript($input.params().querystring.get($queryParam))" #if($foreach.hasNext),#end #end } } ResourceId: !Ref ExecutionResource RestApiId: !Ref ExecutionApi MethodResponses: - StatusCode: 302 ResponseParameters: method.response.header.Location: true ApiGatewayAccount: Type: 'AWS::ApiGateway::Account' Properties: CloudWatchRoleArn: !GetAtt "ApiGatewayCloudWatchLogsRole.Arn" ApiGatewayCloudWatchLogsRole: Type: 'AWS::IAM::Role' Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Principal: Service: - apigateway.amazonaws.com Action: - 'sts:AssumeRole' Policies: - PolicyName: ApiGatewayLogsPolicy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - "logs:*" Resource: !Sub "arn:${AWS::Partition}:logs:*:*:*" ExecutionApiStage: DependsOn: - ApiGatewayAccount Type: 'AWS::ApiGateway::Stage' Properties: DeploymentId: !Ref ApiDeployment MethodSettings: - DataTraceEnabled: true HttpMethod: '*' LoggingLevel: INFO ResourcePath: /* RestApiId: !Ref ExecutionApi StageName: states ApiDeployment: Type: "AWS::ApiGateway::Deployment" DependsOn: - ExecutionMethod Properties: RestApiId: !Ref ExecutionApi StageName: DummyStage # End API Gateway Resources # Begin # Lambda that will be invoked by API Gateway LambdaApprovalFunction: Type: 'AWS::Lambda::Function' Properties: Code: ZipFile: Fn::Sub: | const { SFN: StepFunctions } = require("@aws-sdk/client-sfn"); 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.query.action; const taskToken = event.query.taskToken; const statemachineName = event.query.sm; const executionName = event.query.ex; const stepfunctions = new 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: event.query.taskToken }) .then(function(data) { redirectToStepFunctions(context.invokedFunctionArn, statemachineName, executionName, callback); }).catch(function(err) { console.error(err, err.stack); callback(err); }); } Description: Lambda function that callback to AWS Step Functions FunctionName: LambdaApprovalFunction Handler: index.handler Role: !GetAtt "LambdaApiGatewayIAMRole.Arn" Runtime: nodejs18.x LambdaApiGatewayInvoke: Type: "AWS::Lambda::Permission" Properties: Action: "lambda:InvokeFunction" FunctionName: !GetAtt "LambdaApprovalFunction.Arn" Principal: "apigateway.amazonaws.com" SourceArn: !Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ExecutionApi}/*" LambdaApiGatewayIAMRole: Type: "AWS::IAM::Role" Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Action: - "sts:AssumeRole" Effect: "Allow" Principal: Service: - "lambda.amazonaws.com" Policies: - PolicyName: CloudWatchLogsPolicy PolicyDocument: Statement: - Effect: Allow Action: - "logs:*" Resource: !Sub "arn:${AWS::Partition}:logs:*:*:*" - PolicyName: StepFunctionsPolicy PolicyDocument: Statement: - Effect: Allow Action: - "states:SendTaskFailure" - "states:SendTaskSuccess" Resource: "*" # End Lambda that will be invoked by API Gateway # Begin state machine that publishes to Lambda and sends an email with the link for approval HumanApprovalLambdaStateMachine: Type: AWS::StepFunctions::StateMachine Properties: RoleArn: !GetAtt LambdaStateMachineExecutionRole.Arn DefinitionString: Fn::Sub: | { "StartAt": "Lambda Callback", "TimeoutSeconds": 3600, "States": { "Lambda Callback": { "Type": "Task", "Resource": "arn:${AWS::Partition}:states:::lambda:invoke.waitForTaskToken", "Parameters": { "FunctionName": "${LambdaHumanApprovalSendEmailFunction.Arn}", "Payload": { "ExecutionContext.$": "$$", "APIGatewayEndpoint": "https://${ExecutionApi}.execute-api.${AWS::Region}.amazonaws.com/states" } }, "Next": "ManualApprovalChoiceState" }, "ManualApprovalChoiceState": { "Type": "Choice", "Choices": [ { "Variable": "$.Status", "StringEquals": "Approved! Task approved by ${Email}", "Next": "ApprovedPassState" }, { "Variable": "$.Status", "StringEquals": "Rejected! Task rejected by ${Email}", "Next": "RejectedPassState" } ] }, "ApprovedPassState": { "Type": "Pass", "End": true }, "RejectedPassState": { "Type": "Pass", "End": true } } } SNSHumanApprovalEmailTopic: Type: AWS::SNS::Topic Properties: Subscription: - Endpoint: !Sub ${Email} Protocol: email LambdaHumanApprovalSendEmailFunction: Type: "AWS::Lambda::Function" Properties: Handler: "index.lambda_handler" Role: !GetAtt LambdaSendEmailExecutionRole.Arn Runtime: "nodejs18.x" Timeout: "25" Code: ZipFile: Fn::Sub: | console.log('Loading function'); const { SNS } = require("@aws-sdk/client-sns"); exports.lambda_handler = (event, context, callback) => { 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 apigwEndpint = event.APIGatewayEndpoint; console.log('apigwEndpint = ' + apigwEndpint) const approveEndpoint = apigwEndpint + "/execution?action=approve&ex=" + executionName + "&sm=" + statemachineName + "&taskToken=" + encodeURIComponent(taskToken); console.log('approveEndpoint= ' + approveEndpoint); const rejectEndpoint = apigwEndpint + "/execution?action=reject&ex=" + executionName + "&sm=" + statemachineName + "&taskToken=" + encodeURIComponent(taskToken); console.log('rejectEndpoint= ' + rejectEndpoint); const emailSnsTopic = "${SNSHumanApprovalEmailTopic}"; 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 += 'Check the following information and click "Approve" link if you want to approve. \n\n' emailMessage += 'Execution Name -> ' + executionName + '\n\n' emailMessage += 'Approve ' + approveEndpoint + '\n\n' emailMessage += 'Reject ' + rejectEndpoint + '\n\n' emailMessage += 'Thanks for using Step functions!' const sns = new SNS(); var params = { Message: emailMessage, Subject: "Required approval from AWS Step Functions", TopicArn: emailSnsTopic }; sns.publish(params) .then(function(data) { console.log("MessageID is " + data.MessageId); callback(null); }).catch( function(err) { console.error(err, err.stack); callback(err); }); } LambdaStateMachineExecutionRole: Type: "AWS::IAM::Role" Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Principal: Service: states.amazonaws.com Action: "sts:AssumeRole" Policies: - PolicyName: InvokeCallbackLambda PolicyDocument: Statement: - Effect: Allow Action: - "lambda:InvokeFunction" Resource: - !Sub "${LambdaHumanApprovalSendEmailFunction.Arn}" LambdaSendEmailExecutionRole: Type: "AWS::IAM::Role" Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Principal: Service: lambda.amazonaws.com Action: "sts:AssumeRole" Policies: - PolicyName: CloudWatchLogsPolicy PolicyDocument: Statement: - Effect: Allow Action: - "logs:CreateLogGroup" - "logs:CreateLogStream" - "logs:PutLogEvents" Resource: !Sub "arn:${AWS::Partition}:logs:*:*:*" - PolicyName: SNSSendEmailPolicy PolicyDocument: Statement: - Effect: Allow Action: - "SNS:Publish" Resource: - !Sub "${SNSHumanApprovalEmailTopic}" # End state machine that publishes to Lambda and sends an email with the link for approval Outputs: ApiGatewayInvokeURL: Value: !Sub "https://${ExecutionApi}.execute-api.${AWS::Region}.amazonaws.com/states" StateMachineHumanApprovalArn: Value: !Ref HumanApprovalLambdaStateMachine