DEV Community

Cover image for A Beginner's Guide to the Serverless Application Model (SAM)
Allen Helton for AWS Heroes

Posted on • Originally published at readysetcloud.io

A Beginner's Guide to the Serverless Application Model (SAM)

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
Enter fullscreen mode Exit fullscreen mode

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    
Enter fullscreen mode Exit fullscreen mode

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 *)"
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
cicirello profile image
Vincent A. Cicirello

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.

Collapse
 
allenheltondev profile image
Allen Helton

Thank you, Vincent.