LoginSignup
8
3

More than 5 years have passed since last update.

SAM(Serverless Application Model)でStep Functionsを定義する

Posted at

AWS Step Functions楽しいですね!!
今まではAWSの各種サービスを使ってLambda間を繋いだり、失敗を適切に処理してリトライをするようにしていましたが、Step Functionsがきたことによってその辺りの手間が一気に楽になりました!!!

もうこれはバリバリ使うしかないですよ!!
と、言いたいところですが、2016年12月現在ではCloudFormationに対応していないため、CloudFormationのラッパーのSAMでも利用することができません。
せっかく、SAMで複数のLambdaやAPI Gatewayを定義できるようになったのに、そこにStep Functionsを組み込むことができないのは開発フローに組み込むことができないためとても面倒くさいです。

そこで、ないなら作ればいいじゃない!ということでCloudFormationのLambda-backed Custom Resourcesを使って、SAMを使った開発フローに組み込んでしまいます。

Lambda-backed Custom Resources

AWSTemplateFormatVersion: "2010-09-09"
Resources:
  StepFunctionsCustomResource:
    Type: "AWS::Lambda::Function"
    Properties:
      Handler: index.handler
      Role: !GetAtt StepFunctionsCustomResourceRole.Arn
      Code:
        ZipFile: |
          "use strict"
          require('child_process').execSync('export HOME=/tmp && npm install aws-sdk@2.7.13', {cwd: '/tmp'});
          let AWS = require('/tmp/node_modules/aws-sdk/');
          let sf = new AWS.StepFunctions();
          let response = require('cfn-response');

          exports.handler = (event, context, callback) => {
            Promise.resolve().then(() => {
              switch (event.RequestType) {
                case "Create":
                  return new Promise((resolve, reject) => {
                    let params = {
                      name: event.ResourceProperties.Name,
                      definition: JSON.stringify(event.ResourceProperties.Definition, (k, v) => v === 'true' ? true : v === 'false' ? false : v),
                      roleArn: event.ResourceProperties.RoleArn
                    };
                    sf.createStateMachine(params, (err, data) => err ? reject(err) : resolve(data));
                  });
                case "Update":
                  return new Promise((resolve, reject) => {
                    let params = {
                      stateMachineArn: `arn:aws:states:${process.env.AWS_REGION}:${context.invokedFunctionArn.split(":")[4]}:stateMachine:${event.ResourceProperties.Name}`
                    };
                    sf.deleteStateMachine(params, (err, data) => err ? reject(err) : resolve(data));
                  }).then(() => {
                    return new Promise((resolve, reject) => {
                      let params = {
                        name: event.ResourceProperties.Name,
                        definition: JSON.stringify(event.ResourceProperties.Definition, (k, v) => v === 'true' ? true : v === 'false' ? false : v),
                        roleArn: event.ResourceProperties.RoleArn
                      };
                      sf.createStateMachine(params, (err, data) => err ? reject(err) : resolve(data));
                    });
                  });
                case "Delete":
                  return new Promise((resolve, reject) => {
                    let params = {
                      stateMachineArn: `arn:aws:states:${process.env.AWS_REGION}:${context.invokedFunctionArn.split(":")[4]}:stateMachine:${event.ResourceProperties.Name}`
                    };
                    sf.deleteStateMachine(params, (err, data) => err ? reject(err) : resolve(data));
                  });
                default:
                  throw new Error(`Unkown RequestType: '${event.RequestType}'`);
              }
            }).then((data) => {
              let responseData = {
                stateMachineArn: data.stateMachineArn
              };
              response.send(event, context, response.SUCCESS, responseData);
            }).catch((err) => {
              let responseData = {
                Error: err.toString()
              };
              response.send(event, context, response.FAILED, responseData);
            });
          };
      Runtime: nodejs4.3
      Timeout: 30
  StepFunctionsCustomResourceRole:
    Type: "AWS::IAM::Role"
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          -
            Effect: "Allow"
            Principal:
              Service:
                - "lambda.amazonaws.com"
            Action:
              - "sts:AssumeRole"
      Path: "/"
      Policies:
        -
          PolicyName: StepFunctionsCustomResourceRolePolicy
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              -
                Effect: "Allow"
                Action:
                  - "logs:CreateLogGroup"
                  - "logs:CreateLogStream"
                  - "logs:PutLogEvents"
                Resource: "arn:aws:logs:*:*:*"
              -
                Effect: "Allow"
                Action:
                  - "states:*"
                Resource: "*"
              -
                Effect: "Allow"
                Action:
                  - "iam:PassRole"
                Resource: "*"
  StepFunctionsCustomResourceInstanceProfile:
    Type: "AWS::IAM::InstanceProfile"
    Properties:
      Path: "/"
      Roles:
        -
          Ref: "StepFunctionsCustomResourceRole"
Outputs:
  StepFunctionsCustomResourceArn:
    Value: !GetAtt StepFunctionsCustomResource.Arn

実際にプロジェクトに組み込んでる例はこちらです。

基本的にはLambda-backed Custom ResourcesでAWS SDKを使いStep Functionsを作成、破棄するだけになります。
しかし、ちょっと面倒臭い話で2016年12月現在にランタイムでNode 4.3を選択すると、デフォルトでインストールされているAWS SDKはv2.6.9になります。
これを最新のSDKに入れ替えないとStep Functionsが使えないため

require('child_process').execSync('export HOME=/tmp && npm install aws-sdk@2.7.13', {cwd: '/tmp'});

/tmpディレクトリ以下にSDKをインストールし

let AWS = require('/tmp/node_modules/aws-sdk/');

でインストールしたSDKを読み込む必要があります。
このあたり、もう少し待てば改善されると思いますので最新のSDKがインストールされしだいこの処理は削除します。

また、これはどこの問題かわかっていないのですが、Lambda-backed Custom Resourcesに渡されるプロパティの値はすべて文字列になります。
Step Functionsに渡すDefinitionではEndなどの値がboolean値になるため、このままでは作成時にエラーになってしまいます。
そこで、パラメーターの作成時に"true"trueに、"false"falseに変換してあげる必要があります。

let params = {
  name: event.ResourceProperties.Name,
  definition: JSON.stringify(event.ResourceProperties.Definition, (k, v) => v === 'true' ? true : v === 'false' ? false : v),
  roleArn: event.ResourceProperties.RoleArn
};

数値でも同様のことがありそうですが、まだ自分が必要になっていないのでそこまで作っていません。

SAMでStep Functionsを定義する

AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'
Description: A starter AWS Lambda function.
Parameters:
  StepFunctionsCustomResourceArn:
    Type: String
    Description: Role Arn of StepFunctions Custom Resource
  LambdaExectionRoleArn:
    Type: String
    Description: Role Arn of Lambda execution
Resources:
  MyFunction:
    Type: 'AWS::Serverless::Function'
    Properties:
      Handler: src/index.handler
      Runtime: nodejs4.3
      CodeUri: .
      Description: A starter AWS Lambda function.
      MemorySize: 1536
      Timeout: 10
      Role:
        Ref: LambdaExectionRoleArn
  StepFunctions:
    Type: 'Custom::StepFunctions'
    Properties:
      ServiceToken:
        Ref: StepFunctionsCustomResourceArn
      Name: test-step
      RoleArn: arn:aws:iam::125043710017:role/service-role/StatesExecutionRole-us-east-1
      Definition:
        Comment: "A Hello World example of the Amazon States Language using an AWS Lambda Function"
        StartAt: HelloWorld
        States:
          HelloWorld:
            Type: Task
            Resource: !GetAtt MyFunction.Arn
            End: true

実際にプロジェクトに組み込んでる例はこちらです。

先ほど作ったLambda-backed Custom ResourcesのARNを受け取り、実際にSAMの定義ファイルにType: 'Custom::StepFunctions'として組み込みます。
簡単ですね!!!

おわりに

早くSAMで正式サポートしてくれればこのあたりの面倒の処理を全て削除できるので早くサポートしてほしいですね!

今回、リンクで貼ったプロジェクトの各種解説に関してはこちらの記事を参照してください。

というわけでStep Functionsで遊ぶ作業に戻ります!!!

8
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
3