Cloudfront can be simply defined as a CDN (Content Delivery Network), caching your static assets in a datacenter nearer to your viewers. But Cloudfront is a lot more complex and versatile than this simple definition. Cloudfront is a “pull” CDN, which means that you don’t push your content to the CDN. The content is pulled into the CDN Edge from the origin at the first request of any piece of content.
In addition to the traditional pull and cache usage, Cloudfront can also be used as:
A Networking Router
A Firewall
A Web Server
An Application Server
Why is using a CDN relevant?
The main reason is to improve the speed of delivery of static content. By caching the content on the CDN edge, you not only reduce the download time from a few seconds to a few milliseconds, but you also reduce the load and amount of requests on your backend (Network, IO, CPU, Memory, …).
Static content can be defined as content not changing between two identical requests done in the same time frame.
Identical can be as simple as the same URI, or as fine grained as down to the authentication header. The time frame can range between 1 second to 1 year. The most common case is caching resources like Javascript or CSS and serving the same file to all users forever. But caching a JSON response tailored to a user (Authentication header) for a few seconds reduces the backend calls when the user has the well-known “frenetic browser reload syndrome”.
Edges, Mid-Tier Caches, and Origins
Cloudfront isn’t “just” some servers in datacenters around the world. The service is a layered network of Edge Locations and Regional Edge Caches (or Mid-Tier Caches).
Edge Locations are distributed around the globe with more than 400 points of presence in over 90 cities across 48 countries. Each Edge Location is connected to one of the 13 Regional Edge Caches.
Regional Edge Caches are transparent to you and your visitors, you can’t configure them or access them directly. Your visitors will interact with the nearest Edge Location, which will connect to the attached Regional Edge Cache and finally to your origin. Therefore, in this article, we will refer to Cloudfront as the combination of Edge Locations and Region Edge Caches.
What Have We Learned?
Cloudfront is more than just a simple “pull-cache-serve” service
You improve delivery speed to your visitors
You can increase resilience by always using a healthy backend
You improve overall speed to your backend by leveraging AWS’s backbone
You can modify any request to tailor the response to your visitor’s device or region
You don’t always need a backend
You protect your backend by reducing the number of calls reaching it
Hey there, welcome to the 3rd Scenario in “Building a Step Functions Workflow “
In the first part of this series, we built a Step Functions workflow for a simple apartment booking scenario using the AWS Step Functions low code visual editor.
In the second part of this series, we built the same workflow using CDK as IaC, AppSync and Python, while invoking the Step Functions execution from a Lambda function.
In this post, we’ll look at how to build the same workflow, using SAM as IaC, AppSync, and Python.
So while building out a bigger system(Apartment Complex Management System), I encountered an interesting problem.
I’ll assume that most of us have reserved or booked either an apartment or hotel, or flight online.
For this scenario, let’s go with apartments. So when you reserve an apartment, here’s a breakdown in the simplest form of the series of steps that occur after that:
The apartment is marked as reserved, probably with a status change. Let’s say the apartment status changes from vacant to reserved.
This apartment is made unavailable for reserving by others for a particular period of time.
The client is required to make payment within that period of time
If payment isn’t made within that time, the reservation is canceled, and the apartment status changes back from reserved to vacant.
If payment is made, then apartment status changes from reserved to occupied/paid
Building out this business logic using custom code is very possible but inefficient.
Why?
Because as developers, good ones for that matter, we always have to be on the lookout for tools that’ll help us carry out tasks in an efficient and scalable manner.
The series of steps outlined above serves as a good use case for AWS Step Functions.
The sequence of a service interaction is important
State has to be managed with AWS service calls
Decision trees, retries, and error handling logic are required.
Solutions Architecture
Let me quarterback this entire architecture for you, please. Here’s what’s happening:
A frontend application sends a mutation to AppSync.
A Lambda resolver is invoked by AppSync based on that mutation.
Lambda gets the input from the mutation and starts a Step Functions workflow based on the input.
We’ll use Flutter and Amplify to build out the front-end application in the next tutorial.
Create And Initialize a SAM Application
Open any Terminal/Command line interface, type in the command
sam init
And then follow the instructions as seen in these screenshots:
Choose Python 3.8 as your runtime environment
I gave the project name 'samWorkshopApp'
Once your application has been created, open it up in your IDE, and let’s proceed. For reference, I’m using Pycharm.
Activate your virtualenv like this on mac or linux machines:
'source .venv/bin/activate'
If you are a Windows platform, you would activate the virtualenv like this:
'.venv\\Scripts\\activate.bat'
Once the virtualenv is activated, you can install the required dependencies.
From the root directory of the project, install all dependencies in 'requirements.txt' by running the command 'pip install -r requirements.txt'
Initially, here’s how my folder structure looks like:
There are a couple of changes we are about to make:
Inside the 'functions' directory, delete all folders, and then create a folder called lambda.
Delete everything inside the 'statemachine' folder, then create a file inside that same folder called `booking_step_functions.asl.json.
This file would contain the state machine definition for our workflow. We visually defined this workflow in part 1 of this series. Copy the ASL(Amazon States Language) for the workflow below and paste it inside the file we’ve created above:
Remember, we have to create a GraphQL API, attach a schema and a database, and connect a Lambda resolver to it. This Lambda would be responsible for invoking the Step Functions workflow.
Open up the 'template.yaml' and add this GraphQl API and API key to the resources section
A GraphQL API always works with a GraphQL schema. In the root directory, create a folder called ‘graphql’, and inside that folder, create a file called 'schema.graphql'. Type in the following ‘graphql’ schema into the file:
This schema has a single mutation 'addStepFunction' that sends an input('id' and 'arn') to a Lambda resolver. The Lambda resolver uses this input to start a Step Functions execution. Let’s define the schema in 'templates.yaml' under resources.
Let’s create a lambda function that we’ll attach to a data source and then attach that data source to an AppSync resolver Inside the 'functions/lambda' folder, create a file called 'app.py' and type in the following code:
For now, this lambda function simply takes an input('id' and 'arn') and outputs('id' and 'arn'). Later on, we’ll use this lambda function to start the Step Functions workflow.
Let’s define the Lambda function in 'template.yaml' alongside its 'role':
In our workflow, we save apartment attributes to a database. Let’s go ahead and create the database. It’s a DynamoDB with a single primary key of ID.
SamStepFunctionsTable:
Type: AWS::Serverless::SimpleTable # More info about SimpleTable Resource: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-simpletable.html
Properties:
PrimaryKey:
Name: Id
Type: String
ProvisionedThroughput:
ReadCapacityUnits: 1
WriteCapacityUnits: 1
State Machine
A couple of steps above, we saved the Step Functions workflow in a file called 'booking_step_functions.asl.json'. We have to create a state machine resource in 'template.yml', link to that file, do some variable substitutions like the DB name, and also provide DynamoDB read and write policies for the 'update' and 'get' item DynamoDB methods.
So let’s go ahead and define the step machine resource under ‘Resources’ in 'template.yml':
SamStepFunctionStateMachine:
Type: AWS::Serverless::StateMachine # More info about State Machine Resource: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-statemachine.html
Properties:
DefinitionUri: statemachine/booking_step_function.asl.json
DefinitionSubstitutions:
DDBUpdateItem: !Sub arn:${AWS::Partition}:states:::dynamodb:updateItem
DDBGetItem: !Sub arn:${AWS::Partition}:states:::dynamodb:getItem
DDBTable: !Ref SamStepFunctionsTable
Policies: # Find out more about SAM policy templates: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-policy-templates.html
- DynamoDBWritePolicy:
TableName: !Ref SamStepFunctionsTable
- DynamoDBReadPolicy:
TableName: !Ref SamStepFunctionsTable
After all variable substitutions, the 'booking_step_function.asl.json' file looks like this now:
{
"Comment":"This state machine updates the status of a booked transaction in the DB, waits for payment to be made and then updates again or passes",
"StartAt":"Change Apartment Status",
"States":{
"Change Apartment Status":{
"Type":"Task",
"Resource":"${DDBUpdateItem}",
"Parameters":{
"TableName":"${DDBTable}",
"Key":{
"Id":{
"S.$":"$.details.accountId"
}
},
"ConditionExpression":"attribute_exists(Id)",
"UpdateExpression":"SET bookedStatus = :bookedStatus",
"ExpressionAttributeValues":{
":bookedStatus":{
"S.$":"$.details.bookedStatus"
}
}
},
"Next":"Wait",
"ResultPath":"$.updateResult",
"Catch":[
{
"ErrorEquals":[
"States.ALL"
],
"Comment":"Items Doesn't Exist",
"Next":"Fail",
"ResultPath":"$.updateError"
}
]
},
"Wait":{
"Type":"Wait",
"Seconds":60,
"Next":"Get Booking Status"
},
"Get Booking Status":{
"Type":"Task",
"Resource":"${DDBGetItem}",
"Parameters":{
"TableName":"${DDBTable}",
"Key":{
"id":{
"S.$":"$.details.accountId"
}
}
},
"Next":"Has the Apartment been Paid ?",
"ResultPath":"$.getItem",
"Catch":[
{
"ErrorEquals":[
"States.ALL"
],
"Comment":"Couldn't find item",
"Next":"Fail"
}
]
},
"Has the Apartment been Paid ?":{
"Type":"Choice",
"Choices":[
{
"And":[
{
"Variable":"$.getItem.Item.Id.S",
"StringEquals":"1234567"
},
{
"Variable":"$.getItem.Item.bookedStatus.S",
"StringEquals":"Paid"
}
],
"Next":"Apartment Paid"
}
],
"Default":"Not Paid(Revert Apartment Status)"
},
"Not Paid(Revert Apartment Status)":{
"Type":"Task",
"Resource":"${DDBUpdateItem}",
"Parameters":{
"TableName":"${DDBTable}",
"Key":{
"Id":{
"S.$":"$.getItem.Item.Id.S"
}
},
"UpdateExpression":"SET bookedStatus = :bookedStatus",
"ExpressionAttributeValues":{
":bookedStatus":{
"S":"PENDING"
}
}
},
"End":true,
"ResultPath":"$.notPaid"
},
"Fail":{
"Type":"Fail"
},
"Apartment Paid":{
"End":true,
"Type":"Pass"
}
}
}
We import the Step Functions class from the boto3 client and use it to start a Step Functions execution by passing in the StateMachineArn we get from deploying the project, a unique name for the state machine execution, and the state machine input.
Deploy
Deploy the app to your AWS account using 'sam build' and 'sam deploy'
Once deployment is successful, grab the Step Functions arn and proceed to testing in AppSync.
Testing
Sign in to your AWS console and search for AppSync. Open up AppSync and click on your newly deployed AppSync project.
Conclusion
In this post, we built a Step Functions workflow using Appsync, SAM, and Python. This workflow mimics a real-life scenario of booking/reserving an apartment.
We saw how to invoke Step Functions from a Lambda.
We covered defining a state machine in a yaml file with variable substitution.
We broke down how to use IaC to create Applications with Step Functions.
In the next post, we’ll invoke the Step Functions workflow from a mobile frontend application built with amplify and flutter. Stay tuned!
More Screenshots
Rosius Ndimofor
Senior Serverless Developer
Rosius is a Senior Serverless Developer at Serverless Guru, an AWS Serverless Hero, AWS community builder, and frequent blogger.