【初心者向け】AWS SAMテンプレートを使って、AWS Step Functionsのステートマシンを構築する

記事タイトルとURLをコピーする

こんにちは。AS部の兼安です。
今回はAWS SAMテンプレートを使って、AWS Step Functionsのステートマシンを構築する方法を紹介します。

本記事の対象者

AWS Step FunctionsはステートマシンをGUIで編集できますが、ヒューマンエラー防止と効率化を考えると、コードで管理することが望ましいです。
本記事では、AWS SAMを使って、Step Functionsのステートマシンを構築します。
AWS Step Functionsを使っているけれど、もう少し深く知り、便利に使いたい方におすすめです。

AWS Step FunctionsとAWS SAMとは

AWS Step Functionsは、AWS上で 処理や条件分岐、エラー処理などのワークフローを作れるサービスです。
昔からあるジョブ管理システムのAWS版とも言えます。

AWS Step Functions 構築したワークフローのことを「ステートマシン」と呼びます。

docs.aws.amazon.com

AWS SAMは、AWSのリソースをテンプレートで管理するツールです。
サーバーレス系のサービスの構築がしやすくなっています。
AWS SAMを使えば、CLIでAWS Step Functionsのステートマシンを構築することができます。

docs.aws.amazon.com

AWS公式のサンプルを使って、AWS Step Functionsのステートマシンを構築する

AWS SAMのインストール

AWS SAMを使う場合は、AWS SAM CLIをインストールする必要があります。
下記サイトに従い、インストール作業を行います。

docs.aws.amazon.com

AWS公式のサンプルに従い、AWS SAMテンプレートを初期化する

AWSの公式サイトにサンプルがあります。
これに従うことで、AWS Lambdaを順次実行するステートマシンを構築することができます。
これを使って説明をしてみます。

docs.aws.amazon.com

サイトの内容に従い、sam initコマンドを実行、ウィザードに従って進めます。

$ sam init

You can preselect a particular runtime or package type when using the `sam init` experience.
Call `sam init --help` to learn more.

Which template source would you like to use?
        1 - AWS Quick Start Templates
        2 - Custom Template Location
Choice: 1

Choose an AWS Quick Start application template
        1 - Hello World Example
        2 - Data processing
        3 - Hello World Example with Powertools for AWS Lambda
        4 - Multi-step workflow
        5 - Scheduled task
        6 - Standalone function
        7 - Serverless API
        8 - Infrastructure event management
        9 - Lambda Response Streaming
        10 - Serverless Connector Hello World Example
        11 - Multi-step workflow with Connectors
        12 - GraphQLApi Hello World Example
        13 - Full Stack
        14 - Lambda EFS example
        15 - Hello World Example With Powertools for AWS Lambda
        16 - DynamoDB Example
        17 - Machine Learning
Template: 4

Which runtime would you like to use?
        1 - dotnet6
        2 - go1.x
        3 - go (provided.al2)
        4 - java21
        5 - java17
        6 - java11
        7 - java8.al2
        8 - java8
        9 - nodejs20.x
        10 - nodejs18.x
        11 - nodejs16.x
        12 - python3.9
        13 - python3.8
        14 - python3.12
        15 - python3.11
        16 - python3.10
        17 - ruby3.2
        18 - ruby2.7
Runtime: 16

Based on your selections, the only Package type available is Zip.
We will proceed to selecting the Package type as Zip.

Based on your selections, the only dependency manager available is pip.
We will proceed copying the template using pip.

Would you like to enable X-Ray tracing on the function(s) in your application?  [y/N]: 

Would you like to enable monitoring using CloudWatch Application Insights?
For more info, please view https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch-application-insights.html [y/N]: 

Would you like to set Structured Logging in JSON format on your Lambda functions?  [y/N]: 

Project name [sam-app]: 
                                                                                                                                                
Cloning from https://github.com/aws/aws-sam-cli-app-templates (process may take a moment)                                                       

    -----------------------
    Generating application:
    -----------------------
    Name: sam-app
    Runtime: python3.10
    Architectures: x86_64
    Dependency Manager: pip
    Application Template: step-functions-sample-app
    Output Directory: .
    Configuration file: sam-app/samconfig.toml
    
    Next steps can be found in the README file at sam-app/README.md
        

Commands you can use next
=========================
[*] Create pipeline: cd sam-app && sam pipeline init --bootstrap
[*] Validate SAM template: cd sam-app && sam validate
[*] Test Function in the Cloud: cd sam-app && sam sync --stack-name {stack-name} --watch

ポイントは以下の通りです。

  • Which template source would you like to use?の質問で、1 - AWS Quick Start Templatesを選択します。
  • Choose an AWS Quick Start application templateの質問で、4 - Multi-step workflowを選択します。
  • Which runtime would you like to use?の質問で、今回は16 - python3.10を選択しています。 ここは好みで選択で大丈夫です。
  • その他の質問は、デフォルトのままで大丈夫です。

コマンドが完了するとソース一式ができます。
できあがるソースの構成も、上記の公式サイトにあるのでご覧ください。

テンプレートの中身を確認

できたものを動かす前にちょっと中身を確認してみます。
Lambda関数やState Functionsのステートマシン本体の定義は、template.yamlに記述されています。
ステートマシンの中身は、state-machine-definition.jsonに記述されています。

template.yamlのステートマシンの定義がこちらです。

  StockTradingStateMachine:
    Type: AWS::Serverless::StateMachine # More info about State Machine Resource: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-statemachine.html
    Properties:
      DefinitionUri: statemachine/stock_trader.asl.json
      DefinitionSubstitutions:
        StockCheckerFunctionArn: !GetAtt StockCheckerFunction.Arn
        StockSellerFunctionArn: !GetAtt StockSellerFunction.Arn
        StockBuyerFunctionArn: !GetAtt StockBuyerFunction.Arn
        DDBPutItem: !Sub arn:${AWS::Partition}:states:::dynamodb:putItem
        DDBTable: !Ref TransactionTable
      Events:
        HourlyTradingSchedule:
          Type: Schedule # More info about Schedule Event Source: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-property-statemachine-schedule.html
          Properties:
            Description: Schedule to run the stock trading state machine every hour
            Enabled: False # This schedule is disabled by default to avoid incurring charges.
            Schedule: "rate(1 hour)"
      Policies: # Find out more about SAM policy templates: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-policy-templates.html
        - LambdaInvokePolicy:
            FunctionName: !Ref StockCheckerFunction
        - LambdaInvokePolicy:
            FunctionName: !Ref StockSellerFunction
        - LambdaInvokePolicy:
            FunctionName: !Ref StockBuyerFunction
        - DynamoDBWritePolicy:
            TableName: !Ref TransactionTable

DefinitionUriプロパティでステートマシンの定義を書いたJSONファイルのパスを指定しています。
これにより、JSONでステートマシンを定義することができます。

DefinitionUri: statemachine/stock_trader.asl.json

docs.aws.amazon.com

DefinitionUrlプロパティではなく、Definitionプロパティを使えば、template.yamlに直接ステートマシンの定義を書くこともできます。
個人的には、マネジメントコンソールで既存のステートマシンの定義をJSONエクスポートできるので、JSONで管理する方が楽だと思います。

LambdaのARNなどをステートマシンの定義ファイルへ値を渡す仕組み

DefinitionSubstitutions:
    StockCheckerFunctionArn: !GetAtt StockCheckerFunction.Arn
    StockSellerFunctionArn: !GetAtt StockSellerFunction.Arn
    StockBuyerFunctionArn: !GetAtt StockBuyerFunction.Arn
    DDBPutItem: !Sub arn:${AWS::Partition}:states:::dynamodb:putItem
    DDBTable: !Ref TransactionTable

template.yamlDefinitionSubstitutionsは、JSONファイルに値を渡すためのプロパティです。
statemachine/stock_trader.asl.json側で、以下のように使用できます。

"Resource": "${StockCheckerFunctionArn}",

ステートマシンでは呼び出すLambda関数やDynamoDBテーブルのARNを指定します。
Lambda関数やDynamoDBテーブルもSAMの中で作るので、このようにJSONに値を渡すような形に仕上げる必要があります。

ポリシーの指定の仕組み

      Policies: # Find out more about SAM policy templates: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-policy-templates.html
        - LambdaInvokePolicy:
            FunctionName: !Ref StockCheckerFunction
        - LambdaInvokePolicy:
            FunctionName: !Ref StockSellerFunction
        - LambdaInvokePolicy:
            FunctionName: !Ref StockBuyerFunction
        - DynamoDBWritePolicy:
            TableName: !Ref TransactionTable

template.yamlのこの部分でステートマシンに付与するポリシーを指定しています。
LambdaInvokePolicyDynamoDBWritePolicyの部分は、使える文字列が決まっていて、下記サイトで確認ができます。

docs.aws.amazon.com

FunctionNameTableNameの部分は、各ポリシーの詳細を見ることで知ることができます。
LambdaInvokePolicyなら以下のように書いてあります。

"Statement": [
  {
    "Effect": "Allow",
    "Action": [
      "lambda:InvokeFunction"
    ],
    "Resource": {
      "Fn::Sub": [
        "arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:${functionName}*",
        {
          "functionName": {
            "Ref": "FunctionName"
          }
        }
      ]
    }
  }
]
          "functionName": {
            "Ref": "FunctionName"
          }

とあるので、FunctionName: !Ref StockCheckerFunctionのように書きます。
Lambdaの関数名を渡すと、それを元に内部でARNを組み立ててくれます。
逆に、内部でARNを組み立てようとするので、ARNそのものを渡してしまうとARN+ARNのような無効な文字列になってしまうので注意です。

なお、この指定の仕方だとステートマシン名+ランダム文字列のロールができます。
Policies属性ではなくRole属性を使えば、別途作成したIAMロールを指定することもできますので、お好みでどうぞ。

ビルドとデプロイ

AWS SAMはビルド>デプロイの順でコマンド実行することで、AWSリソースを作成します。

aws sam build
aws sam deploy

これでStep Functionsにステートマシンが作成されます。
このサンプルはNameが固定値で指定されていないので、ステートマシンの名前にランダム文字列が付与されます。

作成したステートマシンを実行する

作成したステートマシンは、実行を開始するで実行できます。

実行すると、パラメータの入力画面が出ます。
パラメータを入力して続行するとステートマシンが実行されます。

ステートマシンの動きを知る

せっかくなのでできあがったステートマシンの動きを見ていきましょう。
作成されたステートマシンを見ると分岐があるのが確認できます。
これはどのように動いているのか、調べてみます。

Buy or Sell?の部分(ステートと呼びます)をクリックして、定義タブをクリックすると、この分岐の定義が見ることができます。
そこで、$.stock_priceが50以下なら次のステートBuy Stockに進むよう指定されているのがわかります。

定義入力を見比べると、$.stock_price$の部分は入力パラメータのことなのだとわかります。

1つ目のステートCheck Stock Valueの入力、そして2つ目のステートBuy or Sell?の入力と見ていくと、入力の内容がステートマシンが進むのに応じて変化していることがわかります。
最初は、実行時に入力されたパラメータがそのまま入力となっていて、1つ目のステートCheck Stock Valueが終わると、次のステートに入力される値が変わっているのがわかります。

ここで1つ目のステートCheck Stock Value詳細を見てみます。
Lambdaを実行するステートであることが確認できます。

このLambdaのソースコードは、サンプルの中にあります。
functions/stock_checker/app.pyです。

def lambda_handler(event, context):
    """Sample Lambda function which mocks the operation of checking the current price 
    of a stock.

    For demonstration purposes this Lambda function simply returns 
    a random integer between 0 and 100 as the stock price.

    Parameters
    ----------
    event: dict, required
        Input event to the Lambda function

    context: object, required
        Lambda Context runtime methods and attributes

    Returns
    ------
        dict: Object containing the current price of the stock
    """
    # Check current price of the stock
    stock_price = randint(
        0, 100
    )  # Current stock price is mocked as a random integer between 0 and 100
    return {"stock_price": stock_price}

ソースコードを見ると、2つ目のステートBuy or Sell?の入力は、1つ目のステートCheck Stock Valueで実行したLambdaの戻り値そのままであることがわかります。

    return {"stock_price": stock_price}

特に何もしていない場合、入力パラメータの値(=$)は、良くも悪くももステートの結果で書き変わることがわかります。
Lambdaを実行するステートなら、Lambdaの戻り値がそのまま次のステートの入力パラメータとなります。

入力パラメータが書きかわらないようにするには

ステートの結果で入力パラメータが書きかわらないようにするには、ResultPathプロパティを使い、結果を別の場所に書き込むようにします。
ResultPathプロパティの使い方は、下記の記事で触れているのでご覧ください。

blog.serverworks.co.jp

まとめ

AWS SAMを使って、AWS Step Functionsのステートマシンを構築する方法を紹介しました。
実は、AWS Step Functionsのステートマシンは、CloudFormationでも作ることができますが、AWS SAMを使うと、ステートマシンの定義をJSONファイルで書けるので、より簡単に作成できます。
次回はAWS Step FunctionsでECSのタスクを実行する方法を紹介します。

blog.serverworks.co.jp

兼安 聡(執筆記事の一覧)

アプリケーションサービス部 DS1課所属
AWS12冠。
広島在住です。
最近認定スクラムマスターになりました。今日も明日も修行中です。