State Machine Definition in CloudFormation
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!