Step Functionのパラメータに「LambdaでDynamoDBから取得した数値」を使うときはint型に変換しよう

StepFunctionのパラメータに「LambdaでDynamoDBから取得した数値」をそのまま使うと、Step Functionsの実行が失敗します。事前にint型へ変換すれば防げます。
2020.07.15

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

Step Functionsを使うとき、Lambdaで取得したパラメータに従って制御することがあります。 たとえば、Choiceで分岐させたり、Waitを使ったりです。

そんなStep Functionsですが、「DynamoDBから取得した数値」をそのまま使うとエラーになっちゃった話です。

エラーになったこと

次のようなステートマシンがあります。最初のInitで動くLambdaが「DynamoDBから数値を取得」し、その数値を用いてWaitさせています。

StepFunctionsの定義

このとき、DynamoDBから取得した数値をそのまま使うと、Step Functionsがエラーで失敗したのです。

Step Functionsの実行が失敗した

Step Functions失敗の詳細ログ

実行イベント履歴のログはこちら。

{
  "error": "States.Runtime",
  "cause": "An error occurred while executing the state 'Wait' (entered at the event id #7). The SecondsPath parameter cannot be parsed as a long value: $.waitSeconds == 10.0"
}

エラーの原因

上記に書いている通り、StepFunctionのSecondsPathでlong型が使えないためです。

「long型? どこから?」

「あなたのlong型はDynamoDBから。」

そう、DynamoDBのNumberを取得したとき、PythonではDecimal型として扱われるのです。

DynamoDBのNumber型はLambda(Python)ではDecimal型になる

Lambdaコード

app.py

import boto3
import json
import os

dynamodb = boto3.resource('dynamodb')
table_name = os.environ['TABLE_NAME']

def lambda_handler(event, context):
    table = dynamodb.Table(table_name)
    res = table.get_item(Key={
        'userId': '1234'
    })

    wait_seconds = res['Item']['waitSeconds']

    print(wait_seconds)
    print(type(wait_seconds))

    return {
        'waitSeconds': wait_seconds
    }

AWS SAM定義

template.yaml

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: StepFunctionsSample

Resources:
  # StepFunctionsから起動されるLambda
  SfnInitFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: hello_world/
      Handler: app.lambda_handler
      Runtime: python3.7
      Timeout: 5
      Environment:
        Variables:
          TABLE_NAME: !Ref ParameterTable
      Policies:
        - arn:aws:iam::aws:policy/AmazonDynamoDBReadOnlyAccess

  SfnInitFunctionLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub /aws/lambda/${SfnInitFunction}

  # 設定値が格納されているDynamoDBテーブル
  ParameterTable:
    Type: AWS::DynamoDB::Table
    Properties:
      AttributeDefinitions:
        - AttributeName: userId
          AttributeType: S
      KeySchema:
        - AttributeName: userId
          KeyType: HASH
      BillingMode: PAY_PER_REQUEST

  # StepFunctionsのステートマシン
  SampleStateMachine:
    Type: AWS::Serverless::StateMachine
    Properties:
      Name: Sample-State-Machine
      Definition:
        StartAt: Init
        States:
          Init:
            Type: Task
            Resource: !GetAtt SfnInitFunction.Arn
            Next: Wait
          Wait:
            Type: Wait
            SecondsPath: $.waitSeconds
            End: true
      Role: !GetAtt SampleStateMachineRole.Arn

  # StepFunction用のIAMロール
  SampleStateMachineRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: !Sub states.${AWS::Region}.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaRole

DynamoDBの様子

DynamoDBの様子

{
  "userId": "1234",
  "waitSeconds": 10
}

対応策

簡単です。int型に直しましょう。

app.py

import boto3
import json
import os

dynamodb = boto3.resource('dynamodb')
table_name = os.environ['TABLE_NAME']

def lambda_handler(event, context):
    table = dynamodb.Table(table_name)
    res = table.get_item(Key={
        'userId': '1234'
    })

    wait_seconds = res['Item']['waitSeconds']

    print(wait_seconds)
    print(type(wait_seconds))

    return {
        'waitSeconds': int(wait_seconds)
    }

無事に動きました。

Step Functionsが成功した様子

さいごに

地味にハマるポイントでした。どなたかの参考になれば幸いです。

参考