🔁

Step FunctionsでNextTokenを返すAPIをループ処理する

に公開

AWS Step Functionsで、レスポンスアイテム数に上限がある、NextToken/Maker/ContinuationToken等を返すList/Describe APIの結果を全部ループして処理する方法です。

Mapステートで1レスポンス内のリストをループ処理し、さらにそれをループするようなステートマシンを作成します。

参考

こちらをパク参考にしました。

https://dev.to/stevensmiley/handling-paginated-api-responses-in-aws-step-functions-1emf

サンプル

IAM ListRoles APIで、アカウントに存在するロールをすべてリストして処理するサンプルです。

このAPIは、MaxItemsを超えるロールがまだ存在するときMarker(NextToken)を返します。続いて、その値をリクエストに含めてAPIを実行することで、残りのロール一覧を取得できます。それでもすべてのロールを取得できていない場合は、さらに別のMarkerを返します。

CloudFormationテンプレート

AWSTemplateFormatVersion: 2010-09-09

Resources:
  StateMachineRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: states.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: StepFunctionsPolicy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - iam:ListRoles
                Resource: '*'

  StateMachine:
    Type: AWS::StepFunctions::StateMachine
    Properties:
      StateMachineName: !Sub ${AWS::StackName}-StateMachine
      StateMachineType: STANDARD
      RoleArn: !GetAtt StateMachineRole.Arn
      DefinitionString: |
        {
          "Comment": "Sample state machine handling paginated API responses",
          "StartAt": "ListRoles",
          "States": {
            "ListRoles": {
              "Type": "Task",
              "Parameters": {
                "MaxItems": 30
              },
              "Resource": "arn:aws:states:::aws-sdk:iam:listRoles",
              "Next": "ForEachRole"
            },
            "ForEachRole": {
              "Type": "Map",
              "ItemsPath": "$.Roles",
              "MaxConcurrency": 1,
              "ItemProcessor": {
                "ProcessorConfig": {
                  "Mode": "INLINE"
                },
                "StartAt": "DoSomething",
                "States": {
                  "DoSomething": {
                    "Type": "Pass",
                    "End": true
                  }
                }
              },
              "ResultPath": null,
              "Next": "IfNextTokenExists"
            },
            "IfNextTokenExists": {
              "Type": "Choice",
              "Choices": [
                {
                  "Variable": "$.Marker",
                  "IsPresent": true,
                  "Next": "ListMoreRoles"
                }
              ],
              "Default": "Success"
            },
            "ListMoreRoles": {
              "Type": "Task",
              "Parameters": {
                "MaxItems": 30,
                "Marker.$": "$.Marker"
              },
              "Resource": "arn:aws:states:::aws-sdk:iam:listRoles",
              "Next": "ForEachRole"
            },
            "Success": {
              "Type": "Succeed"
            }
          }
        }

Mapステートで結果を棄てる("ResultPath": null)ことで、ListRolesステートの結果をIfNextTokenExistsステートに渡しています。ResultPathの使い方はStep Functions ワークフローで ResultPath を使用して状態出力を指定するをご参照ください。

ステートマシンのプロパティにはDefinitionを使いたかったのですが、そうすると"ResultPath": nullがエラーになってしまったため、DefinitionStringを使用しています。

2025/4/4更新:JSONata版

JSONataを使用したサンプルも作成しました。

最初にAPI実行を回しきった後、まとめてMapステートでループする構成にしてみました。

ListMoreRolesステートは無くすこともできたのですが、読みにくくなると感じたため、残しました。

{
  "Comment": "Sample state machine handling paginated API responses",
  "QueryLanguage": "JSONata",
  "StartAt": "ListRoles",
  "States": {
    "ListRoles": {
      "Type": "Task",
      "Arguments": {
        "MaxItems": 30
      },
      "Resource": "arn:aws:states:::aws-sdk:iam:listRoles",
      "Assign": {
        "roleNames": "{% $states.result.Roles.RoleName %}"
      },
      "Next": "IfNextTokenExists"
    },
    "IfNextTokenExists": {
      "Type": "Choice",
      "Choices": [
        {
          "Next": "ListMoreRoles",
          "Condition": "{% $exists($states.input.Marker) %}"
        }
      ],
      "Default": "ForEachRole"
    },
    "ListMoreRoles": {
      "Type": "Task",
      "Arguments": {
        "MaxItems": 30,
        "Marker": "{% $states.input.Marker %}"
      },
      "Resource": "arn:aws:states:::aws-sdk:iam:listRoles",
      "Assign": {
        "roleNames": "{% $append($roleNames, $states.result.Roles.RoleName) %}"
      },
      "Next": "IfNextTokenExists"
    },
    "ForEachRole": {
      "Type": "Map",
      "Items": "{% $roleNames %}",
      "MaxConcurrency": 1,
      "ItemProcessor": {
        "ProcessorConfig": {
          "Mode": "INLINE"
        },
        "StartAt": "DoNothing",
        "States": {
          "DoNothing": {
            "Type": "Pass",
            "End": true
          }
        }
      },
      "End": true
    }
  }
}

Discussion