コールバックURLを生成するsfn-callback-urlsアプリケーションをStep Functionsで使ってみた

AWS Serverless Application Repositoryの公開アプリケーション 「sfn-callback-urls」を利用すると承認要求用のURLが生成でき、ステートマシンに組み込むことで承認者のアクション(URLのクリック)に合わせた、ワークフローを作成することが可能になります。本エントリではsfn-callback-urlsアプリケーションをステートマシンから利用してみたいと思います。
2020.03.24

構成

最終的な構成のイメージです。破線で囲ったsfn-callback-urls以外は手動で作成します。

sfn-callback-urlsデプロイ

サーバレスアプリケーションのコンソールにてsfn-callback-urlsで検索を行います。該当のアプリケーションが表示されたら、アプリケーション名をクリックします。

IAMのチェックを付与し、他の設定はデフォルトのまま「デプロイ」をクリックします。

数分でデプロイが完了します。

裏ではアプリケーションのSAMテンプレートが実行されていますので、CloudFormationのコンソールからも状況を確認することが可能です。

SNS トピックの作成

SNSトピック、サブスクリプションを作成します。 サブスクリプションのプロトコルは「Eメール」で、エンドポイントに承認者のメールアドレスを指定します。トピック名は任意です。作成されたトピックのARNは後ほど利用します。

Lambda Function作成

以下定義でLambda Functionを作成します。

  • 関数名…ApprovalEmailsFunction
  • ランタイム…Python 3.8
  • 実行ロール…基本的な Lambda アクセス権限で新しいロールを作成

IAMロール設定

Lambda Function作成時に作成されたロールに権限を付与します。IAMコンソールで表示するリンクをクリックします。

「インラインポリシーの追加」をクリックします。

2つのインラインポリシーを設定します。

  • SNS
    • アクション…Publish
    • リソース…先程作成したSNSトピックのARN
  • Lambda
    • アクション…InvokeFunction
    • リソース…sfn-callback-urlsアプリケーションでデプロイされたserverlessrepo-sfn-callback-urls-CreateUrls-xxxxxxxxxxxのARN

任意のポリシー名を指定し「ポリシーの作成」をクリックします。

コード

Labmda Functionのコンソールに戻り、関数コードに以下を記述します。

import json, os, boto3
def lambda_handler(event, context):
    print('Event:', json.dumps(event))
    # Switch between the two blocks of code to run
    # This is normally in separate functions
    if event['step'] == 'SendApprovalRequest':
        print('Calling sfn-callback-urls app')
        input = {
            # Step Functions gives us this callback token
            # sfn-callback-urls needs it to be able to complete the task
            "token": event['token'],
            "actions": [
                # The approval action that transfers the name to the output
                {
                    "name": "approve",
                    "type": "success",
                    "output": {
                        # watch for re-use of this field below
                        "name_in_output": event['name_in_input']
                    }
                },
                # The rejection action that names the rejecter
                {
                    "name": "reject",
                    "type": "failure",
                    "error": "rejected",
                    "cause": event['name_in_input'] + " rejected it"
                }
            ]
        }
        response = boto3.client('lambda').invoke(
            FunctionName=os.environ['CREATE_URLS_FUNCTION'],
            Payload=json.dumps(input)
        )
        urls = json.loads(response['Payload'].read())['urls']
        print('Got urls:', urls)

        # Compose email
        email_subject = 'Step Functions example approval request'

        email_body = """Hello {name},
        Click below (these could be better in HTML emails):

        Approve:
        {approve}

        Reject:
        {reject}
        """.format(
            name=event['name_in_input'],
            approve=urls['approve'],
            reject=urls['reject']
        )
    elif event['step'] == 'SendConfirmation':
        # Compose email
        email_subject = 'Step Functions example complete'

        if 'Error' in event['output']:
            email_body = """Hello,
            Your task was rejected: {cause}
            """.format(
                cause=event['output']['Cause']
            )
        else:
            email_body = """Hello {name},
            Your task is complete.
            """.format(
                name=event['output']['name_in_output']
            )
    else:
        raise ValueError

    print('Sending email:', email_body)
    boto3.client('sns').publish(
        TopicArn=os.environ['TOPIC_ARN'],
        Subject=email_subject,
        Message=email_body
    )
    print('done')
    return {}

環境変数に以下を指定し、Lambda Functionを更新して保存します。

  • TOPIC_ARN…先程作成したSNSトピックのARN
  • CREATE_URLS_FUNCTION…sfn-callback-urlsアプリケーションでデプロイされたserverlessrepo-sfn-callback-urls-CreateUrls-xxxxxxxxxxxのARN

ステートマシン作成

IAMロール設定

ステートマシンにアタッチするIAMロールを作成します。

  • AWSサービス…StepFunctions
  • ポリシー…AWSLambdaRole
  • ロール名…ApprovalEmailsStateMachineRole

ステートマシン定義

以下の設定でステートマシンを作成します。

  • タイプ…標準
  • ステートマシン名…ApprovalEmails
  • IAMロール…ApprovalEmailsStateMachineRole

定義

{
    "Version": "1.0",
    "StartAt": "SendApprovalRequest",
    "States": {
        "SendApprovalRequest": {
            "Type": "Task",
            "Resource": "arn:aws:states:::lambda:invoke.waitForTaskToken",
            "Parameters": {
                "FunctionName": "ApprovalEmailsFunction",
                "Payload": {
                    "step.$": "$$.State.Name",
                    "name_in_input.$": "$.name",
                    "token.$": "$$.Task.Token"
                }
            },
            "ResultPath": "$.output",
            "Next": "SendConfirmation",
            "Catch": [
                {
                    "ErrorEquals": [ "rejected" ],
                    "ResultPath": "$.output",
                    "Next": "SendConfirmation"
                }
            ]
        },
        "SendConfirmation": {
            "Type": "Task",
            "Resource": "arn:aws:states:::lambda:invoke",
            "Parameters": {
                "FunctionName": "ApprovalEmailsFunction",
                "Payload": {
                    "step.$": "$$.State.Name",
                    "output.$": "$.output"
                }
            },
            "End": true
        }
    }
}

2つのステートを含むステートマシンが作成できました。

以降の動作確認でも触れますが、SendApprovalRequestステートでは、waitForTaskTokenを利用しているので、Lambda Function呼び出し後、タスクトークンを受け取るまで後続のステートは実行されません。waitForTaskTokenの詳細については以下を確認ください。

動作確認

最初に呼び出しされるステートSendApprovalRequestで、nameを受け取っているので以下の入力を与えステートマシンを実行します。

{
  "name": "Saka"
}

SendApprovalRequestステートの処理

Parametersフィールドで指定した$$.State.Nameなどのコンテキストオブジェクトを渡し、Lambda FunctionApprovalEmailsFunctionが実行されます。

ApprovalEmailsFunctionでは渡された値を判定(このステートから呼び出しされるていることを確認)し、承認要求用のURLを生成します。このURLはSNSを通し承認者にメール通知されます。

ApprovalEmailsFunction呼び出し時にwaitForTaskTokenを付与していますので、処理が完了してもステートマシンにタスクトークンが返されるまで、状態は進行中のままとなります。

なお、ここではタイムアウトを設定していないので、サービスクォータに達するまで1年間タスクトークンを待機します。

承認者の対応

SendApprovalRequestステートの処理が完了すると、承認者にメールが届きます。

承認、拒否でURLが異なりますのでいずれかのURLをクリックします。ここでは承認のURLをクリックしました。

SendConfirmationステートの処理

承認者がURLをクリックすると、API Gateway経由でLambda Functionserverlessrepo-sfn-callbac-ProcessCallbackFunction-xxxxxxxxxxxxが呼び出しされます。このLambda Functionでタスクトークンをステートマシンに返し、SendConfirmationステートが実行されます。

SendConfirmationステートでも、Lambda FunctionApprovalEmailsFunctionを呼び出しますが、SendApprovalRequesステートとは異なる値がLambda Functionに渡されます。

このステートから呼び出しされたApprovalEmailsFunctionは、承認、拒否など承認者がクリックしたURLに合わせたメッセージを生成し承認者にメール通知を行います。今回は承認をクリックしていたので、以下のようなメッセージとなりました。

これで、ステートマシンの処理は完了となります。

なお、拒否のURLをクリックすると以下のような動作となります。

さいごに

sfn-callback-urlsアプリケーションをStep Functionsから利用してみました。今回は承認要求用URLの生成にLambda Functionserverlessrepo-sfn-callback-urls-CreateUrls-xxxxxxxxxxxを呼び出しましたが、API経由でURLを作成するLambda Functionserverlessrepo-sfn-callback-urls-CreateUrlsForApi-xxxxxxxxxも提供されています。(デプロイされています)業務に合わせたカスタマイズは必要になると思いますが、こちらのアプリケーションを利用することで承認を伴うようなワークフローの作成が捗りそうですね。

参考