To preface this rant, I feel the need to clarify that I absolutely love working with Serverless and AWS professionally. Over the course of more than 2 years, I have deployed numerous applications across various industries, gaining valuable experience. Serverless has allowed us and our clients to scale and grow into multi-million dollar ARR businesses without ever having to worry about infrastructure.
This said however, I still don’t like to recommend this stack to people. I always say something along the lines of:
“Oh Serverless is great if you’re being paid to learn it.”
Hopefully, in this post I can describe some of my pain points with Amazons official offerings around Serverless, & Why I’ll be using SAM over the third party Serverless Framework for most of my personal projects from here on out.
Serverless Framework?
If I’m being completely honest, deploying to a serverless environment is a complete shit-show. There’s heaps of ways to do it, they’re all slightly different, but they all compile down to one thing in the end: Cloudformation.
In almost all my projects whether it be at work, or personally, I used the Serverless Framework. It abstracts writing cloudformation in relation to Lambdas’ and makes it easier to focus on writing code. However, Serverless Framework is not AWS Native, and I only ever deploy to AWS. Also, a lot of the functionality I take advantage of in Serverless Framework is based on community plugins which can break at any time with zero warning. I want my infrastructure, which is critical to my work, to be backed by a large body & stable, not something maintained by someone in their free time.
Serverless also seems like they’re trying to push their Dashboard and extra tooling, which I don’t need.
Serverless Framework L’s
- Lots of functionality I use is based entirely on community plugins which can break
- Serverless Framework tries to support all cloud providers when I only need one
- Serverless Framework has their own motives and goals with the library to sell their dashboard
Serverless Framework W’s
- Easy to use straight up plain Cloudformation if required
- Lots of community support & plugins
- Can easily start focusing on your applications code
CDK?
CDK instantly gets written off by me just by glancing at its docs. It’s an extreme abstraction of Cloudformation. Where you write Typescript, run a magic CLI command, and it spits out Cloudformation files. Any sort of black box transformer seems like an issue waiting to happen, especially when it’s tied to a completely different language.
I don’t want another layer that can potentially break when deploying my infrastructure. That’s what I’m trying to avoid & one of the reasons I want to swap from the Serverless Framework. No matter the abstraction you will have to manually write and manage your cloudformation at some point. Adding extreme abstractions on top of it only delays the inevitable.
CDK also requires the use of AWS SAM to run locally, so I don’t really understand at a glance why I’d want to use CDK over SAM? Personally, I haven’t used CDK at all. I jumped straight into SAM, but from the brief foray into CDKs docs I don’t feel like I’d ever use it.
SAM
AWS SAM is so close to being what I want it to be. It’s an extremely light abstraction of Cloudformation at a glance, and it allows for extremely easy local testing & deployment. However, it is not without its downsides.
SAM Requires the use of Docker, which the Serverless Framework does not. This adds an extra hurdle to initial development when getting started. I’m not speaking for everybody here, but personally I find docker (on macOS1) a bit of a nightmare to set up. Do I install docker desktop? or docker engine? Do I install it through brew
or do I use the pkg on the website? These are all things that as a developer who’s done it before are trivial questions, but I can see these being asked by coworkers who have never used docker before. This isn’t a slam on SAM, but it is a pain point I do foresee briefly in the future if professionally the company I worked for migrated from Serverless Framework to SAM.
Another major pain point with SAM for me was getting DynamoDB to work. In a perfect world DynamoDB should just work right out of the box and get stood up if your template file contains table definitions. Serverless Framework had2 a plugin which did this. Instead, Amazon recommend you run the DynamoDB .jar
locally, or stand up a docker container yourself. The docker steps also add an extra layer of confusion for new developers since it recommends adding your apps image (what?) to the docker-compose
and linking them. If you’re just running a SAM app locally all you need to do is create a virtual docker network and put Dynamo’s container on it, then start sam with an extra flag:
sam local start-api --docker-network <network-name>
This is not mentioned in the docs, and no error is thrown on SAMs end about this. It’s up to the developer to notice there’s no connection, realise it’s due to SAM actually being in a docker container, and then knowing how to put both containers on the same network…
This stackoverflow answer is how I found out about this.
Oh, by the way there’s no way to seed table definitions from SAM into this container now that it’s stood up. Here’s the gross script I whipped up to seed from a template file into the container. There is probably a better way, but I’d rather focus on writing my actual applications code. (SAM should do this if there are table definitions!!!!)
# Convert the Template YAML into JSON that create-table expects
# Dump this to a temp file so its easy to "while read" later
yq -o=json '.Resources | filter(.Type == "AWS::DynamoDB::Table")' ../template.yaml \
| jq \
'.[] | {
TableName: "local\(.Properties.TableName[1][1])",
KeySchema: .Properties.KeySchema,
AttributeDefinitions: .Properties.AttributeDefinitions,
GlobalSecondaryIndexes: .Properties.GlobalSecondaryIndexes,
BillingMode: "PAY_PER_REQUEST"
}' \
| jq -c '.' > tables-seed.txt
# Remove Null GSI if it exists since create-table will crash if a value is null
sed -i '' 's/,"GlobalSecondaryIndexes":null//g' tables-seed.txt
# Run create table against each line in the seed data file
while read -r line; do
aws dynamodb create-table --no-cli-pager --endpoint-url http://localhost:8000 --cli-input-json "$line"
done < tables-seed.txt
My final pain point was SAMs built in ESBuild integration. I love ESBuild. It’s made my life 10x easier when working with Typescript. However, I was unable to get the built-in auto-build working when deploying.
This is how I bundle manually:
esbuild src/handler.ts \
--target=es2020 \
--platform=node \
--external:aws-sdk \
--sourcemap=linked \
--outfile=.build/handler.js \
--bundle
This is how SAM can do auto bundling:
Resources:
HelloWorldFunction:
Type: AWS::Serverless::Function
Properties:
# ... Function properties
Metadata:
BuildMethod: esbuild
BuildProperties:
Format: esm
Minify: false
OutExtension:
- .js=.mjs
Target: "es2020"
Sourcemap: true
External:
- aws-sdk
EntryPoints:
- handler.ts
I have no idea why, but in one of my projects, the SAM bundle is 21.25MB
while the manually bundled one is only 1.3MB
. The SAM bundle also doesn’t work in a Lambda… Which is entirely the point. Luckily its relativity easy to use your own bundler, but you lose out on live reloading. I’ll definitely come back to this one day, it’s likely a poorly documented feature or there’s something wrong with my config.
AWS SAM L’s
- Abstracted docker container management, not an issue until you want to use DynamoDB
- DynamoDB is not handled out of the box with SAM
- Cannot seed from a SAM template into a DynamoDB instance
- Documentation & Community support is lacking
- Struggled to get an API gateway stood up
- This was confusing:
HttpMethod: '*'
Cors: "'*'"
(Quotes in quotes AND inconsistent!)
- This was confusing:
- Connectors seem cool but are an abstraction that makes things confusing and seem like they’d become a nightmare to maintain, easier to write IAM roles.
- Built in ESBuild just straight up didn’t work when I used it, worked when I manually built with ESBuild.
AWS SAM W’s
- Running API Gateway locally rocks
- ESBuild if it worked would be amazing
- Not extremely abstracted from the underlying cloudformation
- Easy to use existing tooling or own tools alongside, or in replacement of, SAMs tooling.
Less complaining!
Even though I’ve just spent the last while ranting about pain points in SAM, I can see its potential. It’s almost there. I want to use it over the serverless framework. The main pain points I talked about were huge hurdles for me getting started which I only jumped over because I was being paid to jump over them. But, now that I know how to do it properly; I’ll likely be using SAM for all my personally projects going forward.
The benefits of a serverless stack for me far outweigh the cons if I don’t factor in the time learning how to properly use it. Serverless is amazing, and I do see it as the future of cloud computing, but the documentation and new developer journey is abysmal and extremely discouraging.
Recommendations for new Developers
- Use SAM, or plain Cloudformation.
- Use plain IAM roles to define permissions:
MyFunction:
Type: AWS::Serverless::Function
Properties:
# ...Properties
Policies:
- Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- dynamodb:Query
- dynamodb:Scan
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
- dynamodb:BatchGetItem
Resource:
- !Sub ${myTable.Arn}
- !Sub ${myTable.Arn}/index/*
- Include your stage name (prod/dev) in table names and names of other infrastructure items.
- Learn how to properly take advantage of DynamoDB (I’ll make a post about this one day!).
- Properly index your data.
- Use multiple tables.
- Be mindful of Read/Write units.
- If using SAM set up a custom API Gateway API, it’s worth it for when you do eventually need it:
ApiGatewayApi:
Type: AWS::Serverless::Api
Properties:
StageName: !Ref Stage
Cors: "'*'"
MethodSettings:
- ResourcePath: "/*"
HttpMethod: "*"
ThrottlingRateLimit: 100
ThrottlingBurstLimit: 500
- Use Express & Typescript with a single handler for most APIs:3
/** handler.ts */
import express from 'express'
import serverless from 'serverless-http';
= express() // Theres waaayyyyy more stuff here usualy.
const app = serverless(app, {})
export const handler
.get('/my-function', (request, response) => {})
app.get('/my-function/sub', (request, response) => {}) app
MyFunction:
Type: AWS::Serverless::Function
Properties:
# ... Properties
Events:
Root:
Type: Api
Properties:
Path: /my-function
Method: any
RestApiId:
Ref: ApiGatewayApi
Sub:
Type: Api
Properties:
Path: /my-function/{any+}
Method: any
RestApiId:
Ref: ApiGatewayApi
- Ensure everything runs locally as well as once deployed!
- If you’ve split your infrastructure into separate stacks it should be easy to deploy to a temporary testing stage.
- If working with multiple developers it is a horrible idea to test by deploying to
dev
orprod
.
- Set up GitHub actions, Bitbucket Pipelines etc. to deploy when pushing to
dev
orprod
, keep it predictable! - If it seems too complicated it probably is. Serverless (sadly) currently has lots of ways of doing one thing. Research everything!
We use macOS for development at the company I currently work for at the time of writing, we develop iOS applications which requires the use of XCode.↩︎
Major plugin became unmaintained and broke everyone’s builds.↩︎
Example on my webmentions repo has a single handler.↩︎