Step FunctionでのループをCDKで試したメモ

## 概要
Step Functionの理解を深める。
[Lambda を使用してループを反復する](https://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/tutorial-create-iterate-pattern-section.html)をCDKで書いてみた。

## callbackとreturnの書き換え
例では戻り値をcallbackに渡しているが、冗長なのでreturnで返す形に書き換えた。
([Node.js の AWS Lambda 関数ハンドラー](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/nodejs-handler.html))


[ソースコード](https://github.com/hibohiboo/aws-cdk-v2/blob/2046e79638a2000e1f791b8aaf98c669379be79e/cdk/lib/step-functions-stack.ts)

以下の２つのソースは等価となる。
```js
exports.handler = (event, context, callback) => {
    console.log(event);
    const Payload = { message: "hello", firstEvent: event }
    callback(null, { Payload });
};
```

```js
exports.handler = async (event) => {
    console.log(event);
    const Payload = { message: "hello", firstEvent: event }
    return { Payload }
};
```

なお、以下のソースは等価ではない(asyncがない）。この場合、出力のPayloadが`null`となる。
```js
exports.handler = (event) => {
    console.log(event);
    const Payload = { message: "hello", firstEvent: event }
    return { Payload }
};
```

## Lambda を使用してループを反復する を cdkで書いてみる
[公式のシンプルなループ](https://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/tutorial-create-iterate-pattern-section.html)を試す

[ソースコード](https://github.com/hibohiboo/aws-cdk-v2/blob/20a3e172f64c3993ece95f44a5ac88ba4fae0b2a/cdk/lib/step-functions-stack.ts)

`LambdaInvoke`でオプションを設定しないとシンプルな定義JSONにならないので注意。[*][*1]

```ts
import * as cdk from 'aws-cdk-lib';
import * as stepfunc from 'aws-cdk-lib/aws-stepfunctions';
import * as tasks from 'aws-cdk-lib/aws-stepfunctions-tasks';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { aws_events, aws_events_targets } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { Choice, Condition, DefinitionBody, Pass, Result } from 'aws-cdk-lib/aws-stepfunctions';

interface StepFunctionSampleStackProps extends cdk.StackProps { }
export class StepFunctionSampleStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props: StepFunctionSampleStackProps) {
    super(scope, id, props);
    const lambdaParamsDefault = {
      runtime: lambda.Runtime.NODEJS_20_X,
      handler: 'index.handler',
      timeout: cdk.Duration.seconds(10),
    }

    const iteratorFunction = new lambda.Function(this, 'iteratorFunction', {
      ...lambdaParamsDefault,
      code: lambda.Code.fromInline(`
exports.handler = async (event) => {
  let index = event.iterator.index
  const step = event.iterator.step
  const count = event.iterator.count
 
  index = index + step
  return { index, step, count, continue: index < count };
};
      `),
    });

    const configureCount = new Pass(this, 'ConfigureCount', {
      result: Result.fromObject({ count: 10, index: 0, step: 1 }),
      resultPath: '$.iterator',
    });
    const iteratorJob = new tasks.LambdaInvoke(this, 'Iterator', {
      lambdaFunction: iteratorFunction,
      resultPath: '$.iterator',
      payloadResponseOnly: true, // デフォルトfalse。falseではParamtersが自動設定される
      retryOnServiceExceptions: false // デフォルトtrue。trueではリトライ2回が自動設定される
    });
    const exampleWork = new Pass(this, 'ExampleWork', {
      comment: "Your application logic, to run a specific number of times",
      result: Result.fromObject({ success: true }),
      resultPath: '$.result',
    });
    const isCountReached = new Choice(this, 'IsCountReached');
    const condition1 = Condition.booleanEquals('$.iterator.continue', true);
    // StateMachine
    const definition = configureCount.next(iteratorJob).next(
      isCountReached.when(condition1, exampleWork.next(iteratorJob)).otherwise(new Pass(this, 'Done'))
    );
    const stateMachine = new stepfunc.StateMachine(this, 'StepFunctionSampleStateMachine', {
      comment: "Iterator State Machine Example",
      definitionBody: DefinitionBody.fromChainable(definition),
    });

    // EventBridge Rule1（毎時5分に起動）
    new aws_events.Rule(this, 'rule1', {
      ruleName: 'sampleRule1',
      schedule: aws_events.Schedule.cron({
        minute: '5',
      }),
      targets: [new aws_events_targets.SfnStateMachine(stateMachine)],
    });
  }
}
```

## 入力のtimeを書き換えてみる
[ソースコード](https://github.com/hibohiboo/aws-cdk-v2/blob/e33b52ef3f08c1b045f0c893972d5510cf745a3e/cdk/lib/step-functions-stack.ts)

`{"time": "2000-01-01T00:00:00Z"}`をinputに渡すとする。
これをループごとに+1日してみる。

```diff
    const iteratorFunction = new lambda.Function(this, 'iteratorFunction', {
      ...lambdaParamsDefault,
      code: lambda.Code.fromInline(`
exports.handler = async (event) => {
  let index = event.iterator.index
  const step = event.iterator.step
  const count = event.iterator.count
 
  index = index + step
-  return { index, step, count, continue: index < count };
+  const iterator = { index, step, count, continue: index < count }

+  const dt = new Date(event.time);
+  dt.setDate(dt.getDate() + 1);

+  return { iterator, time: dt.toISOString() };
};
      `),
    });

    const iteratorJob = new tasks.LambdaInvoke(this, 'Iterator', {
      lambdaFunction: iteratorFunction,
-      resultPath: '$.iterator',
      payloadResponseOnly: true,
      retryOnServiceExceptions: false
    });
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/728994a4-93e9-2c10-4999-c17066ca6362.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/59a78b1a-2289-3b0b-4196-84103711deaf.png)

## 出力の引き回し

[ソースコード](https://github.com/hibohiboo/aws-cdk-v2/blob/5b6638208cbe8cf90487be051bbbdb7c5a27580e/cdk/lib/step-functions-stack.ts)
`resultPath`を設定することで、各関数の実行結果を引き回すことができる。
returnしない場合は`null`となる。


```ts
    const firstFunction = new lambda.Function(this, 'FirstFunction', {
      ...lambdaParamsDefault,
      code: lambda.Code.fromInline(`
        exports.handler = async (event) => {
          console.log(event);
          return 'first';
        };
      `),
    });
    const secondFunction = new lambda.Function(this, 'SecondFunction', {
      ...lambdaParamsDefault,
      code: lambda.Code.fromInline(`
      exports.handler = async (event) => {
        console.log(event);
      };
      `),
    });
    // definite state machine
    const firstJonb = new tasks.LambdaInvoke(this, 'UpstreamTask', {
      lambdaFunction: firstFunction,
      resultPath: '$.hoge',
      payloadResponseOnly: true,
    });
    const secondJob = new tasks.LambdaInvoke(this, 'MainFunctionTask', {
      lambdaFunction: secondFunction,
      resultPath: '$.huga',
      payloadResponseOnly: true,
    });


```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/9d0ff641-29a0-efd3-3b07-f1610aa891ad.png)

なお、今のソースだとループごとに結果はリセットされる。(iteratorのLambdaのreturnが引き回しを想定していない）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/5fcf306f-e864-391e-3e4b-a7174353bc7f.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/f5a45459-b99c-909e-01bc-4055984104ed.png)

## 並列
[ソースコード](https://github.com/hibohiboo/aws-cdk-v2/blob/e3094fe678342d753d7b8b41c11f8158fa11522f/cdk/lib/step-functions-stack.ts)



```ts
    const parallel = new Parallel(this, 'parallel');
    parallel.branch(firstJonb.next(secondJob));
    parallel.branch(thirdJob);
    const loopJob = exampleWork
      .next(parallel)
      .next(
        new Pass(this, 'filter', {
          inputPath: '$[0]',
        }),
      )
      .next(iteratorJob);
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/a4fe0443-a50d-5a26-239f-867323a9f8ae.png)


## 参考
[cdk - state machine](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_stepfunctions.StateMachine.html)
[cdk -choice](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_stepfunctions.Choice.html)
[cdkを使ったStepFunctionsでのループ込実装手続きを振り返りつつ書いてみた](https://dev.classmethod.jp/articles/loop-with-sfn-by-cdk-manual/)
[[AWS CDK] Step FunctionsステートマシンでLambdaInvokeでの”payloadResponseOnly”の指定による動作の違いを確認してみた][*1]

[*1]:https://dev.classmethod.jp/articles/aws-cdk-i-checked-the-difference-in-behavior-due-to-the-specification-of-payloadresponseonly-in-the-lambdainvoke-in-the-step-functions-state-machine/