Introduction
When you follow along in this series [1], you might have been irritated by the amount of work to test your functions. It isn’t a problem to test only the unit test for the accept function, but when you have to test the unit test for the decrypt function, the unit test for the update_db function and the performance test as well (and after that, have to start the statistics function), you will have seen the same screens over and over again. For each function, you have created a new test set before you can start the function. This costs time and it isn’t time well spent.
Step functions
There is a nice feature in AWS to solve this problem. It is called “Step functions”, and I used it to start all tests (except for the smoke test) with one click on a button. So go to the Step Functions service in AWS. You will see the step function for our tests already. Click on the link AMIS_shop_tests:
Click on “Start execution”:
As I said before, none of the tests do anything with the event data that is given as a parameter. You can just leave it as it is and click on “Start execution”:
You can see that three of the four tests are started in parallel. The accept unit test and the decrypt unit test cannot be started at the same time, because they use the same SNS topic, the same support_echo function and the same SQS queue to get the logging from the support_echo function back to the test lambda. When you scroll a little bit down then you can see the following image:
When you wait a while (or press F5), then you can see that the colors will change when tests are done:
When a task is running (blue) or when a task is done (green or red), then you can click on it, you will see the details of the task on the right:
I clicked on the Unitttest Accept task. When you follow along, you can click on the CloudWatch Logs, which brings you to the logs of the Unittest accept cloudwatch logs.
You might ask how to get this state machine. To see the code of it, click on the Code tab. It is in a very tiny window, so I will copy the code to this blog as well:
{
"Comment": "Combine all tests (except for the smoketest) for the shop example.",
"StartAt": "Parallel",
"States": {
"Parallel": {
"Type": "Parallel",
"Next": "End of tests",
"Branches": [
{
"StartAt": "Unittest accept",
"States": {
"Unittest accept": {
"Type" : "Task",
"Resource": "arn:aws:lambda:eu-west-1:300577164517:function:AMIS_unittest_test_accept",
"Next" : "Unittest decrypt"
},
"Unittest decrypt": {
"Type" : "Task",
"Resource": "arn:aws:lambda:eu-west-1:300577164517:function:AMIS_unittest_test_decrypt",
"End" : true
}
}
},
{
"StartAt": "Unittest update_db",
"States": {
"Unittest update_db": {
"Type": "Task",
"Resource": "arn:aws:lambda:eu-west-1:300577164517:function:AMIS_unittest_test_update_db",
"End": true
}
}
},
{
"StartAt": "Performance test",
"States": {
"Performance test": {
"Type": "Task",
"Resource": "arn:aws:lambda:eu-west-1:300577164517:function:AMIS_perftest_test",
"Next": "Wait 4 min"
},
"Wait 4 min": {
"Type": "Wait",
"Comment": "Wait untill the CloudWatch logs are available for the Lambda function. Mind, that the logs are earlier available in the user interface than they are available for Lambda function...",
"Seconds": 240,
"Next": "Get statistics"
},
"Get statistics": {
"Type": "Task",
"Resource": "arn:aws:lambda:eu-west-1:300577164517:function:AMIS_perftest_get_stats",
"End": true
}
}
}
]
},
"End of tests": {
"Type": "Pass",
"End": true
}
}
}
You can see that each step in this state machine has a Type (for example: Task for a Lambda function, or Pass for the “End of tests” task, or Wait for the “Wait 4 min” task). In every step, you can add comments. The other parameters, for example Seconds and Resource, are dependent on the type of the step. Most tasks have either a “Next” (to indicate which step comes next) or an “End”: true field. The AWS Step functions service will parse the code and draw a nice picture for us.
When in my example one of the Lambda functions crash, the other tasks will be stopped and canceled. You will still have to look at the logs of the unit tests and the performance statistics to see if the outcome is what you expected. I leave it to the reader to write a Lambda function that read the logs of the unit tests and the logs of the performance test and to draw conclusions from those logs. It shouldn’t be too difficult to integrate it in this state machine.
Doing different things based on the outcome of a step
In this example, all steps will be done. You might search for a state machine where this isn’t the case. A nice example is on the dashboard of the Step functions. Click in the left menu on “Step functions”.
You will see a more complex example, where an order is processed based on the outcome of previous step functions.
You can get this, by using the event information in the parameter of the Lambda function and giving back results via the return statement. The state machine can use that data to decide which route to take for the following steps. There are also (quite interesting) examples in the documentation.
Let’s try this out!
Let’s create a simple function which will either return 200 (Ok) or 500 (Not Ok) and let the Step function do something different based on the outcome. The state machine will look like:
Click on Step functions and after that, click on Create state machine:
After that, click on “Start with a template”:
You will see different templates, choose for Choice state:
There are two different types of state machines: standard (where every state will be called exactly one time) and Express (where states will be called at least once). Leave the type to “Standard” and scroll down.
We already saw how you can use Lambda functions to be part of the state machine. The FirstState is a Lambda function. Rename it to “Deciding Lambda Function”. When you changed it, press the refresh button, you will see the change in the graphics next to the code as well.
At line 7, you see a red dot: we need to have a valid resource ID for our Lambda function. That Lambda function doesn’t exist yet, so go to Lambda and create a new Lambda function:
Leave the default “Author from scratch”, fill in “AMIS_deciding_function” as name and choose “Python 3.8” as runtime. Click on the arrow “Choose or create an execution role”:
Click “Use an existing role” here, you can use the “AMIS_lambda_unittest_role” for this. After that, click on “Create function”:
The default function code that you see here is enough for our goal. We will use the statusCode field to decide if we will wait for 1 second (statusCode = 200) or that we will wait for 5 seconds (statusCode = 500).
Copy the Amazon Resource Name (ARN) which is in the top of the screen:
Go back to the Step function and replace the resource of the Deciding Lambda Function with the ARN you just copied (mind that the account ID is part of the ARN, don’t use my ARN because this will not work).
You can see in the code that Choice will look at variable $.foo. Replace this with $.statusCode. Let it go to state “Correct” when the status code is 200 and let it go to state “Error” when the status code is 500.
Replace the “Default State” by “Correct”. Make it a wait state (you might want to look in the code above in the “Wait 4 min” state to see how to do this) where you will wait for 1 second. Replace “FirstMatchState” by “Error” and make this a wait state where you will wait for 5 seconds. All other states can be removed.
When you are done (and you corrected the errors that these actions might have been caused), you can test your function: click Next. (If you didn’t succeed in creating the code, you can find the full code at the end of this blog).
Give the state machine a name (I choose for AMIS_Choicestate) and choose the existing role AMIS_step_function_shop_test_role.
Scroll down and click on “Create state machine”.
You can now click on “Start execution” and test your function. When you click on “Correct” and then on Input, you can see that the output of your Lambda function is passed to the following states. I leave it to you to test the error state 😉
Costs
You might ask yourself: “why, Frederique, were you so against using sleep statements in Lambda functions to wait for results (you created quite a complex way to get the CloudWatch results faster via an SQS queue), and are now using a solution where you wait for 4 seconds before analyzing the logs”.
It is a fair question. I could have processed the results of the performance test faster. In the current situation all the objects between the API Gateway and the AMIS-shop table in DynamoDB are exactly the same as they will be in the production environment. When I would have processed the logs while the performance test is running, then one might think that this can influence the results of the test (though, in theory, this shouldn’t be the case because the processing of the logs is done independently of the processing of the Lambda functions). I also think that you will need a statistics function in the production environment to see if everything is still working as you designed it.
Another reason is the way AWS deals with the costs. In Step Functions, you pay per change from one step to another. It doesn’t matter if I wait 2, 4 or 10 minutes, the costs of my Step Functions are the same. In Lambda, the costs are higher if you wait longer.
Conclusion
Step functions can be used for many great things, again: look in the documentation of the Step functions for examples. One of the nice things about Step Functions is that you can also call them from the API Gateway: you might use Express Workflows (for high volume traffic) when you have a lot of data to process.
In my Shop example, I definitely would use Step Functions to save myself the trouble to start every test by hand. I could also use my step function with tests in a CI/CD pipeline.
Play along
When you want to play along you can use my git repository for that [2]. You might want to practice with the Step Functions: create a simple Lambda function that returns a value and create a state machine that uses that function to call either a Lambda function that will process the output, or call a Lambda function that will return an error. You can make it as difficult as you like.
Links
– Introduction: https://technology.amis.nl/2020/04/26/example-application-in-aws-using-lambda/
– Lambda and IAM: https://technology.amis.nl/2020/04/29/aws-shop-example-lambda/
– SNS: https://technology.amis.nl/2020/05/02/aws-shop-about-the-aws-simple-notification-service-sns/
– DynamoDB: https://technology.amis.nl/2020/05/05/aws-shop-dynamodb-the-aws-nosql-database/
– API Gateway (1): https://technology.amis.nl/2020/05/09/aws-shop-api-gateway-1/
– API Gateway (2): https://technology.amis.nl/2020/05/13/aws-shop-example-api-gateway-2/
– Unit tests: https://technology.amis.nl/2020/05/21/aws-shop-example-unit-tests/
– Smoke- and performance tests: https://technology.amis.nl/2020/05/28/aws-shop-example-smoke-and-performance-tests/
[2] https://github.com/FrederiqueRetsema/AMIS-Blog-AWS , directory shop-2
Code state machine:
{
"Comment": "An example of the Amazon States Language using a choice state.",
"StartAt": "Deciding Lambda Function",
"States": {
"Deciding Lambda Function": {
"Type": "Task",
"Resource": "arn:aws:lambda:eu-west-1:300577164517:function:AMIS_deciding_function",
"Next": "ChoiceState"
},
"ChoiceState": {
"Type" : "Choice",
"Choices": [
{
"Variable": "$.statusCode",
"NumericEquals": 200,
"Next": "Correct"
},
{
"Variable": "$.statusCode",
"NumericEquals": 500,
"Next": "Error"
}
]
},
"Correct": {
"Type" : "Wait",
"Seconds": 1,
"End": true
},
"Error": {
"Type" : "Wait",
"Seconds": 5,
"End" : true
}
}
}