serverless

SAM and Serverless Framework, on the same flat

SAM and Serverless Framework, on the same flat

Having spent a Saturday finding out what SAM hands CloudFormation, the obvious next question is whether the other tool that writes CloudFormation for you writes better CloudFormation. So I ported the flat to serverless.yml — five functions, the device registry, the IoT rule, the schedules, the alarms — and deployed it beside the real stack under a different stage.

The first thing to say is that this is not a runtime comparison. Both of them produce a CloudFormation template and hand it to CloudFormation; you can see Serverless Framework's with serverless package, which leaves it in .serverless/cloudformation-template-update-stack.json. Same service, same change sets, same eventual consistency, same three-in-the-morning rollback behaviour. Everything below is about the authoring layer and the CLI, because that is all there is to differ about.

One role or five

This is the difference that actually changed my mind about something, so it goes first.

SAM attaches policies per function. The sensor ingest function gets CRUD on one table and nothing else, because that is where its Policies: block is:

SensorIngestFunction:
  Type: AWS::Serverless::Function
  Properties:
    Policies:
      - DynamoDBCrudPolicy:
          TableName: !Ref DeviceRegistryTable

Serverless Framework, by default, gives the whole service one role:

provider:
  name: aws
  runtime: python3.8
  region: eu-west-1
  iamRoleStatements:
    - Effect: Allow
      Action: [dynamodb:GetItem, dynamodb:PutItem, dynamodb:UpdateItem]
      Resource: !GetAtt DeviceRegistryTable.Arn
    - Effect: Allow
      Action: [iot:Publish]
      Resource: '*'

Every function in the service can do everything in that list. The function that reads a temperature can publish to IoT. The function that answers Alexa can write to the table. Nothing in my flat is going to be attacked over this and that is not the point — the point is that the default shape of the file encourages one union of every permission any function needs, and a union only ever grows.

There is a plugin, serverless-iam-roles-per-function, and it works, and the fact that per-function IAM is a plugin rather than the default tells you what the tool considers normal. SAM's per-function policies plus the policy template list is the single strongest thing it has, and this is where I felt it.

Variables, where Serverless Framework is simply better

CloudFormation has parameters, mappings and !Sub. Serverless Framework has a resolver that reads whatever you point it at:

custom:
  stage: ${opt:stage, self:provider.stage}
  config: ${file(./config/${self:custom.stage}.yml)}

provider:
  environment:
    SKILL_ID: ${ssm:/home-automation/skill-id}
    TABLE: ${self:custom.config.tableName}

The SAM equivalent of that SSM lookup is a template parameter typed AWS::SSM::Parameter::Value<String> plus a line in samconfig.toml, and the equivalent of the per-stage config file is a Mappings: block, which is a lookup table pretending to be a data structure. On this, it is not close. Serverless Framework treats the deployment as something with inputs. SAM treats it as a document with parameters, and the difference shows up every single time you want the dev stack to point somewhere else.

Stages

sls deploy --stage dev does the obvious thing: every resource name carries the stage, so two stacks coexist without you thinking about it. SAM's answer is a samconfig.toml with an environment table and --parameter-overrides, and resource naming you do yourself with !Sub '${AWS::StackName}-...' on every resource that needs a name.

I only have one stage here, so this cost me nothing and would have cost me a lot at work.

The deploy loop

The thing I did not expect to care about:

sls deploy function -f sensorIngest

That uploads a zip and calls UpdateFunctionCode. It takes about five seconds. SAM has no equivalent — sam deploy is a change set every time, and for this stack that is somewhere between forty and ninety seconds depending on what moved. Over a weekend of fiddling with one handler, the difference is the difference between staying in the problem and going to make tea.

The counterweight is that sls deploy function deliberately skips CloudFormation, so the stack and the deployed code drift apart until the next full deploy. That is fine and it is also exactly the kind of fine that stops being fine on a team.

SAM's answer to the same problem is sam local invoke against an event file, which I use constantly and which is genuinely better than the framework's equivalent for a stack like mine — serverless-offline is built around HTTP APIs, and there is no HTTP anywhere in this flat.

Plugins are the ecosystem and the bill

Everything Serverless Framework does not do natively, it does with a plugin. State machines are serverless-step-functions. Vendored Python dependencies with native extensions are serverless-python-requirements with dockerizePip: true. Both work well.

Both also mean that a Python project on a machine with no Node.js installed now has a package.json, a node_modules, and a version matrix between the framework and each plugin that upgrades independently of it. SAM ships one binary from AWS and its state machine support is a resource type.

I want to be fair about the direction that cuts: the plugin list is also why Serverless Framework can do things SAM has no story for at all, and if I needed one of those tomorrow I would install it without much soul-searching.

The part that is not technical

Serverless Framework is a product with a company behind it. There is a dashboard, there are org and app keys in the file, the CLI would like you to log in, and it phones home unless told not to. None of that is unreasonable and all of it is a third party standing in the path between my laptop and my own AWS account, for a stack that turns lamps on.

SAM is a CloudFormation transform and a CLI, both from the vendor whose API I am calling anyway. For this project that asymmetry decides it, and I accept that for a project with a delivery deadline it would decide nothing at all.

Where I landed

The flat stays on SAM. Per-function IAM by default, no second vendor, and the local invoke loop is the one I actually use.

If I were starting something with three stages and four people, I would want Serverless Framework's variables and its stage handling badly enough to take the plugin sprawl, and I would install serverless-iam-roles-per-function on day one before anyone had the chance to grow the shared role.

What nags at me after two weekends is that both files are YAML with a preprocessor in front. Every argument above is about whose preprocessor is better, and none of it is about the thing I keep wanting, which is a loop. Four schedules are four near-identical blocks in both tools. That is a different post, and I think I am going to have to write the stack a third time to have it.

Deyan Peev

Written by

Deyan Peev

Founding Engineer · Sofia, Bulgaria

Deyan Peev

Founding Engineer in Sofia, Bulgaria. Currently at 1club.

Elsewhere

© 2026 Deyan Peev