【新機能】AWS Step Functions の Infrastructure as Code テンプレート生成を検証してみた
はじめに
来月に迫ったAWS re:Inventにワクワクが止まらないakiraです。
生成AIが世界中に大きな変化をもたらした1年であっただけに、どんな新サービスや新機能が発表されるのか楽しみですね!
今年は現地で参加させていただく予定ですので、re:Inventに関するブログもお楽しみにお待ち下さい!
先日のアップデートでStep FunctionsのIaCテンプレート作成機能が追加されました。
IaCテンプレートの出力機能はとても嬉しいと感じた一方で、同様の機能を提供しているCloudFormationのIaCジェネレーターとの使い分けが気になったので検証してみました。
アップデート概要
今回のアップデートはAWSコンソール上から直接Step Functionsの定義内容をCloudFormation、もしくはAWS Serverless Application Model (SAM) テンプレートにてエクスポートできるようになったというものになっています。
各Step Functionsの関数のアクションタブから出力が実行可能となっています。
今回はCloudFormationの出力機能に絞って検証を進めてみます。
検証実施
本ブログ内では今回の新機能による出力がどのような場面で使えそうか、他の類似機能との使い分けなどについて考えてみます。
Step Functionsの準備
今回は検証のためStep FunctionsのテンプレートからLambda関数のオーケストレーションを作成してみます。こちらを実行すると裏でCloudFormationが実行されてリソースが作成されるので、出力内容と比べてどうなるかも見てみたいと思います。
出力結果
最初に出力結果を記載します(アカウントIDのみ置換済み)。
出力の大まかな方法は以下の手順を参照ください。
- 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
- テンプレートに実行された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ジェネレーターで出力した際は複数リソースをまとめて出力ができるので、今回の機能とは少し利用場面が違いそうです。
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ジェネレーター等を用いて同一のテンプレートで管理するほうが自然かなと感じました。
本ブログがどなたかのお役に立てれば幸いです。