State Machine Definition in CloudFormation

Photo by chuttersnap on Unsplash

There are always options…

Step Functions can be deployed in a few ways. Using serverless (the framework) is one, using CloudFormation is another. I’ve done both, but here I am discussing one’s options when developing using CloudFormation only.

CloudFormation can be defined as YAML or JSON. Again I’ve used both and I find that even though YAML makes the file somewhat simpler for short templates of little complexity, it can quickly become pretty much unreadable for complex nested CloudFormation templates which we all dislike anyway. But in that case context, which is what JSON natively gives you, helps a lot. I don’t know about you but I can spot matching curly braces quicker than whitespace and I am yet to find a good linter for YAML. Even Amazon fails to validate indentation in many cases and one CloudFormation resource intended incorrectly will simply not deploy, leaving you wondering why that is…

However, when using native CloudFormation to define Step Functions YAML can improve readability. Especially when trying to pass parameters in the State Machine Definition.

Comparison

So let’s compare what this looks like…

In JSON:

{
    "Resources": {
       "MyStateMachine": {
          "Type": "AWS::StepFunctions::StateMachine",
             "Properties": {
                "StateMachineName" : "HelloWorld-StateMachine",
                "DefinitionString" : {
                   "Fn::Join": [
                      "\n",
                      [
                         "{",
                         "    \"StartAt\": \"HelloWorld\",",
                         "    \"States\" : {",
                         "        \"HelloWorld\" : {",
                         "            \"Type\" : \"Task\", ",
                         "            \"Resource\" : \"arn:aws:lambda:us-east-1:111122223333:function:HelloFunction\",",
                         "            \"End\" : true",
                         "        }",
                         "    }",
                         "}"
                      ]
                   ]
                },
   	      "RoleArn" : "arn:aws:iam::111122223333:role/service-role/StatesExecutionRole-us-east-1",
            "Tags": [
                    {
                        "Key": "keyname1",
                        "Value": "value1"
                    },
                    {
                        "Key": "keyname2",
                        "Value": "value2"
                    }
                ]
            }
        }
    }
}

or even worse…

{  
   "Resources":{  
      "MyStateMachine":{  
         "Type":"AWS::StepFunctions::StateMachine",
         "Properties":{  
            "StateMachineName":"HelloWorld-StateMachine",
            "DefinitionString":"{\"StartAt\": \"HelloWorld\",
            \"States\": {\"HelloWorld\": {\"Type\": \"Task\", \"Resource\":
            \"arn:aws:lambda:us-east-1:111122223333;:function:HelloFunction\", \"End\": true}}}",
            "RoleArn":"arn:aws:iam::111122223333:role/service-role/StatesExecutionRole-us-east-1;"
         }
      }
   }
}

And this becomes worse still if you want to parametrise the Lambda name and thus use a Ref or if you want to use an intrinsic function like ${AWS::AccountId} or ${AWS::Region}.

With YAML in this instance we can declare the DefinitionString attribute nicely as JSON with a Fn::Sub on top so that we can parametrise the entire thing as we like:

...
MyStateMachine:
    Type: AWS::StepFunctions::StateMachine
    Properties:
        StateMachineName: DataProcessingStateMachine
        DefinitionString:
            Fn::Sub: |
                {
                    "StartAt": "GetData",
                    "States": {
                        "GetData": {
                            "Type": "Task",
                            "Resource": "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${StepFunctionGetDataLambdaName}",
                            "ResultPath": "$.task_response",
                            "Next": "ProcessData",
                            "Retry": [
                                {
                                    "ErrorEquals": [ "States.ALL" ],
                                    "IntervalSeconds": 5,
                                    "BackoffRate": 2,
                                    "MaxAttempts": 3
                                }
                            ],
                            "Catch": [
                                {
                                    "ErrorEquals": [ "States.ALL" ],
                                    "Next": "SendToErrorSQSQueue"
                                }
                            ]
                        },
...

That’s it! I hope this has been helpful!

Avatar
Vasileios Vlachos
Cloud Engineer

I am a value driven engineer, helping my clients maximise their ROI from their cloud deployments.

Next
Previous