AWS CDKでASL YAMLファイルを読み込んでステートマシンを作る場合、DefinitionBody.fromFile()とdefinitionSubstitutionsプロパティを使おう

AWS CDKでASL YAMLファイルやASL JSONファイルを読み込んでStep Functionsのステートマシンを作りたい場合があります。 その場合でも、aws_stepfunctions.CfnStateMachine() や aws_sam.CfnStateMachine() といったL1コンテキストではなく、aws_stepfunctions.StateMachine() のL2コンテキストで十分使えます。
2024.06.11

データアナリティクス事業本部の笠原です。

みなさん、AWS CDKでStep Functionsのステートマシンを作る際、どのように作りますか?

一般的には、以下のようなコードで DefinitionBody.fromChainable を使ってコードで記述するケースが多いかと思います。

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as sfn 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';

declare const submitLambda: lambda.Function;
declare const getStatusLambda: lambda.Function;

export class SampleStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
	  super(scope, id, props);

		const submitJob = new tasks.LambdaInvoke(this, 'Submit Job', {
		  lambdaFunction: submitLambda,
		  // Lambda's result is in the attribute `guid`
		  outputPath: '$.guid',
		});

		const waitX = new sfn.Wait(this, 'Wait X Seconds', {
		  time: sfn.WaitTime.secondsPath('$.waitSeconds'),
		});

		const getStatus = new tasks.LambdaInvoke(this, 'Get Job Status', {
		  lambdaFunction: getStatusLambda,
		  // Pass just the field named "guid" into the Lambda, put the
		  // Lambda's result in a field called "status" in the response
		  inputPath: '$.guid',
		  outputPath: '$.status',
		});

		const jobFailed = new sfn.Fail(this, 'Job Failed', {
		  cause: 'AWS Batch Job Failed',
		  error: 'DescribeJob returned FAILED',
		});

		const finalStatus = new tasks.LambdaInvoke(this, 'Get Final Job Status', {
		  lambdaFunction: getStatusLambda,
		  // Use "guid" field as input
		  inputPath: '$.guid',
		  outputPath: '$.Payload',
		});

		const definition = submitJob
		  .next(waitX)
		  .next(getStatus)
		  .next(new sfn.Choice(this, 'Job Complete?')
		    // Look at the "status" field
		    .when(sfn.Condition.stringEquals('$.status', 'FAILED'), jobFailed)
		    .when(sfn.Condition.stringEquals('$.status', 'SUCCEEDED'), finalStatus)
		    .otherwise(waitX));

		new sfn.StateMachine(this, 'StateMachine', {
		  definitionBody: sfn.DefinitionBody.fromChainable(definition),
		  timeout: Duration.minutes(5),
		  comment: 'a super cool state machine',
		});
  }
}

私はASL YAMLで定義をゴリゴリ書きたい人なので、CDKでもASL YAMLで定義できれば最高です。

VS Codeで書ければ、ステートマシンのプレビューも簡単に確認できます。

DevelopersIOでもCDKにASL YAMLファイルを読み込んでステートマシンを構築している記事がありますが、 aws_stepfunctions.CfnStateMachine() を使ったり、 AWS SAMの aws_sam.CfnStateMachine() を使ったりしてます。これらは、いずれもL1コンストラクトで定義してます。

私は、L2コンストラクトである aws_stepfunctions.StateMachine() を使っています。 あまり記事になっていないようなので、今回はL2コンストラクトでの読み込み方を示します。

実行環境

  • Node.js: 20.13.1
  • aws-cdk: 2.142.1
  • typescript: 5.4.5

CDKでの記述

こんな感じで書けます。

lib/sample-stack.ts

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { StateMachine, DefinitionBody } from 'aws-cdk-lib/aws-stepfunctions';

export class SampleStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // Lambda
    const fn = new lambda.Function(this, 'myFunction', {
      // 省略
    });

    // State Machine
    const mainStateMachine = new StateMachine(this, 'mainStateMachine', {
      stateMachineName: 'mainStateMachine',
      definitionBody: DefinitionBody.fromFile(
        'src/step_functions/state_machines/main_state_machine.asl.yaml',
      ),
      definitionSubstitutions: {
        myFunctionArn: fn.functionArn,
      },
    });
  }
}

StateMachine()クラスのInitializerで、Construct Propsの definitionBody プロパティと definitionSubstitutions プロパティを使います。

definitionBody プロパティでは DefinitionBody.fromFile() メソッドを使って、ASL YAMLファイルを読み込めます。 fromFile() メソッドでは、ASL JSONファイルも読み込めます。

definitionSubstitutions プロパティでは ASL YAMLファイル内に記載されたプレースホルダーを置換するマッピングをkey-valueで定義します。

ちなみに、 DefinitionBody.fromFile() の他に DefinitionBody.fromString() メソッドもあります。 こちらはASL JSON文字列を渡すことができますが、残念ながらASL YAML文字列を渡すとエラーになります。

ASL YAMLでの記述

ASL YAMLファイルは以下のように、記述します。

プレースホルダーは ${} で定義します。

src/step_functions/state_machines/main_state_machine.asl.yaml

Comment: >-
  Main State Machine
StartAt: MainExec
States:
  MainExec:
    Type: Task
    Resource: ${myFunctionArn}
    End: true

まとめ

意外と簡単にL2コンストラクトでも使えます。

どうしてもL1コンストラクトで定義しないといけない場合を除いて、できるだけL2コンストラクトで定義するとコードがスッキリしますね。

参考