I've recently done some work to compare the AWS Serverless Application Model to the Serverless Framework for building and deploying .NET Core based Lambda functions to AWS, and have started to deploy a number of Lambda functions that were built using the Serverless Application Model (SAM).
There are a number of existing articles out there that compare the two frameworks, so rather than write another one I'll cut to the chase and say that I prefer SAM over the Serverless Framework for a number of reasons. I would suggest you evaluate both frameworks for yourself because you may have different criteria for evaluation. This article is going to give an overview of SAM and at the end I'll offer a brief comparison with the Serverless Framework. Other articles in this series will talk more about deploying services built with SAM, and other things that we've encountered while building these applications.
If you're at all familiar with CloudFormation then you will find writing SAM templates very familiar, because they are just CloudFormation templates with some custom resources and a Transform:
AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: A simple SAM Template Globals: Timeout: 60 MemorySize: 512 Resources: ListFunction: Type: AWS::Serverless::Function Properties: CodeUri: ./src/ListFunctions/ Handler: ListFunctions::ListFunctions.LambdaEntryPoint::FunctionHandlerAsync Runtime: dotnetcore3.1
AWS::Serverless:: resources may look like custom Resource Providers, they actually have far more in common with Macros that can manipulate a template using a transform built using a Lambda function. The SAM Translator is the code that is responsible for converting SAM templates into CloudFormation templates.
Because this is a CloudFormation template with some additions, you can add standard CloudFormation resources intermingled with the SAM resources. Developer tools that work with CloudFormation templates for editing and linting (cfn-lint) also support SAM templates.
In fact you can combine your own macros with the SAM Serverless transform to manipulate regular CloudFormation resources or even SAM resources, which can be useful because there are gaps in what the SAM resources support.
Something that's important to note here is that you submit the template containing the
AWS::Serverless::resources to the CloudFormation service and it handles the transformation server-side. There are a few modifications that the SAM CLI may make to the template client-side to replace local references to code with an S3 Bucket and Key, but these are similar to the existing functionality that's part of the
aws cloudformation packagecommand.
Many properties of AWS::Serverless::Function match the properties of AWS::Lambda::Function, which is unsurprising because after the transform is applied the one is replaced with the other plus additional resources.
Some of the biggest benefits to using the
AWS::Serverless:Function are these properties:
The Events allows you to specify the triggers for a Lambda function that you would normally need to specify in separate CloudFormation resources. There are current 14 different Event Sources including API Gateway (both the REST and HTTP versions), S3 and SQS.
One big limitation of the S3 event is that it only works if the S3 bucket is declared in the same SAM template. This is because CloudFormation only allows you to specify S3 events directly on the S3 bucket resource. In a lot of cases the S3 bucket will have a different lifecyle than the lambda function and you would not want CloudFormation to try and delete it (which will fail if it contains objects) if you delete the Lambda function.
CloudFormation Custom Resources can be used to add S3 lambda triggers separately from the bucket creation.
With the Api and HttpApi events there's some extra magic going on - if you don't specify the
RestApiId (for the Api event) or
ApiId (for the HttpApi event) it will automatically create an API Gateway using the mappings in your events.
Events: HttpApiEvent: Type: HttpApi Properties: Path: / Method: GET
Example HttpApi event
Unfortunately the Events section lacks supports for Application Load Balancers, which are becoming a popular alternative to API Gateway for Lambda functions because they are significantly cheaper than REST API Gateways and can be used to easily build internal APIs (something that is possible but more difficult with REST API Gateways and not currently possible with HTTP API Gateways). That doesn't mean you can't define a Lambda function using
AWS::Serverless:Function and target it with an ALB, but you will need to create the
AWS::ElasticLoadBalancerV2::ListenerRule resources yourself.
This is one of those gaps that can be plugged with CloudFormation macros that operate on the SAM resources, like this example: https://github.com/glassechidna/sam-alb
Doesn't everyone love writing IAM policies? The Policies property can be used in a number of ways, but one really useful feature is being able to use Policy Templates that provide well designed sets of permissions for certain resources.
Policies: - DynamoDBReadPolicy: TableName: !Ref DyanamoDBTable - KMSDecryptPolicy: KeyId: !Ref DynamoDBKmsKey
Granting a Lambda function to read from a specific table and decrypt data using the related KMS key
It will also add the necesary permissions for CloudWatch logs, and other resources declared on the
AWS::Serverless:Function resource - i.e. if you specify VPC subnets to run the function within your VPC then it will add the necessary permissions automatically.
The standard workflow for SAM applications is to run sam build to build and package any code referenced in
AWS::Serverless::Function resources, then sam deploy to upload the lambda zip files to S3 and then create a CloudFormation stack (by default using change sets) to deploy everything.
Create a new SAM project. There are quick start examples for most if not all of the supported runtimes:
$ sam init Which template source would you like to use? 1 - AWS Quick Start Templates 2 - Custom Template Location Choice: 1 Which runtime would you like to use? 1 - nodejs12.x 2 - python3.8 3 - ruby2.7 4 - go1.x 5 - java11 6 - dotnetcore3.1 7 - nodejs10.x 8 - python3.7 9 - python3.6 10 - python2.7 11 - ruby2.5 12 - java8 13 - dotnetcore2.1
While these aren't the most complicated examples, they set up the directory structure in a way that works well with SAM and include testing set-up. You can also use custom templates if you have your own way of organizing your projects.
build command looks for
template.yml in the current directory by default, and figures out what to build based upon the contents of the template. The template could potentially contain several lambda functions using different runtimes, and it would package the code pointed to be the
CodeUri property using an appropriate builder for the runtime. All of these files end up in an
There are few arguments you might need to specify in certain cases:
--use-containerfor runtimes that have native dependencies. It will pull down a docker image based upon the Lambda environment for the function runtime and build within that. This can be important for Python where some dependencies may need to be compiled and if built on another platform (e.g. Windows) will not work in the Lambda runtime which is based upon Amazon Linux (1 or 2 depending upon the runtime).
--regionif using Lambda layers - it will retrieve the layer from your AWS account. If the Layer ARN is provided via a Parameter you will also need to provide the value using
Other parameters can override the build directory and the manifest file (e.g. requirements.txt for Python, a .csproj file for C#, pom.xml or build.gradle for Java) location or the template name if you're not using
package command is less commonly used now because it's functionality has been rolled into the
deploy command. This command packages the results of the
build and uploads to s3 and then replaces the local references in the template with references to the objects uploaded to S3 and returns the new template (either to standard out or a file). The command supports these resource types and attributes:
Resource : AWS::ServerlessRepo::Application | Location : LicenseUrl Resource : AWS::ServerlessRepo::Application | Location : ReadmeUrl Resource : AWS::Serverless::Function | Location : CodeUri Resource : AWS::Serverless::Api | Location : DefinitionUri Resource : AWS::Serverless::StateMachine | Location : DefinitionUri Resource : AWS::AppSync::GraphQLSchema | Location : DefinitionS3Location Resource : AWS::AppSync::Resolver | Location : RequestMappingTemplateS3Location Resource : AWS::AppSync::Resolver | Location : ResponseMappingTemplateS3Location Resource : AWS::AppSync::FunctionConfiguration | Location : RequestMappingTemplateS3Location Resource : AWS::AppSync::FunctionConfiguration | Location : ResponseMappingTemplateS3Location Resource : AWS::Lambda::Function | Location : Code Resource : AWS::ApiGateway::RestApi | Location : BodyS3Location Resource : AWS::ElasticBeanstalk::ApplicationVersion | Location : SourceBundle Resource : AWS::CloudFormation::Stack | Location : TemplateURL Resource : AWS::Serverless::Application | Location : Location Resource : AWS::Lambda::LayerVersion | Location : Content Resource : AWS::Serverless::LayerVersion | Location : ContentUri Resource : AWS::Glue::Job | Location : Command.ScriptLocation Resource : AWS::StepFunctions::StateMachine | Location : DefinitionS3Location
You are required to specify the
--s3-bucket parameter and you can supply and optional
--s3-prefix (useful if you want to share a bucket between multiple serverless applications but want to group related artifacts). The S3 key will be automatically generated using an md5 of the uploaded resource, and the command will by default skip uploading if the md5 of the new resource matches an existing S3 key.
One thing to note about this approach is that it works much better running locally than from a CI/CD pipeline. Locally the build may skip recompilation if nothing has changed or copy files and retaining their timestamps; but when using a fresh checkout in a pipeline you are typically performing a fresh build, and even if all the files in the resulting artifact are identical, if they're zipped up for a Lambda function the different timestamps will cause the md5 to be different than any previous build.
deploy command includes the functionality in
package in newer versions of the SAM CLI, and also deploys the application using CloudFormation. It makes use of ChangeSets and will display the pending changes before executing them and then displays the CloudFormation events as they occur. The command has quite a lot of options (many of them are familiar from the related CloudFormation commands), so when using SAM locally you can specify the
--guided option and it will prompt you to make certain decisions and ask for values for all the parameters in the CloudFormation template. These values are then stored in a toml configuration file which will be read the next time you run
package command, you are not required to specify the S3 bucket to upload the files to (although you can), and by default it will create a new bucket in your account for SAM deployments if one does not already exist.
In addition to packaging and deploying to AWS, the
sam local commands allow you to run Lambda functions locally within a Docker container that matches the AWS runtime. It also has some basic support for REST API Gateway applications.
sam local generate-eventcan generate sample events from 22 different services. This can save you from scouring the documentation to find examples, but typically these events are just one of the possible events from the service and you will probably need to tweak it for use in testing your lambda functions.
sam local invokewill run your Lambda function in a docker container. You supply an event from either
generate-eventor a local file. If you have a single function in your template it will automatically call that, but if there are multiple functions you will need to supply it's LogicalId from the template. The output from the function is almost identical to what you would see in CloudWatch logs, including the timing and utilized memory information. Note that because it starts the container each time you're incurring the Lambda cold start penalty every time.
sam local start-apican be used for templates containing a REST API Gateway either directly defined or where the Lambda functions use the
Apievent type. It does not support HTTP API Gateways currently. It starts a server in Docker and listens on localhost:3000 by default. This way you can make HTTP calls rather than using the API Gateway events that you need to supply to
sam local invoke.
sam local start-lambdaalso starts a HTTP server, but this one allows you to invoke the lambda functions using the CLI or SDKs as if they were running in AWS. This may be useful for running a series of tests against a lambda without running
sam local invokeeach time.
Of course there are limitations to what this supports. If your lambda function calls another AWS service (as many do), then it will call out to AWS and you will need to have valid credentials (and those resources will need to have been deployed). It is possible with some work to invoke certain local versions of AWS services (e.g. those from localstack), and this may be useful for automated testing, but nothing will be a perfect emulation of the AWS environment.
This is particularly true when you need to verify that your IAM permissions for calling other services are correct, as when running a function locally and invoking services in AWS it will be using the AWS credentials you supply it with rather than the role that has been defined for the function.
There are some integrations with a number of IDEs (e.g. Visual Studio Code) that allow you to debug your Lambda function when it's run locally like this. I've had mixed luck getting this to work consistently with .NET Core, but it can be useful.
Because the Serverless Transform used in SAM templates is essentially a pre-defined macro that is implemented as part of the CloudFormation service, there is no requirement that you use the SAM CLI with your SAM templates.
For example, you might be building the Lambda Zip files as part of your CI/CD pipeline and then use the same Zip file in multiple accounts (e.g. dev/test/prod) without rebuilding it. The SAM CLI doesn't really support this workflow, but you can still use the
AWS::Serverless:: resources in your template.
You will not be able to use the substitutions for local resources that the SAM CLI supports if you are using the CloudFormation commands instead, although
aws cloudformation packagedoes support uploading local Zip files to S3 and returning a transformed template. If you upload to S3 as part of your CI/CD pipeline you can then make the Bucket name and Key CloudFormation parameters that the servless resources reference.
- Unlike SAM, the Serverless Framework supports multiple Cloud platforms. If you have a multi-cloud strategy this can be useful.
- Serverless supports a plugin model that extends what the platform can do - with SAM you are somewhat constrained by what has been implemented by the platform and it can sometimes be frustrating waiting for new functionality to be supported.
- There is a Commercial (Pro) version that offers a monitoring platform, CI/CD support and other administration features. If your Cloud applications are largely built up of functions this might be a useful option instead of using Cloud specific or third party tools to monitor your applications.
- There are a lot of examples for using the Serverless Framework on their website, but they are dominated by nodeJS (that I believe was the only language supported when it first launched) - if you're looking for C# examples you won't find anything more than the most basic Hello World.
- The CLI is responsible for interacting with the Cloud platform. In the case of AWS it still uses CloudFormation, but the templates are generated by the CLI from the
For me this last point is the most significant. The full serverless.yml is pretty comprehensive and provides support for a lot of things that SAM does not, and you can even include raw CloudFormation resources in the
resources section of the template, but you are still reliant upon the serverless CLI to generate the CloudFormation template. This means you are required to use the CLI as part of your CI/CD pipelines, and if there are problems with the CloudFormation stack you are dealing with an auto-generated template and trying to understand how that related back to your serverless template.
Because I am very familiar with CloudFormation my bias is towards SAM which allows me to write better CloudFormation rather than Serverless that just makes use of it behind the scenes. Like I said at the start of this article, that's my personal bias and if you don't have the same background in CloudFormation you may find that Serverless works better for you.
In future articles in this series I'll be writing about some different approaches to using .NET Core to build Lambda functions, and some of the challenges of deploying and using these functions in AWS.