pfy.ch

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

Serverless Framework W’s

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

AWS SAM W’s

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

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/*
ApiGatewayApi:
  Type: AWS::Serverless::Api
  Properties:
    StageName: !Ref Stage
    Cors: "'*'"
    MethodSettings:
      - ResourcePath: "/*"
        HttpMethod: "*"
        ThrottlingRateLimit: 100
        ThrottlingBurstLimit: 500
/** handler.ts */
import express from 'express'
import serverless from 'serverless-http';

const app = express() // Theres waaayyyyy more stuff here usualy.
export const handler = serverless(app, {})

app.get('/my-function', (request, response) => {})
app.get('/my-function/sub', (request, response) => {})
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

© 2024 Pfych