Serverless FrameworkのStep Functions用プラグインでState LanguageをYAMLで記述する #reinvent

はじめに

こんにちは、中山です。

昨日feedlyを眺めていたらかなり便利そうなツールを発見したのでご紹介したいと思います。Serverless FrameworkでStep Functionsを実行するプラグインがリリースされました!最高。作者の方が書かれたQiitaエントリとプラグインのGitHubリポジトリは以下の通りです。

このプラグインを利用することで以下のことができます。かなり便利そうです。この冬はこれで決まり感がありますね。最高!

  • Step FunctionsのステートをJSONではなくYAMLで書ける
    • ある程度の長さになってくるとやはりJSONではツライです
  • ステートマシンの作成/更新/削除をCLIから操作できる
    • AWS CLIを生で叩くのはツライです

現時点(2016/12/31)でプラグインに指定可能なオプションは以下の通りです。

# デプロイ用オプション
$ sls deploy stepf -h
Plugin: ServerlessStepFunctions
deploy stepf .................. Deploy Step functions
    --state / -t (required) ............ Name of the State Machine
# Invoke用オプション
$ sls invoke stepf -h
Plugin: ServerlessStepFunctions
invoke stepf .................. Remove Step functions
    --state / -t (required) ............ Name of the State Machine
    --data / -d ........................ String data to be passed as an event to your step function
# 削除用オプション
$ sls remove stepf -h
Plugin: ServerlessStepFunctions
remove stepf .................. Remove Step functions
    --state / -t (required) ............ Name of the State Machine

それでは早速使ってみましょう。

使ってみる

検証に利用した各種ツールのバージョンは以下の通りです。バージョンが異なる場合は結果が変わる可能性があります。その点ご了承ください。

  • Serverless Framework: 1.4.0
  • serverless-step-functions: 0.1.2

プラグインのインストールは以下のコマンドで実行します。

$ npm install --save serverless-step-functions

まずは手っ取り早くテンプレートからLambda関数と serverless.yml を生成してプラグインの動作を確認してみます。

$ sls create -t aws-python -n step-functions-plugin-test
Serverless: Generating boilerplate...
 _______                             __
|   _   .-----.----.--.--.-----.----|  .-----.-----.-----.
|   |___|  -__|   _|  |  |  -__|   _|  |  -__|__ --|__ --|
|____   |_____|__|  \___/|_____|__| |__|_____|_____|_____|
|   |   |             The Serverless Application Framework
|       |                           serverless.com, v1.4.0
 -------'

Serverless: Successfully generated boilerplate for template: "aws-python"

Lambda関数をデプロイしておきます。

$ sls deploy -v
Serverless: Creating Stack...
Serverless: Checking Stack create progress...
<snip>

READMEを参考に serverless.yml を以下のようにしてみました。

  • serverless.yml
service: step-functions-plugin-test

provider:
  name: aws
  runtime: python2.7
  region: us-east-1
  stage: dev

plugins:
  - serverless-step-functions

functions:
  hellofunc:
    handler: handler.hello

stepFunctions:
  hellostepfunc:
    Comment: "A Hello World example of the Amazon States Language using an AWS Lambda Function"
    StartAt: HelloWorld
    States:
      HelloWorld:
        Type: Task
        Resource: hellofunc
        End: true

ステートマシンをデプロイしてみます。

$ sls deploy stepf -t hellostepfunc -v
Serverless: Start to deploy hellostepfunc step function...

Serverless: Finish to deploy hellostepfunc-dev step function

デプロイした内容を確認してみます。

# ステートマシンが作成されたことを確認
$ aws stepfunctions list-state-machines
{
    "stateMachines": [
        {
            "creationDate": 1483102366.55,
            "stateMachineArn": "arn:aws:states:us-east-1:************:stateMachine:hellostepfunc-dev",
            "name": "hellostepfunc-dev"
        }
    ]
}
# 内容を確認
$ aws stepfunctions describe-state-machine \
  --state-machine-arn "$(aws stepfunctions list-state-machines \
    --query 'stateMachines[?name==`hellostepfunc-dev`].stateMachineArn' \
    --output text)"
{
    "status": "ACTIVE",
    "definition": "{\"Comment\":\"A Hello World example of the Amazon States Language using an AWS Lambda Function\",\"StartAt\":\"HelloWorld\",\"States\":{\"HelloWorld\":{\"Type\":\"Task\",\"Resource\":\"arn:aws:lambda:us-east-1:************:function:step-functions-plugin-test-dev-hellofunc\",\"End\":true}}}",
    "name": "hellostepfunc-dev",
    "roleArn": "arn:aws:iam::************:role/serverless-step-functions-executerole-us-east-1",
    "stateMachineArn": "arn:aws:states:us-east-1:************:stateMachine:hellostepfunc-dev",
    "creationDate": 1483116193.335
}

YAMLで定義したState LanguageがJSONに変換されています。最高。出力が見づらい場合は以下のようにすればOKです。

$ aws stepfunctions describe-state-machine \
  --state-machine-arn "$(aws stepfunctions list-state-machines \
    --query 'stateMachines[?name==`hellostepfunc-dev`].stateMachineArn' \
    --output text)" \
  --query 'definition' \
  --output text \
  | jq .
{
  "Comment": "A Hello World example of the Amazon States Language using an AWS Lambda Function",
  "StartAt": "HelloWorld",
  "States": {
    "HelloWorld": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:us-east-1:************:function:step-functions-plugin-test-dev-hellofunc",
      "End": true
    }
  }
}

ステートマシンに関連付けるIAM Roleも作成してくれているようです。内容を確認してみるとStep Functions用のAssume RoleとLambda関数をInvokeするためのポリシーが付いています。

$ aws iam get-role \
  --role-name serverless-step-functions-executerole-us-east-1
{
    "Role": {
        "AssumeRolePolicyDocument": {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Action": "sts:AssumeRole",
                    "Effect": "Allow",
                    "Principal": {
                        "Service": "states.us-east-1.amazonaws.com"
                    }
                }
            ]
        },
        "RoleId": "*********************",
        "CreateDate": "2016-12-30T16:43:09Z",
        "RoleName": "serverless-step-functions-executerole-us-east-1",
        "Path": "/",
        "Arn": "arn:aws:iam::************:role/serverless-step-functions-executerole-us-east-1"
    }
}
$ aws iam list-attached-role-policies \
  --role-name serverless-step-functions-executerole-us-east-1
{
    "AttachedPolicies": [
        {
            "PolicyName": "serverless-step-functions-executepolicy-us-east-1",
            "PolicyArn": "arn:aws:iam::************:policy/serverless-step-functions-executepolicy-us-east-1"
        }
    ]
}
$ aws iam get-policy-version \
  --policy-arn "$(aws iam list-attached-role-policies \
    --role-name serverless-step-functions-executerole-us-east-1 \
    --query 'AttachedPolicies[].PolicyArn' --output text)" \
  --version-id v1
{
    "PolicyVersion": {
        "CreateDate": "2016-12-30T16:43:10Z",
        "VersionId": "v1",
        "Document": {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Action": [
                        "lambda:InvokeFunction"
                    ],
                    "Resource": "*",
                    "Effect": "Allow"
                }
            ]
        },
        "IsDefaultVersion": true
    }
}

続いてステートマシンを実行してみましょう。

$ sls invoke stepf -t hellostepfunc -v
Serverless: Start function hellostepfunc...
.
{ executionArn: 'arn:aws:states:us-east-1:************:execution:hellostepfunc-dev:7324c3c7-5e36-4292-b745-8e2da3faae48',
  stateMachineArn: 'arn:aws:states:us-east-1:************:stateMachine:hellostepfunc-dev',
  name: '7324c3c7-5e36-4292-b745-8e2da3faae48',
  status: 'SUCCEEDED',
  startDate: 2016-12-30T17:09:11.418Z,
  stopDate: 2016-12-30T17:09:12.458Z,
  input: '{}',
  output: '{"body": "{\\"input\\": {}, \\"message\\": \\"Go Serverless v1.0! Your function executed successfully!\\"}", "statusCode": 200}' }

正常に実行されました!作成したステートマシーンを削除してみます。

# 削除
$ sls remove stepf -t hellostepfunc -v
Serverless: Remove hellostepfunc
# ステータスが削除中となっていることを確認
$ aws stepfunctions describe-state-machine \
  --state-machine-arn "$(aws stepfunctions list-state-machines \
    --query 'stateMachines[?name==`hellostepfunc-dev`].stateMachineArn' \
    --output text)"
{
    "status": "DELETING",
    "definition": "{\"Comment\":\"A Hello World example of the Amazon States Language using an AWS Lambda Function\",\"StartAt\":\"HelloWorld\",\"States\":{\"HelloWorld\":{\"Type\":\"Task\",\"Resource\":\"arn:aws:lambda:us-east-1:************:function:step-functions-plugin-test-dev-hellofunc\",\"End\":true}}}",
    "name": "hellostepfunc-dev",
    "roleArn": "arn:aws:iam::************:role/serverless-step-functions-executerole-us-east-1",
    "stateMachineArn": "arn:aws:states:us-east-1:************:stateMachine:hellostepfunc-dev",
    "creationDate": 1483116193.335
}
# 削除されたことを確認
$ aws stepfunctions list-state-machines
{
    "stateMachines": []
}

正常に削除されましたね。次はもう少し複雑なState Languageにしてみます。 Lambda関数を新規に作成してインプットの内容に応じて処理を分岐させるようにステートマシンを追加してみます。

  • choice.py
def hello(event, context):
    return {'statusCode': int(event['statusCode'])}
  • serverless.yml
service: step-functions-plugin-test

provider:
  name: aws
  runtime: python2.7
  region: us-east-1
  stage: dev

plugins:
  - serverless-step-functions

functions:
  hellofunc:
    handler: handler.hello
  choicefunc:
    handler: choice.handler

stepFunctions:
  hellostepfunc:
    Comment: "A Hello World example of the Amazon States Language using an AWS Lambda Function"
    StartAt: HelloWorld
    States:
      HelloWorld:
        Type: Task
        Resource: hellofunc
        End: true
        
  choicestepfunc:
    Comment: Choice Step Function
    StartAt: FirstState
    States:
      FirstState:
        Type: Task
        Resource: choicefunc
        Next: ChoiceState
      ChoiceState:
        Type: Choice
        Choices:
          - Variable: $.statusCode
            NumericEquals: 200
            Next: FirstMatchState
        Default: DefaultState
      FirstMatchState:
        Type: Pass
        Next: EndState
      DefaultState:
        Type: Fail
        Cause: No Matches!
      EndState:
        Type: Pass
        End: true

両方共再度デプロイします。なお、作者の方も書かれていますが現状Step Functionsにアップデート用API自体が用意されていないので、一度作成したステートマシンの設定は編集できません。そのため、アップデートする場合はステートマシンを一度削除してから新規で再作成するというフローで擬似的に実装されています。つまり、 修正したステートの記述に文法間違いなどがありデプロイに失敗した場合ステートマシンが更新されずにただ削除される という点は注意した方が良いと思います。このあたり、今後改善されるとうれしいですね。

一応ワークアラウンドは用意されていて、例えば -s または --stage でデプロイする環境を切り替えるという方法があります。もしくはAWSが公式に提供しているState Languageのlintツールを利用するという手もあります。例えば以下のように使えます。ちょっとエグいですが。。。 Resource に指定するのは本来ARNなのでその点はエラーとなってしまうようです。 ~/.aws/cli/alias に引数を取る形で設定しておけば使い回しも可能です。

$ awk '/hellostepfunc/,/^$/' serverless.yml \
  | ruby -ryaml -rjson -e 'puts JSON.pretty_generate(YAML.load(ARGF)["hellostepfunc"])' \
  | statelint  /dev/stdin
One error:
 State Machine.States.HelloWorld.Resource is "hellofunc" but should be A URI

デプロイされた内容を確認してみましょう。

$ aws stepfunctions describe-state-machine \
  --state-machine-arn "$(aws stepfunctions list-state-machines \
    --query 'stateMachines[?name==`choicestepfunc-dev`].stateMachineArn' \
    --output text)" \
  --query 'definition' --output text \
  | jq .
{
  "Comment": "Choice Step Function",
  "StartAt": "FirstState",
  "States": {
    "FirstState": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:us-east-1:************:function:step-functions-plugin-test-dev-choicefunc",
      "Next": "ChoiceState"
    },
    "ChoiceState": {
      "Type": "Choice",
      "Choices": [
        {
          "Variable": "$.statusCode",
          "NumericEquals": 200,
          "Next": "FirstMatchState"
        }
      ],
      "Default": "DefaultState"
    },
    "FirstMatchState": {
      "Type": "Pass",
      "Next": "EndState"
    },
    "DefaultState": {
      "Type": "Fail",
      "Cause": "No Matches!"
    },
    "EndState": {
      "Type": "Pass",
      "End": true
    }
  }
}

良さそうですね。Invokeしてみます。

$ sls invoke stepf -t choicestepfunc -d "$(jo Code=200)"
Serverless: Start function choicestepfunc...

{ executionArn: 'arn:aws:states:us-east-1:************:execution:choicestepfunc-dev:a4ab03b0-d648-4785-9cad-d4120a1336c8',
  stateMachineArn: 'arn:aws:states:us-east-1:************:stateMachine:choicestepfunc-dev',
  name: 'a4ab03b0-d648-4785-9cad-d4120a1336c8',
  status: 'SUCCEEDED',
  startDate: 2016-12-30T18:00:54.456Z,
  stopDate: 2016-12-30T18:00:54.962Z,
  input: '{"Code":200}',
  output: '{"statusCode": 200}' }
}

意図した動きをしてくれているようです。最高。

まとめ

いかがだったでしょうか。

Serverless FrameworkからStep Functionsを利用するプラグインをご紹介しました。かなり便利そうなのでどんどん使っていきたいと思います!最高!

本エントリがみなさんの参考になれば幸いに思います。