예제 인간 승인 프로젝트 배포 - AWS Step Functions

기계 번역으로 제공되는 번역입니다. 제공된 번역과 원본 영어의 내용이 상충하는 경우에는 영어 버전이 우선합니다.

예제 인간 승인 프로젝트 배포

이 자습서에서는 AWS Step Functions 실행이 작업 중에 일시 중지하고 사용자가 이메일에 응답할 때까지 대기할 수 있는 인간 승인 프로젝트를 배포하는 방법을 알아봅니다. 사용자가 작업을 진행하도록 승인하면 워크플로우는 다음 상태로 진행합니다.

이 자습서에 포함된 AWS CloudFormation 스택을 배포하면 다음을 포함하여 필요한 모든 리소스가 생성됩니다.

  • Amazon API Gateway 리소스

  • 모든 함수 AWS Lambda

  • AWS Step Functions 스테이트 머신

  • Amazon Simple Notification Service 이메일 주제

  • 관련 AWS Identity and Access Management 역할 및 권한

참고

AWS CloudFormation 스택을 생성할 때 액세스할 수 있는 유효한 이메일 주소를 제공해야 합니다.

자세한 내용은 AWS CloudFormation 사용 설명서의 CloudFormation 템플릿AWS::StepFunctions::StateMachine 리소스 사용을 참조하십시오.

1단계: AWS CloudFormation 템플릿 만들기

  1. AWS CloudFormation 템플릿 소스 코드 단원에서 예제 코드를 복사합니다.

  2. AWS CloudFormation 템플릿의 소스를 로컬 컴퓨터의 파일에 붙여넣습니다.

    이 예제의 경우 파일은 human-approval.yaml입니다.

2단계: 스택 만들기

  1. AWS CloudFormation 콘솔에 로그인합니다.

  2. 스택 생성을 선택한 다음 새 리소스 사용(표준)을 선택합니다.

  3. Create stack(스택 생성) 페이지에서 다음을 수행합니다.

    1. 사전 조건 - 템플릿 준비 섹션에서 준비된 템플릿을 선택합니다.

    2. 템플릿 지정 섹션에서 템플릿 파일 업로드를 선택한 다음 파일 선택을 선택하여 이전에 만든 템플릿 소스 코드가 포함된 human-approval.yaml 파일을 업로드합니다.

  4. Next(다음)를 선택합니다.

  5. 스택 세부 정보 지정 페이지에서 다음 작업을 수행합니다.

    1. 스택 이름에 스택 이름을 입력합니다.

    2. 파라미터에 유효한 이메일 주소를 입력합니다. 이 이메일 주소를 사용하여 Amazon SNS 주제를 구독합니다.

  6. 다음을 선택한 다음 다음을 다시 선택합니다.

  7. 검토 페이지에서 IAM 리소스를 생성할 AWS CloudFormation 수 있음을 확인합니다를 선택한 다음 [Create] 를 선택합니다.

    AWS CloudFormation 스택 생성을 시작하고 CREATE_IN_PROGRESS 상태를 표시합니다. 프로세스가 완료되면 CREATE_COMPLETE 상태가 표시됩니다 AWS CloudFormation .

  8. (선택 사항) 스택에 리소스를 표시하려면 스택을 선택하고 [Resources ] 탭을 선택합니다.

    리소스 표시

3단계: Amazon SNS 구독 승인

Amazon SNS 주제가 생성되면 구독 확인을 요청하는 이메일이 전송됩니다.

  1. 스택을 생성할 때 제공한 이메일 계정을 엽니다. AWS CloudFormation

  2. no-reply@sns.amazonaws.com에서 보낸 AWS Notification - Subscription Confirmation 메시지를 엽니다.

    이메일에는 Amazon SNS 주제에 대한 Amazon 리소스 이름과 확인 링크가 있습니다.

  3. Confirm subscription(구독 확인) 링크를 선택합니다.

    구독이 확인됨!

4단계: 상태 시스템 실행

  1. HumanApprovalLambdaStateMachine페이지에서 실행 시작을 선택합니다.

    실행 시작 대화 상자가 표시됩니다.

  2. 실행 시작 대화 상자에서 다음을 수행합니다.

    1. (선택 사항) 실행을 식별하려면 이름 상자에 해당 실행의 이름을 지정하면 됩니다. 기본적으로 Step Functions는 고유한 실행 이름을 자동으로 생성합니다.

      참고

      Step Functions를 사용하면 상태 머신, 실행 및 활동의 이름과 ASCII가 아닌 문자가 포함된 레이블을 만들 수 있습니다. 이러한 비 ASCII 이름은 Amazon에서 사용할 수 없습니다. CloudWatch CloudWatch 지표를 추적할 수 있도록 하려면 ASCII 문자만 사용하는 이름을 선택하십시오.

    2. 입력 상자에 다음 JSON 입력을 입력하여 워크플로를 실행합니다.

      { "Comment": "Testing the human approval tutorial." }
    3. 실행 시작을 선택합니다.

      Lambda Callback 태스크에서 ApprovalTest상태 머신 실행이 시작되고 일시 중지됩니다.

    4. Step Functions 콘솔은 실행 ID가 제목인 페이지로 이동합니다. 이 페이지를 실행 세부 정보 페이지라고 합니다. 실행이 진행되는 동안 또는 완료된 후에 이 페이지에서 실행 결과를 검토할 수 있습니다.

      실행 결과를 검토하려면 그래프 보기에서 개별 상태를 선택한 다음 단계 세부 정보 창에서 개별 탭을 선택하여 입력, 출력 및 정의가 포함된 각 상태의 세부 정보를 각각 봅니다. 실행 세부 정보 페이지에서 볼 수 있는 실행 정보에 대한 자세한 내용은 실행 세부 정보 페이지 - 인터페이스 개요 섹션을 참조하세요.

      콜백 대기 중인 실행
  3. 앞서 Amazon SNS 주제에 사용한 이메일 계정에서 승인 필요 양식이라는 제목의 메시지를 엽니다 AWS Step Functions.

    이 메시지에는 Approve(승인)Reject(거부)에 대한 별도의 URL이 포함됩니다.

  4. Approve(승인) URL을 선택합니다.

    선택을 기반으로 워크플로우가 계속됩니다.

    콜백 대기 중인 실행

AWS CloudFormation 템플릿 소스 코드

이 AWS CloudFormation 템플릿을 사용하여 사람의 승인 프로세스 워크플로의 예를 배포할 수 있습니다.

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 += 'Please 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