AWS

【新機能】AWS Step Functions の Infrastructure as Code テンプレート生成を検証してみた

akira

はじめに

来月に迫ったAWS re:Inventにワクワクが止まらないakiraです。
生成AIが世界中に大きな変化をもたらした1年であっただけに、どんな新サービスや新機能が発表されるのか楽しみですね!
今年は現地で参加させていただく予定ですので、re:Inventに関するブログもお楽しみにお待ち下さい!

先日のアップデートでStep FunctionsのIaCテンプレート作成機能が追加されました。

Announcing Infrastructure as Code template generation for AWS Step Functions
Announcing Infrastructure as Code template generation for AWS Step Functions

IaCテンプレートの出力機能はとても嬉しいと感じた一方で、同様の機能を提供しているCloudFormationのIaCジェネレーターとの使い分けが気になったので検証してみました。

アップデート概要

今回のアップデートはAWSコンソール上から直接Step Functionsの定義内容をCloudFormation、もしくはAWS Serverless Application Model (SAM) テンプレートにてエクスポートできるようになったというものになっています。

各Step Functionsの関数のアクションタブから出力が実行可能となっています。
今回はCloudFormationの出力機能に絞って検証を進めてみます。

検証実施

本ブログ内では今回の新機能による出力がどのような場面で使えそうか、他の類似機能との使い分けなどについて考えてみます。

Step Functionsの準備

今回は検証のためStep FunctionsのテンプレートからLambda関数のオーケストレーションを作成してみます。こちらを実行すると裏でCloudFormationが実行されてリソースが作成されるので、出力内容と比べてどうなるかも見てみたいと思います。

出力結果

最初に出力結果を記載します(アカウントIDのみ置換済み)。
出力の大まかな方法は以下の手順を参照ください。

Q
Step Functionsから出力したCloudFormationテンプレート
Resources:
  StateMachinea87505c1:
    Type: AWS::StepFunctions::StateMachine
    Properties:
      Definition:
        StartAt: Check Stock Price
        States:
          Check Stock Price:
            Type: Task
            Resource: ${InvokeFunction_Resource_deb110f2}
            Next: Generate Buy/Sell recommendation
          Generate Buy/Sell recommendation:
            Type: Task
            Resource: ${InvokeFunction_Resource_8f8ba574}
            ResultPath: $.recommended_type
            Next: Request Human Approval
          Request Human Approval:
            Type: Task
            Resource: arn:aws:states:::sqs:sendMessage.waitForTaskToken
            Parameters:
              QueueUrl: ${sqssendMessage_QueueUrl_3f671719}
              MessageBody:
                Input.$: $
                TaskToken.$: $$.Task.Token
            ResultPath: null
            Next: Buy or Sell?
          Buy or Sell?:
            Type: Choice
            Choices:
              - Variable: $.recommended_type
                StringEquals: buy
                Next: Buy Stock
              - Variable: $.recommended_type
                StringEquals: sell
                Next: Sell Stock
          Buy Stock:
            Type: Task
            Resource: ${InvokeFunction_Resource_8b002b22}
            Next: Report Result
          Sell Stock:
            Type: Task
            Resource: ${InvokeFunction_Resource_e9d46185}
            Next: Report Result
          Report Result:
            Type: Task
            Resource: arn:aws:states:::sns:publish
            Parameters:
              TopicArn: ${snspublish_TopicArn_6886afd6}
              Message:
                Input.$: $
            End: true
      DefinitionSubstitutions:
        InvokeFunction_Resource_deb110f2: >-
          arn:aws:lambda:ap-northeast-1:{ACCOUNTID}:function:StepFunctionsSample-HelloLam-CheckStockPriceLambda-ZqxGovxelnoG
        InvokeFunction_Resource_8f8ba574: >-
          arn:aws:lambda:ap-northeast-1:{ACCOUNTID}:function:StepFunctionsSample-Hello-GenerateBuySellRecommend-KY4bhrlieYCB
        sqssendMessage_QueueUrl_3f671719: >-
          https://sqs.ap-northeast-1.amazonaws.com/{ACCOUNTID}/StepFunctionsSample-HelloLambda-1a8e34b9-b0-RequestHumanApprovalSqs-oPuTSYqEm0Iz
        InvokeFunction_Resource_8b002b22: >-
          arn:aws:lambda:ap-northeast-1:{ACCOUNTID}:function:StepFunctionsSample-HelloLambda-1a8-BuyStockLambda-WPCd8sq3XS3R
        InvokeFunction_Resource_e9d46185: >-
          arn:aws:lambda:ap-northeast-1:{ACCOUNTID}:function:StepFunctionsSample-HelloLambda-1a-SellStockLambda-qGIROmAf52bC
        snspublish_TopicArn_6886afd6: >-
          arn:aws:sns:ap-northeast-1:{ACCOUNTID}:StepFunctionsSample-HelloLambda-1a8e34b9-b03b-469b-8b97-80a93990b203-ReportResultSnsTopic-oHfFf0dtTRWf
      RoleArn: >-
        arn:aws:iam::{ACCOUNTID}:role/StepFunctionsSample-Hello-StockTradingStateMachineR-mo09Vbj3ewrv
      StateMachineName: StateMachinea87505c1
      StateMachineType: STANDARD
      EncryptionConfiguration:
        Type: AWS_OWNED_KEY
      TracingConfiguration:
        Enabled: true
      LoggingConfiguration:
        Level: "OFF"
        IncludeExecutionData: false

Q
テンプレートに実行されたCloudFormationテンプレート

※Lambda等のリソースを含むのでかなり長いテンプレートになっています

---
AWSTemplateFormatVersion: 2010-09-09
Description: AWS Step Functions sample project introducing lambda function integration. 
Resources:
  BuyStockLambda:
    Type: 'AWS::Lambda::Function'
    Properties:
      Handler: index.lambdaHandler
      Code:
        ZipFile: |
          const crypto = require("crypto");

          function getRandomInt(max) {
              return Math.floor(Math.random() * Math.floor(max)) + 1;
          }

          /**
          * Sample Lambda function which mocks the operation of buying a random number of shares for a stock.
          * For demonstration purposes, this Lambda function does not actually perform any  actual transactions. It simply returns a mocked result.
          * 
          * @param {Object} event - Input event to the Lambda function
          * @param {Object} context - Lambda Context runtime methods and attributes
          *
          * @returns {Object} object - Object containing details of the stock buying transaction
          * 
          */
          exports.lambdaHandler = async (event, context) => {
              // Get the price of the stock provided as input
              const stock_price = event["stock_price"]
              var date = new Date();
              // Mocked result of a stock buying transaction
              let transaction_result = {
                  'id': crypto.randomBytes(16).toString("hex"), // Unique ID for the transaction
                  'price': stock_price.toString(), // Price of each share
                  'type': "buy", // Type of transaction(buy/ sell)
                  'qty': getRandomInt(10).toString(),  // Number of shares bought / sold(We are mocking this as a random integer between 1 and 10)
                  'timestamp': date.toISOString(),  // Timestamp of the when the transaction was completed
              }
              return transaction_result
          };
      Role: !GetAtt 
        - LambdaFunctionRole
        - Arn
      Runtime: nodejs18.x
  GenerateBuySellRecommendationLambda:
    Type: 'AWS::Lambda::Function'
    Properties:
      Handler: index.lambdaHandler
      Code:
        ZipFile: |
          /**
          * Sample Lambda function which mocks the operation of recommending buying or selling of stocks.
          * For demonstration purposes this Lambda function simply returns a "buy" or "sell" string depending on stock price.
          * 
          * @param {Object} event - Input event to the Lambda function
          * @param {Object} context - Lambda Context runtime methods and attributes
          *
          * @returns {String} - Either "buy" or "sell" string of recommendation.
          * 
          */
          exports.lambdaHandler = async (event, context) => {
              const { stock_price } = event;
              // If the stock price is greater than 50 recommend selling. Otherwise, recommend buying.
              return stock_price > 50 ? 'sell' : 'buy';
          };
      Role: !GetAtt 
        - LambdaFunctionRole
        - Arn
      Runtime: nodejs18.x
  CheckStockPriceLambda:
    Type: 'AWS::Lambda::Function'
    Properties:
      Handler: index.lambdaHandler
      Code:
        ZipFile: |
          function getRandomInt(max) {
              return Math.floor(Math.random() * Math.floor(max));
          }

          /**
          * 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.
          * 
          * @param {Object} event - Input event to the Lambda function
          * @param {Object} context - Lambda Context runtime methods and attributes
          *
          * @returns {Object} object - Object containing the current price of the stock
          * 
          */
          exports.lambdaHandler = async (event, context) => {
              // Check current price of the stock
              const stock_price = getRandomInt(100)  // Current stock price is mocked as a random integer between 0 and 100
              return { 'stock_price': stock_price }
          };
      Role: !GetAtt 
        - LambdaFunctionRole
        - Arn
      Runtime: nodejs18.x
  LambdaFunctionEventSourceMapping:
    Type: 'AWS::Lambda::EventSourceMapping'
    Properties:
      BatchSize: 10
      Enabled: true
      EventSourceArn: !GetAtt 
        - RequestHumanApprovalSqs
        - Arn
      FunctionName: !GetAtt 
        - ApproveSqsLambda
        - Arn
  SellStockLambda:
    Type: 'AWS::Lambda::Function'
    Properties:
      Handler: index.lambdaHandler
      Code:
        ZipFile: |
          const crypto = require("crypto");

          function getRandomInt(max) {
              return Math.floor(Math.random() * Math.floor(max)) + 1;
          }

          /**
          * Sample Lambda function which mocks the operation of selling a random number of shares for a stock.
          * For demonstration purposes, this Lambda function does not actually perform any  actual transactions. It simply returns a mocked result.
          * 
          * @param {Object} event - Input event to the Lambda function
          * @param {Object} context - Lambda Context runtime methods and attributes
          *
          * @returns {Object} object - Object containing details of the stock selling transaction
          * 
          */
          exports.lambdaHandler = async (event, context) => {
              // Get the price of the stock provided as input
              const stock_price = event["stock_price"]
              var date = new Date();
              // Mocked result of a stock selling transaction
              let transaction_result = {
                  'id': crypto.randomBytes(16).toString("hex"), // Unique ID for the transaction
                  'price': stock_price.toString(), // Price of each share
                  'type': "sell", // Type of transaction(buy/ sell)
                  'qty': getRandomInt(10).toString(),  // Number of shares bought / sold(We are mocking this as a random integer between 1 and 10)
                  'timestamp': date.toISOString(),  // Timestamp of the when the transaction was completed
              }
              return transaction_result
          };
      Role: !GetAtt 
        - LambdaFunctionRole
        - Arn
      Runtime: nodejs18.x
  ApproveSqsLambda:
    Type: 'AWS::Lambda::Function'
    Properties:
      Handler: index.lambdaHandler
      Code:
        ZipFile: |
          const { SFN } = require("@aws-sdk/client-sfn");

          /**
          * Sample Lambda function that will automatically approve any task submitted to sqs by state machine.
          * For demonstration purposes this Lambda function simply returns a random integer between 0 and 100 as the stock price.
          * 
          * @param {Object} event - Input event to the Lambda function
          * @param {Object} context - Lambda Context runtime methods and attributes
          * 
          */
          exports.lambdaHandler = (event, context, callback) => {
              const stepfunctions = new SFN();

              // For every record in sqs queue
              for (const record of event.Records) {
                  const messageBody = JSON.parse(record.body);
                  const taskToken = messageBody.TaskToken;

                  const params = {
                      output: "\"approved\"",
                      taskToken: taskToken
                  };

                  console.log(`Calling Step Functions to complete callback task with params ${JSON.stringify(params)}`);

                  // Approve
                  stepfunctions.sendTaskSuccess(params, (err, data) => {
                      if (err) {
                          console.error(err.message);
                          callback(err.message);
                          return;
                      }
                      console.log(data);
                      callback(null);
                  });
              }
          };
      Role: !GetAtt 
        - ManualApprovalFunctionRole
        - Arn
      Runtime: nodejs18.x
  ReportResultSnsTopic:
    Type: 'AWS::SNS::Topic'
  LambdaFunctionRole:
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Action:
              - 'sts:AssumeRole'
            Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
      ManagedPolicyArns:
        - 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole'
  StockTradingStateMachineRole:
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Action:
              - 'sts:AssumeRole'
            Effect: Allow
            Principal:
              Service:
                - states.amazonaws.com
      ManagedPolicyArns: []
      Policies:
        - PolicyName: StockTradingStateMachineRolePolicy0
          PolicyDocument:
            Statement:
              - Action:
                  - 'lambda:InvokeFunction'
                Resource: !GetAtt 
                  - CheckStockPriceLambda
                  - Arn
                Effect: Allow
        - PolicyName: StockTradingStateMachineRolePolicy1
          PolicyDocument:
            Statement:
              - Action:
                  - 'lambda:InvokeFunction'
                Resource: !GetAtt 
                  - GenerateBuySellRecommendationLambda
                  - Arn
                Effect: Allow
        - PolicyName: StockTradingStateMachineRolePolicy2
          PolicyDocument:
            Statement:
              - Action:
                  - 'lambda:InvokeFunction'
                Resource: !GetAtt 
                  - BuyStockLambda
                  - Arn
                Effect: Allow
        - PolicyName: StockTradingStateMachineRolePolicy3
          PolicyDocument:
            Statement:
              - Action:
                  - 'lambda:InvokeFunction'
                Resource: !GetAtt 
                  - SellStockLambda
                  - Arn
                Effect: Allow
        - PolicyName: StockTradingStateMachineRolePolicy4
          PolicyDocument:
            Statement:
              - Action:
                  - 'sqs:SendMessage*'
                Resource: !GetAtt 
                  - RequestHumanApprovalSqs
                  - Arn
                Effect: Allow
        - PolicyName: StockTradingStateMachineRolePolicy5
          PolicyDocument:
            Statement:
              - Action:
                  - 'sns:Publish'
                Resource: !Ref ReportResultSnsTopic
                Effect: Allow
        - PolicyName: StockTradingStateMachineRolePolicy6
          PolicyDocument:
            Statement:
              - Action:
                  - "xray:PutTraceSegments"
                  - "xray:PutTelemetryRecords"
                  - "xray:GetSamplingRules"
                  - "xray:GetSamplingTargets"
                Effect: Allow
                Resource: "*"
  RequestHumanApprovalSqs:
    Type: 'AWS::SQS::Queue'
    Properties:
      SqsManagedSseEnabled: true
  ManualApprovalFunctionRole:
    Type: 'AWS::IAM::Role'
    Properties:
      Policies:
        - PolicyName: SQSReceiveMessagePolicy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Action:
                  - 'sqs:ReceiveMessage'
                  - 'sqs:DeleteMessage'
                  - 'sqs:GetQueueAttributes'
                  - 'sqs:ChangeMessageVisibility'
                Resource: !GetAtt 
                  - RequestHumanApprovalSqs
                  - Arn
                Effect: Allow
        - PolicyName: CloudWatchLogsPolicy
          PolicyDocument:
            Statement:
              - Action:
                  - 'logs:CreateLogGroup'
                  - 'logs:CreateLogStream'
                  - 'logs:PutLogEvents'
                Resource: !Sub 'arn:${AWS::Partition}:logs:*:*:*'
                Effect: Allow
        - PolicyName: StatesExecutionPolicy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Action:
                  - 'states:SendTaskSuccess'
                  - 'states:SendTaskFailure'
                Resource: !Ref StockTradingStateMachine
                Effect: Allow
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Action: 'sts:AssumeRole'
            Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
  StockTradingStateMachine:
    Type: 'AWS::StepFunctions::StateMachine'
    Properties:
      DefinitionSubstitutions:
        GenerateBuySellRecommendationLambdaArn: !GetAtt 
          - GenerateBuySellRecommendationLambda
          - Arn
        RequestHumanApprovalSqsUrl: !Ref RequestHumanApprovalSqs
        ReportResultSnsTopicArn: !Ref ReportResultSnsTopic
        SellStockLambdaArn: !GetAtt 
          - SellStockLambda
          - Arn
        CheckStockPriceLambdaArn: !GetAtt 
          - CheckStockPriceLambda
          - Arn
        BuyStockLambdaArn: !GetAtt 
          - BuyStockLambda
          - Arn
      RoleArn: !GetAtt 
        - StockTradingStateMachineRole
        - Arn
      TracingConfiguration:
        Enabled: true
      DefinitionString: |
        {
            "StartAt": "Check Stock Price",
            "States": {
                "Check Stock Price": {
                    "Type": "Task",
                    "Resource": "${CheckStockPriceLambdaArn}",
                    "Next": "Generate Buy/Sell recommendation"
                },
                "Generate Buy/Sell recommendation": {
                    "Type": "Task",
                    "Resource": "${GenerateBuySellRecommendationLambdaArn}",
                    "ResultPath": "$.recommended_type",
                    "Next": "Request Human Approval"
                },
                "Request Human Approval": {
                    "Type": "Task",
                    "Resource": "arn:aws:states:::sqs:sendMessage.waitForTaskToken",
                    "Parameters": {
                        "QueueUrl": "${RequestHumanApprovalSqsUrl}",
                        "MessageBody": {
                            "Input.$": "$",
                            "TaskToken.$": "$$.Task.Token"
                        }
                    },
                    "ResultPath": null,
                    "Next": "Buy or Sell?"
                },
                "Buy or Sell?": {
                    "Type": "Choice",
                    "Choices": [
                        {
                            "Variable": "$.recommended_type",
                            "StringEquals": "buy",
                            "Next": "Buy Stock"
                        },
                        {
                            "Variable": "$.recommended_type",
                            "StringEquals": "sell",
                            "Next": "Sell Stock"
                        }
                    ]
                },
                "Buy Stock": {
                    "Type": "Task",
                    "Resource": "${BuyStockLambdaArn}",
                    "Next": "Report Result"
                },
                "Sell Stock": {
                    "Type": "Task",
                    "Resource": "${SellStockLambdaArn}",
                    "Next": "Report Result"
                },
                "Report Result": {
                    "Type": "Task",
                    "Resource": "arn:aws:states:::sns:publish",
                    "Parameters": {
                        "TopicArn": "${ReportResultSnsTopicArn}",
                        "Message": {
                            "Input.$": "$"
                        }
                    },
                    "End": true
                }
            }
        }
Outputs:
  StateMachineArn:
    Value:
      Ref: StockTradingStateMachine
  ExecutionInput:
    Description: Sample input to StartExecution.
    Value:
      >
        {}

検証結果

細かい記載方法などを上げていくときりが無いのですが、機能としての思想が現れていると思う箇所を2つあげさせていただきます。

出力対象リソースの違い

コードを参照いただいたとおりではあるのですが、Step Functionsからの出力の場合他のリソースは一切記載されていません。Step Functionsから呼び出されるリソースはすべてARNがDefinitionSubstitutions句で記載されておりテンプレートには含まれません。

今回のStep FunctionsテンプレートでLambda等が含まれるのは当然ですが、IaCジェネレーターで出力した際は複数リソースをまとめて出力ができるので、今回の機能とは少し利用場面が違いそうです。

Generate templates from existing resources with IaC generator
Generate templates from existing resources with IaC generator

Step Functionsの定義の記載方法

Step Functionsからの出力の場合、定義はYAML形式での出力になっています。一方、Step Functionsテンプレートでは定義がJSON形式で記載されStringで定義されていました。

記載方法に優劣はないものの、前者のほうがCloudFormationを記載する際にフォーマッター等でエラーを検知しやすくなるため良い場面もあるかともいます。プロジェクトに寄ってはルールで定められている場合もあるかもしれません。手動作成のStep Functions定義を出力することでYAML形式に変換して時間削減できる場面もありそうです。

おまけ

肝心の出力したテンプレートですが、定義内の "ResultPath": null, が原因でそのままではデプロイ出来ませんでした…

このあたリの調整はこれからの機能改善に期待する箇所になりそうです!

Step Functionsからの出力方法

作成したStep Functionsから以下のような設定で出力しました。出力するリソースの範囲を指定する箇所などはなかったのでこちら以外は特に設定していません。

まとめ

今回のStep Functionsのアップデートでの出力ではあくまでStep Functionsのみに絞った出力にはとても効果的に感じました。他のリソースの参照部分がDefinitionSubstitutionsを用いて記載されているので、他の環境で既存のリソースを活かしつつStep Functionsのみを再現したい場合に大活躍するのでは無いでしょうか!

一方で、Lambda等の関連リソースはエクスポートされないため、別で作成する or 管理する必要があります。一般的なIaC管理の場合は、IaCジェネレーター等を用いて同一のテンプレートで管理するほうが自然かなと感じました。

本ブログがどなたかのお役に立てれば幸いです。

AUTHOR
akira
akira
記事URLをコピーしました