When moving to the cloud, be it in a migration or greenfield project, a responsibility you might not be accustomed to is declaring your infrastructure as code. Infrastructure as code is exactly as it sounds - you declare your cloud resources like Lambda functions, EC2 instances, and databases in a file and use it to deploy the exact same setup everywhere you go. Big upgrade from pointing and clicking like we used to do in the past.
Naturally, there are several options available to declare your cloud resources. The options with the most popularity are the CDK, AWS CloudFormation, SST, Serverless framework, Terraform, and AWS SAM. There are others, but when talking about Infrastructure as Code (IaC), these are the ones you hear about most often.
I'm an avid SAM user. I prefer it to alternatives because of its declarative nature and that it's native to AWS. There are plenty of examples out there that show how to build with it, but I've been struggling to find anything that gives you the basics. I've noticed an increased interest in SAM lately, so let's talk about how to cut through some of the noise and get down to building your first project with it.
SAM has two major components - a CLI and a template transform. The first thing we need to discuss is this transform and what exactly it means to us.
The SAM Template
In CloudFormation, you can use macros to perform custom processing like adding permissions automatically between resources or doing find-and-replace operations. This is what SAM does. It uses an AWS::Serverless transform on a CloudFormation template to give users access to rich components that would be a nightmare to build in CloudFormation.
To say it another way, a template built with SAM is written in CloudFormation but with a bunch of handy shortcuts.
Let's take a look at the most common resources and how to use them. You can also check out the AWS documentation for a full list of supported resource shortcuts.
Lambda Functions
Arguably the lifeblood of any serverless application, creating a Lambda function and declaring triggers for it is ridiculously easy.
SendApiRequestFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: functions/create-article
Policies:
- AWSLambdaBasicExecutionRole
- Version: 2012-10-17
Statement:
- Effect: Allow
Action: dynamodb:PutItem
Resource: !GetAtt MyDDBTable.Arn
Events:
FromApi:
Type: Api
Properties:
RestApiId: !Ref MyApi
Path: /articles
Method: POST
The CodeUri
property points to a local directory that contains the function files. When you deploy, SAM will build your function, zip up the package, then upload it to S3 for you automatically.
With the Policies
object, you can mix and match both AWS-managed policies, SAM policy templates, and custom policies you build. I prefer to stick with managed and custom policies to make sure I practice the principle of least privilege.
The last section is where real power comes into the SAM transform. At the time of writing, you can configure event triggers from 19 different sources. Just add a named event in the Events
object and throw in the required properties. SAM will take that and create all the necessary resources and permissions to trigger your function.
Global Definitions
You probably noticed there were some important function configuration pieces missing like Runtime
, Handler
, Timeout
, MemorySize
, and Architectures
. These are all important pieces of config and often don't vary from function to function. To save time and reduce template size, we can define these properties in the Globals section of the template, which will apply across all functions automatically.
Globals:
Function:
Runtime: nodejs18.x
Architectures:
- arm64
Timeout: 5
MemorySize: 1024
Handler: index.handler
Each one of these properties is overrideable at the individual function level if you need to increase a timeout, change a runtime, or boost memory size. This is a nice time saver and makes your template more succinct and easier to read.
Step Function Workflow
I build more Step Function Workflows than I do Lambda functions. I use direct integrations with the AWS SDK to minimize my function usage whenever possible. Since you can connect an API Gateway endpoint directly to a synchronous express Step Function workflow, I have had very little reason to build as many Lambda functions as I used to.
Let's take a look at the definition of a workflow that runs a report and sends me an email every Friday morning.
ReportNewsletterStatsStateMachine:
Type: AWS::Serverless::StateMachine
Properties:
Type: STANDARD
DefinitionUri: workflows/report-newsletter-stats.asl.json
DefinitionSubstitutions:
DynamodbQuery: !Sub arn:${AWS::Partition}:states:::aws-sdk:dynamodb:query
TableName: !Ref MyDDBTable
SendApiRequestFunction: !GetAtt SendApiRequestFunction.Arn
DynamodbPutItem: !Sub arn:${AWS::Partition}:states:::dynamodb:putItem
EventBridgePutEvents: !Sub arn:${AWS::Partition}:states:::events:putEvents
Policies:
- Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- dynamodb:PutItem
- dynamodb:Query
Resource: !GetAtt MyDDBTable.Arn
- Effect: Allow
Action: lambda:InvokeFunction
Resource: !GetAtt SendApiRequestFunction.Arn
- Effect: Allow
Action: events:PutEvents
Resource: !Sub arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:event-bus/default
Events:
Trigger:
Type: Schedule
Properties:
Schedule: "cron(0 15 ? * FRI *)"
Step Function workflows are where SAM really shines in my opinion. You can provide the resource with a path to a local parameterized asl file in the DefinitionUri
property and upon deployment, SAM will automatically do string replacements for everything defined in the DefinitionSubstitutions
object.
This allows you to have dynamic, flexible workflow definitions that you can take and deploy anywhere (even to the GovCloud)! A snippet from the parameterized definition would look like this:
"Set New Subscriber Count": {
"Type": "Task",
"Resource": "${DynamodbPutItem}",
"Parameters": {
"TableName": "${TableName}",
"Item": {
"pk": {
"S": "newsletter"
},
"sk": {
"S.$": "States.Format('subscribers#{}', States.ArrayGetItem(States.StringSplit($$.Execution.StartTime, 'T'), 0))"
},
"count": {
"S.$": "States.Format('{}', $.contact_count)"
}
}
},
"End": true,
"ResultPath": null
}
The values contained in the ${}
notation directly map to the DefinitionSubstitutions in the State Machine resource. These could be static values, parameters, or values from other resources contained in your SAM template.
REST, HTTP, and GraphQL APIs
Application developers tend to build a lot of public-facing APIs. In pretty much every template I build, I include at least one API. Similar to Lambda functions and Step Function workflows, creating a REST API in SAM feels like magic. Let's look at an example:
ReadySetCloudApi:
Type: AWS::Serverless::Api
Properties:
TracingEnabled: true
StageName: v1
DefinitionBody:
Fn::Transform:
Name: AWS::Include
Parameters:
Location: ./openapi.yaml
That's it. Yes, really.
The majority of the content is contained in that ./openapi.yaml
file referenced in the DefinitionBody property. That file is an Open API specification that defines all the endpoints, which resource backs each one, and declares the request and response schemas. I'm a firm believer in API-first development and that your specification doc should be written before implementing any code - especially for public-facing APIs.
When you define the backing resources and request schemas in your spec file, SAM will automatically create a huge amount of supporting resources:
- Permissions from API Gateway to the backing resource
- API Gateway stage
- API Gateway deployment
- API Gateway routes
- API Gateway request models
All of these resources are necessary to integrate API Gateway with things like Lambda and Step Functions, but you don't have to worry about them! They are completely handled for you by SAM. Bonus points - if you define request schemas, you get incoming request validation at API Gateway, meaning you can guarantee the shape of the input when it hits your Lambda function or other downstream resource.
HTTP APIs are similar with subtle nuances. GraphQL APIs on the other hand, are quite a bit different.
An AppSync GraphQL API has a lot of moving parts. Between resolvers, functions, schemas, and auth, you have a multitude of cloud resources that make up one of these APIs. Fortunately, SAM puts them all together in a single GraphQLApi resource. You can define everything in a single resource and all the connections, permissions, and relationships will be generated for you automatically. You also get automatic resource packaging and deploying for your function and resolver code!
The SAM CLI
The benefits of SAM don't stop at resource definition! It also comes with a CLI that manages some heavy-duty tasks to make building, deploying, and debugging your serverless applications a snap. That said, it can be easy to get overwhelmed by all the capabilities of the SAM CLI.
I make the joke that while I might be an advanced SAM user, I'm also a basic one.
Despite all the cool local and remote debugging capabilities, I stick with two commands: sam build
and sam deploy
.
As a beginner, this is all you'll need too. The sam build
command will install all your package dependencies and update your template file to point to compiled resources in the .aws-sam folder it creates. You can build your functions in a container, build them in parallel, and use cached versions if your resources didn't change. This is a powerful command that gets your application ready for deployment.
The other command you'll like is sam deploy
. This takes the built files from the .aws-sam directory, uploads the assets to S3, then creates all the resources in your AWS account. You can even use the --guided
flag to walk you through a wizard to configure things like stack name, artifact management, and defining deployment parameters. The deploy command takes out any guesswork and difficulty out of getting your app to the cloud.
Pro tip - For local development, save the output of the sam deploy --guided
command to a config file named samconfig.toml. This will remember your build and deployment settings so you don't have to pass in command line arguments every time.
Summary
You don't need to be an expert to get the most out of SAM. Take advantage of the resources in your template file, build, and deploy. There are more advanced use cases, but 99% of us won't need to do anything fancy outside of the basics (I still don't, 5 years in).
There are plenty of additional features available in the SAM CLI and a lot more resources you can define in your templates that I did not cover. But for beginners looking to get into SAM, this should get you going.
If you're looking for examples, you can check out my GitHub, it's full of SAM templates that cover a wide range of use cases. Serverless Land is another fantastic resource full of reference material. If you are trying to build something but can't quite figure it out in SAM, remember - it's all just CloudFormation. Browse the docs to see how to define that stubborn resource.
As always, if you have any questions, feel free to reach out to me on Twitter or LinkedIn. I'm more than happy to help you get started or work through a tricky issue.
Happy coding!
Top comments (2)
Nice post. You can make your code blocks look nicer with syntax highlighting. To do that, put the language name immediately after the 3 backticks that start the code block without a space between the backticks and language name.
Thank you, Vincent.