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

[AWS Step Functions](https://aws.amazon.com/jp/blogs/news/new-aws-step-functions-build-distributed-applications-using-visual-workflows/)楽しいですね！！
今までは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

```yaml
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
```

https://github.com/k-kinzal/EBA7CE416DD416B0E91442E9D5C72EF70B0111E365DF861D9A4B89BA7DAD1E3C/blob/a48c5858488c35f5cacc96ebc2666915a754eb66/resources.yml#L13-L127

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

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

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

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

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

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

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

```js
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を定義する

```yaml
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
```

https://github.com/k-kinzal/EBA7CE416DD416B0E91442E9D5C72EF70B0111E365DF861D9A4B89BA7DAD1E3C/blob/a48c5858488c35f5cacc96ebc2666915a754eb66/template.yml

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

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

# おわりに

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

* [SAM(Serverless Application Model) + Node + Flowtype でAWS Lambdaの開発サイクルを作る](http://qiita.com/kinzal/items/7a09659399d9be40d674)

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

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